设计模式(3)-单例模式

时间:2022-08-12 20:47:41

简介

什么时候使用单例模式

  • 对于一个类来说,如果创建了两个对象或更多对象,程序会出错。
  • 需要频繁实例化然后销毁的对象。
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  • 有状态的工具类对象。
  • 频繁访问数据库或文件的对象。

什么是单例模式

单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

类型

对象创建型模式

要点

  • 只能有一个实例。
  • 必须自己创建自己的唯一实例。
  • 必须给整个系统提供这一实例。

从具体实现角度来说,就是以下三点:

  • 单例模式的类只提供私有的构造函数。
  • 类定义中含有一个该类的静态私有对象。
  • 该类提供了一个静态的公有的函数用于创建或获取它本身的静态私有对象。

实现

测试是否能实现单例模式的工具类

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestSingleton {

public static void main(String[] args) throws InterruptedException {

final Set<String> set = Collections.synchronizedSet(new HashSet<String>());
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
Singleton singleton = Singleton.getInstance();
set.add(singleton.toString());
}
});
}
Thread.sleep(5000);
System.out.println("一共创建了" + set.size() + "个实例");
for (String str : set) {
System.out.println(str);
}
executor.shutdown();
}
}

懒汉式-线程不安全

描述:这种实现最大的问题就是不支持多线程,所以严格意义上它并不算单例模式。
是否多线程安全:否
优点:

  • 避免了饿汉式的那种在没有用到的情况下创建事例,资源利用率高,不执行getInstance()就不会被实例,可以执行该类的其他静态方法。

缺点:

  • 不支持多线程

代码实例

public class Singleton {  
private static Singleton instance;

private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

测试是否支持多线程

执行TestSingleton .java的main()方法几次,观察结果,发现确实不支持多线程

懒汉式-线程安全

描述:支持多线程,但效率低
是否多线程安全:是
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
代码实例

public class Singleton {  
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

测试是否支持多线程

执行TestSingleton .java的main()几次,测试并观察结果,发现确实支持多线程

饿汉式

描述:支持多线程,这种方式比较常用,但容易产生垃圾对象。
是否多线程安全:是
优点

  • 没有加锁,执行效率会提高。
  • 在类加载的同时已经创建好一个静态对象,调用时反应速度快 。

缺点

  • 类加载时就初始化,浪费内存。

代码实例

public class Singleton {  
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}

测试是否支持多线程

执行TestSingleton .java的main()几次
测试并观察结果,发现确实支持多线程

双检锁/双重校验锁(DCL,即 double-checked locking)

描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。getInstance() 的性能对应用程序很关键。
是否多线程安全:是
优点 :资源利用率高,不执行getInstance()就不被实例,可以执行该类其他静态方法
缺点 :第一次加载时反应不快,由于java内存模型一些原因偶尔失败
代码实例

public class Singleton {  
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

测试是否支持多线程

执行TestSingleton .java的main()几次,测试并观察结果,发现确实支持多线程

登记式/静态内部类

描述:这种方式能达到双检锁方式一样的功效,但实现更简单。
是否多线程安全:是
优点:资源利用率高,不执行getInstance()不被实例,可以执行该类其他静态方法。
缺点 :第一次加载时反应不够快。
代码实例

public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

测试是否支持多线程

执行TestSingleton .java的main()几次,测试并观察结果,发现确实支持多线程

枚举

描述:这种实现方式还没有被广泛采用,但据说这是实现单例模式的最佳方法。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式。
是否多线程安全:是
优点:它更简洁,不仅能避免多线程同步问题,而且还自动支持序列化机制。
缺点:枚举是JDK1.5以后才支持的。
代码实例

class Resource{
}

public enum Singleton{
INSTANCE;
private Resource instance;
Singleton() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}

上面的类Resource是需要应用单例模式的资源,具体可以表现为网络连接,数据库连接,线程池等等。
获取资源的方式很简单,只要 Singleton.INSTANCE.getInstance() 即可获得所要实例。

测试是否支持多线程

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestSingleton {

public static void main(String[] args) throws InterruptedException {

final Set<String> set = Collections.synchronizedSet(new HashSet<String>());
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
Resource singleton = Singleton.INSTANCE.getInstance();
set.add(singleton.toString());
}
});
}
Thread.sleep(1000);
System.out.println("一共创建了" + set.size() + "个实例");
for (String str : set) {
System.out.println(str);
}
executor.shutdown();
}
}

执行TestSingleton .java的main()几次
测试并观察结果,发现确实支持多线程

总结

单例模式各种实现方法的优缺点

详见每种单例实现的介绍

优点

  • 只能创建一个对象,节省内存空间。
  • 避免频繁的创建销毁对象,可以提高性能。
  • 避免对共享资源的多重占用。
  • 可以全局访问。

缺点

  • 扩展困难,因为单例模式没有抽象层。
  • 职责过重,在一定程度上违背了“单一职责原则”。
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

注意事项
- 使用时不能用反射模式创建单例,否则会实例化一个新的对象。
- 使用懒单例模式时注意线程安全问题。
- 饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式单例模式)。

饿汉式单例和懒汉式单例的不同

Tables 饿汉式单例 懒汉式单例
实例化对象时机 单例类被加载时候 调用实例方法时
是否支持多线程 支持 需要加synchronized
反应时间
资源利用率
延迟加载

适用环境

  • 资源共享的情况下,避免由于资源操作导致的性能或损耗。
  • 控制资源的情况下,方便资源之间的互相通信。

使用场景

  • Windows中的任务管理器
  • 日志
  • 数据库连接池
  • 线程池

单例模式的对象长时间不用会被jvm垃圾收集器收集吗?
待补充

问题

在一个JVM中会出现多个单例吗?

除非使用反射,否则在一个JVM中不会出现多个单例。

单例类可以被继承吗?
为了单例类在外部被实例化,构造方法被声明成了私有,所以单例类不可以被继承。因为子类会默认调用父类的构造方法,如果父类的构造方法是私有的,就会报错。

单例模式和static方式区别,该如何选择

区别:单例模式,归根结底还是创建了类的实例。而static方式不会创建任何实例,只是把类加载到了内存。
选择:如果只是想提供一个通用公共接口方法的话,就好比一些以Utils结尾的类,那就用static方式比较好,如果有更多事要做的话,比如管理一些状态,对象等,就用单例模式。

在软件开发中,你在哪里用到了单例模式?

待补充。