1.单例设计模式的介绍
1.1什么是单例设计模式
单例设计模式,就是采取一定的方法保证在整个软件系统种,对某个类仅仅能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)
1.2哪些地方经常用到单例设计模式?
比如Hibernate的SessionFactory,其充当数据存储源的代理,并负责创建Session对象。SessionFactory是重量级的,一般只需要一个就够了,所以在这里采用了单例设计模式,保证这个类的在系统中就一个对象实例,需要用到的时候直接用,也不会涉及到频繁的销毁创建,减少资源的占用,提高了资源的占用率。
1.3单例设计模式的八种方式
1.饿汉式(静态常量)
2.饿汉式(静态代码块)
3.懒汉式(线程不安全)
4.懒汉式(线程安全,同步方法)
5.懒汉式(线程安全,同步代码块)
6.双重检查(double-check)
7.静态内部类
8.枚举
2.单例设计模式八大方式
2.1饿汉式(静态常量)
2.1.1实践步骤
1.私有化构造器 => 其实这步基本上都是必须的,因为Java不允许进行重载构造器。
2.类的内部创建对象(在static variable赋值的时候进行实例化一个单例对象)
3.对外暴露一个静态的公共方法 => 一般命名为getInstance
2.1.2实践代码
2.1.2.1实现单例模式
这就是饿汉式单例模式(静态变量),在类初始化的时候,就将这个实例创建出来,在调用静态方法getInstance的时候,返回这个实例。
注意:在类初始化时,会进行为静态变量赋值,这个过程JVM保证了绝对的线程安全。
java
/**
* 懒汉式单例设计模式(静态变量)
*/
public class SingletonTest1 {
private SingletonTest1() {}
private static SingletonTest1 instance = new SingletonTest1();
public static SingletonTest1 getInstance() {
return instance;
}
}
2.1.2.2测试单例模式
进行测试,看看两个对象是不是同一个实例,主要是使用操作符==以及hashCode进行测试。
java
public class Test {
public static void main(String[] args) {
SingletonTest1 instance = SingletonTest1.getInstance();
SingletonTest1 instance2 = SingletonTest1.getInstance();
System.out.println(instance == instance2);
System.out.println(instance.hashCode() == instance2.hashCode());
}
}
测试结果:

2.1.3优缺点和总结
2.1.3.1优点
这种写法比较简单,就是在类初始化的时候完成实例化,将实例化的单例对象赋值给静态变量。JVM在底层进行保证了类初始化为静态变量赋值的时候,是线程安全的。
总结:简单,线程安全。
2.1.3.2缺点
在类初始化的时候就完成实例化,没有达到懒加载的效果。
分析一下,什么时候会触发类的初始化,因为类只有初始化后才会对static variable进行赋值。
触发类初始化的条件:
1.访问类的static variable,static function。
2.反射使用Class.forName(classNmae)进行获取Class<?>对象的时候。
3.new一个对象的时候会初始化类。
4.调用类的main方法时会初始化类。
根据分析,单例类构造器已经被private私有化了,所以这个导致类初始化的可能不存在。
main方法进行调用的时候进行初始化,这个可能性也不大。
反射和访问静态字段有一定的可能性,所以说如果类因为反射forName/访问静态字段导致类初始化,而不是访问的getInstance方法(其它的静态字段被访问),这样就可能导致不会被懒加载,这个单例对象会随着类加载被创建出来,如果这个单例对象创建出来后一直不被使用,就很有可能导致资源的浪费,创建出来不用,2B.
总结:没有懒加载,资源的浪费。
2.1.3.3结论
到底要不要使用这种单例模式,其实是要进行辩证的角度去分析的,如果感觉懒加载没有必要,比如说这个类和该类的实例化的对象占用的内存很小,或者我们确定一定能用到这个单例对象,那就直接饿汉式即可,这种单例模式编码简单,线程安全,在一定程度上可以符合我们对单例设计模式的需求。
但是如果这个单例本身占用系统资源比较多,或者我们不确定到底会不会用到这个单例对象,使用饿汉式单例模式就有点不合理了,这样可能会导致系统资源的浪费。
所以这个问题要从不同角度去分析,不能一概而论,具体业务具体分析。
2.2饿汉式(静态代码块)
2.2.1实践步骤
1.私有化构造器 => 其实这步基本上都是必须的,因为Java不允许进行重载构造器。
2.类的内部创建对象(在static variable初始化时不进行赋值,在static code block中进行初始化,其实也是在类初始化阶段进行初始化的,只是换了一种写法而已)
3.对外暴露一个静态的公共方法 => 一般命名为getInstance。
2.2.2实践代码
2.2.2.1实现单例模式
进行设计一个饿汉式设计模式(静态代码块),这个饿汉式设计模式区别于上面的static variable的方式,就是在于进行加载的地方不同,但是加载的逻辑是一样的,都是在触发类初始化的时进行对静态字段赋值。
java
public class SingletonTest2 {
private static SingletonTest2 instance;
static {
instance = new SingletonTest2();
}
private SingletonTest2() {}
public static SingletonTest2 getInstance() {
return instance;
}
}
2.2.2.2测试单例模式
测试代码:
java
public class Test {
public static void main(String[] args) {
SingletonTest2 instance = SingletonTest2.getInstance();
SingletonTest2 instance2 = SingletonTest2.getInstance();
System.out.println(instance == instance2);
System.out.println(instance.hashCode() == instance2.hashCode());
}
}
测试结果:

