一、单例模式
背景:
对于某些系统来说,只有一个实例很重要,例如:一个系统中可以存在多个打印任务,但是只能有一个正在执行打印任务;售票窗口,共有100张票,可以多个窗口同时售票,但是不能超售(这里的余票就是单例);在Spring中创建的Bean实例默认都是单例模式存在的
概念:
单例模式
确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例.在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例.每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。总之,选择单例模式就是为了避免不一致状态,避免政出多头
必要性
有些对象的创建非常耗时和耗内存,但是签好这些对象在我们应用中只需要使用1个,如果不能得到控制,会造成资源浪费;
例如:办公室的打印机,可以4、5个人共用一台打印机,就没必要每人一台,例如线程池和数据连接池
写法种类:
- 懒汉式单例
- 饿汉式单例
特点:
- 单例类只能有一个实例
- 单例类必须自己创建自己唯一实例
- 单例类必须给所有其他对象提供这一实例
二、 懒汉式、饿汉式、登记式单例实现
懒汉式单例
// 懒汉式单例,在第一次调用时实例化自己
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {};
public static LazySingleton getInstance() {
if (instance == null) {
// 在多线程的环境下,可能被执行多次,造成线程不安全
instance = new LazySingleton();
}
return instance;
}
}
LazySingleton
通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,LazySingleton的唯一实例只能通过getInstance()方法访问。
在多线程的环境下验证懒汉式单例的线程安全
public class TestLaySingletonSecure {
public static void main(String[] args) {
// 使用 FixedThreadPool 生成 20 个线程
ExecutorService executorService = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +":" + LazySingleton.getInstance());
}
});
}
executorService.shutdown();
}
}
运行结果:
pool-1-thread-1:singleton.LazySingleton@eb4851
pool-1-thread-2:singleton.LazySingleton@2648c8
pool-1-thread-3:singleton.LazySingleton@2648c8
pool-1-thread-4:singleton.LazySingleton@2648c8
pool-1-thread-5:singleton.LazySingleton@2648c8
pool-1-thread-6:singleton.LazySingleton@2648c8
pool-1-thread-8:singleton.LazySingleton@2648c8
pool-1-thread-7:singleton.LazySingleton@2648c8
pool-1-thread-9:singleton.LazySingleton@2648c8
pool-1-thread-10:singleton.LazySingleton@2648c8
pool-1-thread-11:singleton.LazySingleton@2648c8
pool-1-thread-12:singleton.LazySingleton@2648c8
pool-1-thread-13:singleton.LazySingleton@2648c8
pool-1-thread-14:singleton.LazySingleton@2648c8
pool-1-thread-15:singleton.LazySingleton@2648c8
pool-1-thread-16:singleton.LazySingleton@2648c8
pool-1-thread-17:singleton.LazySingleton@2648c8
pool-1-thread-18:singleton.LazySingleton@2648c8
pool-1-thread-19:singleton.LazySingleton@2648c8
pool-1-thread-20:singleton.LazySingleton@2648c8
Process finished with exit code 0
分析:有一个实例是 singleton.LazySingleton@eb4851,说明返回的不是同一个实例,这就是线程安全问题
原因:在对 instance 实例是否为空判断的代码段中,如果此时有两个线程分别为 A 和 B,如果线程 A 读取到 instance 为 null,然而此时 CPU 资源被线程 B 抢去了,这时线程 A 还没有对 instance 实例化,因此线程 B 获取的 instance 仍然为 null,就会对 instance 进行实例化,然后 CPU 资源被 A 抢去了,由于之前线程 A 读取的 instance 为 null,并不知道线程 B 已经对 instance 进行实例化,就会再次对 instance 进行实例化,这样就对 instance 实例化了两次,线程 A 和 线程 B 返回的不是一个实例
懒汉式单例没有考虑线程安全问题,是线程不安全的,并发环境下很可能出现多个Singleton实例,要实现线程安全,有以下三种方式,都是对getInstance这个方法改造,保证了懒汉式单例的线程安全
解决懒汉式线程不安全的问题
方式一:在getInstance方法上加同步
public synchronized static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
方式二:双重检查锁定
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
// 进行两次判断,加入两个线程A,B,线程A先进入实例化对象,如果不加if判断,B线程再次进入时又会实例化一次,这样单例就失效了
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
方式三:静态内部类
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static final Singleton getInstance() {
return LazyHolder.INSTANCE;
}
资源加载和性能对比:
方式一:在方法上加同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟大部分情况下是不需要同步的
方式二:在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗
方式三:利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种
饿汉式单例
// 饿汉式单例类,在类初始化时,已经自行实例化
public class Singleton1 {
private Singleton1() {}
private static final Singleton1 singleton = new Singleton1();
// 静态工厂方法
public static Singleton1 getInstance() {
return singleton;
}
}
在多线程的环境下验证饿汉式单例的线程安全性
public class TestSingletonSecure {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + Singleton.getInstance());
}
});
}
executorService.shutdown();
}
}
运行结果:
pool-1-thread-1:singleton.Singleton@1201d0e
pool-1-thread-3:singleton.Singleton@1201d0e
pool-1-thread-2:singleton.Singleton@1201d0e
pool-1-thread-7:singleton.Singleton@1201d0e
pool-1-thread-6:singleton.Singleton@1201d0e
pool-1-thread-10:singleton.Singleton@1201d0e
pool-1-thread-11:singleton.Singleton@1201d0e
pool-1-thread-5:singleton.Singleton@1201d0e
pool-1-thread-4:singleton.Singleton@1201d0e
pool-1-thread-14:singleton.Singleton@1201d0e
pool-1-thread-8:singleton.Singleton@1201d0e
pool-1-thread-12:singleton.Singleton@1201d0e
pool-1-thread-9:singleton.Singleton@1201d0e
pool-1-thread-15:singleton.Singleton@1201d0e
pool-1-thread-13:singleton.Singleton@1201d0e
pool-1-thread-16:singleton.Singleton@1201d0e
pool-1-thread-17:singleton.Singleton@1201d0e
pool-1-thread-18:singleton.Singleton@1201d0e
pool-1-thread-20:singleton.Singleton@1201d0e
pool-1-thread-19:singleton.Singleton@1201d0e
Process finished with exit code 0
分析:返回的都是同一个实例,在多线程环境下不会穿线线程不安全的问题
原因:饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的。
三、懒汉式和饿汉式的区别
懒汉式
:比较懒,只有当调用getInstance的时候,才回去初始化这个实例
饿汉式
:类一旦被加载,就把单例初始化完成,保证getInsatnce的时候,单例是已经存在的
线程安全:
懒汉式
:线程不安全,但是可以通过上述方式一、二、三来实现线程安全
饿汉式
:线程安全,可以直接用于多线程而不会出现问题
资源加载和性能
懒汉式
:会延迟加载,第一次调用的时候才会实例化对象,第一次调用做初始化,如果要做的工作比较多,性能会有延迟加载现象
饿汉式
:在类创建的时候就实例化一个静态对象,不管之后会不会使用单例,都会占一定的内存,但是第一次调用的时候会很快,因为其资源已被初始化完成。
更多设计模式
:23种设计模式
原文传送门:https://blog.csdn.net/jason0539/article/details/44956775