8.java并发编程之线程安全和单例模式

线程安全单例

饿汉单例

   // 问题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懒汉单例

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 方法,我截图了一下最终结果。

image.jpg

结论:CAS 以原子方式更新内存中相应的值,从而保证了多线程环境下共享变量更新操作的同步。的确,这种方式可以保证每次调用getInstance() 方法得到的一定是同一个实例。因此,从功能实现的角度来看,这种做法达到了预期的目的。

但是,经过分析和测试,却发现这种方式有一些预期之外的弊病:可能会创建不止一个对象。

CAS 本身的操作的确是原子方式,但是包装 CAS 指令的方法并非是全程同步的,当然,在包含 CAS 指令的方法开始调用之前,参数计算过程中更不是互斥执行的!当一个线程测试 instance.get() == null 得到 true 之后,往下它就一定会调用 new Singleton()。因为,这并不是 CAS 方法的一部分,而是它的参数。在调用一个方法之前,需要先将其参数压入栈,当然,需要先计算参数表达式,因此,产生如上结果也就不难预料了。

CAS 与锁的区别在于,它是非阻塞的,也就是说,它不会去等待一个条件,而是一定会去执行,结果要么成功,要么失败。它的操作时间是可预期的。如果我们的目的是一定要成功执行 CAS,那就需要不断循环执行直至成功,同时,建立在成功预期之上大量的准备工作是值得的,但是,如果我们不希望操作一定成功,那为成功操作而做的准备工作就浪费掉了。

枚举单例

// 问题1:枚举单例是如何限制实例个数的 // 问题2:枚举单例在创建时是否有并发问题 // 问题3:枚举单例能否被反射破坏单例 // 问题4:枚举单例能否被反序列化破坏单例 // 问题5:枚举单例属于懒汉式还是饿汉式 // 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做 enum Singleton { INSTANCE; }

#

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值