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

单例类只能有一个实例。

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

你可能感兴趣的:(彻底搞懂单例模式的懒汉式饿汉式 双检索 线程安全问题)