回字有四种写法,那你知道单例有五种写法吗 (2)

每一次调用getINSTANCE()方法都会上锁,这是完全没有必要的嘛,因为只有对象还没有实例化的时候我才需要上锁以保证线程安全,对象都实例化了,自然也不用担心后续的调用会new出新的对象。 所以我们这个锁,可以加在if (INSTANCE == null) 判断语句块里面:

public class Singleton03 { private Singleton03() {} private static Singleton03 INSTANCE; public static Singleton03 getINSTANCE() { if (INSTANCE == null) { // 只有在对象还没有实例化的时候才上锁 synchronized (Singleton03.class) { INSTANCE = new Singleton03(); } } return INSTANCE; } }

这样就能节约一些性能,但是这样并没有做到线程安全哦! 因为很多线程进入到if (INSTANCE == null) 判断语句后,虽说是因为锁不能同时new对象了,但是如果锁一旦释放,那么其他线程依然会执行到INSTANCE = new Singleton03()语句,从而破坏了单例。所以在synchronized代码块内还要加一层判断:

public class Singleton03 { private Singleton03() {} // 注意,使用双重检验写法要加上volatile关键字,避免指令重排(有个印象就行,这不是本文的重点) private static volatile Singleton03 INSTANCE; public static Singleton03 getINSTANCE() { if (INSTANCE == null) { // 只有在对象还没有实例化的时候才上锁 synchronized (Singleton03.class) { // 额外加一层判断 if (INSTANCE == null) { INSTANCE = new Singleton03(); } } } return INSTANCE; } }

synchronized代码块外面一层判断,里面一层判断,就是有名的双重检测(DCL)了!里面的这一层判断加了之后呢,第一个线程的锁一旦释放也不用担心了,因为此时对象已经实例化,后续的线程也执行不了new语句,从而保证了线程安全!

优缺点

优点:节约资源(只有需要该对象的时候才会实例化)

缺点:写法复杂,耗性能(还是上了锁,还是耗性能)

虽然双重校验比synchronized懒汉式写法减少了很多锁性能消耗,但毕竟还是上了锁,所以为了解决这个锁性能消耗问题了,又引申出下一种写法。

内部类

话不多说,直接上代码:

public class Singleton04 { // 老套路,将构造函数私有化 private Singleton04() {} // 声明一个内部类,内部类里持有实例的引用 private static class Inner { public static final Singleton04 INSTANCE = new Singleton04(); } // 公共方法 public static Singleton04 getINSTANCE() { return Inner.INSTANCE; } }

这个写法非常像饿汉式写法,单例三元素还是那三元素,只不过多加了一个内部类,将实例引用放到内部类里而已。为啥要这样写呢?因为JVM保证了内部类的线程安全,即一个内部类在整个程序中不会被重复加载,并且如果你没有使用到内部类的话,是不会加载这个内部类的。这就非常巧妙的实现了线程安全以及节约资源的好处!

优缺点

优点:写法简单、节约资源(只有调用了getINSTANCE()方法才会加载内部类,才会实例化对象)、线程安全(JVM保证了内部类的线程安全)

缺点:会被序列化或者反射破坏单例

这个缺点可以说是吹毛求疵,因为之前所有写法都会被序列化、反射破坏单例。虽然说是吹毛求疵,但咱们搞技术的还是得做到了解全部细节,我来演示一下怎样破坏这个单例

通过反射破坏单例 public static void main(String[] args) throws Exception { // 创建100个线程同时访问实例 for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Singleton04.getINSTANCE().hashCode()); }).start(); } // 反射破坏单例 Class<Singleton04> clazz = Singleton04.class; // 拿到无参构造函数并将其设置为可访问,无视private Constructor<Singleton04> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); // 创建对象 Singleton04 singleton04 = constructor.newInstance(); System.out.println("反射:" + singleton04.hashCode()); }

运行结果如下:

... 2115147268 2115147268 反射:1078694789 2115147268 2115147268 ...

如果是通过正常的访问实例方法,是完全可以做到单例的要求,但是如果用反射的形式来创建一个对象,则就破坏了单例,一个程序中就出现了多个不同的实例对象。那么为了解决这个吹毛求疵的问题,聪明的前辈们想到了一个完美的写法!

枚举 // 注意,这里是枚举 public enum Singleton05 { // 实例 INSTANCE; // 公共方法 public static Singleton05 getINSTANCE() { return INSTANCE; } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwfsjd.html