【设计模式系列6】单例模式的8种写法及如何防止单例被破坏

懒汉式单例的特点是:被外部类调用的时候内部类才会加载

示例1(普通写法)

package com.zwx.design.pattern.singleton.lazy;

public class LazySingleton {

private static LazySingleton lazySingleton = null;

private LazySingleton() {

}

public static LazySingleton getInstance(){

if(null == lazySingleton){

lazySingleton = new LazySingleton();

}

return lazySingleton;

}

}

上面的写法是最简单的一种懒汉式单例写法,但是存在线程安全问题,多线程情况下会有一定几率返回多个单例对象,这明显违背了单例对象原则,那么如何优化上面的代码呢?答案就是加上synchronized关键字

示例2(synchronized写法)

package com.zwx.design.pattern.singleton.lazy;

import com.zwx.design.pattern.singleton.hungry.HungrySingleton;

public class LazySingleton {

private static LazySingleton lazySingleton = null;

private LazySingleton() {

}

public synchronized static LazySingleton getInstance(){

if(null == lazySingleton){

lazySingleton = new LazySingleton();

}

return lazySingleton;

}

}

示例2的写法仅仅是在getInstance()方法上面加了synchronized关键字,其他地方没有任何变化。用 synchronized 加锁,在线程数量比较多情况下,如果CPU分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。那么,有没有一种更好的方式,既兼顾线程安全又提升程序性能呢?答案是肯定的。接下来就在介绍一种双重检查锁(double-checked locking)单例写法

示例3(DCL写法)

package com.zwx.design.pattern.singleton.lazy;

public class LazyDoubleCheckSingleton {

private volatile static LazyDoubleCheckSingleton lazySingleton = null;

private LazyDoubleCheckSingleton() {

}

public static LazyDoubleCheckSingleton getInstance(){

if(null == lazySingleton){//1

synchronized (LazyDoubleCheckSingleton.class){//2

if(null == lazySingleton){//3

lazySingleton = new LazyDoubleCheckSingleton();//4

}

}

}

return lazySingleton;//5

}

}

这里的写法将同步放在了方法里面的第一个非空判断之后,这样可以确保对象不为空的时候不会被阻塞,但是第二个非空判断的意义是什么呢?我们假设线程A首先获得锁,进入了第3行,还没有释放锁的时候,线程B又进来了,这时候因为线程还没有执行对象初始化,所以判空成立,会进入第2行等待获得锁,这时候当线程A释放锁之后,线程B会进入到第3行,这时候因为第二个判空判断对象不为空了,所以就会直接返回,如果没有第2个判空,这时候就会产生新的对象了,所以需要两次判空!

大家可能注意到这里的变量定义上加了volatile关键字,为什么呢?这是因为DCL在可能会存在失效的情况:

第4行代码:lazySingleton = new LazyDoubleCheckSingleton();

大致存在以下三步:

(1)、分配内存给对象

(2)、初始化对象

(3)、将初始化好的对象和内存地址建立关联(赋值)

而这3步由于CPU指令重排序,不能保证一定按顺序执行,假如线程A正在执行new的操作,第1步和第3步都执行完了,但是第2步还没执行完,这时候线程B进入到方法中的第1行代码,判空不成立,所以直接返回了对象,而这时候对象并没有初始化完全,所以就会报错了,解决这个问题的办法就是使用volatile关键字,禁止指令重排序(jdk1.5之后),保证按顺序执行上面的三个步骤。想要详细了解volatile关键字是如何解决重排序问题的,可以点击这里

示例4(内部类写法)

package com.zwx.design.pattern.singleton.lazy;

public class LazyInnerClassSingleton {

private LazyInnerClassSingleton(){

}

public static final LazyInnerClassSingleton getInstance(){

return LazyHolder.LAZY;

}

private static class LazyHolder{

private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();

}

}

上面的写法巧妙的利用了内部类的特性,LazyHolder里面的逻辑需要等到外面方法调用时才执行。

这种写法看起来很完美,没有加锁,也保证了懒加载,但是这种单例模式也有问题,那就是可以被反射或者序列化破坏单例,下面我们写一个反射破坏单例的例子

package com.zwx.design.pattern.singleton.lazy;

import java.lang.reflect.Constructor;

public class LazyInnerClassSingletonTest {

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

Class<?> clazz = LazyInnerClassSingleton.class;

Constructor constructor = clazz.getDeclaredConstructor(null);

constructor.setAccessible(true);

Object o1 = constructor.newInstance();

Object o2 = LazyInnerClassSingleton.getInstance();

System.out.println(o1 == o2);//false

}

}

上面这个结果输出的结果为false,说明产生了2个对象,当然,要防止反射破坏单例很简单,我们可以把上面例子中的构造方法加一个判断就可以了:

private LazyInnerClassSingleton(){

//防止反射攻击

if(null != LazyHolder.LAZY){

throw new RuntimeException(“不允许构造多个实例”);

}

}

这样虽然防止了反射破坏单例,但是依然可以被序列化破坏单例,下面就让我们验证一下序列化是如何破坏单例的!

首先对上面的类实现序列化接口

public class LazyInnerClassSingleton implements Serializable

接下来开始对单例对象类进行序列化和反序列化测试:

package com.zwx.design.pattern.singleton.lazy;

import com.zwx.design.pattern.singleton.seriable.SeriableSingleton;

import java.io.*;

import java.lang.reflect.Constructor;

