日常工作开发过程中,项目中或多或少的会用到设计模式,或者在查看源码的过程中也会发现设计模式无处不在。为了使自己的代码漂亮,以及能力的提升,因此系统的开始学习设计模式且实现方式均以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.一些区分性对象,例如类型啊,状态等可以使用枚举
本文深入探讨了Java中的单例模式,包括懒汉式、双重锁懒汉式、饿汉式、静态内部类和枚举实现方式。详细分析了各种实现的优缺点,如线程安全、延迟加载等问题,并提到了反射和序列化对单例模式的挑战。同时,列举了Spring框架中ReactiveAdapterRegistry的单例实现作为示例。
2337

被折叠的 条评论
为什么被折叠?



