从单例模式到并发编程、volatile

  1. 单例模式:一个只能创建一个对象的类。

  2. 饿汉式,直接在内部创建好对象,需要时return,优点是需要时直接就能获取对象,且并发安全,缺点是如果一直不需要的话,会造成资源浪费:

    public class test {
        private static test aInstance = new test();
        private test(){}
        public static test getInstance(){
            return aInstance;
        }    
    }
    
  3. 简单懒汉式(懒加载):需要时再创建:

    public class test {
        private static test aInstance = null;
        private test(){}
        public static test getInstance(){
            if(aInstance == null)
                aInstance = new test();
            return aInstance;
        }   
    }
    
  4. 简单懒汉式的缺点是并发不安全,可能有多个线程同时进入到 if 语句内,导致创建多个实例,解决方法很简单,加锁

    public class test {
        private static test aInstance = null;
        private test(){}
        public static synchronized test getInstance(){
            if(aInstance == null)
                aInstance = new test();
            return aInstance;
        }   
    }
    
  5. 加锁后,解决了线程安全的问题,但是,当我们正确创建了唯一实例后,线程不安全的问题就不存在了,以后每次都只是 return aInstance,但由于加了锁,带来了不必要的时间开销,解决方法是缩小锁的范围

    public class test {
        private static test aInstance = null;
        private test(){}
        public static test getInstance(){
            if(aInstance == null)
                synchronized (test.class){
                    if(aInstance == null)
                        aInstance = new test();
                }
            return aInstance;
        }
    }
    

    这种写法要注意的是,在锁内部又对 aInstance 进行了一次判空,原因是:如果没有第二次判空,仍然是线程不安全的,因为可能有多个线程同时越过了第一次判空卡在了锁前,虽然锁能保证每次只进去一个线程,但是仍会创建多个实例。因此,在锁内必须要再进行一次判空,这种写法叫双重检测锁(DCL,double check lock)

  6. 但是,DCL仍然有问题,因为 new 不是原子操作,在指令重排的情况下,可能会有某线程获得一个未初始化的实例对象(原子性及有序性问题)。解决方法是用 volatile 禁止指令重排(适用于jdk1.5之后):

    public class test {
        private static volatile test aInstance = null;
        private test(){}
        public static test getInstance(){
            if(aInstance == null)
                synchronized (test.class){
                    if(aInstance == null)
                        aInstance = new test();
                }
            return aInstance;
        }
    }
    
  7. 那么,有没有其他方法实现类似的效果呢?

  8. 静态内部类,在外部类加载时,静态内部类不会加载,因此只有在运行到Holder.aInstance时加载内部类,初始化单例,且初始化时保证线程安全,不过缺点是无法传参

    public class test {
        private test(){}
        private static class Holder {
            static test aInstance = new test();
        }
        public static test getInstance(){
            return Holder.aInstance;
        }
    }
    
  9. 以上的写法都无法抵御反射攻击以及序列化攻击,而能抵御这两种攻击的简单高效方法是通过:枚举类型

    public enum test {
        INSTANCE;
        public void speak(){
            System.out.println("hello!");
        }
        public static void main(String[] args) {
            test.INSTANCE.speak();
        }
    }
    
  10. JDK中的单例模式:Runtime类(饿汉式)。

  11. 指令重排:编译器优化重排(不改变单线程语义)、指令并行重排(不存在数据依赖的前提下,在流水线基础上进一步优化)、内存系统重排序。后两者属于处理器重排。

    数据依赖:对同一变量,写后写,写后读,读后写。

    对于编译器重排,可以人为限定优化规则,来禁止某些类型的编译器重排;而对于处理器重排,则需要通过插入底层指令来进行避免,而对于不同的处理器架构,需要的指令也不同,但是,我们期望其呈现出的效果总是一致的,这也就引出了所谓内存屏障概念

    内存屏障:store store, store load, load load, load store。

  12. happens-before,JMM(Java Memory Model)对指令重排做出的限定,保证特定规则下重排的结果一定符合按某种先后顺序推演的结果即多线程可见性)。JMM并非完全禁止了指令重排,而是在保证结果正确性的前提下,给编译器、CPU留了些优化余地。再次强调,happens-before是多线程的。

  13. 并发编程不安全问题根源:可见性、有序性、原子性。

    可见性:多核心下工作在不同CPU的线程间只能看到局部的工作内存(这里借用了JMM的说法,注意,工作内存包括缓存、寄存器、写缓冲区等),由此引发的不同步问题(单核多线程不存在可见性问题)。

    缓存一致性协议:多核心为解决缓存(cache)不一致问题,在硬件层面实现了各核心的缓存(cache)的一致性。常见的有MESI协议。但是,各核心的工作内存间仍无法保证瞬时一致

    原子性:单核多线程间切换,导致一条高级编程语言(i++)对应的多条CPU指令被拆散,引发的不一致问题。

    有序性:编译器、处理器重排序。

  14. X86架构下volatile原理: volatile会增加一条lock前缀空指,在写的时候让store buffer的值立刻刷入缓存,由此触发MESI,使得其他缓存的缓存行数据失效,保证可见性(lock同时也能禁止指令重排序)。详情参考:知乎相关问题:volatile与MESI的关系,罗一鑫的回答以及对应评论内存屏障今生之Store Buffer, Invalid Queue

    volatile一图流(原创),求了再别问我啥原理了orz:
    在这里插入图片描述

  15. 为了便于以后回想本部分,列一些关键点。

    Java中广义的volatile实现原理是插入内存屏障(参见第11条):volatile写前后各一个,volatile读后边两个。而一旦落实的具体实现,由于不同的CPU架构下,会出现的处理器重排序情况是不同的,故要插入的内存屏障,以及内存屏障的具体实现方式也是不同的。

    一般而言,内存屏障的主要实现原理都是,写屏障:处理完store buffer回写cache;读屏障:处理完invalid queue。

    x86支持MESI,采用TSO模型,没有invalid queue,故只需解决store load。

参考资料:

  1. DoubleCheckedLocking
  2. 深入理解单例模式:静态内部类单例原理
  3. happens-before八大规则通俗易懂的讲解
  4. happens-before通俗讲解
  5. 并发编程三大问题:可见性、有序性、原子性的由来
  6. volatile,synchronized原理 <=> 可见性、有序性
  7. 从Java多线程可见性谈Happens-Before原则
  8. Java内存模型与指令重排
  9. Java并发编程之happens-before和as-if-serial语义
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值