面试官:你简历上有熟悉设计模式,那你给我说一下单例模式实现及线程安全吧

云栖号资讯:【点击查看更多行业资讯】
在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来!


前言

单例应用的太广泛,大家应该都用过,本文主要是想聊聊线程安全D ` F b 7 x的单例以及反序列化破坏单例的情况。

1、概念

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

关键点:

  • 私有化构造函数
  • 通过一个静态方法或枚举返回单例类对象
  • 确保单例类的对象有且只有一个,尤其是多线程环境下
  • 确保单例类对象在反序列化时不会重新构建对象

2、实现

2.1、线程安全的单例

2.1.2、饿汉模式

饿汉模式:不管有没有调用getInstance方法,只要类加载了,我就给你new出来B Z P ) . J(a)

public clasA t ; Zs A {
private static final A a = new A();
public static A getInstance() {
return a;
}
private A() {}
}

以下两点保证了以上代码的线程安全:

  • 调用一个类的静态方法8 7 U W I m的时候会触发类的加载(如果类没加载过)
  • 类只会加载(被加载到虚拟机内存的过程,包括5个阶段)一次
  • static变量在类初始化的时候(类加载过程的最后一个阶段)会去赋值静态变量

2.1.2、懒汉模式

懒汉模式:延迟加载,用到再去new

public class B {
private static volatile B b3 ) q M =;
public static synchronized B getIne + i astance() {
if (b == null) {
b = new( / _ B();
}
return b;
}
private B() { }
}

要保证线程安全,最简单x C h 9 ,的方式是加同步锁。synchroized保证了多个线程串行的去调f ( j } } K x F {用getInstance(),既然是串行,那就不会存在什么线程安全问题了。但是这实现,每次读都要加锁,其实我们想要做的只是让他写(new1 j 7)的时候加锁。

2.1.3、Double Check Lock (DCL)

public8 6 q class B {
priva) ( 5 N & Lte static volatile{ d O x u B b;
public static synchronized B getInstance0() {
if (b == null) {
synchronized$ ] y (B.class) {
b = new B();
}
}
return b;
}
public static B getInstance(m % p I B C u) {
if (b == null) {
synchronized (B.class) {
if (b == null) {
b = new B();
}
}
}
return b;
}
private B() { }
}

为了解决懒汉模式的效率问题,我们改造成getInstance0():

但还有个问题 X、Y 两个线程同时进入if (b == null), X先进[ & e同步代码块,new了一个B,返回h ? c @ S { u : KX ; $ p B 8 } OY等到X释放锁之后,它也进了同步代码块,也会new一个B。

getInstance0()解决了效率问题,但它不是线程安全的。我们有进行了一次改造: getInstance():

getInstance在同步块里7 M h P面,又做了一次if (b == null)的判断,确保了Y线程不会再new B,保证了线程安全。

getInsta } o f Tance() 也正是所谓的双重检查锁定(double checked locking)。

这里还有一个关键点:private static volatile B b;Y ( 3 U xb是用volatile修饰的。

这个主要是因为new 并不是原子的。

B b = new B();

可以简单的分解成一下步骤:

  • 分配对象内存
  • 初始化对象
  • 设置引用指向分配的内存地址

2,3 直接可能发生指令重排序,就是说对象还未初始化完成,就让b指向了一块内存地址,这] Q a ( v , W时候b就不是null了。

2.1.4、静态内部类单例模式

puZ / V mblic class C {
private C() {}
public static C getInstance() {
return C. + ` v ` n qHolder.c;
}
private static class CHolder {
private static final C c = new C();
}
}

静态内E f & 4 % g $ !部类的线程安全9 , # |也是由jvm保证的,在v + P q调用Cholder.c的时候,去加载CHolder类,new 了一个y P w 2 ` n &c。

J k l O u J 0 (的来说,这个方n $ n式比DCL还是高H H ? 8 y点的,因为DCL加了volatile,效率上还是略微T c h S f [ 0有些些影响。

上面介绍的3种线程安全的单例,在有种极端的情况,单例模式有可能被破坏:反序列化

Java序列化就是指把Java对象转换为字节序列的过程
Java反序列化就是L I v指把字节序列恢复为Java对象的过程。

反序列化的时候,会重新构造一个对象% v 3 4,破坏单例模式。我们看下代码验证下:g W } p ] s ~ 4 q