2.2.3优缺点
这种饿汉式(静态代码块)的单例模式和饿汉式(静态变量)单例模式的优缺点,是否考虑使用的思路时一样的。
2.3懒汉式(线程不安全)
2.3.1实现步骤
1.私有化构造器 => 其实这步基本上都是必须的,因为Java不允许进行重载构造器。
2.准备一个static variable进行存储单例对象,初始化时不进行赋值,默认为null。
3.对外暴露一个静态的公共方法 => 一般命名为getInstance,在这个方法中进行执行初始化单例对象和返回单例对象的操作。
2.3.2实践代码
2.3.2.1实现单例模式
饿汉式单例模式的设计就是在getInstance中进行判断一下存储单例对象的static variable是否为null,如果为null就进行new 一个对象,如果不为null就直接返回。
但是要注意这段代码是线程不安全的设计,如果在单例对象还没有初始化的时候,也就是instance为null的时候,多个线程如果一起调用getInstance这个方法就会出现明显的问题,如果A线程进入if代码块后,还没有执行完new的指令,B线程也进来了,A线程执行完后进行返回出去了A线程new的单例对象,B线程执行完后进行返回出去了B线程new的单例对象,就会导致两个工作线程使用的不是一个单例对象,导致出现相应的问题。
java
public class SingletonTest3 {
private static SingletonTest3 instance;
private SingletonTest3() {
}
public static SingletonTest3 getInstance() {
if (instance == null) {
instance = new SingletonTest3();
}
return instance;
}
}
2.3.2.2测试单例模式
测试代码:
java
public class Test {
public static void main(String[] args) {
SingletonTest3 instance = SingletonTest3.getInstance();
SingletonTest3 instance2 = SingletonTest3.getInstance();
System.out.println(instance == instance2);
System.out.println(instance.hashCode() == instance2.hashCode());
}
}
测试结果:

2.3.3模拟出现错误的场景
模拟的是两个线程获取的不是同一个单例对象。
2.3.3.1测试代码
java
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<SingletonTest3> taskOne = new FutureTask<>(() -> {
SingletonTest3 instance = SingletonTest3.getInstance();
return instance;
});
FutureTask<SingletonTest3> taskTwo = new FutureTask<>(() -> {
SingletonTest3 instance = SingletonTest3.getInstance();
return instance;
});
Thread thread = new Thread(taskOne);
Thread thread2 = new Thread(taskTwo);
thread.start();
thread2.start();
System.out.println(taskOne.get());
System.out.println(taskTwo.get());
if (taskOne.get() != taskTwo.get()) {
System.out.println("出现错误");
}
}
2.3.3.1测试结果
确实会出现错误。

