「这是我参与2022首次更文挑战的第30天,活动详情查看:2022首次更文挑战」
什么是单例模式
保证一个类只有一个实例,并且提供一个全局访问点
应用场景
重量级的对象,不需要多个实例,如线程池,数据库连接池
懒汉模式:
单线程实现
延迟加载, 只有在真正使用的时候,才开始实例化。根据定义以及类图我们大概可以先写出如下代码
package com.jony.designpattern.singleton;
public class LazySingletonTest {
public static void main(String[] args) {
Lazysingleton instance1=Lazysingleton.getInstance();
Lazysingleton instance2=Lazysingleton.getInstance();
System.out.println(instance1==instance2);
}
}
class Lazysingleton{
private static Lazysingleton instance;
//创建一个私有构造,防止从外部被new
private Lazysingleton(){};
//在创建一个共有的方法,为外部提供调用
public static Lazysingleton getInstance() {
//如果实例是null,则创建一个
if(instance==null){
instance=new Lazysingleton();
}
return instance;
}
}
执行结果如下:
这就创建了一个懒汉模式,但是当线程的时候,进行并发访问的时候,可能多个线程同时进行判断 if(instance==null) 这样就创建了多个实例。
package com.jony.designpattern.singleton;
public class LazySingletonTest {
public static void main(String[] args) {
new Thread(()->{
Lazysingleton instance=Lazysingleton.getInstance();
System.out.println(instance);
}).start();
new Thread(()->{
Lazysingleton instance=Lazysingleton.getInstance();
System.out.println(instance);
}).start();
new Thread(()->{
Lazysingleton instance=Lazysingleton.getInstance();
System.out.println(instance);
}).start();
}
}
class Lazysingleton{
private static Lazysingleton instance;
//创建一个私有构造,防止从外部被new
private Lazysingleton(){};
//在创建一个共有的方法,为外部提供调用
public static Lazysingleton getInstance() {
//如果实例是null,则创建一个
if(instance==null){
instance=new Lazysingleton();
}
return instance;
}
}
执行结果:
可以看到执行的结果会创建多个实例,因此我们需要给代码进行一些一下改造,能够在多线程的情况下也可以完美实现。
多线程实现
通过给方法添加synchronized 添加锁,这样就可以避免多线程下创建多个实例了
以上代码我们再次分析,这样有N个并发线程进来,不论是否已经创建了实例,所有线程都需要等待锁,这样对我们的性能是有很大损耗的,因此我们需要再次对代码进行改造,只有未创建实例的情况再进行加锁,否则直接获取实例。
class Lazysingleton{
private static Lazysingleton instance;
//创建一个私有构造,防止从外部被new
private Lazysingleton(){};
//在创建一个共有的方法,为外部提供调用
public static Lazysingleton getInstance() {
//如果实例是null,则创建一个
if(instance==null){
synchronized (Lazysingleton.class){
instance=new Lazysingleton();
}
}
return instance;
}
}
这样我们就可以提供程序的性能了,只有instance==null的情况下才进行加锁(虽然也有可能多个线程等待锁,但是也比之前性能提高很多),只要不等于null,则直接返回实例。
编译器(JIT),CPU 有可能对指令进行重排序,导致使用到尚未初始化的实例,可以通过添加volatile 关键字进行修饰
jvm加载字节码文件简单分析
到上步看似没问题了,但是实际上以上代码还是会有问题,我们写的java代码在编译后形成.class字节码文件,下面我们编写一个段简单代码,看一下字节码文件
1、首先创建java代码
package com.jony.designpattern.singleton;
public class LazyTest {
public static void main(String[] args) {
LazyTest t=new LazyTest();
}
}
2、编译java代码
3、查看字节码文件
4、字节码文件查看
5、主要分析main函数
5-1、可以看到构造函数第一行为new 执行#2 然后#2又指向#21,然后我们可以得知,jvm帮我们在堆区以utf8编码创建了一个LazTest内地地址,同时在栈空间创建一个引用这个内存地址的引用。
5-2、dup实际就是将栈空间的对象进行复制
5-3、invokespecial ,将第一步在一开始在栈空间创建的对象删除
5-4、将堆空间的引用赋值到我们实际的LazyTest t上面。
因此最终我们jvm加载字节码文件主要是如下几步
1、分配内存空间
2、初始化
3、引用赋值
最终编译器(JIT),CPU 有可能对指令进行重排序,导致以上三个步骤不是按照1-2-3进行执行,就可能导致,我们的对象指向了一个不存在的地址,造成空指针异常,我们就可以通过添volatile关键字进行修饰,防止执行指令重排。
最终形成如下代码:
class Lazysingleton{
private volatile static Lazysingleton instance;
//创建一个私有构造,防止从外部被new
private Lazysingleton(){};
//在创建一个共有的方法,为外部提供调用
public static Lazysingleton getInstance() {
//如果实例是null,则创建一个
if(instance==null){
synchronized (Lazysingleton.class){
instance=new Lazysingleton();
}
}
return instance;
}
}
饿汉模式:
类加载的 初始化阶段就完成了 实例的初始化 。本质上就是借助于jvm类加载机制,保证实例的唯一性(初始化过程只会执行一次)及线程安全(JVM以同步的形式来完成类加载的整个过程)。
实现代码
package com.jony.designpattern.singleton;
public class HungrySingletonTest {
public static void main(String[] args) {
HungrySingleton instance=HungrySingleton.getInstance();
HungrySingleton instance1=HungrySingleton.getInstance();
System.out.println(instance==instance1);
}
}
class HungrySingleton{
//类加载额时候就创建对象
private static HungrySingleton instance=new HungrySingleton();
//私有构造,不让外部类new
private HungrySingleton(){};
public static HungrySingleton getInstance(){
return instance;
}
}
实现原理
饿汉模式,不会发生懒汉模式的相关问题,这归功于类加载机制,来保证单例实例。
类加载过程
1,加载二进制数据到内存中, 生成对应的Class数据结构,
2,连接: a. 验证, b.准备(给类的静态成员变量赋默认值),c.解析
3,初始化: 给类的静态变量赋初值
只有在真正使用对应的类时,才会触发初始化 如(当前类是启动类即main函数所在类,直接进行new 操作,访问静态属性、访问静态方法,用反射访问类,初始化一个类的子类等.)
静态内部类:
1).本质上是利用类的加载机制来保证线程安全
2).只有在实际使用的时候,才会触发类的初始化,所以也是懒加载的一种形式,因为我们在不使用内部类的时候,jvm是不会加载的,只有在调用的时候才会进行初始化。
实现代码
class InnerClassSingleton{
//创建静态内部类
private static class InnerClassHolder{
private static InnerClassSingleton instance=new InnerClassSingleton();
}
//私有化构造函数
private InnerClassSingleton(){};
public static InnerClassSingleton getInstance(){
return InnerClassHolder.instance;
}
}
反射是如何攻击单例的
通过以下代码,我们通过反射机制就可以获得一个实例对象,这样和正常获得的实例就不一样了。
反射创建实例,上面三种创建的单例模式就都不安全,不能保证仅创建一个实例了
package com.jony.designpattern.singleton;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class InnerClassSingletonTest {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//通过反射获取对象
Constructor<InnerClassSingleton> declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor();
//设置可读
declaredConstructor.setAccessible(true);
//获得实例
InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance();
//使用new获得实
InnerClassSingleton instance=InnerClassSingleton.getInstance();
System.out.println(innerClassSingleton==instance);
}
}
class InnerClassSingleton{
//创建静态内部类
private static class InnerClassHolder{
private static InnerClassSingleton instance=new InnerClassSingleton();
}
//私有化构造函数
private InnerClassSingleton(){};
public static InnerClassSingleton getInstance(){
return InnerClassHolder.instance;
}
}
对饿汉模式和静态内部类,防护反射创建
我们在私有构造方法里面,对实例进行一些判断,如果实例已经存在则抛出异常。
class InnerClassSingleton{
//创建静态内部类
private static class InnerClassHolder{
private static InnerClassSingleton instance=new InnerClassSingleton();
}
//私有化构造函数
private InnerClassSingleton(){
if(InnerClassHolder.instance!=null){
throw new RuntimeException("不允许多个实例");
}
}
public static InnerClassSingleton getInstance(){
return InnerClassHolder.instance;
}
}
深入反射创建实例
我们进入newInstance方法
具体方法
可以看到如果实例是ENUM的时候,是不允许创建实例的,因此ENUM是不是也可以实现一个安全的单例呢?下面我们来测试一下
枚举类型的单例
通过代码执行结果,我们得知枚举其实也是单例模式的,那么枚举实现单例的原理是怎么样的呢?
1)天然不支持反射创建对应的实例,且有自己的反序列化机制
2)利用类加载机制保证线程安全
单例序列化
首先我们先进入Serializable这个类
可以看到如下说明
因此我们可以利用 指定方法来替换从反序列化流中的数据 如下
ANY‐ACCESS‐MODIFIER Object readResolve() throws ObjectStreamException;
下面我们来通过内部类单例来进行序列化
package com.jony.designpattern.singleton;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class InnerClassSingletonTest {
public static void main(String[] args) {
}
}
class InnerClassSingleton implements Serializable {
static final long serialVersionUID = 42L;
//创建静态内部类
private static class InnerClassHolder {
private static InnerClassSingleton instance = new InnerClassSingleton();
}
//私有化构造函数
private InnerClassSingleton() {
if (InnerClassHolder.instance != null) {
throw new RuntimeException("不允许多个实例");
}
}
public static InnerClassSingleton getInstance() {
return InnerClassHolder.instance;
}
Object readResolve() throws ObjectStreamException {
return InnerClassHolder.instance;
}
}