单例模式
什么是单例模式
定义:
单例(Singleton)模式:是一种创建型设计模式,一个类只有一个实例,同时提供对该实例的全局访问点。
模式优点:
- 确保一个类只有一个实例,单例对象仅在第一次被请求时才被初始化
为什么有人想要控制一个类只有一个实例?
最常见的原因就是控制对某些共享资源的访问,达到节约资源的目的,频繁的去创建与销毁,会消耗我们一部分系统资源来处理这个过程。比如:I/O(配置信息类)与数据库连接、Spring中的单例模式Bean的生成和使用,代码中需要设置一些全局的属性并保存等
- 为该实例提供全局访问点。
就像全局变量一样,单例模式允许您从程序中的任何位置访问某个对象。
缺点:
1)违反了单一职责原则;功能都写在一个类中,基于一个对象。
2)在多线程环境中需要进行特殊处理,以便多个线程不会多次创建单例对象;
3)在并发测试中,单例模式不利于代码调试,也不能模拟生成一个新的对象。
创建方式:
java单例创建的方式有8种:
单例设计模式分类两种:
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
序号 | 方式 |
---|---|
1 | 饿汉式(静态常量,线程安全) |
2 | 饿汉式(静态代码块,线程安全) |
3 | 懒汉式(线程不安全) |
4 | 懒汉式(同步方法,线程安全) |
5 | 懒汉式(同步代码块,线程不安全) |
6 | 双重检查(线程安全) |
7 | 静态内部类(线程安全) |
8 | 枚举(线程安全) |
饿汉式(静态常量,线程安全)
类的实例在类加载时创建,所以是线程安全的。
写单例模式的步骤: 以下是饿汉式的写法:
(1)必须在该类中,自己先创建出一个对象,私有化;
(2)私有化自身的构造器,防止外界通过构造器创建新的对象。
(3)向外暴露一个公共静态方法用于获取自身的对象;
1 | /** |
1 | public class SingletonEagerTest { |
- 优点:
写法简单,且是线程安全的,因为在类装载的时候就完成了实例化。如果单例类不使用大量资源可以使用这种方法。
- 缺点
即使客户端应用程序可能不使用它也会创建实例,没有懒加载,一般除非客户端调用该getInstance
方法,否则我们应该避免实例化;
这种方式不提供任何的异常处理。
饿汉式(静态代码块,线程安全)
该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的方式1基本上一样,当然该方式也存在内存浪费问题。
1 | ublic class Singleton { |
懒汉式(线程不安全)
1 | public class SingletonLazyTest { |
- 优点
实现了懒加载,即调用getInstance
方法时才创建对象,但只适合在单线程环境下运行。
- 缺点
如果多个线程同时在 if 条件内,则可能会破坏单例模式,两个线程将获得不同实例。
1 | public class SingletonLazyTest { |
我用四个线程去执行getInstance
方法,多次运行时,就可能存在不同的实例对象。结果如下:
扩展:多线程需要处理的问题
这里简单的说下原因:
随着技术的发展,CPU的运算速度也突飞猛进,与内存(主存)、磁盘的访问速度逐减拉开了距离。除了提升频率,后来甚至引入了L1、L2、L3三级缓存。
电脑访问磁盘的一个过程:
在存储层面,速度最快的是 CPU 中的寄存器(小于1kb),CPU比内存速度快很多,由 CPU 直接访问内存效率较低,为了提高内存访问速度,在 CPU 和内存间增加了高速缓存(高速小容量存储器)。
寄存器:种类挺多,CPU访问寄存器几乎没有延迟,但是容量很小。早期的计算机用的是8位寄存器,到后来16位、32位、64位,简单的理解就是单位时间内能处理二进制数据的长度。
在执行程序时为了提高性能,编译器和CPU常常会对指令做重排序。
指令队列在CPU执行时不是串行的, 当某条指令执行时消耗较多时间时, CPU资源足够时并不会在此无意义的等待,而是开启下一个指令,开启下一条指令是有条件的, 即上一条指令和下一条指令不存在相关性。
同样JVM 允许在不影响代码最终结果的情况下,可以乱序执行。但是在单线程环境的执行结果是不能被改变的,所以我们无需担心重排顺序会干扰到他们。
在《深入理解Java虚拟机》中,描述到:
JVM(Java Virtual Machine,Java虚拟机):是一种抽象的计算机,基于堆栈架构,它有自己的指令集和内存管理,它加载 class 文件,分析、解释并执行字节码。JVM 主要分为三个子系统:类加载器、运行时数据区和执行引擎。java虚拟机规范定义了java内存结构和内存模型,用于屏蔽各种硬件和操作系统的内存访问差异,以实现跨平台的效果。
- 类加载器
主要功能是处理类的动态加载,还有链接,并且在第一次引用类时进行初始化。
- 运行时数据区
它约定了在运行时程序代码的数据比如变量、参数等等的存储位置。主要包括:堆、java栈、方法区、pc寄存器和本地方法栈。
1)堆:线程共享,存放类实例对象和数组。堆所占内存的大小由-Xmx指令和-Xms指令来调节。
2)栈:线程私有,存储局部变量表、操作栈、动态链接、方法出口,对象指针,JVM只会对java栈执行两种操作压栈或出栈。
3)方法区:线程共享,存储被装载类型的信息;
JDK1.6及之前运行时常量池逻辑包含字符串常量池存放在方法区;
JDK1.7 字符串常量池被从方法区拿到了堆中,运行时常量池剩下的东西还在方法区;
JDK1.8 移除了永久代用元空间取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(堆外内存)。
4)pc寄存器:保存有当前正在执行的JVM指令的地址;
5)本地方法栈:服务于 Native 方法;
- 执行引擎
运行时数据区存储着要执行的字节码,执行引擎将会读取并逐个执行。包括:解释器、JIT编译器、GC垃圾收集器。
在java中每个线程创建时JVM都会为其创建一个工作内存(即栈空间),工作内存是每个线程的私有数据区域,而共享变量则都存储在主内存中,主内存是共享内存区域,所有线程都可以访问。
如果存在两个线程同时对一个主内存中的实例对象的变量进行操作就有可能诱发线程安全问题。为了解决这个问题,JVM定义了一组规则,
通过这组规则来决定一个线程对共享变量的写入何时对另一个线程可见,这组规则就是JMM(Java Memory Model Java,java内存模型),它就是用来描述多线程如何正确的通过内存进行交互和使用共享数据。
在JMM中,有两条规定:
1)线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;
2)不同线程之间无法访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
JMM整体是围绕着程序执行的原子性、有序性、可见性展开的。
- 原子性
原子性是指是一个操作是不可中断的,即多线程环境下两个线程间的操作互不干扰。JMM规定了内存交互操作有8种。其中read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)六个指令直接提供原子操作。我们可以认为java基本数据类型的变量(除了long和double)的读写操作都是原子的。对于lock(锁定)、unlock(解锁),虚拟机没有将操作直接开放给用户使用,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者Lock锁接口的实现类来保证程序执行的原子性。volatile不具备原子性。
例如:线程对主内存的读操作:
read可能远早于use, 在它们中间可能会发生的其他线程的读写。所以在多线程情况下,可能发生线程安全问题。
- 可见性
可见性就是指当一个线程修改了共享变量值时,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量这种依赖主内存作为传递媒介的方式来实现可见性。其中被volatile修饰的变量,可以在修改后强制刷新到主存,并在使用时从主存获取刷新,普通变量则不行。除了volatile修饰的变量,Java还有两个关键字能实现可见性,他们是synchronized和final。 - 有序性
在执行程序时为了提供性能,做了指令重排。java提供了volatile和synchronized两个关键字保证线程之间的有序性。
当然频繁的加锁和使用volatile来解决多线程安全问题,势必会影响性能,所以,其实JMM中还为我们提供了happens-before原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它可能判断线程是否安全。
懒汉式(同步方法,线程安全)
1 | /** |
为什么代码块同步,这样写达不到效果呢。同步代码块(锁类、锁对象)主要通过monitorenter(对monitor+1)和monitorexit(对monitor-1)指令来实现,每个对象都有一个自己的monitor(监视器锁),通过指令来进入监视器获取锁和释放锁。而instance = new SingletonLazySafe();创建对象的过程(分配内存空间、初始化对象、分配内存指针,也会存在指令重排)并不是原子的,当另外一个线程在还没拿到instance的指针的时候就进入了if(instance == null),那同样会出现单例类存在多个对象。
双重检查(线程安全)
具体分析查看JUC 内存中 volatile关键字的详解。
1 | /** |
双重检查是线程安全的,synchronized的两种方式都可以实现线程同步问题。
其实在jdk1.6之前,synchronized锁会调用底层的操作系统实现(互斥原语mutex),这会频繁的切换CPU的状态,效率比较低,因此也叫重量级锁。所以oracle官方在jdk1.6之后对synchronized锁进行了升级。升级之后主要存在四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
静态内部类(线程安全)
1 | /** |
- 优点
在jdk1.5之前java内存模型并不是那么完美。为了解决线程安全问题,Bill Pugh 提出了一种不同的方法来使用内部静态辅助类创建Singleton 类。即达到了线程安全的目的,又实现了懒加载。所以以前这种写法也比较多。
枚举(线程安全)
1 | /** |
- 优点
jdk1.5中新增了枚举,枚举值是全局访问的,是线程安全的;
- 缺点
枚举类型有些不灵活,也没有实现懒加载。
存在的问题
问题演示
破坏单例模式:
使上面定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射。
序列化反序列化
Singleton类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}Test类:
1
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
30public class Test {
public static void main(String[] args) throws Exception {
//往文件中写对象
//writeObject2File();
//从文件中读取对象
Singleton s1 = readObjectFromFile();
Singleton s2 = readObjectFromFile();
//判断两个反序列化后的对象是否是同一个对象
System.out.println(s1 == s2);
}
private static Singleton readObjectFromFile() throws Exception {
//创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Think\\Desktop\\a.txt"));
//第一个读取Singleton对象
Singleton instance = (Singleton) ois.readObject();
return instance;
}
public static void writeObject2File() throws Exception {
//获取Singleton类的对象
Singleton instance = Singleton.getInstance();
//创建对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Think\\Desktop\\a.txt"));
//将instance对象写出到文件中
oos.writeObject(instance);
}
}上面代码运行结果是
false
,表明序列化和反序列化已经破坏了单例设计模式。反射
Singleton类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}Test类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class Test {
public static void main(String[] args) throws Exception {
//获取Singleton类的字节码对象
Class clazz = Singleton.class;
//获取Singleton类的私有无参构造方法对象
Constructor constructor = clazz.getDeclaredConstructor();
//取消访问检查
constructor.setAccessible(true);
//创建Singleton类的对象s1
Singleton s1 = (Singleton) constructor.newInstance();
//创建Singleton类的对象s2
Singleton s2 = (Singleton) constructor.newInstance();
//判断通过反射创建的两个Singleton对象是否是同一个对象
System.out.println(s1 == s2);
}
}上面代码运行结果是
false
,表明序列化和反序列化已经破坏了单例设计模式
注意:枚举方式不会出现这两个问题。
问题的解决
序列化、反序列方式破坏单例模式的解决方法
在Singleton类中添加
readResolve()
方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。Singleton类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}源码解析:
ObjectInputStream类
1
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
38public final Object readObject() throws IOException, ClassNotFoundException{
...
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);//重点查看readObject0方法
.....
}
private Object readObject0(boolean unshared) throws IOException {
...
try {
switch (tc) {
...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法
...
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
private Object readOrdinaryObject(boolean unshared) throws IOException {
...
//isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
obj = desc.isInstantiable() ? desc.newInstance() : null;
...
// 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
// 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
// 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
Object rep = desc.invokeReadResolve(obj);
...
}
return obj;
}反射方式破解单例的解决方法
1
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
30public class Singleton {
//私有构造方法
private Singleton() {
/*
反射破解单例模式需要添加的代码
*/
if(instance != null) {
throw new RuntimeException();
}
}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}说明:
这种方式比较好理解。当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此中操作。
JDK源码解析-Runtime类
Runtime类就是使用的单例设计模式。
通过源代码查看使用的是哪儿种单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
...
}从上面源代码中可以看出Runtime类使用的是恶汉式(静态属性)方式来实现单例模式的。
使用Runtime类中的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class RuntimeDemo {
public static void main(String[] args) throws IOException {
//获取Runtime类对象
Runtime runtime = Runtime.getRuntime();
//返回 Java 虚拟机中的内存总量。
System.out.println(runtime.totalMemory());
//返回 Java 虚拟机试图使用的最大内存量。
System.out.println(runtime.maxMemory());
//创建一个新的进程执行指定的字符串命令,返回进程对象
Process process = runtime.exec("ipconfig");
//获取命令执行后的结果,通过输入流获取
InputStream inputStream = process.getInputStream();
byte[] arr = new byte[1024 * 1024* 100];
int b = inputStream.read(arr);
System.out.println(new String(arr,0,b,"gbk"));
}
}
总结及应用场景
上面总结了很多单例的实现方式。为什么要延迟实例化单例对象:
1)在静态初始化时,没有足够的信息对单例对象进行初始化。例如:工厂单例就必须等待真正的工厂机器,才能建立通信通道;
2)选择延迟初始化单例对象与获取资源有关,例如:数据库连接,尤其是在一个特定的会话中,它包含的应用程序并不需要该单例对象;
单例模式的应用场景:
1)需要频繁实例化或被共享的场合。比如:日志记录、缓存和线程池;
2)控制硬件级别的操作。比如:驱动程序对象
3)单例模式也可以用于其他设计模式:比如抽象工厂模式、建造者模式、原型模式即门面模式都可以作为单例实现。