2.3.4优缺点和总结
2.3.4.1优点
使用懒汉式(线程不安全)设计模式起到了懒加载的效果,但是仅仅可以在单线程下进行使用。
2.3.4.2缺点
缺点就是在多线程环境下,存在线程不安全的问题。
2.3.4.3结论
结论就是不建议进行使用这种方式,因为这种方式是线程不安全的,线程不安全的问题往往是比较严重的,我们很难保证这个单例类不会被多个线程同时进行使用,所以说这种方式具有不安全的隐患。
2.4懒汉式(同步方法)
2.4.1实现步骤
1.私有化构造器 => 其实这步基本上都是必须的,因为Java不允许进行重载构造器。
2.准备一个static variable进行存储单例对象,初始化时不进行赋值,默认为null。
3.对外暴露一个静态的公共方法 => 一般命名为getInstance,在这个方法中进行执行初始化单例对象和返回单例对象的操作,里面进行使用了同步方法来进行保证线程安全,解决了懒汉式线程不安全的问题。
2.4.2实践代码
2.4.2.1实现单例模式
java
public class SingletonTest4 {
private static SingletonTest4 instance;
private SingletonTest4() {}
private static synchronized SingletonTest4 getInstance() {
if (instance == null) {
instance = new SingletonTest4();
}
return instance;
}
}
2.4.2.2测试单例模式
测试代码:
java
public class Test {
public static void main(String[] args) {
SingletonTest4 instance = SingletonTest4.getInstance();
SingletonTest4 instance2 = SingletonTest4.getInstance();
System.out.println(instance == instance2);
System.out.println(instance.hashCode() == instance2.hashCode());
}
}
测试结果:

2.4.3优缺点和总结
2.4.3.1优点
解决了线程不安全的问题,使用synchronized进行标注的方法,具有线程安全的特性,因为使用synchronized标注的方法,可以保证在多线程的环境下,同一时间,单机JVM上只有一个线程执行该方法。
2.4.3.2缺点
使用synchronized同步方法的这种懒汉式单例模式,虽然可以保证绝对的线程安全(前提是单机JVM,当然也不会出现什么集群多进程共享一个单例对象的这种情况,这成本比较逆天,不建议这样搞)。
比如说A线程和B线程同时调用了getInstance方法,A线程率先抢到了锁,B没有抢到锁,就会进行等待队列中,A线程去执行完getIntance后(过程中进行new初始化了单例对象),B线程此时排队结束,去调用getInstance方法,将实例返回,最大的问题是这个锁加在了整个方法上,这个锁太重了,所有的线程调用getInstance方法的时候都要进行排队,无论是实例创建与否,就会导致效率的下降。
改进方法就是:细化锁粒度,如果实例已经被创建了就直接返回即可,没有被创建就进行synchronized锁排队。
2.4.3.3结论
不建议使用这种方式,因为效率比较低。
2.5懒汉式(同步代码块,线程不安全)
下面进行介绍一种线程不安全,但是有人进行使用的同步代码块方式的懒汉式单例模式。
2.5.1实现步骤
1.私有化构造器 => 其实这步基本上都是必须的,因为Java不允许进行重载构造器。
2.准备一个static variable进行存储单例对象,初始化时不进行赋值,默认为null。
3.对外暴露一个静态的公共方法 => 一般命名为getInstance,进行使用同步代码块将创建单例对象的代码进行包裹住。
2.5.2实践代码
2.5.2.1实现单例模式
为什么说这个单例模式是线程不安全的?虽然这行代码使用synchronized进行锁住了实例化对象的代码,但是它只是保证了在同一时间,仅仅会有一个线程访问创建单例对象的代码,但是其它线程会在synchornized锁头进行排队等候,这样就会导致等待过程中每个线程拿到的单例对象都不一样,会导致混乱问题,后续线程拿到的单例对象也不能确定,反正就是这个代码的实现根本达不到完全的单例模式,是一个破烂,一会用代码进行测试。
java
public class SingletonTest5 {
private static SingletonTest5 instance;
private SingletonTest5() {}
public static SingletonTest5 getInstance() {
if (instance == null) {
synchronized (SingletonTest5.class) {
instance = new SingletonTest5();
}
}
return instance;
}
}
2.5.2.2测试单例模式
测试代码:
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test2 {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(100);
for (int i = 0; i < 100; i++) {
executorService.submit(() -> {});
}
Thread.sleep(1000);
for (int index = 0; index < 100; index++) {
executorService.submit(() -> {
SingletonTest5 instance = SingletonTest5.getInstance();
System.out.println(instance);
});
}
}
}
测试的思路主要是进行开启一个线程池,去多线程循环提交任务去让多个线程同时去调用getInstance去获取单例对象。
由于刚创建的线程去执行任何任务的时候都需要去创建线程去执行,所以先使用一个玄幻去提交任务,预热,让线程池把线程都创建出来。
测试结果:
可以发现一些创建的这100个线程中,使用到了不同的实例,说明这个单例模式的代码是有线程安全问题的。

