多线程Debug窥探单例模式
1. 懒汉式单例模式
通过延迟初始化,降低单例创建期间的资源开销。
懒汉式单例实现,存在线程安全问题
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (lazySingleton == null) {// 断点,suspend:thread
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
线程任务
@Slf4j
public class T implements Runnable {
@Override
public void run() {
LazySingleton instance = LazySingleton.getInstance();
log.info("{} - {}", Thread.currentThread(), instance);
}
}
在主线程中创建两个线程任务T,通过IDEA多线程Debug控制线程的执行顺序,使其在控制台输出不同的实例。
private void singleton() {
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
log.info("end");
// 断点,suspend:thread
}
备注:IDEA如何实现多线程Debug
断点-右键断点-挂起选择thread模式
1.1. 使用
synchronized
解决懒汉式线程安全问题
public synchronized static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
这时候在使用多线程Debug,可以发现其中一个线程任务处于阻塞状态。
这种方法性能开销较大。
备注:synchronized锁静态方法相当于锁整个类文件,synchronized锁普通方法相当于锁堆内存中的对象实例。
1.2. 使用
double check
双重检查解决懒汉式线程安全问题
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton;
private LazyDoubleCheckSingleton() {}
public static LazyDoubleCheckSingleton getInstance() {
if (lazyDoubleCheckSingleton == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazyDoubleCheckSingleton == null) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
存在指令重排序问题,即
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
这行代码实际上执行了三个步骤:- 分配堆内存
- 初始化对象(类加载之后,被线程使用之前)
- 将栈中的变量指向堆内存
解决指令重排序问题:
1)不允许指令重排序。使用
volatile
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton;
通过
volatile
配合double check
双重检查的懒汉式单例模式不仅解决了线程安全问题,而且还兼顾到了性能,这种方式会比直接使用synchronized
来得更好点。2)允许指令重排序,但其它线程不可见,通过
静态内部类
解决。public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {}
private static class InnerClass {
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.staticInnerClassSingleton;
}
}
Class对象的初始化锁,即哪个线程拿到InnerClass对象的初始化锁,哪个对象就去初始化它,而其它线程属于非构造线程,所以即使构造期间出现了指令重排序,那么其它线程也是看不见的。
2. 饿汉式单例模式 使用
final static
修饰实例对象使类在加载完成之后,完成实例对象的初始化,且不能再修改。public class HungrySingleton {
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
这种方法使类加载完成之后就完成实例对象的初始化,若应用里没有使用到这个单例对象就会造成内存资源的浪费。与懒汉式不同,懒汉式是需要使用时才去初始化对象,所以不用考虑资源浪费问题。但是懒汉式存在线程安全问题,而饿汉式则没这种问题,开发者可以根据实际的业务需求选择最恰当的解决方案。
3. 破坏单例模式 序列化反序列化会破坏单例模式:
序列化和反序列化时获取到的是两个不同的实例对象,因为反序列化时,会判断单例类中是否存在
readReslove
方法,如果有的话会调用这个方法返回单例对象,否则会通过反射机制重新获取一个新的单例对象。反射攻击也会破坏单例模式。
【多线程Debug窥探单例模式】使用枚举实现单例,1)枚举单例类不受序列化反序列化破坏,2)屏蔽反射攻击
推荐阅读
- 致离开的人
- 急速瘦身|急速瘦身 就趁现在
- 谈写作牛人堂|谈写作牛人堂|齐帆齐(在写作这?条路上,你还能坚持多久())
- 顺势而为的成熟
- 处理线程池内错误信息打印问题
- 保险的保额与未来通胀
- 吴桂昌(做一家温暖又赚钱的旅行社---技多不压身之分享技能)
- 如何面对那些不舒服的室友
- 三种I/O多路复用方式优缺点比较
- 生活多艰难