zoukankan      html  css  js  c++  java
  • java多线程(三):多线程单例模式,双重检查,volatile关键字

    一.事先准备

    首先准备一个运行用的代码:

    public class Singleton {
    
        public static void main(String[] args) {
            Thread[] threads = new Thread[10];
            for (int i = 0; i < threads.length; i++) {
                threads[i] = new myThread();
            }
    
            for (Thread thread : threads) {
                thread.start();
            }
        }
    
    }
    
    class myThread extends Thread {
        @Override
        public void run() {
            //打印实例的hashCode
            //运行不同的示例时替换类名即可
            System.out.println(Obj.getObj().hashCode());
        }
    }
    

    以下代码都在此基础上运行。

    二.饿汉式

    饿汉式是天生线程安全的。

    /*
    * 饿汉式
    * */
    class Obj{
    
        //一开始就直接初始化对象
        private static Obj obj = new Obj();
    
        //私有有化构造方法避免再new出一个对象
        private Obj() {
        }
    
        //通过get方法获取实例
        public static Obj getObj(){
            return obj;
        }
    }
    
    //输出
    471895473
    471895473
    471895473
    471895473
    471895473
    471895473
    471895473
    471895473
    471895473
    471895473
    

    对应饿汉式,因为饿汉式在类加载时创建实例,而一个类在生命周期中只被加载一次,也就是说,饿汉式在线程访问前就已经创建好了唯一的那个实例,因此无论多少个线程同时访问,最终获取到的都是一个实例。

    当然,实际上饿汉式可能导致系统最终创建了太多无用实例,所以懒汉式仍然还是必要的。

    三.懒汉式

    1.传统懒汉式

    传统懒汉式是非线程安全的,示例如下:

    /*
    * 传统懒汉式
    * */
    class Obj2 {
    
        private static Obj2 obj;
    
        private Obj2() {
        }
    
        //get方法进行同步
        public static Obj2 getObj(){
            if (obj == null){
                obj = new Obj2();
            }
            return obj;
        }
    }
    
    //输出
    1217036164 //创建了多个实例
    102629730
    1217036164
    102629730
    102629730
    102629730
    102629730
    102629730
    

    对于传统懒汉式,因为当某个线程创建实例但是还没来得及写入堆内存时,可能已经有多个线程进入了if代码块,因此可能最后会创建多个实例。

    2.使用内部类

    因为饿汉式是天生线程的,所以也可以通过内部类实现:

    /**
     * 内部类
     */
    class Obj5 {
        
        //内部类
        private static class InitBean {
            //将外部类作为成员变量,饿汉式创建
            private static Obj5 obj5 = new Obj5();
        }
    
        private Obj5() {
        }
    
        //工厂方法,实际上是懒汉式初始化内部类,并且从内部类获取实例
        public static Obj5 getObj() {
            return InitBean.obj5;
        }
    }
    
    //输出
    1217036164
    1217036164
    1217036164
    1217036164
    1217036164
    1217036164
    1217036164
    1217036164
    1217036164
    1217036164
    

    对于这种方法的解释是这样的:

    当调用getObj()方法时,会触发InitBean类的初始化。

    由于Obj5是InitBean的类成员变量,因此在JVM调用InitBean类的类构造器对其进行初始化时,虚拟机会保证一个类的类构造器在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器,其他线程都需要阻塞等待,直到活动线程执行方法完毕。

    总结一下,就是jvm会保障多线程情况下类的正确初始化,所以借助这一点,我们创建一个内部类,然后让内部类初始化的时候创建唯一个实例作为成员变量,而通过外部类的工厂方法来触发内部类的初始化并获取实例。

    3.使用synchronize同步工厂方法

    解决传统懒汉式问题的方法很简单,那就是直接给工厂方法加上synchronize关键字变成同步方法:

    ...
    //直接对工厂方法进行同步
    public static synchronized Obj2 getObj(){
        if (obj == null){
            obj = new Obj2();
        }
        return obj;
    }
    
    //输出
    1696172314
    1696172314
    1696172314
    1696172314
    1696172314
    1696172314
    1696172314
    1696172314
    1696172314
    1696172314
    

    从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率会很低,因为同步块的作用域有点大,而且锁的粒度有点粗。同步方法效率低,所以我们还得进一步缩小锁范围,因此可以考虑使用同步代码块来实现。

    4.双重检查

    要说锁粒度最小,那就是只锁实例化的代码,然后只在实例化前进行一次检查:

    /*
     * 不双重检查
     * */
    class Obj4 {
    
        private volatile static Obj4 obj4;
    
        private Obj4() {
        }
    
        public static Obj4 getObj() {
            //检查是否已有实例
            if (obj4 == null) {
                //没有实例就获取锁进行准备实例化
                synchronized (Obj3.class) {
                    obj4 = new Obj4();
                }
    
            }
            return obj4;
        }
    }
    
    //输出
    2140075580 //创建了多个实例
    1285201119
    1217036164
    102629730
    102629730
    1000492474
    1217036164
    569477593
    1217036164
    1217036164
    

    实际上这样跟传统懒汉式并无区别,因为只检查一次的话,同样会面对第一个示例还在创建,结果其他线程直接通过了if判断的情况,所以我们需要再在同步代码块中进行一次检查

    ...
    //对工厂方法进行双重检查
    public static Obj3 getObj() {
        //检查是否已有实例
        if (obj3 == null) {
            //没有实例就获取锁进行准备实例化
            synchronized (Obj3.class) {
                //再判断是否已经有获取过锁的线程实例化了对象
                if (obj3 == null) {
                    obj3 = new Obj3();
                }
            }
    
        }
        return obj3;
    }
    
    //输出
    1696172314
    1696172314
    1696172314
    1696172314
    1696172314
    1696172314
    1696172314
    1696172314
    1696172314
    1696172314
    

    四.双重检查中的volatile关键字

    在双重检查中,必须使用volatile关键字修饰引用的单例,目的是jvm在创建实例的时候进行禁止指令重排

    要理解指令重排,必须先理解jvm是如何处理new操作的:

    1. 分配对象的内存空间
    2. 初始化对象
    3. 使变量指向对象

    但是由于jvm会因为进行指令重排,所一实际上new操作的步骤可能发生变化

    1. 分配对象的内存空间
    2. 使变量指向对象
    3. 初始化对象

    这就会导致现有的代码出现这样的问题:

    1. 线程一最先获取锁并执行初始化代码,但是发生了指令重排
    2. jvm执行完1后,先执行了3,但是2还没来得及执行锁就被线程二抢占了
    3. 此时线程二能够获取实例了,通过了代码检查,但是线程二获取的这个实例还没有初始化,是个不完整的实例
    4. 线程一抢占锁,执行构造函数完成变量初始化

    显然,如果线程二拿着一个不完整的实例进了业务代码,就会引发各种bug,这种隐患正是由指令重排引起的,所以我们需要使用volatile指令修饰引用的单例

  • 相关阅读:
    ElasticSearch原理
    redis master配置了密码进行主从同步
    redis sentinel 高可用(HA)方案部署,及python应用示例
    Linux Redis集群搭建与集群客户端实现
    字符串倒序
    单链表反转
    【python】面试常考数据结构算法
    面试中的排序算法总结
    Memcached 真的过时了吗?
    Activity生命周期
  • 原文地址:https://www.cnblogs.com/Createsequence/p/12945908.html
Copyright © 2011-2022 走看看