Java多线程---单例模式(有趣易懂版)

2021/04/07 14:57:52

单例模式

单例对象的类只能允许一个实例存在。

特征
  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

饿汉模式

在类加载时,完成实例化,用时直接用。可避免线程同步问题。

public class singleTon {
   
    private static final singleTon instance =new singleTon();
    private singleTon(){
    }
    public static singleTon getInstance(){
   
        return instance;
    }
}

优点

线程安全

缺点

生命周期较长,造成资源浪费

在类装载的时候就完成实例化,没有达到懒加载的效果。如果从始至终从未使用过或最后才用到这个实例,则会造成内存资源的浪费。

懒汉模式

类加载时,先不实例化,等到用到时再加载。

public class SingleTon {
   
    private static SingleTon instance=null;
    private SingleTon(){
   }
    public static SingleTon getInstance(){
   
       if (instance==null){
   
       //用到了,开始实例化
           instance=new SingleTon4();
        }
        return instance;
   }
}

但这个方式是线程不安全的,每个线程来到这儿都会new对象,毫不顾忌啊。那怎么办呢?加锁呗!

public class SingleTon {
   
    private static SingleTon instance=null;
    private SingleTon(){
   }
    public static SingleTon getInstance(){
   
        synchronized (SingleTon.class) {
   
            if (instance==null){
   
                //用到了,开始实例化
                instance=new SingleTon();
            }
        }
        return instance;
    }
}

那么问题来了

加锁不是锁对象吗,那你为啥不锁instance呢?

因为instance还没实例化啊,懒汉懒汉,它用时才实例化,所以它现在还只是个空引用!

那现在加锁了它就线程安全了吗?

并没有!这里的赋值操作不具有原子性。new 对象实际上是拆分成三个步骤:

1.分配内存 2.执行初始化 3.赋值给变量。

还效率低、资源浪费,每个线程都要先来竞争锁,竞争失败就阻塞。竞争成功了也不知道这instance到底还是不是null,反正现在看着是null,那我就去实例!
不好不好!那怎么办呢?
双重校验锁

public class SingleTon4 {
   
	//volatile保证共享变量可见性
  	private static volatile SingleTon instance=null;
	private SingleTon(){
   }
	public static SingleTon getInstance(){
   
    	//双重判断:防止多次实例化
        if (instance==null) {
   
            synchronized (SingleTon.class) {
   
                if (instance == null) {
   
                    //synchronized加锁:保证赋值操作原子性
                    instance = new SingleTon();
                }
            }
        }
        return instance;
	}
}

那么问题又来了!

这volatile在这儿是干嘛的?

这里体现了它的两个作用:保证可见性、禁止重排序

  • 可见性

即:一个线程修改了共享变量,其他线程会立刻看到修改后的新值。
在多线程执行中,原本是要将数据从主内存拿到自己的私有工作区中去修改,然后再放回主内存中,但这个过程中,可能A线程正在改数据还没放回去,B线程又去拷贝这个数据去修改,导致数据不一致。
使用volatile关键字,其实还是让线程们去从主存中读取数据拷贝到私有工作区,但是因为一些底层机制和协议可以使线程一修改,其他线程就可以立马知道更新后的结果,从而实现可见性,但并不能保证线程安全,它不具有原子性。

摘录:volatile底层实现原理

如果对加了volatile修饰的变量进行写操作,JVM就会向处理器发送一条lock前缀的指令,将这个变量在缓存行的数据写回到系统内存。这时只是写回到系统内存,但其他处理器的缓存行中的数据还是旧的,要使其他处理器缓存行的数据也是新写回的系统内存的数据,就需要实现缓存一致性协议。即在一个处理器将自己缓存行的数据写回到系统内存后,其他的每个处理器就会通过嗅探在总线上传播的数据来检查自己缓存的数据是否已过期,当处理器发现自己缓存行对应的内存地址的数据被修改后,就会将自己缓存行缓存的数据设置为无效,当处理器要对这个数据进行修改操作的时候,会重新从系统内存中把数据读取到自己的缓存行,重新缓存。

  • 禁止重排序,建立内存屏障(重点)

JVM在执行代码过程中为了优化性能,会有指令重排序的特性。如实例化对象时的三步可能会重排序:
原本:1.分配内存空间 2.初始化 3.赋值给变量
可能顺序被优化为:1–3—2
当1、3执行完后,对方发现变量不为null,就直接返回了,实际上该变量还没初始化完,引用里保存的是无效对象。

静态内部类

一个集 饿汉模式和懒汉模式 优点于一身的设计模式

class SingleTon3 {
   
    private SingleTon3(){
   }
    //静态内部类:当SingleTon3类加载时,Holder类不会加载,等到第一次调用getInstance()时涉及到instance的调用了,才开始类加载,实现了懒加载
    private static class Holder{
   
        //只实例化一次
        static SingleTon3 instance=new SingleTon3();
    }
    public static SingleTon3 getInstance(){
   
        return Holder.instance;
    }
}

推荐阅读
深入理解单例模式

写在最后的话

由于我还是个站在Java世界边缘的小白,学了一点就写了这么个学习笔记。文章肯定有很多问题,最近有被提醒修改了一些,应该还有很多地方表述不恰当或不清楚,我会通过后期学习再进行修改完善的,同时也欢迎各位读者批评指正,谢谢✌。