设计模式[java]-单例模式

本文深入探讨了Java中的单例模式,包括懒汉式、双重锁懒汉式、饿汉式、静态内部类和枚举实现方式。详细分析了各种实现的优缺点,如线程安全、延迟加载等问题,并提到了反射和序列化对单例模式的挑战。同时,列举了Spring框架中ReactiveAdapterRegistry的单例实现作为示例。

设计模式[java]-单例模式

日常工作开发过程中,项目中或多或少的会用到设计模式,或者在查看源码的过程中也会发现设计模式无处不在。为了使自己的代码漂亮,以及能力的提升,因此系统的开始学习设计模式且实现方式均以java语言实现。

懒汉式

懒汉式,顾名思义,形容跟人一样很懒,用到得时候才会想起。就跟我们农村说话比较粗鲁得那种,屎憋到沟门子了,么办法了,才去做这件事。在java中创建对象也是一个道理。使用这种方式就是当用到该对象的时候才会去创建。具体的实现方式:
public class SingTon2 {

/**
 * 构造器私有
 */
private SingTon2(){

}

private static SingTon2 singTon= null;

/**
 * 向外暴露统一调用的方式
 * @return
 */
public static SingTon2 getInstance(){
    if(singTon == null){ 
        singTon = new SingTon2();
    }
    return singTon;
}

}
该类在使用到getInstance()方法时才会对singTon实例进行创建,单线程情况下是没有任何问题,但是在多线程情况下存在线程不安全问题,因为在线程A 在singTon == null判断过程后准备创建singTon实例的时候,线程A线程处理其他问题了,另外线程B也判断singTon == nul 了,从而执行 singTon = new SingTon2(),但是后面线程A忙完其他事情又回来执行singTon = new SingTon2()创建实例,因此当前对象就存在两个实例了。因此存在线程不安全。
优点:延迟加载
缺点:线程不安全

双重锁懒汉式
处理线程不安全可以使用加同步锁处理,加锁一般使用同步代码块和同步方法,同步方法会每次调用方案的时候都会操作锁,浪费资源,因此使用同步代码块是解决懒汉式的有效方式。
public class SingleTonTest2 {

/**
 * 构造器私有
 */
private SingleTonTest2() {
}

/**
 * 定义静态属性
 * 使用双重校验锁
 * 但是因为singleTonTest = new SingleTonTest2();对于虚拟机做了三件事
 * 1.给singleTonTest分配内存
 * 2.调用SingleTonTest2的构造器初始化成员变量
 * 3.将singleTonTest对象指向分配的内存空间
 * 因为在jvm即时编译器中存在指令重排序的优化,所以第2,3步操作顺序可能不一定,有可能是1-2-3,也有可能1-3-2
 * 如果3先执行完毕,则在3执行完毕,2执行结束之前,singleTonTest对象已经不为null了,线程被占用了,其他线程调用的话该对象已经存在,
 * 所以直接会返回singleTonTest对象,使用的话自然就报错了
 */

/**
 * 解决方式
 * 使用volatile,主要作用是禁止指令重排序的优化
 * 在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。
 * 取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况
 */
private volatile static SingleTonTest2 singleTonTest = null;

public static SingleTonTest2 getInstance() {
    if (singleTonTest == null) {
        synchronized (SingleTonTest2.class) {
            if (singleTonTest == null) {
                singleTonTest = new SingleTonTest2();
            }
        }
    }
        return singleTonTest;
}

}

优点:线程安全,延迟加载。具体在开发实现,例如EventBus实例:
在这里插入图片描述
当然这种也不能确保是万无一失的。
1.使用反射破解单例
因为在java中存在反射,所有的私有的字段,构造器在他这显得无所遁形。例如:

   public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<SingleTonTest2> declaredConstructor = SingleTonTest2.class.getDeclaredConstructor(null);
        //设置可见性
        declaredConstructor.setAccessible(true);
        SingleTonTest2 singleTonTest2 = declaredConstructor.newInstance();
        SingleTonTest2 singleTonTest3 = declaredConstructor.newInstance();
        System.out.println(singleTonTest2 .hashCode()+ "====" + singleTonTest3.hashCode());
    }
   打印结果   13738737====17699851

很明显可以看出两个结果并不相同。当然我们也可以在构造器中添加响应的判断+ 标识位是否做到单例呢?

public class SingleTonTest2 {
    
    private static boolean flag ;

    /**
     * 构造器私有
     */
    private SingleTonTest2() {
        if(!flag){
            flag = true;
        }else{
            throw new RuntimeException("请不要通过反射方式进行创建对象");
        }
        
    }

    private volatile static SingleTonTest2 singleTonTest = null;

    public static SingleTonTest2 getInstance() {
        if (singleTonTest == null) {
            synchronized (SingleTonTest2.class) {
                if (singleTonTest == null) {
                    singleTonTest = new SingleTonTest2();
                }
            }
        }
        return singleTonTest;
    }

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        Constructor<SingleTonTest2> declaredConstructor = SingleTonTest2.class.getDeclaredConstructor(null);
        //设置可见性
        declaredConstructor.setAccessible(true);
        SingleTonTest2 singleTonTest2 = declaredConstructor.newInstance();

