Java单例模式详解,包括各种方式的实现
单例对象(Singleton)是一种常用的设计模式。在 Java 应用中,单例对象能保证在一个 JVM中,该对象只有一个实例存在。这样的模式有几个好处:
- 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
- 省去了 new 操作符,降低了系统内存的使用频率,减轻 GC 压力。
- 有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。
单例模式有好几种实现方式,其中包括线程安全的和线程不安全的,接下来就实现一遍。
饿汉式的单例模式
public class Singleton {
//构造器私有化,防止被实例化
private Singleton() {
}
//持有私有静态实例,防止被引用,饿汉式先创建对象
private static Singleton Instance = new Singleton();
//提供一个对外的方法调用
public static Singleton getInstance() {
return Instance;
}
//多线程验证是否安全
public static void main(String[] args) {
for(int i = 1; i <= 10; i++) {
new Thread(()->{
Singleton instance1 = Singleton.getInstance();
System.out.println(instance1.hashCode());
}).start();
}
}
}
可以看到,饿汉式的单例模式在多线程下是安全的,每个线程获得的都是同一个实例的哈希地址。但是饿汉式也是有缺点,不能实现延迟加载的策略,当数据过多时,提前加载,占据内存,显然不是我们想要的,我们大多数需要使用到的时候再加载实例,这就用到了懒汉式单例模式
懒汉式单例模式(不安全)
方式一:普通懒汉式,不安全
public class LazySingleton {
private LazySingleton() {
//测试输出代码(不必要)
System.out.println(Thread.currentThread().getName());
}
//初始不实例化,需要使用的时候才实例化
private static LazySingleton Instance;
//对外提供调用的方法,实现懒加载的效果
public static LazySingleton getInstance() {
if(Instance == null) {
Instance = new LazySingleton();
}
return Instance;
}
public static void main(String[] args) {
for(int i = 1; i <= 1000; i++) {
new Thread(()->{
LazySingleton.getInstance();
}).start();
}
}
}
可以看到,在多线程下哈希值不一样,说明不同的线程创建了多个实例。
方式二、改进懒汉式
首先会想到对 getInstance 方法加synchronized 关键字,其它的都不变。如下:
public static synchronized LazySingleton getInstance() {
if(Instance == null) {
Instance = new LazySingleton();
}
return Instance;
}
这样虽然线程安全了,但是每一次线程进入该方法中时都是阻塞式进入,每次都只允许一个线程进入,synchronized 关键字锁住的是这个对象,这样的用法,在性能上会有所下降,因为每次调用 getInstance(),都要对对象上锁(每进来一次都要上锁,就算已经实例化Instance,也要上锁),事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进。
方式三、双重检测,效率快许多
public static LazyMan getInstance() {
if(Instance == null) {
synchronized(LazyMan.class) {
if(Instance == null) {
Instance = new LazySingleton();
}
}
}
return Instance;
}
将 synchronized 关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在 instance 为 null,并创建对象的时候才需要加锁,性能有一定的提升。
但是,这样的情况,还是有可能有问题的,看下面的情况:在 Java 指令中创建对象和赋值操作是分开进行的,也就是说 instance = new LazySingleton();语句是分两步执行的。但是JVM 并不保证这两个操作的先后顺序,也就是说有可能 JVM 会为新的 Singleton 实例分配空间,然后直接赋值给 instance 成员,然后再去初始化这个 Singleton 实例。这样就可能出错了。
以 A、B 两个线程为例:
- A、B 线程同时进入了第一个 if 判断
- A 首先进入 synchronized 块,由于 instance 为 null,所以它执行 instance = new LazySingleton();
- 由于 JVM 内部的优化机制(指令重排),JVM 先画出了一些分配给 Singleton 实例的空白内存,并赋值给 instance 成员(注意此时 JVM 没有开始初始化这个实例),然后 A 离开了 synchronized块。
- B 进入 synchronized 块,由于 instance 此时不是 null,因此它马上离开了 synchronized
块并将结果返回给调用该方法的程序。 - 此时 B 线程打算使用 Singleton 实例,却发现它没有被初始化,于是错误发生了。
方式四、使用volatile关键字,防止指令重排,其它不变
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName());
}
private static volatile LazyMan Instance;
public static LazyMan getInstance() {
if(Instance == null) {
synchronized(LazyMan.class) {
if(Instance == null) {
Instance = new LazyMan();
}
}
}
return Instance;
}
public static void main(String[] args) {
for(int i = 1; i <= 1000; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
方式五、改进双重检测,效率一样,防止以上问题
private static synchronized void InitIns() {
if(Instance == null) {
Instance = new LazySingleton();
}
}
public static LazyMan2 getInstance() {
if(Instance == null) {
InitIns();
}
return Instance;
}
此方式也只会创建一次实例,线程安全,效率高
方式六、使用静态内部类的方式
public class LazySingleton {
private LazySingleton() {
}
//静态内部类
private static class SingletonFactory{
private static LazySingleton Instance = new LazySingleton();
}
public static synchronized LazySingleton getInstance() {
return SingletonFactory.Instance;
}
public static void main(String[] args) {
for(int i = 1; i <= 10; i++) {
new Thread(()->{
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(lazySingleton.hashCode());
}).start();
}
}
}
将原来的一个方法中的双重检测改为使用内部类方式。JVM 内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用 getInstance 的时候,JVM 能够帮我们保证 instance 只被创建一次,并且会保证把赋值给 instance 的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。
还没完
看了这么多的单例的实现,以为就OK了?no no no!上述单例模式看起来是安全的,但是一旦用到了反射,就会破坏单例模式的安全性。
public class LazySingleton2 {
private LazySingleton2() {
}
private static volatile LazySingleton2 Instance = null;
public static LazySingleton2 getInstance() {
if(Instance == null) {
synchronized (LazySingleton2.class) {
if(Instance == null) {
Instance = new LazySingleton2();
}
}
}
return Instance;
}
public static void main(String[] args) throws Exception{
LazySingleton2 instance1 = LazySingleton2.getInstance();
Constructor<LazySingleton2> declaredConstructor = LazySingleton2.class.getDeclaredConstructor(null);
//破坏private
declaredConstructor.setAccessible(true);
LazySingleton2 instance2 = declaredConstructor.newInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
可以看到,两个实例的哈希值不同,破坏了单例模式。
此时我们可以在加一个检测,检测是否使用反射创建对象
只需要在构造方法中添加
private LazySingleton2() {
if(Instance != null) {
throw new RuntimeException("请不要使用反射破坏单例");
}
}
我们一个实例用普通方式创建,一个用反射创建,如果使用反射创建对象,破坏了单例之后,就会抛出异常,到时处理异常即可。
但是这就安全了吗?
现在尝试使用多个反射创建对象
public class LazySingleton2 {
private LazySingleton2() {
if(Instance != null) {
throw new RuntimeException("请不要使用反射破坏单例");
}
}
private static volatile LazySingleton2 Instance = null;
public static LazySingleton2 getInstance() {
if(Instance == null) {
synchronized (LazySingleton2.class) {
if(Instance == null) {
Instance = new LazySingleton2();
}
}
}
return Instance;
}
public static void main(String[] args) throws Exception{
Constructor<LazySingleton2> declaredConstructor = LazySingleton2.class.getDeclaredConstructor(null);
//破坏private
declaredConstructor.setAccessible(true);
//这里创建对象发生改变
LazySingleton2 instance1 = declaredConstructor.newInstance();
LazySingleton2 instance2 = declaredConstructor.newInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
还是创建了两个不同的对象,破坏了单例。(⊙﹏⊙)(⊙﹏⊙)(⊙﹏⊙),好烦啊!
设置标志位防止反射破坏
我们设置一个假设为qwdx的标志位
public class LazySingleton2 {
//在此处设置一个标志位
private static boolean qwdx = false;
private LazySingleton2() {
if(qwdx = false) {
qwdx = true;
}else {
throw new RuntimeException("请不要使用反射破坏单例");
}
}
private static volatile LazySingleton2 Instance = null;
public static LazySingleton2 getInstance() {
if(Instance == null) {
synchronized (LazySingleton2.class) {
if(Instance == null) {
Instance = new LazySingleton2();
}
}
}
return Instance;
}
public static void main(String[] args) throws Exception{
Constructor<LazySingleton2> declaredConstructor = LazySingleton2.class.getDeclaredConstructor(null);
//破坏private
declaredConstructor.setAccessible(true);
LazySingleton2 instance1 = declaredConstructor.newInstance();
LazySingleton2 instance2 = declaredConstructor.newInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
处理OK,通过一个标志位,解决反射创建对象破坏单例的问题。
结束了吗?没有,当我们知道该标志位的变量,我们就能用反射破坏它。如下:
public class LazySingleton2 {
private static boolean qwdx = false;
private LazySingleton2() {
if(qwdx == false) {
qwdx = true;
}else {
throw new RuntimeException("请不要使用反射破坏单例");
}
}
private static volatile LazySingleton2 Instance = null;
public static LazySingleton2 getInstance() {
if(Instance == null) {
synchronized (LazySingleton2.class) {
if(Instance == null) {
Instance = new LazySingleton2();
}
}
}
return Instance;
}
public static void main(String[] args) throws Exception{
Constructor<LazySingleton2> declaredConstructor = LazySingleton2.class.getDeclaredConstructor(null);
//破坏private
declaredConstructor.setAccessible(true);
//假设我们知道了标志位变量qwdx,我们就可以破坏它
Field field = LazySingleton2.class.getDeclaredField("qwdx");
field.setAccessible(true);
LazySingleton2 instance1 = declaredConstructor.newInstance();
//将第一个对象的标志位重置
field.set(instance1, false);
LazySingleton2 instance2 = declaredConstructor.newInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
此时,通过反射又得到了两个实例。单例又被破坏。我XXX
怎么能避免这些情况呢?JDK5提出了枚举方式
public class Test{
public static void main(String[] args) {
EnumSingleton instance1 = EnumSingleton.getInstance();
EnumSingleton instance2 = EnumSingleton.getInstance();
System.out.println(instance1 == instance2);
}
}
enum EnumSingleton{
INSTANCE;
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
结果返回 true,证明是同一个对象。
使用反射创建对象
public class Test{
public static void main(String[] args) throws Exception{
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingleton instance1 = declaredConstructor.newInstance();
EnumSingleton instance2 = declaredConstructor.newInstance();
System.out.println(instance1 == instance2);
}
}
enum EnumSingleton{
INSTANCE;
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
结果抛出异常,没有找到该无参构造器的异常,但是枚举类中明明有无参构造器,为什么抛出该异常?
我们反编译一下看看
通过反编译发现其中有一个空参的构造方法(倒数第三行),但是我们在使用反射时,通过空参构造器得到对象发生异常,异常显示无该构造器,说明枚举类在编译时期是一个“欺骗性”的类,可以防止反射创建对象和破坏单例。
我勒个去,单例涉及到多线程时这么复杂啊!加油(ง •_•)ง💪!!!