线程安全的单例模式创建的几种方式
一、线程不安全的懒汉模式,以及优为线程安全的懒汉模式
/**
* 懒汉模式
* 单例实例在第一次使用时进行创建
*/
public class SingletonExample1 {
// 私有构造函数
private SingletonExample1() {
}
// 单例对象
private static SingletonExample1 instance = null;
// 静态的工厂方法
public static SingletonExample1 getInstance() {
if (instance == null) { //第一次线程A与线程B同时执行时会造成拿到的对象不同,创建了两次
instance = new SingletonExample1();
}
return instance;
}
}
说明:这种方式在单线程模式下是没有问题的,但是,在多线程的环境下,线程A与线程B同时执行到17行时,instance为空,则会创建出连个不同的对象返回,两个线程拿到的对象是不同的。所以不安全。
那怎么保证线程安全昵,我们可以用synchronized关键字来修饰该静态工厂方法,保证线程安全,同时只有一个线程可以执行该方法,代码如下:
public class SingletonExample3 {
// 私有构造函数
private SingletonExample3() {
}
// 单例对象
private static SingletonExample3 instance = null;
// 静态的工厂方法
public static synchronized SingletonExample3 getInstance() { //synchronized 修饰
if (instance == null) {
instance = new SingletonExample3();
}
return instance;
}
}
这样确实是实现了线程安全,但是我们不推荐用这种方式,因为在高并发下,同时只有一个线程可以访问该方法,效率低下。那有没有更好的方式昵,有,双重检测机制,两次判空,在判空里面再加上一个锁进行二次判空,保证多线程环境下创建对象的代码只有一个线程执行。代码如下:
public class SingletonExample4 {
// 私有构造函数
private SingletonExample4() {
}
// 单例对象
private static SingletonExample4 instance = null;
// 静态的工厂方法
public static SingletonExample4 getInstance() {
if (instance == null) { // 双重检测机制
synchronized (SingletonExample4.class) { // 同步锁
if (instance == null) {
instance = new SingletonExample4();
}
}
}
return instance;
}
}
这个时候我们在看一下,线程A跟线程B会同时在14行堵住,同时只有一个线程执行,比如当线程A进去之后,线程B会在门口等待A执行完毕,当线程A执行完之后。线程B进去,这个时候instance已经不为空了,就会返回线程A创建的对象。这么来看的话,这个代码确实没问题,是线程安全的懒汉单例模式。
但实际上并不是这样,这个代码还是线程不安全的,这里就涉及到CPU的指令重排了。创建对象时,在CPU中分三步执行,
- memory = allocate() 分配对象的内存空间
- ctor Instance() 初始化对象
- instance = memory 设置instance指向刚分配的内存
那这里存在着什么问题昵,JVM和cpu优化,发生了指令重排,正常的执行顺序是这样没错,但如果在少数情况下,指令重排,就会造成正常的顺序发生变化,如下:
- memory = allocate() 分配对象的内存空间
- instance = memory 设置instance指向刚分配的内存
- ctor Instance() 初始化对象
这样的情况下,当线程A在创建对象时,线程B刚好走到13行,第一次判空,这个时候,线程A在CPU中的指令执行顺序按照重排后的执行。则在第二步的时候,对象还未初始化,就为instance指向了分配的内存。这时候,线程B刚好在13行判空,发现instance不为空,就直接返回了,但这个返回的对象并未初始化,依旧为空,造成问题的发生。
那这个问题如何避免昵,很简单,防止这个时候指令重排就行了,可以使用volatile关键字修饰instance变量,就可以避免指令重排从而使线程安全。完整代码如下:
/**
* 懒汉模式 -》 双重同步锁单例模式
* 单例实例在第一次使用时进行创建
*/
public class SingletonExample5 {
// 私有构造函数
private SingletonExample5() {
}
// 1、memory = allocate() 分配对象的内存空间
// 2、ctorInstance() 初始化对象
// 3、instance = memory 设置instance指向刚分配的内存
// 单例对象 volatile + 双重检测机制 -> 禁止指令重排
private volatile static SingletonExample5 instance = null;
// 静态的工厂方法
public static SingletonExample5 getInstance() {
if (instance == null) { // 双重检测机制 // B
synchronized (SingletonExample5.class) { // 同步锁
if (instance == null) {
instance = new SingletonExample5(); // A - 3
}
}
}
return instance;
}
}
二、单例的饿汉模式
这个较为简单就不做说明了,线程安全的饿汉模式。单例实例在类装载时进行创建
/**
* 饿汉模式
* 单例实例在类装载时进行创建
*/
public class SingletonExample2 {
// 私有构造函数
private SingletonExample2() {
}
// 单例对象
private static SingletonExample2 instance = new SingletonExample2();
// 静态的工厂方法
public static SingletonExample2 getInstance() {
return instance;
}
}
或者用静态代码块的形式加载,需要注意的是,instance的声明要在静态代码块前面,代码如下:
/**
* 饿汉模式
* 单例实例在类装载时进行创建
*/
public class SingletonExample6 {
// 私有构造函数
private SingletonExample6() {
}
// 单例对象
private static SingletonExample6 instance = null;
static {
instance = new SingletonExample6();
}
// 静态的工厂方法
public static SingletonExample6 getInstance() {
return instance;
}
}
三、枚举的方式
推荐使用这种方式。
/**
* 枚举模式:最安全
*/
public class SingletonExample7 {
// 私有构造函数
private SingletonExample7() {
}
public static SingletonExample7 getInstance() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private SingletonExample7 singleton;
// JVM保证这个方法绝对只调用一次
Singleton() {
singleton = new SingletonExample7();
}
public SingletonExample7 getInstance() {
return singleton;
}
}
}
上一篇: js实现跨页勾选复选框
下一篇: 单例模式(Singleton)的创建