单例模式
本文讲述单例设计模式的8种方式,反射和单例的相爱相杀🙃
# 单例模式
单例模式:类的对象有且只有一个
首先控制对象的产生数量:将构造方法私有化(从源头控制对象数量,控制构造方法)
构造方法私有化:
- 任何其他类均无法参生此对象(本质是任何他类均无法调用构造方法,所以无法产生对象)
- 唯一的一个对象产生于类内部
- 唯一的属性为<静态属性>,并且类中提供静态方法取得此对象。因为类的外部无法产生对象,因此无法调用对象方法
【扩充】
Java实例化对象的5种方式:
- new
- 反射
- 工厂方法返回对象
- 调用对象的clone()方法
- 通过序列化 / 反序列化
# 1. 饿汉式–静态常量
饿汉式单例,顾名思义,就是很饥渴🙃,一上来就new
产生实例化对象
/**
* 饿汉式三个核心组成
* 1.构造方法私有化
* 2.类内部提供静态私有域
* 3.类内部提供静态方法返回唯一对象
*/
class Singletons {
//唯一的对象在类加载时产生
private final static Singletons single = new Singletons();
//构造方法私有化
private Singletons() { }
//静态方法-----为什么是静态方法??
//因为在类的外部无法产生对象,因此无法调用对象方法
//通过getter方法取得唯一的对象
public static Singletons getInstance(){
return single;
}
}
public class HungrySingleton01 {
public static void main(String[] args) {
//不能直接new,而是通过 Singleton.getInstance()静态方法取得类中已经产生好的对象
Singletons single = Singletons.getInstance();
Singletons single1 = Singletons.getInstance();
// single == single1
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
因为是静态常量,single和single1一定是同一个对象,所在的内存地址是相同的
饿汉式单例 (静态常量)
【优点】:书写简单,类加载时就完成了实例化,避免了线程同步问题
【缺点】:在类加载就完成实力化,没有达到懒加载的效果。如果从始至终没有使用过这个实例对象,会造成内存浪费
【总结】
- 可用,但是可能会造成内存资源的浪费
# 2. 饿汉式–静态代码块
class Singleton02 {
private static Singleton02 single;
private Singleton02() { }
static {
single = new Singleton02();
}
public static Singleton02 getInstance(){
return single;
}
}
public class HungrySingleton02 {
public static void main(String[] args) {
Singleton02 single = Singleton02.getInstance();
Singleton02 single1 = Singleton02.getInstance();
// single == single1
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
饿汉式单例 (静态代码块)
这种方式的优缺点和上面第一种静态变量的没差别,区别就是初始化的位置不同,初始化的过程放到了静态代码块。
# 3. 懒汉式–线程不安全
当第一次去使用Singleton对象的时候才会为其产生实例化对象
通过一个静态公有方法,当使用到该方法时,才创建对象(懒汉式)
/**
* @Author: Mr.Q
* @Description:懒汉式单例---线程不安全
* 特点: 当第一次去使用Singleton对象的时候才会为其产生实例化对象的操作.
*/
class Singleton {
private static Singleton single;
//private 声明无参构造
private Singleton() { }
//静态公有方法,当使用到该方法时,才创建对象(懒汉式)
public static Singleton getInstance(){
if(single == null) {
single = new Singleton();
}
return single;
}
}
public class LazySingleton {
public static void main(String[] args) {
Singleton single = Singleton.getInstance();
Singleton single1 = Singleton.getInstance();
// single == single1
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
懒汉式单例 (线程不安全)
【优缺点】
这种写法是存在线程安全问题的。类比于上面两种饿汉式单例模式,它们在没有调用时虽然会造成内存资源的浪费,但是是安全的。因为在类加载时就完成了实例化,避免了线程同步问题。
但是这种懒汉式写法,起到了懒加载效果,但是只能在单线程下使用
【线程安全问题分析】
在多线程场景下,一个线程进入了getInstance
方法的if条件判断if(single == null)
,还没来得及继续向下执行,另一个新进入的线程也通过了这个判断语句,这是就会产生多个实例,就不是单例的了。
所以此方法在多线程场景下不可使用。
# 4. 懒汉式–同步方法
既然线程不安全,那我们给他加把锁在getInstance
方法上保证线程安全。
/**
* @Author: Mr.Q
* @Description:懒汉式单例---同步方法(效率太低)
*/
class Singleton04 {
private static Singleton04 single;
//private 声明无参构造
private Singleton04() { }
//静态公有方法,当使用到该方法时,才创建对象(懒汉式)
public synchronized static Singleton04 getInstance(){
if(single == null) {
single = new Singleton04();
}
return single;
}
public void print() {
System.out.println("懒汉式单例(线程安全),同步方法效率太低");
}
}
public class LazySingleton04 {
public static void main(String[] args) {
Singleton04 single = Singleton04.getInstance();
single.print();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
懒汉式单例(同步方法)
【优点】:解决了线程不安全的问题
【缺点】
- 效率太低。每个线程想要获取类的实例时,都要等在
getInstance
这个同步方法外,串型执行。但是由于是单例模式,只会产生一个实例化对象,第一个线程实例化完对象之后,后面的线程便不需要执行if的条件判断了,直接return
即可,但是在进入同步方法时每次都要等待,效率太低。
# 5. 懒汉式–同步代码块
先来说一种错误示范:
在if
条件中添加同步代码块
这段代码看起来很完美,很可惜,它是有问题。主要在于single= new Singleton()
这句,这并非是一个原子操作。
【分析】:
- 在多线程场景下,同时有多个线程进入到了
if
条件内,但是只有一个线程A获取到了锁资源,其余的线程都在阻塞等待 - 等到线程A执行完之后,由于此时其他线程已经进入
if
条件中,不会再去判断。所以下一个线程进入同步代码块,继续产生对象。此时,变破坏了单例模式。
此处由于不是原子操作,编译器可能会产生指令重排的问题,所以需要保证原子性,同时加上双重if
条件判断。懒汉式 (同步代码块)正确的写法应该是双重检查DCL
# 6. 双重检查DCL
volatile
关键字修饰,轻量级锁,可以使值修改后立即更新到主存
private volatile static SafeSingleton single = null;
【这里添加volatile
的原因是】
- 保证可见性
- 保证有序性,禁止指令重排
single = new SafeSingleton();
创建对象这条语句不是原子操作
new
关键字创建对象的过程分为三步:
分配内存空间;
堆内存上创建对象(执行构造方法);
将对象的引用指向堆内存;
由于不是原子操作,就可能产生指令重排的问题。
步骤2
和步骤3
可能会被编译器指令重排,1 -> 2 -> 3
的执行顺序变为了1 -> 3 -> 2
先把对象的引用指向堆空间,然后再在堆上创建对象。(理解为图书馆占座位,人还没到,但是位置上却被占用了)
判断非空,但是实际拿到的对象还未完成初始化去创建,就会出现空指针异常
所以要防止指令重排,保证有序性,及时通知其线程single
的实时状态,就必须加上volatile
关键字来防止指令重排,保证1 -> 2 -> 3
的执行顺序。
class SafeSingleton {
//使用volatile关键字保其可见性
private volatile static SafeSingleton single = null;
private SafeSingleton() { }
//同步代码块上锁
public static SafeSingleton getInstance() {
if(single == null) {
synchronized (SafeSingleton.class) {
//双重检查
if (single == null) {
single = new SafeSingleton();
}
}
}
return single;
}
public void print() {
System.out.println("双重检测锁的DCL单例");
}
}
public class ReflectDCL {
public static void main(String[] args) {
//静态方法取得类中已经产生好的对象
SafeSingleton single = SafeSingleton.getInstance();
single.print();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
【双重检查分析】
Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两次
if(single == null)
的检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断
if(single == null)
直接 return实例化对象,也避免的反复进行方法同步线程安全;延迟加载;效率较高
# 7. 静态内部类
我们首先对静态内部类做一个回顾👉还好面试官还没问,赶紧把【内部类】的知识点补上 (opens new window)
静态内部类也是作为一个外部类的静态成员而存在,创建一个类的静态内部类对象不需要依赖其外部类对象
外部类加载时,静态内部类不会被立即加载,而是在外部类中被使用时才会加载,这符合懒加载的策略。
当我们在外部类中调用静态内部类时,会被加载,并且只会被加载一次,在加载时是线程安全的,保障了线程的安全性。
class StaticInner {
private StaticInner() { }
//静态内部类
private static class Singleton {
private static final StaticInner INSTANCE = new StaticInner();
}
public static StaticInner getSingleton() {
return Singleton.INSTANCE;
}
public void print() {
System.out.println("静态内部类的线程安全的懒汉式单例");
}
}
public class StaticInnerSingle06 {
public static void main(String[] args) {
StaticInner single = StaticInner.getSingleton();
single.print();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
静态内部类
- 这种方式采用了类装载的机制来保证初始化实例时只有一个线程
- 静态内部类方式在外部类被加载时并不会立即变例化,而是在需要实例化时,调用getSingleton方法,才会装载 Singleton内部类,从而完成外部类的实例化。
- 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的
- 优点:避免了线程不安全,利用静态内部类的特点实现延迟加载,效率高
# 8. 枚举
这借助JDK1.5中添加的枚举来实现单例模式。这种写法是最优雅,最安全的。
不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
enum Singleton {
INSTANCE; //属性
public static Singleton getInstance() {
return Singleton.INSTANCE;
}
}
public class Enum07 {
public static void main(String[] args) {
Singleton single = Singleton.getInstance();
Singleton single1 = Singleton.getInstance();
System.out.println(single == single1);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 9. ThreadLocal
多于多线程资源贡献,同步机制采用时间换空间的做法,因为资源只有一份,让不同的线程排队去访问。
ThreadLocal
采取的是空间换时间,ThreadLocal
为每一个线程提供了一份变量副本,人手一份同时访问,不存在相互的竞争和干扰。
class Singleton {
private Singleton() { }
private static final ThreadLocal<Singleton> single = new ThreadLocal<Singleton> () {
@Override
protected Singleton initialValue() {
return new Singleton();
}
};
public static Singleton getInstance() {
return singleton.get();
}
}
public class _08ThreadLocalSingleton {
public static void main(String[] args) {
Singleton single = Singleton.getInstance();
Singleton singles = Singleton.getInstance();
System.out.println(single == singles);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 10. CAS锁
import java.util.concurrent.atomic.AtomicReference;
class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
private Singleton() { }
public static Singleton getInstance() {
while (true) {
Singleton singleton = INSTANCE.get();
if(singleton != null) {
return singleton;
}
singleton = new Singleton();
if(INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
public class SingletonDemo {
public static void main(String[] args) {
Singleton single1 = Singleton.getInstance();
Singleton single2 = Singleton.getInstance();
System.out.println(single1 == single2); //true
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 反射!为所欲为?
先来对反射的内容做个回顾👉反射,就是要为所欲为 (opens new window)
# DCL双重检查破坏
通过反射或者序列化会破坏单例,我们就以线程安全的DCL单例来说明。
还是tittle6的代码,我们通过反射来破坏
public static void main(String[] args) throws Exception {
SafeSingleton single = SafeSingleton.getInstance();
Constructor<SafeSingleton> dc = SafeSingleton.class.getDeclaredConstructor();
dc.setAccessible(true);
SafeSingleton singleCopy = dc.newInstance();
//false,单例被破坏
System.out.println(singleCopy == single);
}
2
3
4
5
6
7
8
结果输出:false
输出为false,说明单例模式创建了两个对象,被反射破坏了。那如何解决呢?
首先,反射是通过无参构造来创建class对象的,我们在SafeSingleton
的构造中再加一把锁来判断:
class SafeSingleton {
//使用volatile关键字保其可见性
private volatile static SafeSingleton single = null;
private SafeSingleton() {
synchronized (SafeSingleton.class) {
if (single != null) {
throw new RuntimeException("Don't destroy by reflection");
}
}
}
//同步代码块上锁
public static SafeSingleton getInstance() {
if(single == null) {
synchronized (SafeSingleton.class) {
//双重检查
if (single == null) {
single = new SafeSingleton();
}
}
}
return single;
}
}
public class ReflectDCL {
public static void main(String[] args) throws Exception {
SafeSingleton single = SafeSingleton.getInstance();
Constructor<SafeSingleton> dc = SafeSingleton.class.getDeclaredConstructor();
dc.setAccessible(true);
SafeSingleton singleCopy = dc.newInstance();
//false,单例被破坏
System.out.println(singleCopy == single);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
问题解决,此时反射无法创建对象。
# 问题又双出现
刚才单例的对象是通过私有构造方法创建的,即调用了getInstance()
方法。但是,我不用这样创建,我唯一一个对象也是通过反射来创建呢?
将
SafeSingleton single = SafeSingleton.getInstance();
换成
SafeSingleton single = dc.newInstance();
这时,单例模式又出幺蛾子了,又被反射爆破了!
# 问题解决
我们可以通过添加一个标志位flag
来判断,防止反射破坏
class SafeSingleton03 {
//使用volatile关键字保其可见性
private volatile static SafeSingleton03 single = null;
//添加标志位
private static boolean flag = false;
private SafeSingleton03() {
synchronized (SafeSingleton03.class) {
if (flag == false) {
flag = true;
}else {
throw new RuntimeException("Don't destroy by reflection");
}
}
}
//同步代码块上锁
public static SafeSingleton03 getInstance() {
if(single == null) {
synchronized (SafeSingleton03.class) {
//双重检查
if (single == null) {
single = new SafeSingleton03();
}
}
}
return single;
}
public static void main(String[] args) throws Exception {
Constructor<SafeSingleton03> dc = SafeSingleton03.class.getDeclaredConstructor();
dc.setAccessible(true);
SafeSingleton03 single = dc.newInstance();
SafeSingleton03 singleCopy = dc.newInstance();
System.out.println(single);
System.out.println(singleCopy);
System.out.println(single == singleCopy);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
再次执行,我们发现标志位法可以拦截两次反射的破坏。
# 问题又双叒出现
在反射中,我们不仅可以获取构造方法呀,还可以获取成员变量呀。那flag
通过反射获取并修改,不就有不行了?
class SafeSingleton03 {
//使用volatile关键字保其可见性
private volatile static SafeSingleton03 single = null;
//添加标志位
private static boolean flag = false;
private SafeSingleton03() {
synchronized (SafeSingleton03.class) {
if (flag == false) {
flag = true;
}else {
throw new RuntimeException("Don't destroy by reflection");
}
}
}
//同步代码块上锁
public static SafeSingleton03 getInstance() {
if(single == null) {
synchronized (SafeSingleton03.class) {
//双重检查
if (single == null) {
single = new SafeSingleton03();
}
}
}
return single;
}
public static void main(String[] args) throws Exception {
Constructor<SafeSingleton03> dc = SafeSingleton03.class.getDeclaredConstructor();
dc.setAccessible(true);
SafeSingleton03 single = dc.newInstance();
//再次通过反射修改属性值
Field flag = SafeSingleton03.class.getDeclaredField("flag");
flag.setAccessible(true);
flag.set(dc,false);
SafeSingleton03 singleCopy = dc.newInstance();
System.out.println(single);
System.out.println(singleCopy);
System.out.println(single == singleCopy);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
通过代码验证,我们发现确实又双出现问题了!
那这,又该怎么搞?
问题,就出在了newInstance
方法上,通过反射来创建对象。
我们点开源码看看👀
咦,枚举自带单例模式,反射还破坏不了。是这样吗?我们继续验证
# 问题最终解决
测试反射能否破坏枚举式单例
enum EnumSingleton {
INSTANCE;
}
public class EnumTest {
public static void main(String[] args) throws Exception {
EnumSingleton single = EnumSingleton.INSTANCE;
Constructor<EnumSingleton> dc = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
dc.setAccessible(true);
EnumSingleton singleCopy = dc.newInstance();
System.out.println(single);
System.out.println(singleCopy);
System.out.println(single == singleCopy);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
至于为什么反射获取的构造方法传入String、int参数,需要通过Jad (opens new window)反编译来查看。不能传入空参构造,否则出现的是
NoSuchMethodException
出现源码中抛出的异常IllegalArgumentException
程序最终抛出:java.lang.IllegalArgumentException: Cannot reflectively create enum objects异常
# 总结
在JDK中,java.lang.Runtime
就是经典的单例模式
掌握这样一些单例模式的奇淫技巧,在历经反射的重重爆破之后,相信你会对单例模式有新的了解!