JUC单例模式练习
版本一:线程不安全
先实现一个线程不安全的单例模式
package singleton;
public class SingleThreadedSingleton {
// 保存唯一的实例
private static SingleThreadedSingleton instance = null;
// 省略实例变量声明
// 无法在外部构造
private SingleThreadedSingleton() {}
public static SingleThreadedSingleton getInstance() {
// check-then-act
if (null == instance) {
System.out.println("生成了一次实例");
instance = new SingleThreadedSingleton();
}
return instance;
}
public void someService() {
System.out.println("Do Some Service");
}
}
编写一个测试类:
package singleton;
public class SingletonTest {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new Simulate()).start();
}
}
public static class Simulate implements Runnable{
@Override
public void run() {
System.out.println(SingleThreadedSingleton.getInstance());
}
}
}
输出:
生成了一次实例
生成了一次实例
singleton.SingleThreadedSingleton@7438001b
生成了一次实例
singleton.SingleThreadedSingleton@327ff0ac
生成了一次实例
singleton.SingleThreadedSingleton@3b979fa1
生成了一次实例
singleton.SingleThreadedSingleton@609221bf
singleton.SingleThreadedSingleton@68f93713
版本二:使用内部锁
只需把上面 check-then-act
的部分添加一个内部锁就可以了:
synchronized (SingleThreadedSingleton.class) {
if (null == instance) {
System.out.println("生成了一次实例");
instance = new SingleThreadedSingleton();
}
}
输出:
生成了一次实例
singleton.SingleThreadedSingleton@3b979fa1
singleton.SingleThreadedSingleton@3b979fa1
singleton.SingleThreadedSingleton@3b979fa1
singleton.SingleThreadedSingleton@3b979fa1
singleton.SingleThreadedSingleton@3b979fa1
可以看到确实只生成了一个实例
版本三:双重检查锁定
DCL(Double-checked Locking)
主要的问题是,上述情景中,实例其实就生成一次,但是每次获取实例都要检查锁、获取锁,而其实实例生成后就不用再担心线程安全的问题了,所以这会造成不必要的性能损耗。
可以把关键代码修改如下:(下面类名修改了,但是没有影响)
if (null == instance) {
synchronized (IncorrectDCLSingleton.class) {
if (null == instance) {
System.out.println("生成了一次实例");
instance = new IncorrectDCLSingleton();
}
}
}
return instance;
这样似乎可以奏效。
但是,根据《Java多线程编程指南》的描述,这样仍然是不安全的!
原因是:
instance = new IncorrectDCLSingleton();
这条指令可以分解为以下伪代码所示的几个独立子操作:
objRef = allocate(IncorrectDCLSingleton); //1
invokeConstructor(objRef); //2
instance = objRef //3
根据重排序规则,临界区内的操作可以在临界区内被重排序,所以假如上述代码被重排序为:1->3->2
的话,此时 objRef 还没有初始化,然后发生了线程切换,另一个线程就会通过 if 判断而返回一个还没有实例化的实例。
解决这个问题得保障有序性,既可以给 instance 实例添加一个 volatile
关键词。
版本四:静态内部类
这个方法可以实现延迟加载的效果,并且比较简单。
利用了类的静态变量只会被创建一次。(思考,为什么?)
package singleton;
public class StaticHolderSingleton {
// 省略实例变量声明
// 无法在外部构造
private StaticHolderSingleton() {
System.out.println("StaticHolderSingleton Inited.");
}
private static class InstanceHolder {
final static StaticHolderSingleton INSTANCE = new StaticHolderSingleton();
}
public static StaticHolderSingleton getInstance() {
System.out.println("getInstance invoked.");
return InstanceHolder.INSTANCE;
}
public void someService() {
System.out.println("Do Some Service.");
}
}
输出:
getInstance invoked.
getInstance invoked.
getInstance invoked.
getInstance invoked.
getInstance invoked.
StaticHolderSingleton Inited.
singleton.StaticHolderSingleton@3fa80c0b
singleton.StaticHolderSingleton@3fa80c0b
singleton.StaticHolderSingleton@3fa80c0b
singleton.StaticHolderSingleton@3fa80c0b
singleton.StaticHolderSingleton@3fa80c0b
版本五:枚举类
这个要建一个枚举类:
package singleton;
public class SingletonTest {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new Simulate()).start();
}
}
public static class Simulate implements Runnable {
@Override
public void run() {
Singleton.INSTANCE.someService();
}
}
public static enum Singleton {
INSTANCE;
Singleton() {
System.out.println("Singleton inited.");
}
public void someService() {
System.out.println("Do some Service");
}
}
}
总结
本文是学习《Java多线程编程实战指南》的练习。
其实上面的几种方式,还有另外的名字,可以参考这篇文章:单例模式 | 菜鸟教程 (runoob.com)
评论区