单例模式与线程安全问题

单例模式的主要作用,是保证在应用程序中一个类只会有一个实例存在。典型的应用场景,比如文件系统建立目录,或数据库连接都需要这样的单例。单例模式有以下几种常见的实现方式:

  • 饿汉式
  • 懒汉式(双检锁)
  • 内部类实现式
  • 枚举实现式

一、饿汉式

//饿汉式
class Singleton {    
    private static Singleton instance = new Singleton();//保证只有一个实例

    private Singleton() {}

    public static Singleton getInstance() { 
        return instance;
    }
}

//饿汉式变体
class Singleton {    
    private static Singleton instance;

    static{
        instance = new Singleton();//保证只有一个实例
    }
    
    private Singleton() {}

    public static Singleton getInstance() { 
        return instance;
    }
}

类被加载时就对instance进行实例化,单实例就被创建了。养兵千日,用兵一时,不管以后用不用的着,先创建再说,这相当于以空间换时间。

优点:写法简单,类装载时完成实例化。避免了线程安全问题。

缺点:无论单例是否使用到,都会一直占用内存空间。

二、(双检锁)懒汉式

懒汉式是当需要实例时才生成该实例。

class Singleton {    
    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() { 
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

if条件是个竞态条件,会存在线程安全问题。存在这样一种情况:A线程if判空通过,但还未创建实例,所以instance==null。而此时来了个B线程,检测到instance==null,判空也通过。这样造成的结果就是A和B两个线程最终都会创建一个实例。所以,这样的懒汉式是非线程安全的。

非安全的改进

为了保证上面的懒汉式的线程安全,我们想到的自然就是加锁。其中一种写法就是对创建实例的语句进行加锁处理,来看看代码。
public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

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

这样的写法是否是线程安全的呢?答案是否定的,这种写法跟前一种写法实际上效果是一样的。假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时就会产生多个实例,所以这种改进毫无意义。

非安全的双检锁方式

那么我们继续改进。在同步代码块中再进行一次判空操作,这种方式叫做双重检查锁(DCL),简称双检锁。

public class Singleton {
     
    private static Singleton singleton;

    private Singleton() {}

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

这种方式看起来无比正确,如果你是这么想的话,那你一定是个小白。实际上这种写法依然存在线程安全问题。具体原因就是指令的重排序,内存不可见等。具体可参考:The "Double-Checked Locking is Broken" Declaration(翻译:可以不要再使用Double-Checked Locking了

安全的双检锁方式

最简单的解决上面双检索不安全的办法就是使用volatile。
public class Singleton {
    //用volatile修饰
    private static volatile Singleton singleton;

    private Singleton() {}

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

正是由于双检锁存在的安全问题,java5开始引入了volatile关键字。这里使用volatile来修饰单例变量,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

使用volatile也有缺陷,就是会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率可能并不高。

三、内部类实现方式

public class Singleton {

    private Singleton() {}
    
    //内部类
    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化(getInstance)时,才会装载SingletonInstance类,从而完成Singleton的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

优点:延迟加载,线程安全,效率高。

四、枚举实现方式

public enum Singleton {
    INSTANCE;
}

借助枚举来实现单例模式,不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。枚举单例在实际项目开发中见的比较少,原因可能是枚举是在JDK1.5中才被加进去的。不过,我们公司项目中就用到了该写法。

优点:实现简单。
缺点:当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。

五、JDK中的单例

JDK中的Runtime类就是用饿汉式单例实现的。

public class Runtime {
    
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
 }

 

参考博客:

《JAVA与模式》之单例模式

探索设计模式之六——单例模式 

posted @ 2017-05-25 12:40  静水楼台/Java部落阁  阅读(388)  评论(3编辑  收藏  举报