单例模式是一种常见的软件设计模式,单例对象的类必须保证只有一个实例存在
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
需要频繁的实例化然后销毁对象
创建对象时耗时过多或者消耗资源过多,但又经常用到的对象
有状态的工具类对象
工具类其实分两种场景
a. 有状态
b. 无状态
有没有状态看其是否有存储功能,
有状态就是有数据存储功能。
有状态对象就是有实例变量的对象,可以保存数据,是非线程安全的。
无状态就是一次操作,不能保存数据,
无状态对象就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。
如果你的工具类要连接多个数据源,那么就需要搞成单例,
如果只需要做解析等这种无状态的操作,那么就可以搞成静态类。
静态类是可以直接引用的,不用 new
频繁访问数据库或文件的对象
线程启动的时候就会创建对象
1 package 单例模式; 2 3 // 饿汉模式 4 public class Singleton { 5 /** 6 * 优点:没有线程安全问题、简单 7 * 缺点:提前初始化会延长类加载器加载类的时间;如果不使用会浪费内存空间;不能传递参数。 8 * */ 9 10 // 创建一个静态对象 11 // 类加载的时候就会实例化这个对象 12 private static final Singleton instance = new Singleton(); 13 14 private Singleton() {}; 15 16 public static Singleton getInstance() { 17 return instance; 18 } 19 }
调用的时候创建对象
1 // 懒汉模式 2 public class Singleton { 3 /** 4 * 优点:解决线程安全,延迟初始化( Effective Java推荐写法) 5 * */ 6 7 private Singleton() {}; 8 9 // 静态方法 10 /** 11 * 静态变量和静态方法的区别: 12 * 1. 静态变量和静态代码块是在类加载的时候执行 13 * 2. 静态方法是调用的时候才会初始化执行 14 * 15 * 懒汉模式和饿汉模式也同样是这个区别 16 * */ 17 public static SingletongetInstance() { 18 return Holder.LAOMA; 19 } 20 21 public static class Holder { 22 private static final SingletonLAOMA = new Singleton(); 23 } 24 }
懒汉模式是相对于饿汉模式而言的,在
JVM
进程启动并在我们主动使用该类的时候不会在内存中初始化一个单例对象,只有当我们调用getInstance()
的时候才去创建对象,它的创建是在我们调用getInstance()
静态方法之后。
1 package 单例模式.双重检查锁; 2 3 public class Singleton { 4 // 1. 使用 volatile 定义了实例的引用 5 private volatile static Singleton uniqueSingleton; 6 7 private Singleton() { 8 9 } 10 11 // 2. 调用 getIntence() 方法去初始化 12 // 双重检查顾名思义 13 // 第一次判断实例化对象是否为空, 14 // 然后通过 synchronized 去加锁 15 // 加锁是为了保证只有一个线程调用,解决并发问题 16 // 加完锁再判断一次实例化对象是否为空 17 // 然后调用 new 18 public Singleton getIntence() { 19 if(null == uniqueSingleton) { 20 synchronized (Singleton.class) { 21 if (null == uniqueSingleton) { 22 uniqueSingleton = new Singleton(); 23 } 24 } 25 } 26 27 return uniqueSingleton; 28 } 29 30 }
在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想的怎么排就怎么排,它需要满足以下两个条件:
在单线程环境下不能改变程序的运行结果
存在数据依赖关系的不允许重排序
编译器进行重排序,但是调用的时候是并发调用的,这个时候就会出问题,例如:
uniqueSingleton = new uniqueSingleton();
new 的过程不是一个原子操作,分为以下三步:
分配内存空间
初始化对象
将对象指向刚分配的内存空间
但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:
分配内存空间
将对象指向刚分配的内存空间
初始化对象
这个时候假如有两个线程 A 和 B,现在考虑重排序后,两个线程发生了以下调用:
Time | Thread A | Thread B |
---|---|---|
T1 |
检查到 uniqueSingleton 为空 |
|
T2 |
获取锁 | |
T3 |
再次检查到 uniqueSingleton 为空 |
|
T4 |
为 uniqueSingleton 分配内存空间 |
|
T5 |
将 uniqueSingleton 指向内存空间 |
|
T6 |
检查到 uniqueSingleton 不为空 |
|
T7 |
访问 uniqueSingleton (此时对象还未完成初始化) |
|
T8 |
初始化 uniqueSingleton |
首先 A 线程先过来,检查 uniqueSingleton
是否为空,为空则获取锁,再次检查一遍是否为空,为空则执行 new 操作,这里因为重排序,new 的操作现在如上所述:
分配内存空间
将对象指向刚分配的内存空间
初始化对象
先为 uniqueSingleton
分配内存空间,再将 uniqueSingleton
指向内存空间,这个时候并没有初始化,B 线程进来了,还是一样的操作,先检查 uniqueSingleton
是否为空,加锁,再次判断,然后同样给 uniqueSingleton
分配空间,将对象指向分配的内存空间,这个时候会发现,两个线程都初始化对象成功了,如果没有加 volatile,就会出现这种情况,重排序之后会实例化多个对象。
在添加 volatile 的情况下,假如又有两个线程过来了,首先第一个线程过来正常执行,先判断是否为空,然后加锁,这个时候,第二个线程过来,第一个线程在执行加锁的时候,还没有进行初始化,第二个线程过来之后也到了加锁这一步,如果两个线程同时加锁成功,到了初始化对象这一步的时候,两个对象都是空,在执行 new 操作会创建出多个对象来,这就是第二次判断的原因,但是,说到这里,认真的小伙伴可能会问,第一次判断有什么作用呢?因为性能的问题,加锁是一个很耗时的问题,所以我们要先做一下判断,阻拦一部分场景,比如,已经创建过的对象就不需要在加锁了
单例模式是不是完全单例的?
有没有办法破坏掉单例?
单例的破坏是可以做到的,这里需要通过反射
通过反射调用会生成多个实例
1 Singleton sc1 = Singleton.getInstance(); 2 Singleton sc2 = Singleton.getInstance(); 3 System.out.println(sc1); // sc1, sc2 是同一个对象 4 System.out.println(sc2); 5 /* 通过反射的方式直接调用私有构造器 */ 6 Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.laoma.Singleton"); 7 Constructor<Singleton> c = clazz.getDeclaredConstructor(null); 8 c.setAccessible(true); // 跳过权限检查 9 Singleton sc3 = c.newInstance(); 10 Singleton sc4 = c.newInstance(); 11 System.out.println("通过反射的方式获取的对象sc3:" + sc3); // sc3, sc4 不是同一个对象 12 System.out.println("通过反射的方式获取的对象sc4:" + sc4); 13 14 // 防止反射获取多个对象的漏洞 15 // 解决反射造成单例模式破坏的问题 16 private Singleton() { 17 if(null !== SingletonClassInstance.instance) 18 throw new RuntimeException(); 19 }
原文:https://www.cnblogs.com/MAQ-space/p/14411111.html