public class C1 implemey p X k c r onts Serializable {
private C1() {3 _ :
SyN 1 h d ] M O [ jstem.out.println("构造方法");
}
public static C1 getInstancD m W p s 6 % Te() {
ret@ 5 z R Zurn CHolder.c;
}
p, v u _rivaten x O ! static class CHolder {
private static. 2 # p D final C1 c = new C1();
}O } } 2 3 4
// 注意这块被注释的代码
//    private Object readResolve(){
//        System.out.println("rea^ C (d resolve");
//        return CHolder.c;
//    }
public static void mE 3 ; N ) s ? l 5ain(String[] args) thr6 B h 0 5 Jows NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
C1 c = C1.getInI V 3stance();
System.out.println(c.toString());
try {
ObjectOutputStream o = n ( Pew ObjectOutputStream(
new FileOutputStream("d:/tmp/c.out"));
o.writeOe W + U Q d z 5 Wbject(c);B I q 9
o.close();
} catch(Exception e) {
e.printStackTrace();
}
C1 c1 = null, c2 = null;
try {
ObjectInputStream in =new ObjectInputStream(
new FileInputStream("d:/tmp/c. X Wout"));
c1 = (C1)in.readObu V { E r Gject();
in.close();
} catch(Exception e) {
e.t e ; h 1 t | Y 8printStackTrace();
}
try {
ObjectInputStream i! h 1 z dn =new ObjectInputStream(
new FileInputStream("d:/tmp/c.out"));
c2 = (C1)in.readObject();
in.close();
} catch(Exception e) {
e.printStackTrace();
}
System.out.println(` 9 W 4 G C * [ ("c1.equals(c2) : " + c1.equals(c2));
System.out.println("c1 == c2 : " + (c1 == c2));
System.out.println(c1);
Systx H O * e r R pem.out.println(c2);
}
}

结果:

构造F b ~ 0 H u R 3 K方法
me.hhy.designpattern.singletonpattern.C1@1540e19d
c1.equals(c2) : false
c1 == c2 : false
me.hhy.designpattern.singletonpattern.C1@135fbaa4
me.hhy.designpattern.singletonpattern.Cr E 7 L j R1@45ee12a7

放开注释的^ F B @代码

构造方法
me.hhy.designpattern.singletonpattern.C1@1540e19d
read resolve
read resolve
c1.equals(c2) : true
c1~  B r 0 S == c2 : tH 4 ; t O Wrue
meL | ..hhyQ s + g D.designpattern.singletonpattern.C1@1540e19d
me.hhy.designpatq V @ N 5tern.singletonpatterT g A = j z 0 x 8n.C1@1540e19d

正如我们看到的那样,加上rev q d ]adResolve就解决了反序列化单例被破坏的问题。

当然,如果没实现Serializaba J 6 : Yle接口,也就不会有这个被破坏的问题… 还是看场m M { p X U景。

关于readResolve的介绍,感兴趣的同学们可以看java.l - l D X ?io.ObjectInputStream#readUnshared方法上的注释(博主看了,看得不是很明白,一知半解,就不误人子弟了)

而我们下面要介绍的枚举单例,并不会有这个问题。

2.1.5、枚举单例

public e_ 6 ` v I D B anum  DEnum {
INSTANCE;
priK e C k B ( 7 Ivate^ d ) D d;
DEnum() {
d = new D();
}
public D getInstance(- H 4 F m p n [ q) {
return d;
}
}
public class D {}

线程安全的保证:

  • 枚举只能拥有私有的构造器
  • 枚举类实际上是一个继承Enum的一个final类
  • 上面的INSTANCE实际是被static final 修饰的

序列化不破坏单c T n K例的保证:

在序列化的时w ; + Z _ c候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的v? O lalueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

2.2 线程不安全的单例

2: 1 ^ l l f m ].2.1、懒汉模式

不过* x N = o Y多介绍了,这个其实在线程安全的单例部分,我们介绍的比较详细了。

public c! d b & 0 * [ $ plass B {
private static volatile B b;
public statW @ ( a E - ?ic B getInstance() {
if (b == null 1 ) _ K c Fl) {
b = new B(b K % | @ @);
}
return b;
}
private B() { }
}

3. 总结

单例的应用实在是太多了,也没必要再去找源码种的经典使用R ) | W e ] N A(因为基本上大家用过)。

枚举单例构造方法还是publicH % 5 0 U a,并不是防止外部直接去new它。个人认为如果一个类要开放给外部使用,用内部类的形式实现单例是最合适的。

【云栖号在线课堂】每天都有产品技K 1 ! $ j 0 C术专家分享!
课程地址:https://yqh.aliyun.com/live

立即加5 R r g 0 T入社群,与专家面对面,及时了解课程最新动态!
【云栖号在线课堂 社群】https://c.tb.cn/F3.Z8gvnK

原文发布时间:2020-08-04
本文作者:程序员伟杰
本文来自:“掘金”,了解相关信息可以关注“掘金”