在多线程环境中的单例模式

在多线程环境中的单例模式

目前有三种实现单例模式的方式:

  1. 饿汉模式
  2. 懒汉模式
  3. 静态内部类

1、饿汉模式

class Singleton{
    // 第一步 构造器私有化
    private Singleton(){}
    // 第二步 自行对外提供实例
    private static final Singleton singleton = new singleton();
    // 第三步 提供外界可以获取实例的方法
    public static Singleton getInstance(){
        return singleton;
    }
}

在多线程的情况下,饿汉模式的性能是非常好的,因为获取实例的方法只是简单的返回实例,并没有任何的锁操作,因此在并行的程序中会有良好的表现。

但是这种方式有这明显的不足,就是实例在什么时候创建是不受控制的,对于静态成员singleton,它会在类第一次初始化的时候被创建。这个时刻并不一定是getInstance()第一次被调用的时候。

当类初始化的时候,实例就会被创建。如果希望精确控制singleton的创建时间,可以采用支持延迟加载策略的方法。这种方法只会在第一次调用getInstance()方法的时候才会创建对象。

2. 懒汉模式

class Singleton{
    // 第一步 构造器私有化
    private Singleton(){}
    // 第二步 创建实例的引用
    private static final Singleton singleton = null;
    // 第三步 提供外界可以获取实例的方法,此时需要判断一下引用的状态。
    public static Singleton getInstance(){
        if(singleton==null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

懒汉模式的实现思想如下:最初,我们并不需要实例化singleton,而当getInstance()方法第一次被调用的时候,创建单例对象。

但是这个模式有一个特别明显的问题,就是没有考虑多线程下的安全问题,在多线程并发的情况下,会并发调用获取实例方法,从而导致系统同时创建多个单例类实例,显然不符合要求。可以通过给getInstance()方法加锁才行。

class Singleton{
    // 第一步 构造器私有化
    private Singleton(){}
    // 第二步 创建实例的引用
    private static final Singleton singleton = null;
    // 第三步 提供外界可以获取实例的方法,此时需要判断一下引用的状态。
    public static synchronized Singleton getInstance(){
        if(singleton==null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

添加synchronized锁虽然可以保证线程安全,但是锁的范围过大,单例类是系统唯一的,在创建出来实例后,后续再加锁就只会造成性能的浪费。需要对代码进行优化,所以又有了双重校验锁的模式。

class Singleton{
    // 第一步 构造器私有化
    private Singleton(){}
    // 第二步 创建实例的引用
    private static final Singleton singleton = null;
    // 第三步 提供外界可以获取实例的方法,此时需要判断一下引用的状态。
    public static synchronized Singleton getInstance(){
        // 第一次校验
        if(singleton==null){
        	synchronized(Singleton.class){
                // 第二次校验
                if(singleton==null){
            		singleton = new Singleton();
                }
        	}
    	}
    	return singleton;
    }
    
}

singleton = new Singleton();并不是原子操作,所以双重校验锁会出现指令重排序问题。可能导致在实例还没又初始化完成之前,就被其他线程使用的问题,可以使用volatile关键字来修饰singleton字段,可以禁止指令的重排序优化。

class Singleton{
    // 第一步 构造器私有化
    private Singleton(){}
    // 第二步 创建实例的引用
    private static volatile Singleton singleton = null;
    // 第三步 提供外界可以获取实例的方法,此时需要判断一下引用的状态。
    public static synchronized Singleton getInstance(){
        // 第一次校验
        if(singleton==null){
        	synchronized(Singleton.class){
                // 第二次校验
                if(singleton==null){
            		singleton = new Singleton();
                }
        	}
    	}
    	return singleton;
    }
    
}

3. 静态内部类

class Singleton{
    // 第一步 构造器私有化
    private Singleton(){}
    //第二步 创建静态内部类
    private static class help{
        private static Singleton singleton = new Singleton();
    }
    // 第三步 提供外界可以获取实例的方法
    public static Singleton getInstance(){
        return help.singleton;
    }
}

可见,双重检查模式是一种丑陋,复杂的方法,而在静态内部类实现方式却可以完美拥有前两种方式的优点,首先getInstance()方法中没有锁,这使得在高并发环境下性能优越。其次,只有在getInstance()才会创建实例,因为这种方法巧妙的使用了内部类和类的初始化方式,私有化内部类,使得我们不可能在外部访问并初始化它。而我们只能在getInstance()内部才能对内部类初始化,利用虚拟机的类初始化机制来创建单例。

posted @ 2021-09-18 20:18  锤子布  阅读(259)  评论(0编辑  收藏  举报