并发编程(二)有序性

【问题的产生】:

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

/**
 * 本程序跟可见性无关,曾经有同学用单核也发现了这一点
 */

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;
            }
        }
    }
}

【最终输出】:
在这里插入图片描述
【图解程序 】:
在这里插入图片描述
//上图是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)可见性问题
    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();      //进入阻塞
    }
}

//——这里是有可能出现问题的。
【 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();      //进入阻塞
    }
}

【 美团面试题 】:

关于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);
    }
}

【最终输出】:
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();
        }
    }
}

【最终输出】:
在这里插入图片描述
【修正程序】:
//只在上述程序的基础上加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;
    }

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

    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;
    }

在这里插入图片描述
//还是无法保证一致性~~~!!!!!!前一个线程刚释放锁,后一个线程就拿到锁了,然后就立即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();
        }
    }
}

【提高效率的细节】:

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

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

【DCL和volatile的问题】:

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

private static volatile Mgr06 INSTANCE;  //JIT

【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();
            }

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
  }
}

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

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值