线程安全单例
饿汉单例
// 问题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;
}