8.java并发编程之线程安全和单例模式 您所在的位置:网站首页 歪歪兔思维导图 8.java并发编程之线程安全和单例模式

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

2022-12-22 01:21| 来源: 网络整理| 查看: 265

线程安全单例 饿汉单例    // 问题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 extends  HashSet { ​  public Object readResolve() {        HashSet hashSet = new HashSet();        hashSet.add("你好");        return hashSet;   } ​    public static void main(String[] args) throws Exception {        Set set = new ReadResolve();        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) 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 extends  HashSet { ​   //去掉readResolve    public static void main(String[] args) throws Exception {        Set set = new ReadResolve();        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) 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 INSTANCE           = new AtomicReference();    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


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有