应用场景
回归到之前我还是只有一家小工厂的电脑城老板的时候,顾客想买电脑的时候,我就new了个电脑工厂给他生产电脑,代码如下。在工厂模式的我们并不会在意,但是呢在单例模式的模式,我们就必须重视起来了!每次来一个顾客买电脑,我就创建了一个工厂来给他生产电脑,那我岂不是亏死了!
// here comes a customer, want to buy huawei computer
String customerHope = "huawei";
// i tell my factory to produce huawei computer
ComputerFactory computerFactory = new ComputerFactory();
Computer computer = computerFactory.produceComputer(customerHope);
//customer plays computer
computer.play();
所以我得保证我是用我这个唯一的工厂来生产电脑的。这就是单例模式的应用场景之一。总结一下,单例模式的应用场景:在需要保证实例是全局唯一的情况下,使用单例模式
,比如说分布式系统中的id生成器,就必须保证全局唯一(否则可能会出现重复id问题),再比如操作系统的CPU,在单核系统中,我们只能使用唯一的那块CPU进行数据运算。单例模式相对来说还是比较好理解的设计模式,单例模式更侧重的是其实现方式。
实现方式
单例模式的实现方式有很多种,可简单分为无种:饿汉式,懒汉式,双重锁,静态内部类,枚举。还是以电脑工厂举例,分别看下实现方式。
饿汉式
饿汉式相对简单,在类加载时就将ComputerFactory进行了实例化,所以一定是线程安全
的。缺点是提前加载的话,会占用一定的内存资源
public class ComputerFactory {
private static ComputerFactory computerFactory = new ComputerFactory();
private ComputerFactory() {
}
public static ComputerFactory getInstance() {
return computerFactory;
}
}
懒汉式
和饿汉式的区别在于,饿汉式是提前将ComputerFactory实例化,而懒汉式是等你需要的时候我再实例化。
public class ComputerFactoryV2 {
private ComputerFactoryV2() {
}
private static ComputerFactoryV2 computerFactoryV2 = null;
public static ComputerFactoryV2 getInstance() {
if (computerFactoryV2 == null) {
computerFactoryV2 = new ComputerFactoryV2();
}
return computerFactoryV2;
}
}
关于线程不安全,所有博客和有经验的程序员都会告诉使用上面的方式会出现线程不安全的情况,那么我们来看看为什么会线程不安全。假设A线程和B线程同时调用getInstance()方法,A线程判断computerFactoryV2null,进入if内部,准备new 新对象时,CPU将时间片交给B线程执行,此时computerFactoryV2仍是null,线程B也判断computerFactoryV2null,进入new ComputerFactoryV2()的操作,那么此时computerFactoryV2就不再是同一实例了。理论分析很抽象,我们用代码来实践一下。
先调整下代码,现在cpu执行速度太快,我们跟不上,我们模拟下延迟,来保证复现。在实例化computerFactoryV2之前,休眠2s。
public class ComputerFactoryV2 {
private ComputerFactoryV2() {
}
private static ComputerFactoryV2 computerFactoryV2 = null;
public static ComputerFactoryV2 getInstance() {
if (computerFactoryV2 == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
computerFactoryV2 = new ComputerFactoryV2();
}
return computerFactoryV2;
}
}
来看执行代码
public static void main(String[] args) {
Runnable runnable = () -> System.out.println(Thread.currentThread().getName() +
"---" + ComputerFactoryV2.getInstance());
new Thread(runnable, "thread1").start();
new Thread(runnable, "thread2").start();
}
执行结果:
thread1---com.yu.designpattern.singleinstance.ComputerFactoryV2@5f292f85
thread2---com.yu.designpattern.singleinstance.ComputerFactoryV2@38ff6483
就可以看到出现了两个不同的对象。那么我们来改进下吧.
双重锁
既然多线程有问题,那么就得用上我们的神器synchronized了。
public class ComputerFactoryV2 {
private ComputerFactoryV2() {
}
private static ComputerFactoryV2 computerFactoryV2 = null;
public static ComputerFactoryV2 getInstance() {
if (computerFactoryV2 == null) {
synchronized (ComputerFactoryV2.class) {
if (null == computerFactoryV2) {
computerFactoryV2 = new ComputerFactoryV2();
}
}
}
return computerFactoryV2;
}
}
当然你可以直接在方法上加synchronized关键字,但这样的话效率相对较低,因为假设实例已经初始化了,但是所有获取实例的线程都得排队获取,就非常影响效率了。所以我们只要锁住new 对象的代码即可。在synchronized中必须要再次判断computerFactoryV2是否为null,否则并不能解决在前面说的多线程的问题。
指令重排
为了避免jvm指令重排,导致多线程不安全问题,需要在ComputerFactoryV2前增加volatile,禁止指令重排。最后完整的代码如下。(指令重排导致线程不安全,实在无法模拟。有小伙伴可以模拟的话,请赐教分享下!)
public class ComputerFactoryV2 {
private ComputerFactoryV2() {
}
private static volatile ComputerFactoryV2 computerFactoryV2 = null;
public static ComputerFactoryV2 getInstance() {
if (computerFactoryV2 == null) {
synchronized (ComputerFactoryV2.class) {
if (null == computerFactoryV2) {
computerFactoryV2 = new ComputerFactoryV2();
}
}
}
return computerFactoryV2;
}
}
来解释下,指令重排导致线程不安全的理论依据,computerFactoryV2 = new ComputerFactoryV2()
是java高级语言,会通过编译器翻译成cpu可读的汇编语言执行,那么new 对象,翻译过来可能是三句语句,假设如下
1. 申请内存
2. 对象初始化
3. 引用指针指向对应内存
指令重排:编译器或CPU为提高执行效率,而将执行指令进行重新排序的过程
经过指令重排后,可能会变成如下顺序
1. 申请内存
2. 引用指针指向对应内存
3. 对象初始化
假设线程A执行到computerFactoryV2 = new ComputerFactoryV2();
并执行到指令重排后的第二步,此时线程B执行到第一个if判断语句,它发现指针不是null了,那么就直接返回当前引用了,但实际呢,computerFactoryV2 还未初始化完成,导致出错!
静态内部类
public class ComputerFactoryV3 {
private static class ComputerFactoryHolder{
private static final ComputerFactoryV3 COMPUTER_FACTORY = new ComputerFactoryV3();
}
private ComputerFactoryV3() {
}
public static ComputerFactoryV3 getInstance() {
return ComputerFactoryHolder.COMPUTER_FACTORY;
}
}
从代码可以看出静态内部类是饿汉式和懒汉式的结合,饿汉式的缺点是提前加载,占用资源。普通懒汉式的问题是线程不安全,巧妙使用静态内部类同时解决了两个的缺点。
反射
为什么会提到反射呢?是因为哪怕我们想法设法使用各种方式保证单例是全局唯一的,但是反射确实轻而易举就破坏了单例的全局唯一。我们来试试看,首先实施双重锁的
测试代码:
try {
Constructor constructor = ComputerFactoryV2.class.getDeclaredConstructor();
constructor.setAccessible(true);
ComputerFactoryV2 computerFactory1 = (ComputerFactoryV2) constructor.newInstance();
ComputerFactoryV2 computerFactory2 = (ComputerFactoryV2) constructor.newInstance();
System.out.println(computerFactory1.equals(computerFactory2));
} catch (Exception e) {
e.printStackTrace();
}
执行结果:
false
很遗憾,哪怕我们做了很多,仍然无法保证他们可以全局唯一。
我们再来试一下静态内部类的方式
Constructor constructor1 = ComputerFactoryV3.class.getDeclaredConstructor();
constructor1.setAccessible(true);
ComputerFactoryV3 computerFactory3 = (ComputerFactoryV3) constructor1.newInstance();
ComputerFactoryV3 computerFactory4 = (ComputerFactoryV3) constructor1.newInstance();
System.out.println(computerFactory3.equals(computerFactory4));
执行结果:
false
仍未为false,说明我们前面说了那么多种方式,都无法抵挡反射。那么怎么办呢?大佬们总归是有办法的,那就是使用枚举。
枚举
public enum ComputerFactoryV4 {
/**
* 电脑工厂
*/
COMPUTER_FACTORY;
public Computer produceComputer() {
return new HuaweiComputer();
}
}
执行方法
Computer computer = ComputerFactoryV4.COMPUTER_FACTORY.produceComputer();
computer.play();
执行结果:
i am playing huawei computer
我们也用反射来尝试一下
执行代码
Constructor constructor2 = ComputerFactoryV4.class.getDeclaredConstructor();
constructor2.setAccessible(true);
ComputerFactoryV4 computerFactoryV4 = (ComputerFactoryV4) constructor2.newInstance();
执行结果
java.lang.NoSuchMethodException: com.yu.designpattern.singleinstance.ComputerFactoryV4.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.yu.designpattern.singleinstance.Main.main(Main.java:41)
执行时会抛出NoSuchMethodException的异常,表明枚举方式是可以防止反射的!
总结
前面主要介绍了单例模式的应用场景和实现方式,但还是想强调学习设计模式,更重要的的学习其设计思想。单例模式的思想简而言之就是保证全局唯一的实例
。此外才是实现方式,由于Java的特点,导致其实现方式分为了许多种。总的来说,最安全最简洁的就是枚举方式。
方式 | 线程安全 | 防反射 |
---|---|---|
枚举 | 是 | 是 |
双重锁 | 是 | 否 |
静态内部类 | 是 | 否 |