线程安全单例
饿汉单例
// 问题1:为什么加 final // 防止被继承重写父类方法 public final class Singleton implements Serializable { // 问题3:为什么设置为私有? 是否能防止反射创建新的实例? // 不能防止反射创建新的实例,可以防止调用构造方法创建新实例。 private Singleton() {} // 问题4:这样初始化是否能保证单例对象创建时的线程安全? // 能 private static final Singleton INSTANCE = new Singleton(); // 问题5:为什么提供静态方法来获取实例INSTANCE,而不是直接将 INSTANCE设置为public,说出你知道的理由 // 懒惰初始化 封装性 泛型 初始化细节处理 public static Singleton getInstance() { return INSTANCE; } // 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例 // 添加readResolve方法 public Object readResolve() { return INSTANCE; } }
ReadResolve
JAVA对象流序列化时的readObject,writeObject,readResolve是怎么被调用的_supermanL的博客
简而言之就是,当我们通过反序列化readObject()方法获取对象时会去寻找readResolve()方法,
如果readResolve方法不存在则直接返回从文件中反序列化出来的对象。
如果readResolve方法存在则按该方法的内容返回对象。
package 序列化; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.HashSet; import java.util.Set; public class ReadResolve<T> extends HashSet<T> { public Object readResolve() { HashSet hashSet = new HashSet(); hashSet.add("你好"); return hashSet; } public static void main(String[] args) throws Exception { Set<String> set = new ReadResolve<String>(); set.add("1"); set.add("2"); System.out.println( "解析之前:" + set); try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\set.obj"))) { oos.writeObject(set); } set.clear(); try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\set.obj"))) { set = (Set<String>) ois.readObject(); } System.out.println( "反序列化以后:" + set); } }
解析之前:[1, 2] 反序列化以后:[你好]
package 序列化; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.HashSet; import java.util.Set; public class ReadResolve<T> extends HashSet<T> { //去掉readResolve public static void main(String[] args) throws Exception { Set<String> set = new ReadResolve<String>(); set.add("1"); set.add("2"); System.out.println( "解析之前:" + set); try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\set.obj"))) { oos.writeObject(set); } set.clear(); try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\set.obj"))) { set = (Set<String>) ois.readObject(); } System.out.println( "反序列化以后:" + set); } }
解析之前:[1, 2] 反序列化以后:[1, 2]
懒汉单例-不安全版
package com.interview.bintest.singleton; /** * 下面是DCL单例懒汉式的demo */ public class DCLSingleton { /** * 因为是单例式,所以构造方法肯定是私有化 * 无法通过new的方式来创建对象 */ private DCLSingleton(){ } /** * 创建一个私有化静态成员变量,用于指向创建出来的单例 */ private static DCLSingleton dclSingleton; /** * 创建一个 公共静态 方法,提供给外部类调用 * 方法则返回单例式(方法内创建) */ public static DCLSingleton getInstance(){ //因为是懒汉式创建,所以我们需要判断一下是否已经被创建了 //没有则创建一个,有则直接返回该实例 if(dclSingleton==null){//A dclSingleton = new DCLSingleton();//B } return dclSingleton; } }
上面的代码在遇到多线程的时候就会产生问题,当x线程到达注释A处,判断完毕,条件成立,此时JVM把cpu的资源切换给y线程。
y线程同样到达A处,因为x线程并没有创建实例,所以y执行了注释B处的代码,即完成了单例的创建。之后线程x被重新唤醒。
因为x线程已经判断完了if中的条件,并且成立,于是x线程也执行了注释B处的代码,又创建了一个单例。
这样就产生了线程不安全的问题。这样问题就来了,new出了两个instance,这还能叫单例吗?
懒汉单例-安全版
于是我们通过synchronized同步代码块来解决这个问题,代码如下
package com.interview.bintest.singleton; /** * 下面是DCL单例懒汉式的demo */ public class DCLSingleton { /** * 因为是单例式,所以构造方法肯定是私有化 * 无法通过new的方式来创建对象 */ private DCLSingleton(){ } /** * 创建一个私有化静态成员变量,用于指向创建出来的单例 */ private static DCLSingleton dclSingleton; /** * 创建一个 公共静态 方法,提供给外部类调用 * 方法则返回单例式(方法内创建) */ public static DCLSingleton getInstance(){ //因为是懒汉式创建,所以我们需要判断一下是否已经被创建了 //没有则创建一个,有则直接返回该实例 synchronized (DCLSingleton.class){ if(dclSingleton==null){//A dclSingleton = new DCLSingleton();//B } } return dclSingleton; } }
此时解决了上面的问题。但是新问题来了。
如果在方法上加上synchronized修饰符,可以保证不会出线程问题了。但是这里有个很大(至少耗时比例上很大)的性能问题。
除了第一次调用时是执行了Singleton的构造函数之外,以后的每一次调用都是直接返回instance对象。
返回对象这个操作耗时是很小的,绝大部分的耗时都用在synchronized修饰符的同步准备上,因此从性能上来说很不划算。
因为synchronized的存在,每个线程在执行注释A的判断之前都会争抢锁,并且每个线程都要锁住了才能判断是否有实例存在。这样就导致了阻塞,因为同一时间下只能有一个线程执行synchronized里的语句,其余的线程都阻塞住。
我们能不能将注释A出的if条件判断提到外面将synchronized代码块包裹住?
问题还是一样的,假设2个线程都通过了判断。
其中一个线程先获得锁进行了创建,后一个线程因为过了判断,所以获得前一个线程释放的锁,又进行一次创建。
为了解决以上的问题,我们就需要进行两次判断,即双重检查锁定。代码如下
DCL懒汉单例
public final class Singleton { private Singleton() { } // 问题1:解释为什么要加 volatile ? // 防止指令重排序 private static volatile Singleton INSTANCE = null; public static Singleton getInstance() { //问题2:对比懒汉单例, 说出这样做的意义 //提高效率:除第一次创建对象之外,其它的线程在访问在第一个if中就返回了,因此不会走到同步块中。 if (INSTANCE != null) { return INSTANCE; } synchronized (Singleton.class) { // 问题3:为什么还要在这里加非空判断, 之前不是判断过了吗 // 防止第一次并发创建多个实例化需要。 if (INSTANCE != null) { // t2 return INSTANCE; } INSTANCE = new Singleton(); return INSTANCE; } } }
问题1:解释为什么要加volatile ?
除了第一次创建对象之外,其它的线程在访问在第一个if中就返回了,因此不会走到同步块中,已经完美了吗? 如上代码段中的注释:假设线程一执行到instance = new Singleton()这句,这里看起来是一句话,但实际上其被编译后在JVM执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情: 1)给instance实例分配内存; 2)初始化instance的构造器; 3)将instance对象指向分配的内存空间(注意到这步时instance就非null了) 如果指令按照顺序执行倒也无妨,但JVM为了优化指令,提高程序运行效率,允许指令重排序。如此,在程序真正运行时以上指令执行顺序可能是这样的: a)给instance实例分配内存; b)将instance对象指向分配的内存空间; c)初始化instance的构造器; 这时候,当线程一执行b)完毕,在执行c)之前,被切换到线程二上,这时候instance判断为非空,此时线程二直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)。 具体来说就是synchronized虽然保证了线程的原子性(即synchronized块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)。
根据以上分析可知,解决这个问题的方法是:禁止指令重排序优化,即使用volatile修饰变量。
问题2:为什么要使用2次判断?
说说双重检查加锁单例模式为什么两次if判断? 去掉内层判断:如果去掉内层if判断,就会实例化多次,这是显而易见的,这就违背了单例模式的单例二字。 去掉外层判断: 1.整个代码都加上了synchronzed,每次访问get方法都会进入同步代码块。效率太低。 2.当线程1走完了内层判断,对象实例化完成后。线程3也调用了getInstace函数,如果没有加外层的判断线程3还是要继续等待线程2的完成,而加上外层判断,就不需要等待了,直接返回了实例化的对象。 我的理解:外层的判断是为了提高效率,里层的判断就是防止第一次并发创建多个实例化需要。
静态内部类懒汉单例
内部类简单介绍
内部类分为对象级别和类级别。
类级内部类指的是,有static修饰的成员变量的内部类,静态内部类。
如果没有static修饰的成员变量的内部类被称为对象级内部类,非静态内部类。
类级内部类相当于其外部类的static成员,它的对象与外部类对象间不存在依赖关系,相互独立,因此可直接创建。
而对象级内部类的实例,是必须绑定在外部对象实例上的。
类级内部类只有在第一次被使用的时候才被会装载。
要想很简单地实现线程安全,可以采用静态初始化器的方式,它可以由JVM来保证线程的安全性,如恶汉式单例,这种实现方式,会在类装载的时候就初始化对象,有可能浪费一定的内存(假设你不需要的话),有一种方法能够让类装载的时候不去初始化对象,就是采用类级内部类,在这个类级内部类里面去创建对象实例。
有上面我们进行的测试可以得出结论,静态内部类和非静态内部类一样,都不会因为外部内的加载而加载(所以是懒汉),同时静态内部类的加载不需要依附外部类,在使用时才加载,不过在加载静态内部类的过程中也会加载外部类
代码如下:
public final class Singleton { private Singleton() { } // 问题1:属于懒汉式还是饿汉式 // 懒汉 private static class LazyHolder { static final Singleton INSTANCE = new Singleton(); } // 问题2:在创建时是否有并发问题 // no public static Singleton getInstance() { return LazyHolder.INSTANCE; } }
Cas实现单例模式
public class Singleton { private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>(); private Singleton() { System.out.println("我被初始化了"); CasSingletonTest.objectcount.getAndIncrement(); } public static Singleton getInstance() { for (;;) { Singleton singleton = INSTANCE.get(); if (null != singleton) { return singleton; } singleton = new Singleton(); if (INSTANCE.compareAndSet(null, singleton)) { return singleton; } } } }
这是网上一位大牛的实现,他的这种非锁 CAS 实现的单例,挺好的。但是平时可能没有人使用,比用锁稍微复杂了一点,这也是为什么没有被列入单例模式的 7 大写法之中了。我在他的基础上,也就是他的构造方法里添加了两行代码。
我主要是想看看它到底是实例化了几次。加上这两行代码,可以方便我观察控制台,和统计实例化的总次数。
然后,我的测试代码如下:
package com.xttblog.canal.test; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; /** * CasSingletonTest * @author www.xttblog.com * @date 2019/2/27 下午2:39 */ public class CasSingletonTest { public static AtomicInteger objectcount = new AtomicInteger(); public static void main(String[] args) throws InterruptedException { final CountDownLatch begin = new CountDownLatch(1); final CountDownLatch last = new CountDownLatch(1000); for(int i=0;i<1000;i++){ new Thread(new Runnable() { @Override public void run() { try { //1.所有的线程都会阻塞在这 begin.await(); System.out.println (Thread.currentThread().getName()+":begin..."); //3.阻塞的1000个线程并发执行 Singleton sba = Singleton.getInstance(); System.out.println(Thread.currentThread().getName()+":OK"); //4.释放门栓 last.countDown(); } catch (InterruptedException ex) { ex.printStackTrace(); } } }).start(); } //2.释放门栓 begin.countDown(); //5.等待1000个线程执行完毕 last.await(); System.out.println("new objects: "+objectcount.get()); } }
关于 CountDownLatch 有不会的,可以看《CountDownLatch 压测教程》一文。
我这里主要是想压测一下,非锁 CAS 单例模式是否会创建多次对象。
运行上面的 main 方法,我截图了一下最终结果。
结论:CAS 以原子方式更新内存中相应的值,从而保证了多线程环境下共享变量更新操作的同步。的确,这种方式可以保证每次调用getInstance() 方法得到的一定是同一个实例。因此,从功能实现的角度来看,这种做法达到了预期的目的。
但是,经过分析和测试,却发现这种方式有一些预期之外的弊病:可能会创建不止一个对象。
CAS 本身的操作的确是原子方式,但是包装 CAS 指令的方法并非是全程同步的,当然,在包含 CAS 指令的方法开始调用之前,参数计算过程中更不是互斥执行的!当一个线程测试 instance.get() == null 得到 true 之后,往下它就一定会调用 new Singleton()。因为,这并不是 CAS 方法的一部分,而是它的参数。在调用一个方法之前,需要先将其参数压入栈,当然,需要先计算参数表达式,因此,产生如上结果也就不难预料了。
CAS 与锁的区别在于,它是非阻塞的,也就是说,它不会去等待一个条件,而是一定会去执行,结果要么成功,要么失败。它的操作时间是可预期的。如果我们的目的是一定要成功执行 CAS,那就需要不断循环执行直至成功,同时,建立在成功预期之上大量的准备工作是值得的,但是,如果我们不希望操作一定成功,那为成功操作而做的准备工作就浪费掉了。
枚举单例
// 问题1:枚举单例是如何限制实例个数的 // 问题2:枚举单例在创建时是否有并发问题 // 问题3:枚举单例能否被反射破坏单例 // 问题4:枚举单例能否被反序列化破坏单例 // 问题5:枚举单例属于懒汉式还是饿汉式 // 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做 enum Singleton { INSTANCE; }