神刀安全网

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


单例设计模式的定义

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

使用套路

  • 构造方法私有化
  • 使用静态方法,供外部获取对象的实例
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);         }     } } 

运行的结果:

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

懒汉线程安全1.png

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

懒汉线程安全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;     } } 

运行结果:

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

懒汉线程安全3.png

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

懒汉线程安全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,同时线程2走到第二个 null == 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();     } } 

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

验证例子:

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

静态内部类1.png

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

静态内部类2.png

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

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » 设计模式-单例设计模式以及volatile关键字

分享到:更多 ()

评论 抢沙发