聊聊并发1:单例模式

0.单例模式用途

单件模式属于工厂模式的特例,只是它不需要输入参数并且始终返回同一对象的引用。
单件模式能够保证某一类型对象在系统中的唯一性,即某类在系统中只有一个实例。

1.单例模式分类

单例模式可以分为懒汉式和饿汉式
饿汉式单例模式:在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快。比如:

No.1 饿汉式

/*
 * 1.饿汉式
 *这种方式基于classloder机制避免了多线程的同步问题,
 *不过,instance在类装载时就实例化,
 *这时候初始化instance显然没有达到lazy loading的效果
 * 
 * */

public class Singleton1 {

    private static Singleton1 instance = new Singleton1();// 直接初始化一个实例对象

    private Singleton1() {// private 类型的构造函数 抱着其他对象不能直接new一个该对象的实例
    }

    public static Singleton1 getInstance() {// 该类唯一的一个public方法
        return instance;
    }

}

那如何提升加载速度呢?我们就需要用到懒汉式的单例模式了。首先我们能想到的就是如下所示的这种做法,首先 不初始化一个实例对象,等拿到是空时再去初始化,但是它有一个问题:在多线程不能正常工作,原因就是:判断是不是为空和实例化对象不是原子操作

No.2 懒汉,线程不安全

/*
 * 懒汉模式,线程不安全
 * 
 * */
public class Singleton2 {
    
    private static Singleton2 instance;
    
    private Singleton2(){}
    
    public static Singleton2 getInstance(){
        if(instance == null){
            instance = new Singleton2();
        }
        return instance;
    } 

}

懒汉模式在使用时,容易引起不同步问题,所以我们很自然的想到用synchronized关键字创建同步"锁"如下:

No.3 懒汉,线程安全

/*
 * 懒汉,线程安全
 * 
 * */
public class Singleton3 {
    
    private static Singleton3 instance;
    
    private Singleton3(){}
    
    public static synchronized Singleton3  getInstance(){
        if(instance == null){
            instance = new Singleton3();
        }
        return instance;
    } 

}

这种做法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是效率很低(因为锁),因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这个时候我们可以用双重同步锁来解决这个问题。它用同步块加锁的方式对代码块进行加锁操作。

No.4 双重校验锁

/*
 * 双重校验锁
 * 
 * */
public class Singleton4 {

    private volatile static Singleton4 instance;

    private Singleton4() {
    }

    public static Singleton4 getInstance() {
        if (instance == null) {
            synchronized (Singleton4.class) {
                if (instance == null) {
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }

}

有人会问:为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
注意:这里的 instance 变量是被声明成 volatile 。为什么?
因为instance = new Singleton4()并不是原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情,这个在我以前的文章里也说了:

  • 1.首先是会给 instance 分配内存
  • 2.调用 Singleton 的构造函数来初始化成员变量
  • 3.将instance对象指向分配的内存空间。

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
那怎么怎么应对,这里我们只需要将 instance 变量声明成 volatile 就可以了。
原因是:volatile关键字除了可以使得变量在不同线程间具有可见性,而且可以阻止JVM将指令重排序。

那还有没有一种方法既不用加锁,也能实现懒加载。用静态内部类。

No.5 静态内部类

/*
 * 静态内部类
 * 
 * 既不用加锁,也能实现懒加载。
 * 
 * */
public class Singleton5 {
    
    private Singleton5(){}
    
    private static class SingletonHolder{
        private static final Singleton5 instance = new Singleton5();
    }
    
    private static Singleton5 getInstance(){
        return SingletonHolder.instance;
    }

}

这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第三种方式不同的是:第三种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三方法就显得更合理。

你可能感兴趣的:(聊聊并发1:单例模式)