首页 > 学院 > 开发设计 > 正文

单例模式与多线程

2019-11-08 02:55:52
字体:
来源:转载
供稿:网友

单例模式都不陌生,有饿汉式单例、懒汉式单例等。

饿汉式单例

饿汉式单例:

public class Singleton { PRivate static Singleton instance = new Singleton(); public static Singleton getInstance(){ return instance; }}

在类第一次加载的时候,单例就完成了初始化。下面来验证饿汉式单例的线程安全性:

public class MyThread extends Thread{ public void run() { System.out.println(Singleton.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() 返回值都一样。


结论:饿汉式单例在类第一次加载的时候完成初始化,是线程安全的。

懒汉式单例

懒汉式单例:

public class Singleton { private static Singleton instance = null; public static Singleton getInstance(){ if(instance == null){ // 1 instance = new Singleton(); // 2 } // 3 return instance; }}

运用了延迟加载,在需要的时候进行初始化。 然而 1、2、3 整体不具有原子性,所以懒汉式单例应该不是线程安全的。


下面证明懒汉式单例是非线程安全的:

public class MyThread extends Thread{ public void run() { System.out.println(Singleton.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


结论:懒汉式单例运用延迟加载在需要时候进行初始化,保证了特定情况下其性能要优于饿汉式单例。然而它却是非线程安全的。

双重检查锁定机制

我们尝试修改代码,目的是把懒汉式单例修改成线程安全的。

第一次尝试 为 getInstance() 加锁:

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

运行得到:

2000544445 2000544445 2000544445

这样修改可以保证线程安全性;但由于锁的独占性,多个线程频繁调用 getIntance() 很可能会阻塞,效率低下。


第二次尝试 继续缩小同步块的范围:

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

运行得到:

hashcode() 返回值都一样。

这种方法基本等同于第一次尝试。


第三次尝试 继续缩小同步块的范围:

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

运行得到:

1175759956 2000544445 2146509683

相比前两次尝试,第三次尝试执行效率会有明显提升;但是破坏了原子性,不是线程安全的,得到的可能不是单例。


第四次尝试 双重检查锁定机制(double check lock)(DCL):

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

运行得到:

hashcode() 返回值都一样。

此方案综合了前面几次尝试的优点:

锁同步保证了原子性,保证线程安全性;第一次空检测对提升性能起到了很大的作用。

但是真的实现了线程安全吗?并没有。

volatile 修正 DCL

在 DCL 基础上,为变量 instance 加上 voltile 关键字,才算是真正的实现了线程安全:

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

运行得到:

hashcode()返回值都一样。


Q:回头来看,为什么必须加 volatile 关键字呢? A:重排序


结论:懒汉式单例,经过基于 volatile 的 DCL 进行改造,能够具有线程安全性。

volatile + synchronized 实现了线程安全性。DCL 的第一个空检测很大程度上优化了性能

final 修正 DCL

增加一个 final 域同样能解决问题:

public class Singleton { private static Singleton instance = null; private final int para; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } private Singleton(){ para = 1; }}

final 域的内存语义

写内存语义:在构造函数内对一个 final 域的写入,与随后将对象引用赋值给引用变量,这两个操作不能重排序; 实现原理:在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障; 写内存语义可以确保在对象的引用为任意线程可见之前,final 域已经被初始化过了。

读内存语义:初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作不能重排序; 实现原理:在读 final 域之前插入一个 LoadLoad 屏障。 读内存语义可以确保如果对象的引用不为 null,则说明 final 域已经被初始化过了。

总之,final 域的内存语义提供了初始化安全保证:只要 this 引用没有在构造函数中“逸出”,不需要同步就可以保证任意线程看到的都是初始化后的值。 注意:此时 Singleton 并非不可变,引入 final 域的目的是能够安全地初始化。

静态内部类实现单例模式

public class Singleton { private static class Holder{ private static Singleton instance = new Singleton(); } public static Singleton getInstance() { return Holder.instance; }}

调用 getInstance() 导致 Holder 类被装载,Holder 对应的的 Class 类型的对象自动创建,JVM 会获得这个 Class 对象初始化锁,这个锁可以同步多个线程对同一个类的初始化。因此指令重排还是可能发生的,但是并不影响获得初始化锁的下一个线程,因为下一个线程进来的时候,上个线程已经完成了类的初始化。

看图更形象一些:

这里写图片描述

可能得到的执行时序:

这里写图片描述


结论:静态内部类实现的单例模式能够保证线程安全,同时具有延迟加载特性。而且代码够简洁哦。


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表