设计模式—单例模式
一、概述
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例, 并且该类只提供一个取得其对象实例的方法(静态方法)。
比如 Hibernate 的SessionFactory,它充当数据存储源的代理,并负责创建 Session 对象。SessionFactory 并不是轻量级的,一般情况下,一个项目通常只需要一个 SessionFactory 就够,这是就会使用到单例模式。
单例模式有八种方式:
- 懒汉式(线程不安全)【不要使用,线程不安全】
- 懒汉式(线程安全,同步方法)【不推荐使用,效率低下】
- 懒汉式(线程不安全,同步代码块)【不要使用,线程不安全】
- 饿汉式(静态常量) 【可以使用,可能造成内存浪费】
- 饿汉式(静态代码块)【可以使用,可能造成内存浪费】
- 双重校验锁(线程安全)【推荐使用】
- 静态内部类(线程安全)【推荐使用】
- 枚举(线程安全,且不能被反射破坏)【推荐使用】
二、懒汉式(线程不安全)【不要使用,线程不安全】
2.1 优缺点说明
-
起到了懒加载 的效果,但是只能在单线程下使用。
-
如果在多线程下,一个线程进入了 if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式
结论:在实际开发中,不要使用这种方式。
2.2 代码案例
public class Singleton1 {
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
System.out.println(Singleton4.getInstance());
}).start();
}
}
}
class Singleton {
//创建静态变量
private static Singleton singleton ;
//私有化构造方法
private Singleton() {
}
public static Singleton getInstance() {
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
执行结果
//产生了两个实例对象
test.designpattern.singleton.Singleton@55080cc6
test.designpattern.singleton.Singleton@377839d0
三、懒汉式(线程安全,同步方法)【不推荐使用,效率低下】
3.1 优缺点说明:
-
解决了线程安全问题
-
效率太低了,每个线程在想获得类的实例时候,执行 getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。方法进行同步效率太低
结论:在实际开发中,不推荐使用这种方式
3.2 代码案例
//只需要在这个方法上添加synchronized关键字即可,其它地方不用修改
public static synchronized Singleton getInstance() {
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
四、懒汉式(线程不安全,同步代码块)【不要使用,线程不安全】
4.1 优缺点说明
-
这种方式,本意是想对**第三种【懒汉式(线程安全,同步方法)】**实现方式的改进,因为前面同步方法效率太低,改为同步产生实例化的的代码块
-
但是这种同步并不能起到线程同步的作用。跟**第二种【懒汉式(线程不安全)】实现方式遇到的情形一 致,假如一个线程进入了if(singleton ==null)**判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例
结论:在实际开发中,不能使用这种方式
4.2 代码案例
/**
使用同步代码块把singleton = new Singleton();包起来
synchronized(Singleton.class){
singleton = new Singleton();
}
*/
public static Singleton getInstance() {
if(singleton == null){
synchronized(Singleton.class){
singleton = new Singleton();
}
}
return singleton;
}
执行结果
test.designpattern.singleton.Singleton@31bfbbf6
test.designpattern.singleton.Singleton@206cd157
test.designpattern.singleton.Singleton@7330523d
五、饿汉式(静态常量)【可以使用,可能造成内存浪费】
5.1 优缺点说明:
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到懒加载的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
这种方式基于类加载机制避免了多线程的同步问题(属于JVM内容了,可以参考《JVM—类加载机制》),不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getInstance 方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到懒加载的效果
结论:这种单例模式可用,可能造成内存浪费
5.2 代码案例
class SingletonTest{
//定义静态常量,是放在方法区的,所有SingletonTest的实例对象共享此常量
private final static SingletonTest SINGLETON = new SingletonTest();
//私有化构造方法
private SingletonTest(){}
public static SingletonTest getInstance(){
return SINGLETON;
}
}
六、饿汉式(静态代码块)【可以使用,可能造成内存浪费】
6.1 优缺点说明
- 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。
- 缺点:在类装载的时候就完成实例化,没有达到懒加载的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
- 这种方式基于类加载机制避免了多线程的同步问题(属于JVM内容了,可以参考《JVM—类加载机制》),不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getInstance 方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到懒加载的效果
结论:这种单例模式可用,但是可能造成内存浪费
6.2 代码案例
class SingletonTest{
//定义静态常量
private static SingletonTest singleton;
//静态代码快
static {
singleton = new SingletonTest();
}
//私有化构造方法
private SingletonTest(){}
public static SingletonTest getInstance(){
return singleton;
}
}
七、双重检查双重校验锁(线程安全)【推荐使用】
7.1 优缺点说明:
-
DCL概念是多线程开发中常使用到的,如代码中所示,我们进行了两次 if (singleton == null)检查,这样就可以保证线程安全了。
-
这样,实例化代码只用执行一次,后面再次访问时,判断 if (singleton == null),直接 return 实例化对象,也避免的反复进行方法同步.
-
线程安全;延迟加载;效率较高
结论:在实际开发中,推荐使用这种单例设计模式
特别强调一点:此静态变量必须使用volatile关键字修饰(private volatile static SingletonDLC singleton;)
7.2 正确代码案例
class SingletonDCL{
//定义静态变量
private volatile static SingletonDCL singleton;
//私有化构造方法
private SingletonDCL(){}
public static SingletonDCL getInstance() {
//第一次校验
if (singleton == null) {
synchronized (SingletonDCL.class) {
//第二次校验
if (singleton == null) {
singleton = new SingletonDCL();
}
}
}
return singleton;
}
}
7.3 演示没有volatile关键字的情况
class SingletonDLC{
//定义静态变量
private static SingletonDLC singleton;
//私有化构造方法
private SingletonDCL(){}
public static SingletonDCL getInstance() {
//第一次校验
if (singleton == null) {
synchronized (SingletonDCL.class) {
//第二次校验
if (singleton == null) {
singleton = new SingletonDCL();
}
}
}
return singleton;
}
}
运行结果
//出现了多个实例对象
test.designpattern.singleton.Singleton@7e129604
test.designpattern.singleton.Singleton@377839d0
test.designpattern.singleton.Singleton@377839d0
test.designpattern.singleton.Singleton@55477623
test.designpattern.singleton.Singleton@7330523d
test.designpattern.singleton.Singleton@55477623
test.designpattern.singleton.Singleton@377839d0
test.designpattern.singleton.Singleton@377839d0
test.designpattern.singleton.Singleton@377839d0
test.designpattern.singleton.Singleton@377839d0
test.designpattern.singleton.Singleton@7e129604
test.designpattern.singleton.Singleton@377839d0
test.designpattern.singleton.Singleton@7e129604
test.designpattern.singleton.Singleton@2bb64628
test.designpattern.singleton.Singleton@90472a2
分析原因
7.3.1 产生多个实例对象
volatile关键的作用就是用来禁止指令重排序和保证线程间的共享数据可见性
- volatile的具体详解请参考网上博客
通过上面的代码演示,可以看出来不使用volatile关键字的话,会产生多个实例对象,正是因为多线程之间没有保证共享数据的可见性。
7.3.2 造成空指针异常
这种情况我是没有测试出来,有兴趣的可以自行测试,看运气吧。
从逻辑上来阐述一下原因
//第一次校验
if (singleton == null) { //第1行
synchronized (SingletonDCL.class) { //第2行
//第二次校验
if (singleton == null) { //第3行
singleton = new SingletonDCL();//第4行
}
}
}
return singleton; //第5行
首先看一下第4行代码:singleton = new SingletonDLC();
java实例化一个对象的操作(new)不是原子性的。上面这句代码在JVM上会被拆分成三个步骤来执行,如下
//第1步:创建SingletonDCL实例,分配内存
17 new #3 <test/designpattern/singleton/SingletonDCL>
//第2步:调用构造器方法,初始化SingletonDCL实例对象
21 invokespecial #4 <test/designpattern/singleton/SingletonDCL.<init>>
//第3步:把SingletonDCL实例对象的引用给到变量singleton
24 putstatic #2 <test/designpattern/singleton/SingletonDCL.singleton>
虚拟机实际运行时,以上指令可能发生重排序。以上代码 2,3 可能发生重排序,但是1 并不会重排序。也就是说 1 这个指令都需要先执行,因为 2,3 指令需要依托 1 指令执行结果。
Java 语言规规定了线程执行程序时需要遵守 intra-thread semantics。**intra-thread semantics ** 保证重排序不会改变单线程内的程序执行结果。这个重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能。
正因为存在指令重排序,则有可能导致如下的情况发生
步骤 | 线程A | 线程B |
---|---|---|
1 | 分配内存 | |
2 | 实例对象的引用给到变量singleton | |
3 | 判断对象不为null | |
4 | 跳出if代码块,返回一个未初始化的实例对象 | |
5 | 初始化对象 |
当线程A在执行第4行代码时,B线程进来执行到第1行代码。假设此时线程A执行的过程中发生了指令重排序,即先执行了1和2,没有执行5。那么由于A线程执行了2导致对象引用指向了一段地址,所以线程B判断对象实例不为null,会直接跳到第5行并返回一个未初始化的对象。
八、静态内部类(线程安全)【推荐使用】
8.1 优缺点说明:
-
这种方式采用了类加载的机制来保证初始化实例时只有一个线程。
-
静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton 的实例化。
-
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
结论:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高,推荐使用
8.2 代码案例
class Singleton4 {
//构造器私有化
private Singleton4(){}
//定义静态内部类
private static class SingletonHolder{
private static final Singleton4 SINGLETON = new Singleton4();
}
public static final Singleton4 getInstance(){
return SingletonHolder.SINGLETON;
}
}
九、枚举(线程安全,且不能被反射破坏)【推荐使用】
9.1 优缺点说明:
-
这借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
-
不能通过 反射方式 来调用私有构造方法。
-
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式
结论:推荐使用,但是实际开发中用的很少
public class Singleton_EnumTest {
public static void main(String[] args) {
Singleton_Enum.INSTANCE.method();
}
}
enum Singleton_Enum{
INSTANCE;
public void method(){
System.out.println("我是使用enum方式创建的单例对象");
}
}
十、反射攻击单例模式
10.1 攻击枚举单例
public class ReflectDestructionSingleton {
public static void main(String[] args) throws Exception {
Constructor<Singleton_Enum> clazz = Singleton_Enum.class.getDeclaredConstructor(String.class, int.class);
clazz.setAccessible(true);
Object o = clazz.newInstance();
}
}
运行结果:枚举不能被反射破坏
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at test.designpattern.singleton.ReflectDestructionSingleton.main(ReflectDestructionSingleton.java:31)
10.2 攻击其他单例
public class ReflectDestructionSingleton {
public static void main(String[] args) throws Exception {
SingletonTest instance = SingletonTest.getInstance();
//获得该类中与参数类型匹配的构造方法
Constructor<SingletonTest> constructor = SingletonTest.class.getDeclaredConstructor();
//获得最高访问权限
constructor.setAccessible(true);
SingletonTest singleton = constructor.newInstance();
System.out.println(instance.hashCode() == singleton.hashCode());
}
}
运行结果:false,两个对象的hashcode不相同,说明创建了两个对象
- 解决办法,在类中添加一个计数器,如果创建过一次实例对象,则计数器加1
class SingletonTest{
//创建计数器
private static int count= 0;
//定义静态常量
private final static SingletonTest SINGLETON = new SingletonTest();
//私有化构造方法
private SingletonTest(){
//构造方法中判断是否已经创建过一次实例,有则抛异常
synchronized (SingletonTest.class){
if(count > 0){
throw new RuntimeException("已经创建过一个实例");
}
count++;
}
}
public static SingletonTest getInstance(){
return SINGLETON;
}
}
十一、单例模式应用的源码分析
JDK中
//一个程序只有一个Runtime
//通过代码可以看出来,JDK中Runtime类采用的是饿汉式单例模式
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
.
.
.
}
单例模式注意事项和细节说明
-
单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
-
当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用 new 的方式
-
单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session 工厂等)