单例模式分析
- 一个单例类只能有一个实例
- 单例类必须自行创建这个实例
- 单例类必须保证全局其他对象都能唯一访问到它
单例模式的对象职责
- 保证一个类只有一个实例
- 为该实例提供一个全局访问节点
为什么使用单例模式
系统资源有限
- 控制某些共享资源(例如,数据库或文件)的访问权限
- 同时读写同一个超大的AI模型文件,或使用外部进程式服务
需要表示为全局唯一的对象
- 系统要求提供一个唯一的序列号生成器
使用单例模式的优势
- 对有限资源的合理利用,保护有限的资源,防止资源重复竟抢
- 更高内聚的代码组件,能提升代码复用性
- 具备全局唯一访问点的权限控制,方便按照统一规则管控权限
- 从负载均衡角度考虑,可以轻松地将 Singleton 扩展成两个、三个或更多实例
由于封装了基数问题,所以在适当的时候可以自由更改实例的数量
使用单例模式的劣势
- 作为全局变量使用时,引用的对象越多,代码修改影响的范围也越大
- 作为全局变量时,在全局变量中使用状态变量时,会造成加/解锁的性能损耗
- 即便能扩展多实例,但耦合性依然很高,因为隐蔽了不同对象之间的调用关系
- 不支持有参数的构造函数
常见的单例模式应用和使用的解决方案
编码实现
饿汉式初始化
饿汉式初始化区别于字面意思中饿汉对食物(资源)的迫切需求,它在开始时无论是否需要/使用这个资源,在类加载时将其初始化。更像是悲观的、贪婪的,在使用之前准备好所需资源。
饿汉式初始化带来的弊端也很明显,程序起初并不需要这些资源。当初始化资源过多时,这些无用资源将会占用系统大量空间,不利于系统运行效率。
package com.designpatternsdemo.singleton;
/**
* <h1>饿汉式单例模式</h1>
*
* @author L
*/
public class HungrySingleton{
private static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){};
public static HungrySingleton getHungrySingleton(){
return hungrySingleton;
}
}
懒汉式初始化
懒汉式和饿汉式的区别在于资源初始化时间的不同,饿汉式在程序运行时初始化资源。懒汉式在即将调用时初始化资源。
package com.designpatternsdemo.singleton;
/**
* <h1>懒汉式初始化</h1>
*
* @author L
*/
public class LazyInitialization{
private static LazyInitialization lazyInitialization;
private LazyInitialization(){
}
public static LazyInitialization getLazyInitialization(){
if(lazyInitialization == null){
//首次调用初始化变量
lazyInitialization = new LazyInitialization();
}
return lazyInitialization;
}
}
编写完成后,我们通过多线程测试其是否线程安全
package com.designpatternsdemo.singleton;
/**
* <h1>主程序类</h1>
*
* @author L
*/
public class Main{
public static void main(String[] args){
Thread thread1 = new Thread(() -> System.out.println(LazyInitialization.getLazyInitialization())
);
Thread thread2 = new Thread(() -> System.out.println(LazyInitialization.getLazyInitialization())
);
Thread thread3 = new Thread(() -> {
try{
//将thread3睡眠200毫秒,等待上一线程初始化完成
Thread.sleep(200);
System.out.println(LazyInitialization.getLazyInitialization());
} catch(InterruptedException e){
throw new RuntimeException(e);
}
});
thread1.start();
thread2.start();
thread3.start();
}
}
/*
控制台打印信息:
已连接到目标 VM, 地址: ''127.0.0.1:11500',传输: '套接字''
com.designpatternsdemo.singleton.LazyInitialization@b16e737
com.designpatternsdemo.singleton.LazyInitialization@6334f86d
com.designpatternsdemo.singleton.LazyInitialization@6334f86d
与目标 VM 断开连接, 地址为: ''127.0.0.1:11500',传输: '套接字''
*/
我们通过控制台打印信息可以看到,在 thread1 和 thread2 两个线程同时访问变量时,分别拿到了不同的变量值。
这是因为在创建变量时
public static LazyInitialization getLazyInitialization(){
if(lazyInitialization == null){
//首次调用初始化变量
lazyInitialization = new LazyInitialization();
}
return lazyInitialization;
}
第一个线程在来到 if 时进入创建变量,当还未创建完成时,lazyInitialization 此时依旧为空,但是 thread2 已经进入 if 判断,此时就有了两次赋值,且 thread1 拿到的数值被后续线程覆盖,成为错误数据。
以下为改进代码:
package com.designpatternsdemo.singleton;
/**
* <h1>懒汉式初始化</h1>
*
* @author L
*/
public class LazyInitialization{
private static LazyInitialization lazyInitialization;
private LazyInitialization(){
}
public static synchronized LazyInitialization getLazyInitialization(){
if(lazyInitialization == null){
//首次调用初始化变量
lazyInitialization = new LazyInitialization();
}
return lazyInitialization;
}
}
/*
已连接到目标 VM, 地址: ''127.0.0.1:13636',传输: '套接字''
com.designpatternsdemo.singleton.LazyInitialization@1297b437
com.designpatternsdemo.singleton.LazyInitialization@1297b437
com.designpatternsdemo.singleton.LazyInitialization@1297b437
与目标 VM 断开连接, 地址为: ''127.0.0.1:13636',传输: '套接字''
*/
双重校验锁模式
经过高并发测试可以发现,由于synchronized的互斥性每次只能由一个线程获取值,多个线程同时获取时,访问速度大大降低。经过需求分析,只在初始化赋值时多线程访问才会出现问题,初始化后获取变量值时,是不必要对变量设置访问限制的。于是就有了双重校验锁模式,即能保证线程安全,又可以保证系统访问效率
package com.designpatternsdemo.singleton;
/**
* <h1>双重校验锁模式</h1>
* DCL,即 double-checked locking
*
* @author L
*/
public class DCL{
private static DCL dcl;
private DCL(){
}
public static DCL getDcl(){
if(dcl == null){
synchronized(DCL.class){
if(dcl == null){
//首次调用初始化变量
dcl = new DCL();
}
}
}
return dcl;
}
}
使用ThreadLocal实现
实现代码:
package com.designpatternsdemo.singleton;
/**
* <h1>使用ThreadLocal实现</h1>
*
* @author L
*/
public class ThreadLocalAccomplish{
private static boolean signal;
public static ThreadLocal<ThreadLocalAccomplish> accomplishThreadLocal = ThreadLocal.withInitial(() -> new ThreadLocalAccomplish(true));
/*
public static ThreadLocal<ThreadLocalAccomplish> accomplishThreadLocal = new ThreadLocal<ThreadLocalAccomplish>(){
@Override
protected ThreadLocalAccomplish initialValue (){
return new ThreadLocalAccomplish();
}
};
*/
private ThreadLocalAccomplish(){
}
private ThreadLocalAccomplish(boolean signal){
ThreadLocalAccomplish.signal = signal;
//初始化时使test+1
//ThreadLocalAccomplish.signal=!ThreadLocalAccomplish.signal;
}
public static ThreadLocalAccomplish getAccomplishThreadLocal(){
return accomplishThreadLocal.get();
}
public static Boolean getSignal(){
return signal;
}
public static void setSignal(Boolean signal){
ThreadLocalAccomplish.signal = signal;
}
}
主程序代码:
package com.designpatternsdemo.singleton;
/**
* <h1>主程序类</h1>
*
* @author L
*/
public class Main{
public static void main(String[] args){
Thread thread1 = new Thread(() -> {
System.out.println("thread1:" + ThreadLocalAccomplish.getAccomplishThreadLocal());
System.out.println("thread1:" + ThreadLocalAccomplish.getSignal());
});
Thread thread2 = new Thread(() -> {
System.out.println("thread2:" + ThreadLocalAccomplish.getAccomplishThreadLocal());
System.out.println("thread2:" + ThreadLocalAccomplish.getSignal());
ThreadLocalAccomplish.setSignal(false);
System.out.println("thread2:" + ThreadLocalAccomplish.getSignal());
});
Thread thread3 = new Thread(() -> {
try{
Thread.sleep(200);
} catch(InterruptedException e){
throw new RuntimeException(e);
}
System.out.println("thread3:" + ThreadLocalAccomplish.getAccomplishThreadLocal());
System.out.println("thread3:" + ThreadLocalAccomplish.getSignal());
});
thread1.start();
thread2.start();
thread3.start();
}
}
/*
已连接到目标 VM, 地址: ''127.0.0.1:4080',传输: '套接字''
thread1:com.designpatternsdemo.singleton.ThreadLocalAccomplish@669a3b49
thread1:true
thread2:com.designpatternsdemo.singleton.ThreadLocalAccomplish@38e66a6e
thread2:true
thread2:false
thread3:com.designpatternsdemo.singleton.ThreadLocalAccomplish@36b46f86
thread3:true
与目标 VM 断开连接, 地址为: ''127.0.0.1:4080',传输: '套接字''
进程已结束,退出代码0
*/
可以发现ThreadLocal为每个变量分别提供了独立的变量副本,线程之间可以做到同时访问互不影响。从而解决了多线程之间访问数据冲突的问题。同时也省去了线程之间的同步操作。