Java - 单例模式与多线程
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java - 单例模式与多线程相关的知识,希望对你有一定的参考价值。
单例模式大家并不陌生,分为饿汉式和懒汉式等。
线程安全的饿汉式单例
饿汉式单例在类第一次加载的时候就完成了初始化,上代码:
public class MyObject { private static MyObject myObject = new MyObject(); public static MyObject getInstance(){ return myObject; } }
下面来验证饿汉式单例的线程安全性:
public class MyThread extends Thread{ public void run() { System.out.println(MyObject.getInstance().hashCode()); } } public class Test { public static void main(String[] args) throws Exception { Thread t1 = new MyThread(); Thread t2 = new MyThread(); Thread t3 = new MyThread(); t1.start(); t2.start(); t3.start(); } }
输出:
763347431 763347431 763347431
三次输出 hashCode 是同一个值,说明饿汉式单例天生就是线程安全的。
结论:饿汉式单例在类第一次加载的时候完成初始化,而且是线程安全的。
非线程安全的懒汉式单例
懒汉式单例为延迟加载,调用 getInstance() 时才被创建,上代码:
public class MyObject { private static MyObject myObject = null;
public static MyObject getInstance(){ if(myObject == null){ myObject = new MyObject(); } return myObject; } }
这种情况下肯定是非线程安全的。因为判断对象为空和创建对象是一个原子性操作,多线程访问产生竞态条件。(竞态条件:多线程并发中,正确的结果取决于多个线程的执行顺序)
下面做一下验证:
public class MyThread extends Thread{ public void run() { System.out.println(MyObject.getInstance().hashCode()); } } public class Test { public static void main(String[] args) throws Exception { Thread t1 = new MyThread(); Thread t2 = new MyThread(); Thread t3 = new MyThread(); t1.start(); t2.start(); t3.start(); } }
输出:
2146509683 810456228 1996534722
hashcode 输出不同,很明显懒汉式单例不具有线程安全性。
结论:懒汉式单例在需要该实例的时候才会进行初始化(仅初始化一次),但却是非线程安全的。在单例类数量少的情况下,这样一个延迟加载和饿汉式单例相比在性能上又没有明显的差距,所以我一般不会选择这种初级的懒汉式单例。
基于 volatile 的 DCL 方案为懒汉式单例提供线程安全性
在懒汉式单例的基础上,我们做一些优化使其具有线程安全的特性。
第一次尝试:
为 getInstance() 加上 synchronized 关键字
public class MyObject { private static MyObject myObject = null;
synchronized public static MyObject getInstance(){ if(myObject == null){ myObject = new MyObject(); } return myObject; } }
输出
2000544445 2000544445 2000544445
得到的是单例,不过如果多个线程频繁调用 getInstance() 的话将会导致程序执行效率非常低下:下个线程想要取得对象锁,必须等到上一个线程释放锁之后才可以继续执行。
第二次尝试:
同步代码块
public class MyObject { private static MyObject myObject = null;
public static MyObject getInstance() { synchronized (MyObject.class) { if (myObject == null) { myObject = new MyObject(); } } return myObject; } }
输出
512965639 512965639 512965639
然而这种方法等同于上一种尝试,唯一区别就是多线程下,上一次尝试中线程被堵在门口一米处进不来,而这一次尝试中线程被堵在门口半米处进不来。
第三次尝试:
继续缩小同步的代码范围
public class MyObject { private static MyObject myObject = null;
public static MyObject getInstance() { if (myObject == null) {
synchronized (MyObject.class) { myObject = new MyObject(); } } return myObject; } }
输出
1175759956 2000544445 2146509683
相比前面几次尝试,执行效率会有显著提升,但是无法得到单例。因为破坏了原子性。
第四次尝试:
下面的代码使用了 DCL(双检查锁机制 double check lock )
public class MyObject { private static MyObject myObject = null;
public static MyObject getInstance() { if (myObject == null) {
synchronized (MyObject.class) { if (myObject == null) { myObject = new MyObject(); } } } return myObject; } }
输出:
1320194849 1320194849 1320194849
然而此时就是线程安全了吗?
No~ 不过我给不出反例推到,大家有经验的请指教一下~
第五次尝试:
我们还需要为“单例”加上 volatile 关键字。
public class MyObject { volatile private static MyObject myObject = null; public static MyObject getInstance() { if (myObject == null) { synchronized (MyObject.class) { if (myObject == null) { myObject = new MyObject(); } } } return myObject; } }
此时才算是线程安全。
回头来看,“单例” 为何必须用 volatile 修饰呢 ?
因为关键字 volatile 能够禁止指令重排序。
Example - “单例” 在没有 volatile 修饰的情况下,线程A 和线程B 都是第一次调用该单例方法,线程A 先执行构造方法;然而该构造方法是一个非原子操作,编译后生成多条指令,由于指令重排序,可能会先执行赋值操作(实际是在内存中开辟一片存储对象的区域后直接返回内存的引用),之后 myObject 便不为空了,但是实际的初始化操作却还没有执行,如果就在此时线程B 进入,就会看到一个不为空的但是不完整(没有完成初始化)的对象。而 volatile 关键字能够禁止指令重排序优化,从而安全的实现单例。
结论:基于 volatile的 DCL方案能够保留延迟加载的特性,同时赋予了该单例线程安全性。
静态内部类实现单例模式
public class MyObject { private static class Holder{ private static MyObject myObject = new MyObject(); } public static MyObject getInstance() { return Holder.myObject; } }
怎样保证的线程安全? 会不会因为多个线程同时调用 getInstance() 出现指令重排导致初始化不完全呢?
JVM 在加载 class 之后就会进行类的初始化,在类的初始化期间,JVM 会去获取一个初始化锁,这个锁可以同步多个线程对同一个类的初始化,因此指令重排还是可能发生的,但是并不影响获得初始化锁的下一个线程,因为下一个线程进来的时候,上个线程已经完成了类的初始化。
从书上搞了个图过来:
结论:静态内部类实现的单例模式能够保证线程安全,同时具有延迟加载特性。而且代码够简洁哦。
以上是关于Java - 单例模式与多线程的主要内容,如果未能解决你的问题,请参考以下文章