ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

彻底搞懂单例模式的懒汉式饿汉式 双检索 线程安全问题

2019-07-02 11:24:37  阅读:590  来源: 互联网

标签:singleton 饿汉 private instance 线程 单例 volatile 搞懂


单例类只能有一个实例。

1、单例类只能有一个实例。
   2、单例类必须自己创建自己的唯一实例。
   3、单例类必须给所有其他对象提供这一实例。

懒汉式单例类.在第一次调用的时候实例化自己

public class Singleton {
private Singleton() {}
private static Singleton single=null;
//静态工厂方法
public static Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
}

Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。
(事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。此问题在此处不做讨论,姑且掩耳盗铃地认为反射机制不存在。)
但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例,要实现线程安全,有以下三种方式,都是对getInstance这个方法改造,保证了懒汉式单例的线程安全,如果你第一次接触单例模式,对线程安全不是很了解,可以先跳过下面这三小条,去看饿汉式单例,等看完后面再回头考虑线程安全的问题:

饿汉式单例类.在类初始化时,已经自行实例化

public class Singleton {
private Singleton1() {}
private static final Singleton1 single = new Singleton1();
//静态工厂方法
public static Singleton1 getInstance() {
return single;
}
}
饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的
class singleton{
  private singleton(){
    //…
  }
  private static singleton instance;
  public static singleton synchronized getinstance(){
    if(instancenull) //1
      instance = new singleton(); //2
    return instance
  }
}
此时可以保证不出错,是单例模式的一种方案,但是问题是每次执行都要用到同步,开销较大。
class singleton{
  private singleton(){
    //…
  }
  private static singleton instance;
  public static singleton getinstance(){
    if(instance
null) { //1
      sycronized(singleton.class){
        if(instance==null)
          instance = new singleton(); //2
      }
    }
    return instance;
  }
}
此写法保证了,当多个进程进入第一个判断锁时,会被同步机制隔离,只有一个程序进入新建对象,再其他线程进入时,instance已经不为null,因此不会新建多个对象。这种方法就叫做双重检查锁,但是也有一个问题,就是java是实行无序写入的机制,在某个线程执行//2代码的过程中,instance被赋予了地址,但是singleton对象还没构造完成时,如果有线程访问了代码//1此时判断instance不为空,但是方法返回的是一个不完整对象的引用。此时可能会产生错误!
还是会存在线程安全问题:当线程A执行到" instance = new singleton();"这一行,而线程B执行到外层"if (instance == null) "时,可能出现instance还未完成构造,但是此时不为null导致线程B获取到一个不完整的instance。
之所以会出现这种情况,要从JVM的指令重排序说起。

关于指令重排序

指令重排序:是编译器在不改变执行效果的前提下,对指令顺序进行调整,从而提高执行效率的过程。
一个最简单的重排序例子:
int a = 1;
String b = “b”;
1
2
对于这两行毫无关联的操作指令,编译器可能会将其顺序调整为:
String b = “b”;
int a = 1;
1
2
此时该操作并不会影响后续指令的执行和执行结果。
再回过头看我们的双检锁内部,对于"instance = new singleton();“这一行代码,它分为三个步骤执行:
1.分配一块内存空间
2.在这块内存上初始化一个 singleton的实例
3.将声明的引用instance指向这块内存
第2和第3个步骤都依赖于第1个步骤,但是2和3之间没有依赖关系,那么如果编译器将2和3调换顺序,变成了:
1.分配一块内存空间
2.将声明的引用instance指向这块内存
3.在这块内存上初始化一个 singleton的实例
当线程A执行到第2步时,instance已经不为null了,因为它指向了这块内存,此时如果线程B走到了"if (instance == null)”,那么线程B其实拿到的还是一个null,因为这块内存还没有初始化,这就出现了问题
指令重排序是导致出现线程不安全的直接原因,而根本原因则是对象实例化不是一个原子操作。
关于原子操作
原子操作:不可划分的最小单位操作,不会被线程调度机制打断,不会有线程切换,整个操作要么不执行,一旦执行就会运行到结束。
我们来看一个简单的例子:
Object a;
Object b = new Object();
a = b;
1
2
3

对于"a = b" 这一操作指令,将a这个引用指向b这一对象的内存,只需要改变a的指针,因此该直接赋值操作是一个不可划分的原子操作。

再看另一个例子:

int i = 0;
i ++;

1
2

对于"i ++"这一操作指令,其实它分为三个步骤执行:

读取i的值
将i的值加1
将新的值赋值给i

类似的还有:

boolean b = true;
b = !b;

1
2

对于这些涉及自身值的操作,由于其最终实现需要划分更小的操作单位,因此均不是原子操作。

对于非原子操作,在多线程下就可能出现线程安全问题,这也是我们的双检锁不安全的根本原因,实例化对象不是一个原子操作。
五、 实现线程安全的双检锁
我们只需要对instance加上一个volatile修饰符便可解决线程安全问题,关于volatile的知识请阅读参考文献对应内容。
public class DoubleCheckLock {
private static volatile DoubleCheckLock instance;

private DoubleCheckLock() {
	// TODO
}

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

}
synchronized和volatile的完美配合,便实现了线程安全的双检锁单例模式。

volatile关键字特性
volatile具有可见性、有序性,不具备原子性。
注意,volatile不具备原子性,这是volatile与java中的synchronized、java.util.concurrent.locks.Lock最大的功能差异,
原子性:如果你了解事务,那这个概念应该好理解。原子性通常指多个操作不存在只执行一部分的情况,如果全部执行完成那没毛病,如果只执行了一部分,那对不起,你得撤销(即事务中的回滚)已经执行的部分。可见性:当多个线程访问同一个变量x时,线程1修改了变量x的值,线程1、线程2…线程n能够立即读取到线程1修改后的值。有序性:即程序执行时按照代码书写的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。(本文不对指令重排作介绍,但不代表它不重要,它是理解JAVA并发原理时非常重要的一个概念)。3.volatile适用场景
适用于对变量的写操作不依赖于当前值,对变量的读取操作不依赖于非volatile变量。适用于读多写少的场景。可用作状态标志。JDK中volatie应用:JDK中ConcurrentHashMap的Entry的value和next被声明为volatile,AtomicLong中的value被声明为volatile。AtomicLong通过CAS原理(也可以理解为乐观锁)保证了原子性。4.volatile VS synchronized
volatilesynchronized修饰对象修饰变量修饰方法或代码段可见性11有序性11原子性01线程阻塞01对比这个表格,你会不会觉得synchronized完胜volatile,答案是否定的,volatile不会让线程阻塞,响应速度比synchronized高,这是它的优点。)

相关参考文章:https://www.cnblogs.com/GtShare/p/9274237.html
https://www.cnblogs.com/GtShare/p/9274237.html

标签:singleton,饿汉,private,instance,线程,单例,volatile,搞懂
来源: https://blog.csdn.net/u014615041/article/details/94433906

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有