• 并发编程(二)有序性


    【问题的产生】:

    程序真的是按照顺序执行的吗?

    /**
     * 本程序跟可见性无关,曾经有同学用单核也发现了这一点
     */
    
    import java.util.concurrent.CountDownLatch;
    
    public class T01_Disorder {
        private static int x = 0, y = 0;
        private static int a = 0, b = 0;
    
        public static void main(String[] args) throws InterruptedException {
    
            for (long i = 0; i < Long.MAX_VALUE; i++) {
                x = 0;
                y = 0;
                a = 0;
                b = 0;
                CountDownLatch latch = new CountDownLatch(2);
    
                Thread one = new Thread(new Runnable() {
                    public void run() {
                        a = 1;
                        x = b;
    
                        latch.countDown();
                    }
    
                });
    
                Thread other = new Thread(new Runnable() {
                    public void run() {
                        b = 1;
                        y = a;
    
                        latch.countDown();
                    }
                });
                one.start();
                other.start();
                latch.await();
                String result = "第" + i + "次 (" + x + "," + y + ")";
                if (x == 0 && y == 0) {
                    System.err.println(result);
                    break;
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    【最终输出】:
    在这里插入图片描述
    【图解程序 】:
    在这里插入图片描述
    //上图是8种可能出现的执行情况。你会发现——出现(0,0)的情况就意味着一定是线程内部执行的时候调换了顺序。

    【 乱序的原因 】:

    //简单说 , 就是为了提高效率。
    【下图】:
    第一条CPU指令是去内存读数据,等待数据的返回;
    第二条CPU指令是本地寄存器自增。
    在这里插入图片描述
    【分析】:
    //CPU的速度要比内存的速度快100倍。如果必须要按顺序执行的话,在等待返回过程需要大量的等待。

    【什么情况下,两条指令可以交换顺序呢? 】:

    如果前后指令之间有依赖关系 , 后续指令必须依赖前面的指令,那么是无法交换顺序的。——如果前后两条指令不存在依赖关系,则是可以进行交换的。

    【 乱序存在的条件 】:

    as——if——serial

    不影响单线程的最终一致性。

    不影响单线程的最终一致性时,各指令可以交换顺序。
    【如】:
    x=1; y=1。

    x=a; y=a。

    //上述两组指令都不影响最终结果。(打乱执行顺序的话)

    【但如下就不行】:
    x=1; x++。

    【程序理解可见性和有序性】:

    Java并发编程实践》 中的一个例子。

     package T04_YouXuXing;
    
    public class T02_NoVisibility {
        private static boolean ready = false;
        private static int number;
    
        private static class ReaderThread extends Thread {
            @Override
            public void run() {
                while (!ready) {
                    Thread.yield();
                }
                System.out.println(number);    //这里的打印有可能会是0.
            }
        }
    
        public static void main(String[] args) throws Exception {
            Thread t = new ReaderThread();
            t.start();
            number = 42;    //没有前后依赖关系。
            ready = true;   //没有前后依赖关系。
            t.join();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 思考—上述程序存在什么问题吗?
      (1)可见性问题
      ready设置为true后不会马上停止,但也有可能马上停止。
      要在number上加volatile关键字。——保证可见性。
      (2)有序性问题
      number = 42; //没有前后依赖关系。
      ready = true; //没有前后依赖关系。
      //这两个指令有可能第二个先执行——此时就会输出0 , 因为还没有执行到设置值为42的那一步。

    【线程的半初始化状态】:

    【对象的创建过程】:

    在这里插入图片描述
    在这里插入图片描述
    0——申请内存;
    4——特殊调用,特殊调用了T的init方法即默认的构造方法。
    7——建立关联,和t变量建立关联。

    【安全性】:
    在这里插入图片描述
    当我们new出一个对象 , 里面成员变量m的值是多少呢?——其实是和上一个使用这一块区域的程序有关系( C语言 )。
    在这里插入图片描述
    //Java中int类型的默认值是 0。
    在这里插入图片描述
    //这一句执行完,m=0 , 这是对象的半初始化状态。
    在这里插入图片描述
    //执行完这一句指令后——m的值才会变为8。
    在这里插入图片描述
    t变量和内存区域建立关联。

    【 this对象逸出 】:

    package T04_YouXuXing;
    
    public class T03_ThisEscape {
    
        private int num = 8;
    
    
        public T03_ThisEscape() {
            new Thread(() -> System.out.println(this.num)      //这里有可能输出中间状态0.
            ).start();
        }
    
        public static void main(String[] args) throws Exception {
            new T03_ThisEscape();
            System.in.read();      //进入阻塞
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    //——这里是有可能出现问题的。
    【 num为何可能输出中间状态呢? 】:
    this存在于局部变量表里面。

    //这两条指令是有可能换顺序的。这样就会先建立关联,关联完之后才调用构造方法 , 结果在调用构造方法的过程当中,新建了一个线程去输出当前的num值——此时因为是先关联的,所以为0 , 就先输出0了。
    在这里插入图片描述
    即——先进行了关联 , 关联完之后再调用的构造方法。

    【防止this逸出现象】:

    可以在构造方法里NEW线程,但是!!!!!!!————不要让它在那里启动。
    什么时候启动?——单独写一个方法。
    【 修改程序 】:

    package T04_YouXuXing;
    
    public class T03_ThisEscape2 {
    
        private int num = 8;
    
        Thread t;
    
        public T03_ThisEscape2() {
            t = new Thread(() -> System.out.println(this.num) );
        }
    
        public void start(){
            t.start();
        }
    
        public static void main(String[] args) throws Exception {
            new T03_ThisEscape2();
            System.in.read();      //进入阻塞
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    【 美团面试题 】:

    关于Object o = new Object( )

    【1–请解释一下对象的创建过程(半初始化)】:

    创建对象是有一个半初始化过程的。Java里半初始化过程是赋默认值的,C++等语言是赋内存上遗留下来的值。

    【2–加问DCL与volatile问题(指令重排)】:

    DCL单例是否要加volatile , 这里面主要涉及指令重排序的问题。
    volatile有两大作用——线程可见&禁止重排。as_if_serial这种机制是为了提高利用率。
    在这里插入图片描述
    单线程中as_if_serial结果幂等 , 执行指令可以随意排序。——但是,这种程序一旦到了多线程中就会出现问题!!!!!!
    如果不想让其排序的话 , 怎么办?——使用volatile可以禁止重排~~~! ! !

    【DCL】:

    饿汉式单例代码:

    package T04_YouXuXing.T05_singleton;
    
    /*
    饿汉式
    类加载到内存之后,就实例化一个单例,JVM保证线程安全;
    唯一缺点————不管用到与否,类装载时就完成实例化;
    * */
    public class Mgr01 {
        private static final Mgr01 INSTANCE = new Mgr01();
        private Mgr01(){};
        public static Mgr01 getInstance(){ return INSTANCE; }
    
        public void m(){
            System.out.println("m");
        }
    
        public static void main(String[] args){
            Mgr01 m1 = Mgr01.getInstance();
            Mgr01 m2 = Mgr01.getInstance();
            System.out.println(m1==m2);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    【最终输出】:
    True
    懒汉式代码:

    package T04_YouXuXing.T05_singleton;
    
    /*
    * 懒汉式写法
    * 【缺点】:多线程访问的时候,你不能够保证一致性,你不能够保证NEW出来的都是同一个对象;
    * */
    public class Mgr03 {
        private static Mgr03 INSTANCE;
    
        private Mgr03(){ }
    
        public static Mgr03 getInstance(){
            if (INSTANCE == null){           //两个线程接连判断都为NULL(还没来得及NEW出来),这时就会新建两个。
                try{
                    Thread.sleep(1);   //让更多的线程进入IF循环来新建对象就能更清晰的展示问题。
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                INSTANCE = new Mgr03();
            }
            return INSTANCE;
        }
    
        public void m(){
            System.out.println(" m ");
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 100; i++) {
                new Thread(
                        ()-> System.out.println(  Mgr03.getInstance().hashCode()  )
                ).start();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    【最终输出】:
    在这里插入图片描述
    【修正程序】:
    //只在上述程序的基础上加synchronized

        public static synchronized Mgr03_02 getInstance(){    
            if (INSTANCE == null){           //两个线程接连判断都为NULL(还没来得及NEW出来),这时就会新建两个。
                try{
                    Thread.sleep(1);   //让更多的线程进入IF循环来新建对象就能更清晰的展示问题。
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                INSTANCE = new Mgr03_02();
            }
            return INSTANCE;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述
    //但是仍然有问题——锁的粒度太粗了~ ~ ~ ! ! !
    【继续修改】:

        public static  Mgr03_03 getInstance(){
    
            //业务代码
            
            if (INSTANCE == null){           //两个线程接连判断都为NULL(还没来得及NEW出来),这时就会新建两个。
                //妄图通过缩减同步代码块的方式提高效率,然后不可行。
                synchronized(Mgr03_03.class) {
                    try {
                        Thread.sleep(1);   //让更多的线程进入IF循环来新建对象就能更清晰的展示问题。
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr03_03();
                }
            }
            return INSTANCE;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述
    //还是无法保证一致性~~~!!!!!!前一个线程刚释放锁,后一个线程就拿到锁了,然后就立即NEW了一个对象。
    【DCL机制】:

    package T04_YouXuXing.T05_singleton;
    
    // DCL  Double Check Lock
    public class Mgr06 {
        private static volatile Mgr06 INSTANCE;  //JIT
    
        private Mgr06(){}
    
        public static Mgr06 getInstance(){
            //业务代码省略
    
            if (INSTANCE == null){    //Double  Check  Lock
    
                synchronized(Mgr06.class) {
    
                    if (INSTANCE==null) {
                        try {
                            Thread.sleep(1);   //让更多的线程进入IF循环来新建对象就能更清晰的展示问题。
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        INSTANCE = new Mgr06();
                    }
                }
            }
            return INSTANCE;
        }
        public void m(){
            System.out.println(" m ");
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 100; i++) {
                new Thread(
                        ()-> System.out.println(  Mgr06.getInstance().hashCode()  )
                ).start();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    【提高效率的细节】:

             if (INSTANCE == null){    //Double  Check  Lock——————这一行可以提高效率
                synchronized(Mgr06.class) {
                    if (INSTANCE==null) {
    
    • 1
    • 2
    • 3

    第一个判断INSTANCE是否==null 是非常有必要的,如果去掉的话,那么所有的线程二话不说一上来全都在抢锁,竞争锁消耗的资源是非常高的。

    【DCL和volatile的问题】:

    【结论】:
    DCL必须要加上volatile

    private static volatile Mgr06 INSTANCE;  //JIT
    
    • 1

    【Q】:不加的话会怎么样呢???
    在这里插入图片描述
    在这里插入图片描述
    //第一个线程进来,刚刚完成半初始化,这个时候发生了指令重排序。
    在这里插入图片描述
    //刚执行完连接的时候,外面就有一个线程进入了IF判断!!!(这个时候虽然没有完成初始化,但是已经赋上了默认值,外面的线程就会拿到半初始化状态的对象!!!)

    【总结】:
    在这里插入图片描述
    thread1刚刚执行完连接那一步,这个时候thread2进入了IF判断,拿到刚半初始化完(没有完全初始化,没有调用构造方法)的对象。

    【3–对象在内存中的存储布局(对象与数组的存储不同)】:

    普通对象

    在这里插入图片描述
    mrakword——标记字。
    class pointer——当你new出一个对象来 , 你这个对象是属于哪一个class的呢? ? ?
    padding——64位虚拟机的话,就是8字节对齐( 将前面三块markword、class pointer、instance data补到能被8整除的字节 ),就和用集装箱装东西一样,用8字节比较规整。
    【UseCompressedClassPointers】:
    使用压缩类指针;
    【UseCompressedOops】:
    OrdinaryObjectPointer
    使用压缩的普通对象指针。

    【类指针】:

    在这里插入图片描述
    //这个类指针默认是启动压缩的 , 压缩完是4byte 。

    【普通对象指针】:

    String s , s就是一个指针,默认这两个的压缩都是开启的。

    【数组类型】:

    在这里插入图片描述
    //唯一区别是多了4字节的数组长度;

    【4–对象头具体包括什么(markword classpointer)synchronized锁信息】:

    对象头主要包括markword 和 class_pointer两部分,class pointer是默认开压缩的4个字节 , 如果不开压缩就是8个字节。

    【 研究一个对象内存布局的工具 】:
    org.openjdk.jol

    全称——JavaObjectLayout
    在这里插入图片描述
    在这里插入图片描述
    //parseInstance——解析这个对象 , 然后变成可以打印的。
    在这里插入图片描述
    Instance size:16 bytes ———一共有16个字节构成。
    在这里插入图片描述
    //指向Object.class
    在这里插入图片描述
    空间的丢失4个字节。空间丢失就是为了补齐的意思。

    【多加一个变量然后进行测试】:
    在这里插入图片描述
    markword ——8 ;
    classpointer——4;
    instance data——4;
    8+4+4=16 , 所以已经是8的倍数了 , 没有必要再去补齐。
    在这里插入图片描述
    在这里插入图片描述
    //最后的4个字节是m变量。

    【再进行一例测试】:
    在这里插入图片描述
    在这里插入图片描述
    //最后补了4个字节。

    【markword中主要包括什么呢 ? 】:

    最主要的是包含锁信息 , 其他的都是次要的。
    锁信息、GC信息、IdentityHashCode 。

    【5–对象怎么定位?(直接 间接)】

    句柄方式
    直接指针
    在这里插入图片描述
    在这里插入图片描述
    //定位就是指——如何通过 t 来找到 T 。

    【6–对象怎么分配?(栈上—线程本地—Eden—Old )】:

    首先会尝试在栈上分配 , 如果能在栈上分配就分配在栈上;分配在栈上的好处就是——一旦弹出,它的生命周期就结束了。
    在这里插入图片描述
    【 什么样的对象能够在栈上分配呢 ? 】:
    逃逸分析;
    标量替换;
    在这里插入图片描述

    如果个儿超级大——扔到老年代。
    如果不大不小——往TLAB中分配。TLAB全称是——Thread Local Allocation Buffer( 线程本地分配缓冲区 ) , E代表伊甸区 ,AGE是年龄 。
    【解释线程本地分配】:
    当我们往一个内存区域里NEW对象的时候 , 总是要分配空间的。多个线程往同一个内存空间里分配对象的时候,必须要经过同步。有线程的协调,就会有效率上的损失。
    【线程本地分配缓冲区】:
    当一个线程启动的时候 , 为这个线程在伊甸区里分配一个小小的空间,这个空间是线程所独享的 , 如果线程NEW了任何对象,就往对应的空间里扔,往自己的兜里扔东西的话,就不需要进行争抢了。

    【7–Object o = new Object( ) 在内存中占用多少字节? 】:

    1)有没有压缩class pointer 。
    2) 有没有压缩oops 。
    3) 看内存是不是32G以下或者以上 。

    【8–为什么hotspot不使用C++对象来代表Java对象?】:

    因为C++中有一个 vtbl 的指针 , 它指向的是虚方法表 , 虚方法表是用来实现多态的。Java中的是oop-class二元机制。

    【9–Class对象是在堆还是在方法区?】:

    在这里插入图片描述
    O.class是给反射用的;
    OOM溢出实际上是方法区里溢出了:
    在这里插入图片描述

    【阶段小结】:

    在这里插入图片描述

    【happens—before原则】:

    【CPU级别】:
    只要你不影响单线程的一致性,指令随便换。

    【JVM级别】:
    对Java的哪些指令不可以互换做了一些规定。

    【CPU用屏障指令阻止乱序】:

    【CPU汇编指令一级】:

    内存屏障

    所谓的内存屏障就是一条特殊的指令 ,当看到这种指令的时候,前面的指令和后面的指令不可以换顺序。
    内存屏障是特殊指令:看到这种指令,前面的必须执行完,后面的才能执行

    intel : lfence(读) sfence(写) mfence(读和写)(CPU特有指令)

    //我们的JVM并不是靠这种底层指令来实现的 , 它并没有去针对不同的CPU从而使用不同CPU的内存屏障指令,不是。

    【JVM要求实现的四种屏障】:

    【JVM层级的内存屏障】:

    在这里插入图片描述
    所有要求实现JVM的Java虚拟机 , 都应该实现自己的JVM级别的内存屏障 , 你的JVM实现应该有这四条的屏障,不论你底层采用什么汇编语言来实现。JVM层级必须得有能实现这四个屏障的效果。
    Load叫 “读” , Store叫 “写”。
    【LoadLoad】:
    在这里插入图片描述
    //两条读之令中间夹了一个指令——LoadLoad , 那么上面的Load指令就不能和下面的Load指令交换顺序。其他的三个屏障可以类比理解。

    【用volatile禁止指令重排】:

    在这里插入图片描述
    //StoreStoreBarrier——在我写之前,以前所有的写指令必须先完成,别人写完我才能写。
    后面还一个指令:
    //StoreLoadBarrier——等我写完别人才能读。
    在这里插入图片描述
    LoadLoadBarrier——等我读完,别人才能读;
    LoadStoreBarrier——等我读完,别人才能写。

    【volatile在hotspot中的实现】:

    volatile的底层实现

    volatile修饰的内存,不可以重排序,对volatile修饰变量的读写访问,都不可以换顺序

    1: volatile i

    2: ACC_VOLATILE

    3: JVM的内存屏障

    ​ 屏障两边的指令不可以重排!保障有序!

    ​ happends-before

    ​ as - if - serial

    4:hotspot实现

    bytecodeinterpreter.cpp

    int field_offset = cache->f2_as_index();
              if (cache->is_volatile()) {
                if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
                  OrderAccess::fence();
                }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    orderaccess_linux_x86.inline.hpp

    inline void OrderAccess::fence() {
      if (os::is_MP()) {   //如果操作系统是多核的。
        // always use locked addl since mfence is sometimes expensive
    #ifdef AMD64
        __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");    //这条指令的核心是lock而非后面的add。
    #else
        __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
    #endif
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    LOCK 用于在多处理器中执行指令时对共享内存的独占使用。
    它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。

    另外还提供了有序的指令无法越过这个内存屏障的作用。
    lock指令是比较特殊的,后面必须要跟其他的指令——表示当我执行后面的指令的时候,对总线/缓存 进行锁定,后面的这个指令不能是空指令,不能是NOP , addl $0,0(%%rsp)即给寄存器加了一个0,相当于是空操作。
    在这里插入图片描述
    //两颗CPU访问同一块内存的话 , 我会将总线进行锁定或者我会把对应的缓存行进行锁定(一个叫总线锁 ,一个叫缓存锁)。

  • 相关阅读:
    「限量招募30人」免费参与SPSS云版本内测
    【C++】template方法undefined reference to(二):C++代码的编译过程
    ES6-ES12部分简单知识点总结,希望对大家有用~
    Flink Yarn Per Job - 提交应用
    字符串——实现 strStr()——KMP
    PostgreSQL Extension 开发环境搭建
    2022/9/25 考试总结
    浅谈JS原型
    Python基础语法入门篇(二)
    Sentinel 滑动窗口实现原理(源码分析)
  • 原文地址:https://blog.csdn.net/fuyuanduan/article/details/127936013