2.5.3优缺点和总结
这个真没啥有点,线程不安全的特性太明显,在编码设计上有严重的问题。
总结:在开发中不能使用这种方式,就算有人用也不要去学习。
2.6懒汉式(double-check)
2.6.1何为double-check
double-check双重检查,这种饿汉式单例模式是线程安全的,并且进行细化了锁粒度,提高了整体的性能,是完美的饿汉式单例设计模式。
double-check主要进行体现在在synchornized锁外进行check,在synchronized锁内也进行了check,这就是双重检查,从根本上做到了线程安全的懒汉式单例模式。
2.6.2实践代码
2.6.2.1实现单例模式
doucle-check懒汉式单例模式,是最安全的懒汉式的单例设计模式,其实主要是在synchornized中又进行了一次if判断,这样就可以让竞争时在wait中进行排队的线程不会去重复创建单例对象,导致出现问题。
java
/**
* 双重检查(double-check)单例模式
*/
public class SingletonTest6 {
private static SingletonTest6 instance;
private SingletonTest6() {
}
public static SingletonTest6 getInstance() {
if (instance == null) {
synchronized (SingletonTest6.class) {
if (instance == null) {
instance = new SingletonTest6();
}
}
}
return instance;
}
}
2.6.2.1测试单例模式
测试代码:
java
/**
* 测试double-check
*/
public class Test1 {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(1000);
AtomicInteger atomicInteger = new AtomicInteger(0);
for (int i = 0; i < 1000; i++) {
executorService.submit(() -> {});
}
Thread.sleep(1000);
for (int index = 0; index < 1000; index++) {
executorService.submit(() -> {
SingletonTest6 instance = SingletonTest6.getInstance();
System.out.println(instance.toString() + " --- " + atomicInteger.getAndIncrement());
});
}
}
}
和上次的测试代码基本一致,不过这次我们使用的线程池增强线程并发量,使用的是1000个线程去并发。
测试结果:
可以发现这次的测试结果清一色的单例。

