多线程(基础三)
单例模式
🔑单例模式是设计模式之一,那设计模式又是啥呢?设计模式就类似于棋谱,是前人写代码时总结出的一些固定的套路,单例模式指是某个类在程序中只存在唯一的实例,不会创建出多个实例。单例模式有两种实现方式:饿汉模式,懒汉模式
饿汉模式
饿汉模式是程序一启动就创建了实例,实例创建的早
class Singleton{
private static Singleton singleton = new Singleton();
public static Singleton getInstance(){
return singleton;
}
private Singleton(){
}
}
✅类属性引用了一个实例对象,(类属性在了类对象里,而类对象在整个程序中只有一份,所以类属性就只有一份)。当 程序一启动,这个Singleton类就会被加载,就会随之创建一个类对象,那类属性又在类对象里,所以也就是,当程序一启动,类属性就被初始化,singleton就引用了Singleton实例,正因为实例创建的比较早,所以叫饿汉模式
✅把构造方法设为私有的,保证类外无法创建实例,而类属性只有一份,那Singleton实例只有一份,这样就实现了单例模式(只能创建一个实例)
懒汉模式(单线程版)
懒汉模式是第一次用的啥时候再创建实例,一直不用就不创建,这就是懒汉模式
class SingletonLazy{
private static SingletonLazy singletonLazy = null;
public static SingletonLazy getInstace(){
if(singletonLazy==null){
singletonLazy = new SingletonLazy();
}
return singletonLazy;
}
private SingletonLazy(){
}
}
✅第一次用的时候创建实例,以后再用直接返回。类加载的时候不创建实例,调用方法的时候再创建,这就是懒汉模式
懒汉模式(多线程版)
思考:上面两种单例模式如果在多线程环境下会出现线程安全问题吗?
✅对于饿汉模式每次调用方法都是读操作,所以不会出现线程安全问题,但是对于懒汉模式,在多线程环境中,下面代码中的if语句和new语句,两个线程可能会同时执行,当其中一个线程判断if条件成立,new对象之前,另一个线程也判断if成立,然后也new对象,这样两个线程就new了两个对象,造成了线程安全问题。
public static SingletonLazy getInstace(){
if(singletonLazy==null){
singletonLazy = new SingletonLazy();
}
return singletonLazy;
}
改进版1:
✅造成线程安全问题的原因主要是if语句和new对象语句不是原子操作,一个线程在执行时,另一个线程也可以执行。 所以解决方式就是加锁,让if语句(读操作)和new对象语句(写操作)是一个原子操作。这样两个线程执行同步代码块就有了先后顺序。这样就不会产生new出两个对象这样的bug了
class SingletonLazy{
private static SingletonLazy singletonLazy = null;
public static SingletonLazy getInstace(){
synchronized (Singleton.class){
if(singletonLazy==null){
singletonLazy = new SingletonLazy();
}
}
return singletonLazy;
}
private SingletonLazy(){
}
}
改进版2:
✅虽然加锁解决了这样的一个问题,但是又产生了一个新问题,那就是只有当第一次初始化的时候可能会产生线程安全问题,等实例创建好之后再调用getInstance()就不会产生线程安全问题了,但是按上面代码,每次调用方法,都要执行同步代码块,也就是每次都要加锁释放锁(加锁释放锁就会产生较大的时间开销),这样就很不合理,创建好实例之后再调用方法就不会产生线程安全问题了,也就没必要在加锁释放锁了,直接返回实例就行了,看下面的改进版
class SingletonLazy{
private static SingletonLazy singletonLazy = null;
public static SingletonLazy getInstace(){
if(singletonLazy==null){
synchronized (Singleton.class){
if(singletonLazy == null){
singletonLazy = new SingletonLazy();
}
}
}
return singletonLazy;
}
private SingletonLazy(){
}
}
✅外层if判定是否已经初始化好了,如果初始化好了,就直接返回,如果没有初始化,尝试加锁然后尝试初始化,内层if是判定当前线程拿到锁之后,再判定一下是否真的要进行初始化
✅如果多个线程并发执行,只有其中一个线程能先拿到锁,其余线程顶多执行到synchronized (Singleton.class)就阻塞了,当第一个拿到锁的线程创建完对象释放锁后,其余线程如果还没进入外层if那就直接返回,如果其余线程进入了外层if,就能尝试加锁,然后执行代码,但是由于第一个线程创建了对象,其余线程就不能创建对象了,所以再用内层的if判定一下是否真的需要创建线程。
public static SingletonLazy getInstace(){
if(singletonLazy==null){
synchronized (Singleton.class){
if(singletonLazy == null){
singletonLazy = new SingletonLazy();
}
}
}
return singletonLazy;
}
改进版3:
上面方法中,有的是读操作,有的是写操作,如果一个线程把数据读到寄存器了,然后另一个线程会不会直接复用寄存器中的值呢,也就是会不会因为编译器优化导致内存可见性问题呢?这个是不确定的,按理来说每个线程有自己的寄存器,不会出现这种复用的情况,但是编译器优化不好说从哪个角度优化,不好说会不会出现复用这种情况,所以加上volatile关键字,禁止编译器优化,是比较稳健的
class SingletonLazy{
volatile private static SingletonLazy singletonLazy = null;