• Java并发编程原理与实战十:单例问题与线程安全性深入解析


    单例模式我想这个设计模式大家都很熟悉,如果不熟悉的可以看我写的设计模式系列然后再来看本文。单例模式通常可以分为:饿汉式和懒汉式,那么分别和线程安全是否有关呢?

    一、饿汉式

    先看代码:

    package com.roocon.thread.t5;
    
    public class Singleton {
    
        private Singleton(){
    
        }
    
        private static Singleton instance = new Singleton();
    
        public static Singleton getInstance(){
           return instance;
        }
    }
    package com.roocon.thread.t5;
    
    public class Main {
    
        public static void main(String[] args) {
            Singleton s1 = Singleton.getInstance();
            Singleton s2 = Singleton.getInstance();
            Singleton s3 = Singleton.getInstance();
            Singleton s4 = Singleton.getInstance();
            System.out.println(s1);
            System.out.println(s1);
            System.out.println(s1);
            System.out.println(s1);
        }
    }

    运行结果:

    com.roocon.thread.t5.Singleton@5cad8086
    com.roocon.thread.t5.Singleton@5cad8086
    com.roocon.thread.t5.Singleton@5cad8086
    com.roocon.thread.t5.Singleton@5cad8086

    所谓的饿汉模式:不管是否使用到instance这个实例,我们都在创建的过程中就对它进行实例化。

    那么,饿汉模式是否会出现线程安全问题呢?

    出现线程安全问题需要满足三个条件:

    1.多线程的环境下

    2.必须有共享资源

    3.对资源进行非原子性操作

    根据以上代码,我们发现,使用饿汉式,在调动getInstance方法时,就只干了一件事,那就是返回Instance实例。这个操作是原子性操作。因此,饿汉式不存在线程安全问题。

    饿汉式的不足:不管是否需要使用到该实例,在创建的时候就已经实例化了。但其实在程序中根本没有用到该实例变量,就没必要先对它进行实例化。

    推荐是在使用它的时候才对它进行实例化,这就是所谓的懒汉式单例模式。

    二、懒汉式

    先看代码:

    package com.roocon.thread.t5;
    
    public class Singleton2 {
    
        private Singleton2(){
    
        }
    
        private static Singleton2 instance;
    
        public static Singleton2 getInstance(){
            if(instance == null) {//1:读取instance的值
                instance = new Singleton2();//2: 实例化instance
            }
            return instance;
        }
    
    }
    package com.roocon.thread.t5;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class MultiThreadMain {
        public static void main(String[] args) {
            ExecutorService threadPool = Executors.newFixedThreadPool(20);
            for (int i = 0; i< 20; i++) {
                threadPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(Thread.currentThread().getName()+":"+Singleton2.getInstance());
                    }
                });
            }
    
         threadPool.shutdown();
    } }

    运行结果:

    pool-1-thread-4:com.roocon.thread.t5.Singleton2@6519891a
    pool-1-thread-14:com.roocon.thread.t5.Singleton2@6519891a
    pool-1-thread-10:com.roocon.thread.t5.Singleton2@6519891a
    pool-1-thread-8:com.roocon.thread.t5.Singleton2@6519891a
    pool-1-thread-5:com.roocon.thread.t5.Singleton2@6519891a
    pool-1-thread-12:com.roocon.thread.t5.Singleton2@6519891a
    pool-1-thread-1:com.roocon.thread.t5.Singleton2@6519891a
    pool-1-thread-9:com.roocon.thread.t5.Singleton2@6519891a
    pool-1-thread-6:com.roocon.thread.t5.Singleton2@6519891a
    pool-1-thread-2:com.roocon.thread.t5.Singleton2@6519891a
    pool-1-thread-16:com.roocon.thread.t5.Singleton2@6519891a
    pool-1-thread-3:com.roocon.thread.t5.Singleton2@1c208db1

    发现,有个实例是Singleton2@1c208db1,也就说明,返回的不是同一个实例。这就是所谓的线程安全问题。

    解释原因:对于以上代码注释部分,如果此时有两个线程,线程A执行到1处,读取了instance为null,然后cpu就被线程B抢去了,此时,线程A还没有对instance进行实例化。

    因此,线程B读取instance时仍然为null,于是,它对instance进行实例化了。然后,cpu就被线程A抢去了。此时,线程A由于已经读取了instance的值并且认为它为null,所以,

    再次对instance进行实例化。所以,线程A和线程B返回的不是同一个实例。

    那么,如何解决呢?

    1.在方法前面加synchronized修饰。这样肯定不会再有线程安全问题。

    package com.roocon.thread.t5;
    
    public class Singleton2 {
    
        private Singleton2(){
    
        }
    
        private static Singleton2 instance;
    
        public static synchronized Singleton2 getInstance(){
            if(instance == null) {//1
                instance = new Singleton2();//2
            }
            return instance;
        }
    
    }

    但是,这种解决方式,假如有100个线程同时执行,那么,每次去执行getInstance方法时都要先获得锁再去执行方法体,如果没有锁,就要等待,耗时长,感觉像是变成了串行处理。因此,尝试其他更好的处理方式。

    2. 加同步代码块,减少锁的颗粒大小。我们发现,只有第一次instance为null的时候,才去创建实例,而判断instance是否为null是读的操作,不可能存在线程安全问题,因此,我们只需要对创建实例的代码进行同步代码块的处理,也就是所谓的对可能出现线程安全的代码进行同步代码块的处理。

    package com.roocon.thread.t5;
    
    public class Singleton2 {
    
        private Singleton2(){
    
        }
    
        private static Singleton2 instance;
    
        public static Singleton2 getInstance(){
            if(instance == null) {
                synchronized (Singleton2.class){
                    instance = new Singleton2();
                }
            }
            return instance;
        }
    
    }

    但是,这样处理就没有问题了吗?同样的原理,线程A和线程B,线程A读取instance值为null,此时cpu被线程B抢去了,线程B再来判断instance值为null,于是,它开始执行同步代码块中的代码,对instance进行实例化。此时,线程A获得cpu,由于线程A之前已经判断instance值为null,于是开始执行它后面的同步代码块代码。它也会去对instance进行实例化。

    这样就导致了还是会创建两个不一样的实例。

    那么,如何解决上面的问题。

    很简单,在同步代码块中instance实例化之前进行判断,如果instance为null,才对其进行实例化。这样,就能保证instance只会实例化一次了。也就是所谓的双重检查加锁机制。

    再次分析上面的场景:

    线程A和线程B,线程A读取instance值为null,此时cpu被线程B抢去了,线程B再来判断instance值为null。于是,它开始执行同步代码块代码,对instance进行了实例化。这是线程A获得cpu执行权,当线程A去执行同步代码块中的代码时,它再去判断instance的值,由于线程B执行完后已经将这个共享资源instance实例化了,所以instance不再为null,所以,线程A就不会再次实行实例化代码了。

    package com.roocon.thread.t5;
    
    public class Singleton2 {
    
        private Singleton2(){
    
        }
    
        private static Singleton2 instance;
    
        public static synchronized Singleton2 getInstance(){
            if(instance == null) {
                synchronized (Singleton2.class){
                    if (instance == null){
                        instance = new Singleton2();
                    }
                }
            }
            return instance;
        }
    
    }

    但是,双重检查加锁并不代码百分百一定没有线程安全问题了。因为,这里会涉及到一个指令重排序问题。instance = new Singleton2()其实可以分为下面的步骤:

    1.申请一块内存空间;

    2.在这块空间里实例化对象;

    3.instance的引用指向这块空间地址;

    指令重排序存在的问题是:

    对于以上步骤,指令重排序很有可能不是按上面123步骤依次执行的。比如,先执行1申请一块内存空间,然后执行3步骤,instance的引用去指向刚刚申请的内存空间地址,那么,当它再去执行2步骤,判断instance时,由于instance已经指向了某一地址,它就不会再为null了,因此,也就不会实例化对象了。这就是所谓的指令重排序安全问题。那么,如何解决这个问题呢?

    加上volatile关键字,因为volatile可以禁止指令重排序

    package com.roocon.thread.t5;
    
    public class Singleton2 {
    
        private Singleton2(){
    
        }
    
        private static volatile Singleton2 instance;
    
        public static Singleton2 getInstance(){
            if(instance == null) {
                synchronized (Singleton2.class){
                    if (instance == null){
                        instance = new Singleton2();
                    }
                }
            }
            return instance;
        }
    
    }

    参考资料:

    《java并发编程与实战》 龙果学院

  • 相关阅读:
    java的装箱和拆箱详解
    java语法基础
    java变量与内存深入了解
    java的配置环境简介
    Python脚本--利用运算符重载简化MongoDB的命令
    Python多线程编程,线程锁
    Python多进程,同步互斥,信号量,锁补充上一篇文章
    Python多进程编程及多进程间的通信,数据传输
    Jmeter深度学习第一天——简单请求、带header请求、返回值乱码问题
    JAVA Spring工程一些配置问题的解决
  • 原文地址:https://www.cnblogs.com/pony1223/p/9375182.html
Copyright © 2020-2023  润新知