参考:《HeadFirst设计模式》
其实关于单例模式的代码网上有很多,但是很多都是直接以Singleton
类名所编写的示例代码。
通过参考这些代码,可能落实到真正的代码中会遇到些困难,例如枚举式单例模式
。
所以本文以实例打印机
为例,站在各位巨人的肩膀上,再次进行单例模式的整理与总结。
1.单利模式存在的意义
在很多应用中,对于某一种对象,我们最多只需要其一个实例存在。比方说:
- 操作系统的打印机进程。
- Java项目的单个数据库的连接池对象。
- Windows操作系统的垃圾回收站。
- 项目的配置类。
- 。。。
为什么上述对象,至多只需要一个实例存在呢?主要原因:
- 避免资源浪费。例如:打印机的打印吞吐量取决于打印机硬件本身,多个打印机进程的存在除了占用资源,毫无意义。
- 避免多实例存在产生错误。例如:多个打印机进程共同操作一个打印机,那么同时打印很多文件时,可能因为资源挣争夺二产生错误。
关于单例模式的定义概括如下:
- 单例模式In Java:保证同一个Java进程中,某些类的实例对象至多只有一个,并提供一个访问它的全局访问点的设计模式。
2.实现单例模式的要点
要保证单例,只需要保证以下三点:
- 对外统一提供一个公共方法用于获取唯一的实例对象:一般定义为getInstance()方法。
- 不允许其他类对当前类进行实例化:当前类的构造函数或实例化方法必须是private的。
- 当前类需要创建实例对象:需要实现当前类的构造函数或实例化方法。
2.1.懒汉式与饿汉式
饿汉式
与懒汉式
的区别在于:对象实例化的时机。饿汉式
:类加载阶段进行对象实例化,速记:开始就加载
。懒汉式
:getInstance首次调用时进行对象实例化,速记:用时再加载
、延时加载
。
3.几种单例模式的Java实现
下面以打印机为应用场景,进行几种单例模式的编码。
3.0.工具类
为了方便测试,编写了工具类PrinterUtil
,此工具类有以下四个作用:
- 提供计数器统计
getInstance()
方法被调用的次数。 - 提供计数器统计
实际初始化
的实例对象个数。 - 提供自定义线程池,用于测试。
- 配置一些全局变量。
工具类PrinterUtil.java如下:
/**
* <p>工具类:测试相关</P>
*
* @author hanchao
*/
public class PrinterUtil {
/**
* 测试相关:调用getInstance方法次数统计
*/
public static LongAdder CALL_COUNTER = new LongAdder();
/**
* 测试相关:对象实例化次数统计
*/
public static LongAdder ACTUAL_COUNTER = new LongAdder();
/**
* 测试相关:线程池
*/
public static ExecutorService getThreadPool() {
return new ThreadPoolExecutor(0, 1024,
6L, TimeUnit.SECONDS,
new SynchronousQueue<>(), new ThreadFactory() {
private AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "design-pattern-thread-" + threadNumber.getAndIncrement());
}
});
}
/**
* 测试相关:最大线程量
*/
public static Integer MAX_THREAD = 1000;
}
3.1.饿汉式(静态变量初始化)[推荐使用]
示例代码:
/**
* <p>饿汉式(静态变量初始化)[推荐使用]</P>
*
* @author hanchao
*/
@Slf4j
public class Printer01 {
/**
* 私有构造函数:防止外部类创建实例
*/
private Printer01() {
log.info("Connect to physical printer...");
PrinterUtil.ACTUAL_COUNTER.increment();
}
/**
* 类初始化时调用私有构造函数,完成对象实例化
*/
private static Printer01 INSTANCE = new Printer01();
/**
* 对外统一的实例获取方法
*/
public static Printer01 getInstance() {
PrinterUtil.CALL_COUNTER.increment();
try {
Thread.sleep(10L);
} catch (InterruptedException e) {
log.error("error!");
}
return INSTANCE;
}
}
测试代码:
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = PrinterUtil.getThreadPool();
CountDownLatch latch = new CountDownLatch(1);
for (int i = 0; i < PrinterUtil.MAX_THREAD; i++) {
executorService.submit(() -> {
Printer01.getInstance();
try {
latch.await();
} catch (InterruptedException e) {
log.error("Error !");
}
});
}
latch.countDown();
executorService.shutdown();
Thread.sleep(1000L);
System.out.println("在1s内,调用getInstance次数:" + PrinterUtil.CALL_COUNTER.sum());
System.out.println("在1s内,实际实例化的对象个数:" + PrinterUtil.ACTUAL_COUNTER.sum());
}
测试结果:
2019-06-27 14:45:26,782 INFO [main-1] pers.hanchao.designpattern.singleton.Printer01:20 - Connect to physical printer...
在1s内,调用getInstance次数:1000
在1s内,实际实例化的对象个数:1
优点:
- 代码实现简单,无需考虑线程安全。
缺点:
- 在未调用之前就实例化,那么从实例化到对象真正被调用这段时间,其实是对资源的一种浪费。
- 未实现延时加载,可能实例对象早早就加载到内存中,但是从始至终都未使用,更是对资源的浪费。
适用性分析:
- 资源充足,无需考虑通过延时加载进行资源节省的场景。
- 实例对象很早并且必定会被使用的场景。
3.2.饿汉式(静态代码块初始化)[推荐使用]
- 第二种
饿汉式
本质与第一种是一致的,其唯一区别是:第一种把对象实例化放在静态变量
,第二种把对象实例化放在静态代码块
。
示例代码:
/**
* <p>饿汉式(静态代码块初始化)[推荐使用]</P>
*
* @author hanchao
*/
@Slf4j
public class Printer02 {
/**
* 私有构造函数:防止外部类创建实例
*/
private Printer02() {
log.info("Connect to physical printer...");
PrinterUtil.ACTUAL_COUNTER.increment();
}
private static Printer02 INSTANCE;
/**
* 类初始化时调用私有构造函数,完成对象实例化
*/
static {
//静态代码块写法的优点:可以进行更多的操作
log.info("Do more thing...");
INSTANCE = new Printer02();
}
/**
* 对外统一的实例获取方法
*/
public static Printer02 getInstance() {
PrinterUtil.CALL_COUNTER.increment();
try {
Thread.sleep(10L);
} catch (InterruptedException e) {
log.error("error!");
}
return INSTANCE;
}
}
测试代码:
- 参考
3.1.饿汉式
的测试代码。
测试结果:
2019-06-27 14:46:17,593 INFO [main-1] pers.hanchao.designpattern.singleton.Printer02:31 - Do more thing...
2019-06-27 14:46:17,597 INFO [main-1] pers.hanchao.designpattern.singleton.Printer02:20 - Connect to physical printer...
在1s内,调用getInstance次数:1000
在1s内,实际实例化的对象个数:1
优点:
- 代码实现简单,无需考虑线程安全。
缺点:
- 在未调用之前就实例化,那么从实例化到对象真正被调用这段时间,其实是对资源的一种浪费。
- 未实现延时加载,可能实例对象早早就加载到内存中,但是从始至终都未使用,更是对资源的浪费。
适用性分析:
- 资源充足,无需考虑通过延时加载进行资源节省的场景。
- 实例对象很早并且必定会被使用的场景。
- 相比于
静态变量饿汉式
。此种方式更方便在实例化阶段做更多的处理。
3.3.懒汉式(无视线程安全)[不能使用]
示例代码:
/**
* <p>懒汉式(无视线程安全)[不能使用]</P>
*
* @author hanchao
*/
@Slf4j
public class Printer03 {
/**
* 私有构造函数:防止外部类创建实例
*/
private Printer03() {
log.info("Connect to physical printer...");
PrinterUtil.ACTUAL_COUNTER.increment();
}
private static Printer03 INSTANCE;
/**
* 对外统一的实例获取方法
*/
public static Printer03 getInstance() {
//调用getInstance时再进行实例化,但是并没有考虑并发情况
if (null == INSTANCE) {
INSTANCE = new Printer03();
}
try {
Thread.sleep(10L);
} catch (InterruptedException e) {
log.error("error!");
}
PrinterUtil.CALL_COUNTER.increment();
return INSTANCE;
}
}
测试代码:
- 参考
3.1.饿汉式
的测试代码。
测试结果:
2019-06-27 14:50:41,799 INFO [design-pattern-thread-2-15] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer...
2019-06-27 14:50:41,802 INFO [design-pattern-thread-3-16] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer...
2019-06-27 14:50:41,801 INFO [design-pattern-thread-6-19] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer...
2019-06-27 14:50:41,801 INFO [design-pattern-thread-4-17] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer...
2019-06-27 14:50:41,801 INFO [design-pattern-thread-5-18] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer...
2019-06-27 14:50:41,802 INFO [design-pattern-thread-1-14] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer...
2019-06-27 14:50:41,802 INFO [design-pattern-thread-7-20] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer...
2019-06-27 14:50:41,802 INFO [design-pattern-thread-8-21] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer...
在1s内,调用getInstance次数:1000
在1s内,实际实例化的对象个数:8
优点:
- 无。
缺点:
- 根本未考虑采取措施保证线程安全。
- 线程不安全,在并发环境下,产生了多个实例对象。
适用性分析:
- 线程不安全,不可用。
3.4.懒汉式(同步方法保证,线程安全,资源争夺)[不能使用]
示例代码:
/**
* <p>懒汉式(同步方法保证,线程安全,资源争夺)[不能使用]</P>
*
* @author hanchao
*/
@Slf4j
public class Printer04 {
/**
* 私有构造函数:防止外部类创建实例
*/
private Printer04() {
log.info("Connect to physical printer...");
PrinterUtil.ACTUAL_COUNTER.increment();
}
private static Printer04 INSTANCE;
/**
* 对外统一的实例获取方法(同步方法)
*/
public static synchronized Printer04 getInstance() {
//调用getInstance时再进行实例化,但是并没有考虑并发情况
if (null == INSTANCE) {
INSTANCE = new Printer04();
}
try {
Thread.sleep(10L);
} catch (InterruptedException e) {
log.error("error!");
}
PrinterUtil.CALL_COUNTER.increment();
return INSTANCE;
}
}
测试代码:
- 参考
3.1.饿汉式
的测试代码。
测试结果:
2019-06-27 14:54:42,587 INFO [design-pattern-thread-1-14] pers.hanchao.designpattern.singleton.Printer04:22 - Connect to physical printer...
在1s内,调用getInstance次数:99
在1s内,实际实例化的对象个数:1
优点:
- 通过
同步方法
保证了线程安全。 - 保证线程安全的措施的代码实现相对简单,因为只需要在原有方法上添加
synchronized
关键字。
缺点:
- 因为
同步方法
锁住了整个方法,所以效率很低,从测试结果来看,1s之内,getInstance()
实际只被调用了99次,远少于其他方式的1000个。
适用性分析:
- 效率太低,不可用,
3.5.懒汉式(同步代码块,单重校验,线程不安全)[不能使用]
示例代码:
/**
* <p>懒汉式(同步代码块,单重校验,线程不安全)[不能使用]</P>
*
* @author hanchao
*/
@Slf4j
public class Printer05 {
/**
* 私有构造函数:防止外部类创建实例
*/
private Printer05() {
log.info("Connect to physical printer...");
PrinterUtil.ACTUAL_COUNTER.increment();
}
private static Printer05 INSTANCE;
/**
* 对外统一的实例获取方法
*/
public static Printer05 getInstance() {
//调用getInstance时再进行实例化,单重校验线程不安全
if (null == INSTANCE) {
synchronized (Printer05.class) {
INSTANCE = new Printer05();
}
}
try {
Thread.sleep(10L);
} catch (InterruptedException e) {
log.error("error!");
}
PrinterUtil.CALL_COUNTER.increment();
return INSTANCE;
}
}
测试代码:
- 参考
3.1.饿汉式
的测试代码。
测试结果:
2019-06-27 15:00:05,769 INFO [design-pattern-thread-1-14] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer...
2019-06-27 15:00:05,773 INFO [design-pattern-thread-11-24] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer...
2019-06-27 15:00:05,774 INFO [design-pattern-thread-10-23] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer...
2019-06-27 15:00:05,774 INFO [design-pattern-thread-9-22] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer...
2019-06-27 15:00:05,775 INFO [design-pattern-thread-8-21] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer...
2019-06-27 15:00:05,775 INFO [design-pattern-thread-7-20] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer...
2019-06-27 15:00:05,775 INFO [design-pattern-thread-6-19] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer...
2019-06-27 15:00:05,776 INFO [design-pattern-thread-5-18] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer...
2019-06-27 15:00:05,777 INFO [design-pattern-thread-4-17] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer...
2019-06-27 15:00:05,778 INFO [design-pattern-thread-3-16] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer...
2019-06-27 15:00:05,778 INFO [design-pattern-thread-2-15] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer...
在1s内,调用getInstance次数:1000
在1s内,实际实例化的对象个数:11
优点:
尝试
去通过同步代码块
保证线程安全。
缺点:
- 线程不安全,在并发环境下,产生了多个实例对象。
适用性分析:
- 线程不安全,不可用。
3.6.懒汉式(同步代码块,双重校验,线程安全)[推荐使用]
示例代码:
/**
* <p>懒汉式(同步代码块,双重校验,线程安全)[推荐使用]</P>
*
* @author hanchao
*/
@Slf4j
public class Printer06 {
/**
* 私有构造函数:防止外部类创建实例
*/
private Printer06() {
log.info("Connect to physical printer...");
PrinterUtil.ACTUAL_COUNTER.increment();
}
private static Printer06 INSTANCE;
/**
* 对外统一的实例获取方法
*/
public static Printer06 getInstance() {
//调用getInstance时再进行实例化,单重校验线程不安全,资源争夺
if (null == INSTANCE) {
synchronized (Printer06.class) {
if (null == INSTANCE) {
INSTANCE = new Printer06();
}
}
}
try {
Thread.sleep(10L);
} catch (InterruptedException e) {
log.error("error!");
}
PrinterUtil.CALL_COUNTER.increment();
return INSTANCE;
}
}
测试代码:
- 参考
3.1.饿汉式
的测试代码。
测试结果:
2019-06-27 15:04:57,221 INFO [design-pattern-thread-1-14] pers.hanchao.designpattern.singleton.Printer06:20 - Connect to physical printer...
在1s内,调用getInstance次数:1000
在1s内,实际实例化的对象个数:1
优点:
- 线程安全。
- 延时加载,节省资源。
- 通过双重校验保证线程安全,效率较高。
缺点:
- 线程安全保障措施的代码实现稍显复杂。
适用性分析:
- 需要考虑通过延时加载进行资源节省的场景。
- 不确定实例对象何时使用或者是否使用的场景。
3.7.内部类[推荐使用]
示例代码:
/**
* <p>内部类[推荐使用]</P>
*
* @author hanchao
*/
@Slf4j
public class Printer07 {
/**
* 私有构造函数:防止外部类创建实例
*/
private Printer07() {
log.info("Connect to physical printer...");
PrinterUtil.ACTUAL_COUNTER.increment();
}
/**
* 将唯一实例放在静态内部类的静态变量中。静态内部类的静态变量在外部类被加载时不会立即实例化,
* 只有在调用getInstance方法时才会加载静态内部类,进而完成静态内部类的静态变量的初始化。
*/
private static class PrinterUtil07Holder {
private static Printer07 INSTANCE = new Printer07();
}
/**
* 对外统一的实例获取方法
*/
public static Printer07 getInstance() {
PrinterUtil.CALL_COUNTER.increment();
try {
Thread.sleep(10L);
} catch (InterruptedException e) {
log.error("error!");
}
return PrinterUtil07Holder.INSTANCE;
}
}
测试代码:
- 参考
3.1.饿汉式
的测试代码。
测试结果:
2019-06-27 15:11:55,177 INFO [design-pattern-thread-2-15] pers.hanchao.designpattern.singleton.Printer07:20 - Connect to physical printer...
在1s内,调用getInstance次数:1000
在1s内,实际实例化的对象个数:1
优点:
- 线程安全。
- 延时加载,节省资源。
- 通过内部类保证线程安全,效率较高。
- 线程安全保障措施的代码实现十分简单。
缺点:
- 无。
适用性分析:
- 需要考虑通过延时加载进行资源节省的场景。
- 不确定实例对象何时使用或者是否使用的场景。
3.8.枚举[推荐使用]
示例代码一:枚举(普通版)[推荐使用]
/**
* <p>枚举(普通版)[推荐使用]</P>
*
* @author hanchao
*/
public enum Printer08 {
/**
* 唯一实例
*/
INSTANCE;
/**
* 枚举的构造方法保证私有性:防止外部类创建实例
*/
Printer08() {
System.out.println("Connect to physical printer...");
PrinterUtil.ACTUAL_COUNTER.increment();
}
/**
* 对外统一的实例获取方法(在枚举实现的单例模式中,此方法可以省略,直接通过Printer08.INSTANCE获取对象实例)
*/
public Printer08 getInstance() {
PrinterUtil.CALL_COUNTER.increment();
try {
Thread.sleep(10L);
} catch (InterruptedException e) {
System.out.println("error!");
}
return this;
}
}
测试代码一:
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = PrinterUtil.getThreadPool();
CountDownLatch latch = new CountDownLatch(1);
for (int i = 0; i < PrinterUtil.MAX_THREAD; i++) {
executorService.submit(() -> {
//更简洁的写法:Printer08 instance = Printer08.INSTANCE;
Printer08.INSTANCE.getInstance();
try {
latch.await();
} catch (InterruptedException e) {
System.out.println("Error !");
}
});
}
latch.countDown();
executorService.shutdown();
Thread.sleep(1000L);
System.out.println("在1s内,调用getInstance次数:" + PrinterUtil.CALL_COUNTER.sum());
System.out.println("在1s内,实际实例化的对象个数:" + PrinterUtil.ACTUAL_COUNTER.sum());
}
示例代码二:枚举(简洁版)[推荐使用]
/**
* <p>枚举(简洁版)[推荐使用]</P>
*
* @author hanchao
*/
public enum Printer09 {
/**
* 唯一实例
*/
INSTANCE;
/**
* 枚举的构造方法保证私有性:防止外部类创建实例
*/
Printer09() {
System.out.println("Connect to physical printer...");
PrinterUtil.ACTUAL_COUNTER.increment();
}
}
测试代码二:
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = PrinterUtil.getThreadPool();
CountDownLatch latch = new CountDownLatch(1);
for (int i = 0; i < PrinterUtil.MAX_THREAD; i++) {
executorService.submit(() -> {
Printer09 instance = Printer09.INSTANCE;
PrinterUtil.CALL_COUNTER.increment();
try {
latch.await();
} catch (InterruptedException e) {
System.out.println("Error !");
}
});
}
latch.countDown();
executorService.shutdown();
Thread.sleep(1000L);
System.out.println("在1s内,调用getInstance次数:" + PrinterUtil.CALL_COUNTER.sum());
System.out.println("在1s内,实际实例化的对象个数:" + PrinterUtil.ACTUAL_COUNTER.sum());
}
测试结果:
在1s内,调用getInstance次数:1000
在1s内,实际实例化的对象个数:1
优点:
- 线程安全。
- 延时加载,节省资源。
- 通过枚举保证线程安全,效率较高。
- 线程安全保障措施的代码实现十分简单。
缺点:
- 以
枚举
实现的方式本身可能不易理解。
适用性分析:
- 需要考虑通过延时加载进行资源节省的场景。
- 不确定实例对象何时使用或者是否使用的场景。
3.总结
资源充足的单例模式
- 3.1.饿汉式(静态变量初始化)[推荐使用]
- 3.2.饿汉式(静态代码块初始化)[推荐使用]
不可用的单例模式:
- 3.3.懒汉式(无视线程安全)[不能使用]
- 3.4.懒汉式(同步方法保证,线程安全,资源争夺)[不能使用]
- 3.5.懒汉式(同步代码块,单重校验,线程不安全)[不能使用]
节省资源的单例模式:
- 3.6.懒汉式(同步代码块,双重校验,线程安全)[推荐使用]
- 3.7.内部类[推荐使用]
- 3.8.枚举[推荐使用]