public class LazyInnerClassSingletonTest {

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

LazyInnerClassSingleton s1 = null;

LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();

FileOutputStream fos = null;

try {

fos = new FileOutputStream(“LazyInnerClassSingleton.obj”);

ObjectOutputStream oos = new ObjectOutputStream(fos);

oos.writeObject(s2);

oos.flush();

oos.close();

FileInputStream fis = new FileInputStream(“LazyInnerClassSingleton.obj”);

ObjectInputStream ois = new ObjectInputStream(fis);

s1 = (LazyInnerClassSingleton)ois.readObject();

ois.close();

System.out.println(s1);

System.out.println(s2);

System.out.println(s1 == s2);//false

}catch (Exception e){

e.printStackTrace();

}

}

}

这时候输出结果为false,说明产生了2个对象,那么我们应该如何防止序列化破坏单例呢?我们可以对LazyInnerClassSingleton类加上readResolve方法就可以防止序列化破坏单例

package com.zwx.design.pattern.singleton.lazy;

import java.io.Serializable;

public class LazyInnerClassSingleton implements Serializable {

private LazyInnerClassSingleton(){

//防止反射攻击

if(null != LazyHolder.LAZY){

throw new RuntimeException(“不允许构造多个实例”);

}

}

//防止序列化破坏单例

private Object readResolve(){

return LazyHolder.LAZY;

}

public static final LazyInnerClassSingleton getInstance(){

return LazyHolder.LAZY;

}

private static class LazyHolder{

private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();

}

}

这是因为JDK源码中会检验一个类中是否存在一个readResolve()方法,如果存在,则会放弃通过序列化产生的对象,而返回原本的对象,也就是说,在校验是否存在readResolve()方法前产生了一个对象,只不过这个对象会在发现类中存在readResolve()方法后丢掉,然后返回原本的单例对象,保证了单例的唯一性,这种写法虽然保证了单例唯一,但是过程中类也是会被实例化两次,假如创建对象的频率增大,就意味着内存分配的开销也随之增大,那么有没有办法从根本上解决问题呢?那么下面就让继续介绍一下注册式单例

三、注册式单例


注册式单例就是将每一个实例都保存到某一个地方,然后使用唯一的标识获取实例

示例1(容器式)

package com.zwx.design.pattern.singleton.register;

public class ContainerSingleton {

private ContainerSingleton(){

}

private static Map<String,Object> ioc = new ConcurrentHashMap<>();

public static Object getBean(String className){

synchronized (ioc){

if(!ioc.containsKey(className)){

Object obj = null;

try {

obj = Class.forName(className).newInstance();

ioc.put(className,obj);

}catch (Exception e){

e.printStackTrace();

}

return obj;

}

return ioc.get(className);

}

}

}

容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的,spring中的单例就是属于此种写法

示例2(枚举式)

package com.zwx.design.pattern.singleton.register;

public enum EnumSingleton {

INSTANCE;

private Object data;

public Object getData() {

return data;

}

public void setData(Object data) {

this.data = data;

}

public static EnumSingleton getInstance(){

return INSTANCE;

}

}

枚举式单例是《Effective java》一书中推荐的写法,这种写法避免了上面的内部类写法中存在的问题(虽然结果唯一,但是过程产生了多个实例对象),是一种效率较高的写法

四、ThreadLocal式单例


ThreadLocal不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全

示例

package com.zwx.design.pattern.singleton.threadlocal;

public class ThreadLocalSingleton {

private ThreadLocalSingleton() {

}

private static final ThreadLocal singleton =

new ThreadLocal() {

@Override

protected ThreadLocalSingleton initialValue() {

return new ThreadLocalSingleton();

}

};

public static ThreadLocalSingleton getInstance(){

return singleton.get();

}

}

测试

package com.zwx.design.pattern.singleton.threadlocal;

import com.zwx.design.pattern.singleton.ExectorThread;

import com.zwx.design.pattern.singleton.ExectorThread3;

public class ThreadLocalSingletonTest {

分享

这次面试我也做了一些总结,确实还有很多要学的东西。相关面试题也做了整理,可以分享给大家,了解一下面试真题,想进大厂的或者想跳槽的小伙伴不妨好好利用时间来学习。学习的脚步一定不能停止!

薪酬缩水,“裸辞”奋战25天三面美团,交叉面却被吊打,我太难了

Spring Cloud实战

薪酬缩水,“裸辞”奋战25天三面美团,交叉面却被吊打,我太难了

Spring Boot实战

薪酬缩水,“裸辞”奋战25天三面美团,交叉面却被吊打,我太难了

面试题整理(性能优化+微服务+并发编程+开源框架+分布式)
}

};

public static ThreadLocalSingleton getInstance(){

return singleton.get();

}

}

测试

package com.zwx.design.pattern.singleton.threadlocal;

import com.zwx.design.pattern.singleton.ExectorThread;

import com.zwx.design.pattern.singleton.ExectorThread3;

public class ThreadLocalSingletonTest {

分享

这次面试我也做了一些总结,确实还有很多要学的东西。相关面试题也做了整理,可以分享给大家,了解一下面试真题,想进大厂的或者想跳槽的小伙伴不妨好好利用时间来学习。学习的脚步一定不能停止!

[外链图片转存中…(img-heojU84V-1721165751635)]

Spring Cloud实战

[外链图片转存中…(img-m1pruKqS-1721165751635)]

Spring Boot实战

[外链图片转存中…(img-eIvqBB5U-1721165751636)]

面试题整理(性能优化+微服务+并发编程+开源框架+分布式)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值