        Field flag = SingleTonTest2.class.getDeclaredField("flag");
        flag.set("flag", false);

        SingleTonTest2 singleTonTest3 = declaredConstructor.newInstance();
        System.out.println(singleTonTest2 .hashCode()+ "====" + singleTonTest3.hashCode());
    }

//获取结果  17699851====4252772

}

2.使用序列化破解。简单来说序列化就是java对象转化成字节序列的过程就是对象的序列化。反序列化就是将字节序列恢复为java对象的过程称为反序列化。
平时使用的场景:1.将java对象字节序列化后保存到磁盘中,存放到文件中 2.从文件中读取字节序列转化成java对象存放内存中
java对象实现serializable就可以序列化。然后通过序列化破坏单例模式

public class SingleTonTest2 implements Serializable {

    /**
     * 构造器私有
     */
    private SingleTonTest2() {

    }

    private volatile static SingleTonTest2 singleTonTest = null;

    public static SingleTonTest2 getInstance() {
        if (singleTonTest == null) {
            synchronized (SingleTonTest2.class) {
                if (singleTonTest == null) {
                    singleTonTest = new SingleTonTest2();
                }
            }
        }
        return singleTonTest;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //将对象序列化写入文件中
        SingleTonTest2 instance = SingleTonTest2.getInstance();
        OutputStream os = new FileOutputStream("SingleTonTest2.text");
        ObjectOutputStream oos = new ObjectOutputStream(os);
        oos.writeObject(instance);

        //从文件中读出字节序列反序列化成对象
        InputStream is = new FileInputStream("SingleTonTest2.text");
        ObjectInputStream ois = new ObjectInputStream(is);
        SingleTonTest2 instance1 = (SingleTonTest2) ois.readObject();
        System.out.println(instance + "=====" + instance1);
//关闭流

    }

//com.hdd.gof.single.SingleTonTest2@308db1=====com.hdd.gof.single.SingleTonTest2@141a79c
//我们可以看到打印结果,并不是一个对象
}

如何反序列只产生一个对象呢?待续。。。

最近看到一个帖子,感触颇深,我把代码发过来,spring源码中有一段:
ReactiveAdapterRegistry.class

public class ReactiveAdapterRegistry {
@Nullable
	private static volatile ReactiveAdapterRegistry sharedInstance;
	public static ReactiveAdapterRegistry getSharedInstance() {
		ReactiveAdapterRegistry registry = sharedInstance;
		if (registry == null) {
			synchronized (ReactiveAdapterRegistry.class) {
				registry = sharedInstance;
				if (registry == null) {
					registry = new ReactiveAdapterRegistry();
					sharedInstance = registry;
				}
			}
		}
		return registry;
	}
}

为啥要在这里面自定义一个局部变量接:
解释:https://www.javacodemonk.com/threadsafe-singleton-design-pattern-java-806ad7e6
https://www.jianshu.com/p/aa6a9a7035a9

饿汉式

饿汉式,就更比较理解了,就是形容比较饥饿,害怕被饿死了,刚开始就创建,天生自带。
实现方式:
public class SingTon1 {

/**
 * 构造器私有
 */
private SingTon1(){

}

private static SingTon1 singTon1 = new SingTon1();

/**
 * 向外暴露统一调用的方式
 * @return
 */
public static SingTon1 getInstance(){
    return singTon1;
}

}

静态代码块
public class SingTon2 {

/**
 * 构造器私有
 */
private SingTon2(){

}

private static SingTon2 singTon1 ;

static {
    singTon1 = new SingTon2();
}

/**
 * 向外暴露统一调用的方式
 * @return
 */
public static SingTon2 getInstance(){
    return singTon1;
}

静态代码块也是变相的饿汉式一种,直接在类加载的过程中就实例化了。
使用这两种方式:
优点:线程安全
缺点:没有延迟加载
jdk中的实例。Runtime类
在这里插入图片描述

静态内部类

静态内部类可以不依赖外部类的实例而被实例化
public class SingleTonTest3 {

private SingleTonTest3() {

}

static class SingleTon {

    private final static SingleTonTest3 singleTonTest = new SingleTonTest3();

}

public static SingleTonTest3 getInstance() {
    return SingleTon.singleTonTest;
}

}

优点:
1.通过反射,是不能从外部类读到内部类的属性的,很好的避免了反射入侵
2.线程安全
3.延迟加载
缺点:
1.需要两个类完成,虽然不会创建内部类对象,但是类对象还是要创建,而且是永久创建
2.创建的单例,后期一旦被销毁,永久销毁

枚举

effective java这种书中推荐,构造方法默认私有
public enum SingleTonTest4 {

INSTANCE;

public void read(){
    System.out.println(1);
}

}
调用枚举类中的方法更简单,SingleTonTest4.INSTANCE.read();
优点:简单,线程安全
缺点:不是延迟加载

单例模式可以有效的保证系统内存中该类对象的唯一,对于一些需要频繁创建销毁的对象,节省了系统资源,提高系统性能,并且提供了简单的调用方式
单例模式的使用场景:
1.工具类
2.读取资源文件类
3.一些区分性对象,例如类型啊,状态等可以使用枚举

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值