单例模式介绍及其线程安全问题

 

    介绍下单例模式,即保证对一个类只实例化一个对象。实际生产例子有,Spring的bean默认创建模式等。

  单例模式的组成:包括一个私有的构造器,一个私有的静态变量,一个公有的静态方法。单例模式本身很简单,主要复杂点是在它在线程并发下的如何保证 线程安全+资源消耗少 的问题。

 

  一.饿汉式单例(线程安全)

 

缺点:直接实例化,资源会浪费。丢失了延迟实例化的性能好处。

 

 二.懒汉式单例(线程不安全)

 

 缺点:线程不安全,如果多个线程能够同时进入 if (instance == null) ,并且此时 instance 为 null,那么会有多个线程执行 instance = new LazySingleton(); 语句,这将导致实例化多次对象。即此类不是单例了。

 

三.线程安全的懒汉式单例例子。

  1>  对 getInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 uniqueInstance。

缺点:会有线程堵塞,性能上不好,不推荐。

 

 

 2>  双重校验锁。先判断 instance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁

缺点:基本无缺点,除了第一次实例化的时候会加锁,可能会有线程堵塞。

  如果两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行instance = new Singleton(); 这条语句,只是先后的问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个 if 语句。

  instance 采用 volatile 关键字修饰也是很有必要的。volatile的作用是 1.内存可见性: 所有线程都能看到共享内存的最新状态  2.防止指令重排。 (volatile修饰的变量并不是原子变量。只是变量的读和写变成了原子操作)

   instance = new LazySingleton(); 这段代码其实是分为三步执行:

  1. 为 instance 分配内存空间
  2. 初始化 instance 
  3. 将 instance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getInstance() 后发现 instance 不为空,因此返回 instance ,但此时 instance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

 

 

 3>静态内部类实现。具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。

内部静态类的变量并不会在类加载的时候就初始化好,而是会在调用的时候才初始化。保证资源的节约。

 

posted @ 2019-02-23 23:05  匿了匿了  阅读(3011)  评论(1编辑  收藏  举报