你真能说清楚单例模式吗?

Posted 码神手记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你真能说清楚单例模式吗?相关的知识,希望对你有一定的参考价值。

码神手记——资深攻城狮的私房笔记。微信公众平台/知乎/头条/简书同步发文。感谢关注与转发。

核心作用

保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。

常见应用场景

  • Windows的Task Manager(任务管理器)是很典型的单例模式

  • Windows的Recycle Bin(回收站)是单例应用。在系统运行中,回收站只维护仅有的一个实例。

  • 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据都new一个对象去读取。

  • 网站的计数器,一般也是采用单例模式实现,否则难以同步。

  • 应用程序的日志应用,一般都使用单例模式实现,一般因为共享的日志文件一直处于打开状态,只能有一个实例去操作,否则内容不好追加。

  • 数据库连接池的设计一般也采用单例模式。

  • 操作系统的文件系统,也是单例模式实现的具体例子,一个操作系统只能有一个文件系统。

  • 在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理。

  • 在Servlet编程中,每个Servlet也是单例。

  • 在Spring MVC框架/struts1框架中,控制器对象也是单例。

单例模式的优点

  • 由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其它依赖对象,则可以在应用启动时直接产生一个单例对象,然后永久驻留内存。

  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

常见的五种单例模式实现方式

  1. 饿汉式(线程安全,调用效率高。但是,不能延时加载)

  2. 懒汉式(线程安全,调用效率不高。但是,可以延时加载)

  3. 双重检测锁式(由于指令重排,在多线程环境下可能出现引用不空,但对象未初始化。不建议使用。)

  4. 静态内部类式(线程安全、调用效率高,可以延时加载)

  5. 枚举单例(线程安全,调用效率高,不能延时加载)

如何选用?

  • 单例对象占用资源少,不需要延时加载:枚举式 好于饿汉式

  • 单例对象占用资源大,需要延时加载:静态内部类式,好于懒汉式

代码示例

饿汉式

package com.liu.singleton;

/**
* 测试饿汉式单例模式
*
* @author Steve
*
*/
public class SingletonDemo01 {
//类初始化时,立即加载(没有延时加载的优势)。加载类时是线程安全的。
private static SingletonDemo01 instance = new SingletonDemo01();

private SingletonDemo01() {
//避免使用反射调用构造器时创建新的对象
if (instance !=null) {
throw new RuntimeException("请使用getInstance方法获取对象" );
}
}
//方法没有同步,调用效率高
public static SingletonDemo01 getInstance() {
return instance ;
}
// 反序列化时,如果定义了该方法,则直接返回此方法指定的对象,不会再单独创建对象
private Object readResolve() throws ObjectStreamException {
return instance ;
}
}

懒汉式

package com.liu.singleton;

/**
* 测试懒汉式单例模式
*
* @author Steve
*
*/
public class SingletonDemo02 {
private static SingletonDemo02 instance ;

private SingletonDemo02() {
//避免使用反射调用构造器时创建新的对象
if (instance !=null) {
throw new RuntimeException("请使用getInstance方法获取对象" );
}
}

public static synchronized SingletonDemo02 getInstance() {
if (null == instance) {
instance = new SingletonDemo02 ();
}
return instance ;
}
// 反序列化时,如果定义了该方法,则直接返回此方法指定的对象,不会再单独创建对象
private Object readResolve() throws ObjectStreamException {
return instance ;
}
}

双重检查锁式

package com.liu.singleton;

/**
* 双重检查锁实现单例模式
*
* @author Steve
*
*/
public class SingletonDemo03 {
private static SingletonDemo03 instance = null;

private SingletonDemo03() {
//避免使用反射调用构造器时创建新的对象
if (instance !=null) {
throw new RuntimeException("请使用getInstance方法获取对象" );
}
}

public static SingletonDemo03 getInstance() {
if (null == instance) {
SingletonDemo03 sc;
synchronized (SingletonDemo03.class) {
sc = instance;
if (sc == null ) {
synchronized (SingletonDemo03.class) {
if (sc == null ) {
sc = new SingletonDemo03();
}
}
instance = sc;
}
}
}
return instance ;
}
// 反序列化时,如果定义了该方法,则直接返回此方法指定的对象,不会再单独创建对象
private Object readResolve() throws ObjectStreamException {
return instance ;
}
}

静态内部类式

package com.liu.singleton;

/**
* 静态内部类实现单例模式
*
* 外部类没有static属性,则不会像饿汉式那样立即加载对象
*
* 只有真正调用getInstance(),才会加载静态内部类。加载类时是线程安全的,由JVM保证。
*
* instance是static final类型,保证了在内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性。
*
* 兼备了并发高效调用和延迟加载的优势
*
* @author Steve
*
*/
public class SingletonDemo04 {
private static class SingletonClassInstance {
private static final SingletonDemo04 instance = new SingletonDemo04();
}

public static SingletonDemo04 getInstance() {
return SingletonClassInstance.instance;
}

private SingletonDemo04() {
//避免使用反射调用构造器时创建新的对象
if (instance !=null) {
throw new RuntimeException("请使用getInstance方法获取对象" );
}
}
// 反序列化时,如果定义了该方法,则直接返回此方法指定的对象,不会再单独创建对象
private Object readResolve() throws ObjectStreamException {
return SingletonClassInstance.instance ;
}
}

枚举式

package com.liu.singleton;

/**
* 使用枚举实现单例模式
*
* 优点:实现简单、枚举本身是单例模式。由JVM从根本上提供保障,避免通过反射和反序列化创建对象的漏洞
*
* 缺点:无延迟加载
*
* @author Steve
*
*/
public enum SingletonDemo05 {

/**
* 这个枚举元素,本身就是单例的
*/
INSTANCE;
/**
* 添加自己需要的操作
*/
public void singletonOperation() {

}
}

多线程测试各种实现方式的效率

package com.liu.singleton;

import java.util.concurrent.CountDownLatch;

/**
* 使用CountDownLatch测试多线程环境下各个单例模式实现方式的效率
*
* @author Steve
*
*/
public class Client3 {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
int threadNum = 10;
final CountDownLatch countDownLatch = new CountDownLatch(threadNum);

for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
Object o = SingletonDemo01.getInstance();
}
countDownLatch.countDown();
}

}).start();
}
countDownLatch.await(); // main线程阻塞,知道计数器值变为0,才会继续往下执行

long end = System.currentTimeMillis();
System. out.println("总耗时:" + (end - start));
}
}

码神手记——资深攻城狮的私房笔记。微信公众平台/知乎/头条/简书同步发文。感谢关注与转发。


以上是关于你真能说清楚单例模式吗?的主要内容,如果未能解决你的问题,请参考以下文章

云原生是什么?你真的能说清楚吗?

面试高频题:Spring和SpringMvc父子容器你能说清楚吗

面试高频题:Spring和SpringMVC父子容器你能说清楚吗

设计模式系列-你真的了解单例模式吗??

关于java单例模式,这篇已经讲得很清楚了,建议收藏!

Spring中使用的设计模式,你都能说全吗?[下]