发布对象
先给出两个概念 , 然后通过singleton
示例代码 ,来看看安全发布对象的四种方法.
发布对象
使一个对象能够被当前范围之外的代码所使用
对象逸出
是一种错误的发布, 当一个对象还没有构造完成时 , 就使得它被其他线程看见
四种方法
- 在静态初始化函数中初始化一个对象的引用
- 将对象的引用保存到
volatile
类型域或者AtomicReference
对象中 - 将对象的引用保存到某个正确构造对象的
final
类型域中 - 将对象的引用保存到某个由锁保护的域中
如何保证一个实例只被初始化一次 , 且线程安全呢?
代码演示 1
package com.mmall.concurrency.example.singleton;
/**
* Created by Charles Date:2018/3/19
*/
import com.mmall.concurrency.annoations.NotThreadSafe;
/**
* 懒汉模式
* 单例的实例在第一次使用的时候创建
* 但是这种写法在多线程情况下回出问题, 它可能会实例化出多个对象
*/
public class SingletonExample1 {
// 私有的构造函数
private SingletonExample1(){
}
// 单例对象
private static SingletonExample1 instance = null;
// 静态工厂方法
public static SingletonExample1 getInstance(){
if (instance == null) {
instance = new SingletonExample1();
}
return instance;
}
}
代码演示2
/**
* 饿汉模式
* 单例的实例在类装载的时候创建
* 这个饿汉模式是线程安全的
* 使用饿汉模式要注意两个问题
* 1.其私有构造函数在实现的时候没有太多的处理,否则可能会造成性能的问题
* 2.这个类在实际的过程中肯定会被使用, 不会造成资源的浪费.
*/
public class SingletonExample2 {
// 私有的构造函数
private SingletonExample2(){
}
// 单例对象
private static SingletonExample2 instance = new SingletonExample2();
// 静态工厂方法
public static SingletonExample2 getInstance(){
return instance;
}
}
代码演示 3
/**
* 懒汉模式
* 单例的实例在第一次使用的时候创建
*
*/
public class SingletonExample3 {
// 私有的构造函数
private SingletonExample3(){
}
// 单例对象
private static SingletonExample3 instance = null;
// 静态工厂方法
public static synchronized SingletonExample3 getInstance(){
if (instance == null) {
instance = new SingletonExample3();
}
return instance;
}
}
上面这个示例是 线程安全的懒汉模式 , 但是它的性能不好, 因为它通过 synchronize
来使得同一时间内, 只允许一个线程来访问的方式来保证安全 , 降低了程序性能 .
代码示例 4
/**
* 懒汉模式 -》 双重同步锁单例模式
* 单例实例在第一次使用时进行创建
* 线程不安全
*/
public class SingletonExample4 {
// 私有构造函数
private SingletonExample4() {
}
// 1、memory = allocate() 分配对象的内存空间
// 2、ctorInstance() 初始化对象
// 3、instance = memory 设置instance指向刚分配的内存
// JVM和cpu优化,发生了指令重排
// 1、memory = allocate() 分配对象的内存空间
// 3、instance = memory 设置instance指向刚分配的内存
// 2、ctorInstance() 初始化对象
// 单例对象
private static SingletonExample4 instance = null;
// 静态的工厂方法
public static SingletonExample4 getInstance() {
if (instance == null) { // 双重检测机制 // B
synchronized (SingletonExample4.class) { // 同步锁
if (instance == null) {
instance = new SingletonExample4(); // A - 3
}
}
}
return instance;
}
}
上面的这个代码不是线程安全的 , 当代码执行 下面这句的时候
instance = new SingletonExample4()
主要进行三部指令 ,如上面的 代码注释部分, 在完成注释中描述的三步之后 instance
就指向了实际分配的内存地址了 . 在单线程模式下, 执行是没有什么问题的 ,但是在多线程模式下, 这三步指令会发生指令重排 , 比如由于指令重排, 线程A 执行到了 第三步 instance = memory 设置instance指向刚分配的内存 , 线程 B在执行时判断到 instance
已经有值了, 就会直接返回 , 但是实际在线程A 哪里初始化对象这一步还没有完成, 线程 B 在拿到这个还没有初始化的 instance
之后 , 一旦去调用 , 就会出现问题了 .所以说这种写法是线程不安全的.
既然 双重检测机制有指令重排的问题, 那如何解决这个问题 , 看下面的代码示例
代码示例 5
/**
* 懒汉模式 --> 双重同步锁单例模式
* 单例的实例在第一次使用的时候创建
*/
@ThreadSafe
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) { // 双重检测机制
synchronized(SingletonExample5.class){ //同步锁
if (instance == null) {
instance = new SingletonExample5();
}
}
}
return instance;
}
}
既然双重检测会发生指令重排, 那么我们就禁止它发生指令重排 提到禁止指令重排 ,应该就想到了一个关键字 volatile
, 所以可以通过 volatile
+ 双重检测机制 来禁止指令重排做到 懒汉模式的线程安全
代码示例 6
/**
* 饿汉模式
* 单例的实例在类装载的时候创建
*
* 使用饿汉模式要注意两个问题
* 1.其私有构造函数在实现的时候没有太多的处理,否则可能会造成性能的问题
* 2.这个类在实际的过程中肯定会被使用, 不会造成资源的浪费.
*/
@ThreadSafe
public class SingletonExample6 {
// 私有的构造函数
private SingletonExample6(){
}
// 单例对象
private static SingletonExample6 instance = null;
static {
instance = new SingletonExample6();
}
// 静态工厂方法
public static SingletonExample6 getInstance(){
return instance;
}
public static void main(String[] args) {
System.out.println(getInstance().hashCode());
System.out.println(getInstance().hashCode());
}
}
使用 静态块 初始化 instance
代码示例 7
public class SingletonExample7 {
// 私有的构造函数
private SingletonExample7() {
}
// 静态工厂方法
public static SingletonExample7 getInstance() {
return Singleton.INSTANCE.getIntance();
}
private enum Singleton {
INSTANCE;
private SingletonExample7 singleton;
Singleton() {
singleton = new SingletonExample7();
}
public SingletonExample7 getIntance() {
return singleton;
}
}
}
枚举模式 实现 线程安全的 饿汉单例模式 , 是最推荐的一种方式 ,相较于懒汉模式, 其在安全性方面更容易保证, 相较于其他饿汉模式, 它是在实际调用的时候才开始初始化, 而在后续的使用中, 可以直接获取里面的值 , 不会造成资源的浪费 .