美文网首页Android开发Android技术知识程序员
设计模式-单例设计模式以及volatile关键字

设计模式-单例设计模式以及volatile关键字

作者: 请叫我张懂 | 来源:发表于2018-01-12 09:58 被阅读57次

    单例设计模式的定义

    在内存中只有一个对象实例

    使用套路

    • 构造方法私有化
    • 使用静态方法,供外部获取对象的实例
    1.饿汉式

    HungrySingleton.java

    public class HungrySingleton {
        private static HungrySingleton mInstance = new HungrySingleton();
    
        /**
         * 构造方法私有化
         */
        private HungrySingleton() {
    
        }
        
        public static HungrySingleton getInstance() {
            return mInstance;
        }
    }
    

    特点: 在类装载的时候,就已经创建实例,而且保证了线程的安全。(使用空间来节约时间,无论有没有使用到该对象,内存中都存在对象的实例)。

    2.1懒汉式(存在线程安全问题)

    LazySingleton.java

    public class LazySingleton {
        private static LazySingleton mInstance;
    
        /**
         * 构造方法私有化
         */
        private LazySingleton() {
    
        }
    
        public static LazySingleton getInstance() {
            if (null == mInstance) {
                mInstance = new LazySingleton();
            }
            return mInstance;
        }
    }
    

    特点: 在对象被使用的时候才创建实例,但是存在线程安全问题。(使用时间来节约空间,每次进来都要判断实例是否为null,这个判断有一定的开销)

    线程安全问题: 单线程时当然不存在任何问题,但是有多个线程进行调用时,就会存在并发问题。如下面一段代码,使用Set数据结构来存放单例的对象。

    Set的特点:
    • 它不允许出现重复元素;

    • 不保证集合中元素的顺序

    • 允许包含值为null的元素,但最多只能有一个null元素。

    TestDemo.java

    public class TestDemo {
        public static Set<LazySingleton> singles = new HashSet<>();
    
        public static void main(String[] args) {
            SingletonRunnable runnable = new SingletonRunnable();
            new Thread(runnable).start();
            new Thread(runnable).start();
            new Thread(runnable).start();
            new Thread(runnable).start();
            new Thread(runnable).start();
            new Thread(runnable).start();
            new Thread(runnable).start();
            new Thread(runnable).start();
            new Thread(runnable).start();
            new Thread(runnable).start();
            new Thread(runnable).start();
            
            ...
            
            //等待线程完成
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println(singles);
        }
    
        private static class SingletonRunnable implements Runnable {
            @Override
            public void run() {
                LazySingleton s = LazySingleton.getInstance();
                singles.add(s);
            }
        }
    }
    

    运行的结果:

    懒汉线程安全1.png 懒汉线程安全2.png

    如图所示,图1 的情况是在多次运行的时候出现的,图2 的情况是正常的情况。说明,LazySingleton.java出现了线程并发访问导致的线程安全问题。所以在使用时要加上 synchronized 同步锁进行解决。

    2.2懒汉式(解决线程安全问题)

    LazySingleton.java

    public class LazySingleton {
    
        private static LazySingleton mInstance;
    
        /**
         * 构造方法私有化
         */
        private LazySingleton() {
    
        }
    
        public static synchronized LazySingleton getInstance() {
            if (null == mInstance) {
                mInstance = new LazySingleton();
            }
            return mInstance;
        }
    }
    

    特点: 多线程并发访问的时候,每个线程获取实例都要进行同步锁的判断,效率较低。

    2.3懒汉式(解决线程安全问题,效率较高)

    LazySingleton.java

    public class LazySingleton {
        private static LazySingleton mInstance;
    
        /**
         * 构造方法私有化
         */
        private LazySingleton() {
    
        }
    
        public static LazySingleton getInstance() {
            if (null == mInstance) {
                synchronized (LazySingleton.class) {
                    if (null == mInstance) {
                        mInstance = new LazySingleton();
                    }
                }
            }
            return mInstance;
        }
    }
    

    特点: 当 mInstance 不为空的时候则无须加上同步锁,保证的效率;当 mInstance 为空的时候加上同步锁,并且再次判空。

    疑问: 为什么要进行两次判空呢?

    只进行单次判空,并且执行 TestDemo.java 的情况如下:

     public class LazySingleton {
        private static LazySingleton mInstance;
    
        /**
         * 构造方法私有化
         */
        private LazySingleton() {
    
        }
    
        public static LazySingleton getInstance() {
            if (null == mInstance) {
                synchronized (LazySingleton.class) {
                    mInstance = new LazySingleton();
                }
            }
            return mInstance;
        }
    }
    

    运行结果:

    懒汉线程安全3.png 懒汉线程安全4.png

    很明显如果少了第二层的 if (null == mInstance) 其实就与没有加 synchronized 关键字的代码是相似的。假设有线程1和线程2两个线程,一同走到了第一层的 null == mInstance ,一同进入的 if 里,在 synchronized (LazySingleton.class) 前排队,虽然线程安全,但是线程1和线程2都会执行 mInstance = new LazySingleton() 对象就不是唯一的了。

    2.4懒汉式(加入volatile关键字)

    并发编程有三个特性:

    • 原子性
    • 可见性
    • 有序性

    volatile:

    1. 可以提供线程共享变量的可见性(体现了并发编程可见性)
    2. 禁止指令重排序(体现了并发编程的有序性)

    首先,我们先理解一下 mInstance = new LazySingleton();在实例化对象的时候执行了下面三个步骤:

    1. mInstance分配内存
    2. 使用LazySingleton的构造函数初始化成员变量
    3. mInstance对象指向分配的内存空间(执行完mInstance就为不等于null

    可以看出new对象的操作并非原子操作,所以编译器和处理器会对指令进行重排序。正常的顺应该为1->2->3,但是重排序之后,可能会将顺序变为1->3->2。假设按1->3->2的步骤进行,在线程1的中走到了3这个时候 mInstance != null 了,只是没有执行构造方法还未完成实例化。同时线程2 调用getInstance() 的时候就会返回 mInstance 对象,从而造成不必要的错误。但是如果加入了 volatile 就可以禁止指令重排序,不会出现上面的那种情况。

    LazySingleton.java

    public class LazySingleton {
        private static volatile LazySingleton mInstance;
    
        /**
         * 构造方法私有化
         */
        private LazySingleton() {
    
        }
    
        public static LazySingleton getInstance() {
            if (null == mInstance) {
                synchronized (LazySingleton.class) {
                    if (null == mInstance) {
                        mInstance = new LazySingleton();
                    }
                }
            }
            return mInstance;
        }
    }
    
    3静态内部类(推荐写法)

    Singleton.java

    public class Singleton {
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            return SingletonHolder.mInstance;
        }
    
        private static class SingletonHolder {
            private static final Singleton mInstance = new Singleton();
        }
    }
    

    特点:这样做既保证了线程安全,也提高了效率,是一个高性能的懒加载。推荐使用该方法来实现单例。

    验证例子:

    静态内部类1.png 静态内部类2.png

    如上图所示,实例化对象是在getInstance之后的。所以,通过静态内部类的使用,让JVM来保证线程的安全,减少了锁增加的耗时,并且是一个懒加载的模式。

    相关文章

      网友评论

        本文标题:设计模式-单例设计模式以及volatile关键字

        本文链接:https://www.haomeiwen.com/subject/gmiqnxtx.html