2.6.3优缺点和总结
2.6.3.1优点
Double-Check概念是多线程开发中经常使用到的,进行两次if检查,保障了线程安全。
满足了:线程安全,懒加载的特征。
2.6.3.2结论
如果确定需要懒加载的情况下,推荐使用double-check懒加载。
2.7静态内部类
2.7.1什么是静态内部类单例模式
静态内部类单例模式就是在单例类中进行一个private的static内部类,在里面定义一个字段在初始化时就进行创建这个类对应的单例对象。
这样其实是饿汉式的变种,但是它既兼顾了饿汉式的线程安全的特性,也获得了懒汉式的懒加载的特性,是一种鱼和熊掌兼得的手段。
为什么说静态内部类可以进行作为饿汉式变种的同时还能进行兼顾懒加载呢?其实就是在于JVM的类加载机制。
前面我们说到饿汉式资源浪费的原因主要是在于如果我们因为一些原因(调用静态字段,反射调用Class.forName,调用main方法)导致类进行初始化,然而流程中根本没有用到那个单例对象,就会出现资源浪费的情况。
但是静态内部类不会跟随外部类的初始化而初始化,这样就可以保障只有调用INSTANCE/反射(这种情况很少),才会进行初始化,这不就是用到的时候才进行初始化吗?这就是一种懒加载啊,所以饿汉式(静态内部类)这种方式可以保证线程安全和懒加载。
2.7.2实践代码
2.7.2.1实现单例设计模式
java
/**
* 静态内部类实现单例设计模式
*/
public class SingletonTest7 {
private SingletonTest7() {
}
private static class Singleton {
public static SingletonTest7 INSTANCE = new SingletonTest7();
}
public static SingletonTest7 getInstance() {
return Singleton.INSTANCE;
}
}
2.7.2.2测试单例设计模式
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 测试代码
*/
public class Test1 {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(1000);
AtomicInteger atomicInteger = new AtomicInteger(0);
for (int i = 0; i < 1000; i++) {
executorService.submit(() -> {});
}
Thread.sleep(1000);
for (int index = 0; index < 1000; index++) {
executorService.submit(() -> {
SingletonTest7 instance = SingletonTest7.getInstance();
System.out.println(instance.toString() + " --- " + atomicInteger.getAndIncrement());
});
}
}
}
2.7.3优缺点和总结
这玩意能有啥缺点。
优点:保证了线程安全,兼顾了懒加载,鱼和熊掌兼得者。
结论:推荐使用。
2.8枚举
2.8.1什么是枚举
枚举是JAVA中天然的单例类。
2.8.2实践代码
2.8.2.1实现单例设计模式
定义一个枚举和枚举单例。
java
/**
* 枚举天然单例
*/
public enum SingletonTest8 {
INSTANCE;
public void method() {
}
}
2.8.2.2测试单例模式
测试代码:
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class Test1 {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(1000);
AtomicInteger atomicInteger = new AtomicInteger(0);
for (int i = 0; i < 1000; i++) {
executorService.submit(() -> {});
}
Thread.sleep(1000);
for (int index = 0; index < 1000; index++) {
executorService.submit(() -> {
SingletonTest8 instance = SingletonTest8.INSTANCE;
System.out.println(instance.toString() + " --- " + atomicInteger.getAndIncrement());
});
}
}
}
和以前一样,老套路。
测试结果:
可以发现进行获取的是统一个示例,枚举是线程安全的单例类,天然可以实现线程安全的单例类。

2.8.3优缺点和总结
2.8.3.1优点
JDK1.5添加枚举进行实现单例模式,不仅仅可以进行避免多线程问题,也能防止在反序列化的时候去创建新对象(JDK底层帮我们进行了完美的处理)
这种方式也是Effective Java作者Josh Blosh提倡的方式。
并且JDK底层保证了枚举设计是懒加载的。
可以总结出Enum实现的单例模式:1.可以保证线程安全。2.可以保证懒加载。3.可以保证反序列化时不会创建新对象。
所以说Enum实现的单例设计模式也是鱼和熊掌兼得的一种,并且是纯天然的设计模式,建议使用。
2.8.3.2结论
建议使用,天然单例。
3.单例模式在JDK源码中的应用
3.1Runtime中的运用
JDK中在Runtime中进行运行到了单例模式。
使用的是饿汉式单例模式,因为JDK能确定这个东西大概率会使用的,临时创建不现实,并且占用的资源不多,故JDK决定进行采用饿汉式单例模式(典型的根据业务场景进行选择)
java
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() {}
3.2单例设计模式的总结
3.2.1减少资源消耗
单例设计模式保证了这个类在系统中仅仅存在一个对象,节省了系统资源,对于一些需要频繁使用的对象,频繁的创建销毁太影响了,使用单例设计模式可以提高系统的性能。
3.2.2使用场景
经常需要用到的对象,并且频繁的创建销毁太影响性能时,就建议进行使用单例设计模式。