zoukankan      html  css  js  c++  java
  • 单例模式与线程安全问题

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

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

    一、饿汉式

    //饿汉式
    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与模式》之单例模式

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

  • 相关阅读:
    面向对象基本原则
    策略模式
    简单工厂模式
    高内聚、低耦合
    UML在代码中的展现
    使用commons-csv简单读写CSV文件
    java反射机制
    SrpingDruid数据源加密数据库密码
    markdown学习经验
    Vue.js学习笔记
  • 原文地址:https://www.cnblogs.com/rouqinglangzi/p/6903134.html
Copyright © 2011-2022 走看看