• java并发多线程纪要


    守护线程:

      在Java中有个规定:当所有非守护线程退出后,整个JVM进程就会退出。守护线程不影响这个JVM进程的退出。

    Interrupt:

      能够被中断的阻塞称为轻量级阻塞,对应的线程的状态是WAITING或者TIMED_WAITING;而像synchronize这种不能被中断的阻塞称为重量级阻塞,对应的状态是BLOCKED。

    interrupt()的精确含意是"唤醒轻量级阻塞",而不是"中断一个线程"。interrupt()相当于给线程发送了一个唤醒信号,如果线程此时恰好处于WAITING或者TIMED_WAITING

    状态就会抛出InterruptedException,并且线程被唤醒。而如果线程此时并没有被阻塞,则线程什么都不会做。但在后续,线程可以判断自己是否收到过其他线程发来的中断

    信号,然后做一些对应的处理。

    锁:

      锁其实是一个“对象”,这对象要完成以下几个事情:

      1、锁对象内部得有一个标志位(state变量),记录自己有没有被某个线程占用。

      2、如果这个锁对象被某个线程占用,它得记录这个线程的thread ID,知道自己是被哪个线程占用了。

      3、这个对象还维护了一个thread id list,记录其他所有阻塞的、等待拿这个锁的线程。在当前线程释放锁之后,从这个thread list里面取一个线程唤醒。

    资源和锁合二为一,使得在Java里面,synchronize关键字可以加在任何对象的成员上面。这意味着,这个对象即是共享资源,同时也具备锁的功能。

    在Java对象的头里面,有一块数据叫做Mark Word。在64位机器上,Mark Word是8字节的。这64位有2个重要字段:锁标志位和占用该锁的threadID。

    Wait()的内部,会先释放锁,然后进入阻塞状态。

    wait()和notify()所作用的对象和synchronize作用的对象是同一个。

    volatile关键字:

       JVM的规范并没有要求64位的long或者double的写入是原子的。在32位的机器上,一个64位变量的写入可能分成两个32位的写操作来执行。这样一来,

      读取的线程就可能读到一个“半值”。解决这办法是在long前面加上volatile。

      内存可见性,指写完之后立即对其他线程可见,他的反面是“不可见”,“稍后才能见”。解决这个问题加上volatile。

      重排序:instance= new Instance():其底层会分为三个操作:(1)分配一块内存(2)在内存上初始化成员变量(3)把instance引用指向内存

        这三个操作中,(2)和(3)可能重排序。先把instance指向内存,再初始化成员变量,因为二者没有先后依赖关系。此时,另外线程可能拿到一个未完全

        初始化的对象,直接访问里面的成员变量,就可能出错。这是典型的“构造函数溢出”问题。解决办法很简单,即使为instance变量加上volatile修饰。

        重排序有一下几类:

        (1)编译器重排。对于没有先后依赖关系的语句,编译器可以重排调整语句的执行顺序。

        (2)CPU指令重排。在指令级别,让没有依赖关系的多条指令并行。

        (3)CPU内存重排。CPU有自己的缓存,指令的顺序和写入主内存的顺序不完全一致。

        这三类是造成内存可见性的问题主要原因。

      volatile的三重功效:64位写入的原子性、内存可见性和禁止重排序。

    as-if-serial语义:

      从CPU和编译器的角度来看,希望尽最大可能进行重排,提升运行效率。重排序的原则是什么?

    多线程之间的数据依赖太复杂,编译器和CPU没办法完全理解这种依赖性并据此作出合理的优化,所以编译器和CPU只能保证每个线程的as-if-serial语义。

    线程间的数据依赖和相互影响,需要编译和CPU上层来确认。上层要告知编译器和CPU在多线程场景下什么时候可以重排什么时候不能重排。

    happen-before:

      什么时候可以重排序,什么时候不能,java引入了JMM,即java内存模型。这个模型就是一套规范,对上,是JVM和开发这之间的协定;对下,是JVM和

    编译器、CPU之间的协定。为了描述这个规范,JMM引入了happen-before描述这两个操作之间的内存可见性。如果A happen-before B,意味着A的执行结果

    必须对B可见,也就是保证跨线程的内存可见性。A happen-before B不代表A一定在B之前执行。因为,对于多线程程序而言,两个操作的执行顺序是不确定的

    happen-before只能确保如果A在B之前执行,则A的执行结果必须对B可见。定义了内存可见性的约束,也就定义了一系列重排的约束。happen-before具有

    传递性,即若A happen-before B, B happen-fore C,则A happen-before C。

      volatile变量的写入,happen-before对应后续对这个变量的读取。

      synchronize的解锁,happen-before对应后续对这个锁的加锁。

    内存屏障:

      为了禁止编译器重排序和CPU重排序,在编译器和CPU层面都有对应的指令,也就是内存屏障。这也正是JMM和happen-before规则的底层实现原理。

     编译器的内存屏障,只是为了告诉编译器不要对指令进行重排序。当编译完成之后,这种内存屏障就消失了,CPU并不会感知到编译器中内存屏障的存在。

      而CPU的内存屏障是CPU提供的指令,可以由开发者显示调用。

     

    JDK中的内存屏障:

      内存屏障是很底层的概念,对java开发者来说,一般用volatile关键字就足够了。但从java8开始,java在Unsafe类中提供了三个内存屏障:

      loadFence()

      storeFence()

      fullFence()

      这三个屏障不是基本的内存屏障。在理论层面,可以把基本的CPU内存屏障分成四种:

      (1)LoadLoad禁止读和读的重排序

      (2)StoreStore禁止写和写的重排序

      (3)LoadStore禁止读和写

      (4)StoreLoad禁止写和读的重排序

      他们的关系:

      loadFence = LoadLoad + LoadStore

      storeFence = StoreStore + StoreLoad

      fullFence = loadFence + storeFence + StoreLoad

      volatile关键字的语义参考做法:

        (1)在volatile写操作的前面插入一个StoreStore屏障。保证volatile写操作不会和之前的写操作重排序。

        (2)在volatile写操作的后面插入一个StoreLoad屏障。保证volatile写操作不会和之前和之后的读操作重排

        (3)在volatile读操作的后面加入一个LoadLoad屏障+LoadStore屏障。保证volatile读操作不会和之后的读操作、写操作重排序。

    final关键字

      final关键字同volatile一样,也有相应的happen-before语义:

      (1)对final域的写,happen-before于后续对final域所在对象的读。

      (2)对final域所在对象的读,happen-before于后续对final域的读。

    这种happen-before语义的限定,保证了final域的赋值,一定在构造函数之前完成,不会出现另外一个线程读到了对象,但对象里面的变量却还没有

    初始化的情形,避免出现构造函数溢出的问题。

    happen-before总结:

      (1)单线程中的每个操作,happen-before于该线程中任意后续操作。

      (2)对volatile变量的写,happen-before于后续对这个变量的读。

      (3)对synchronize的解锁,happen-before于后续对这个锁的加锁。

      (4)对final变量的写,happen-before于final域对象的读,happen-before于后续对final变量的读。

    四个基本原则再加上happen-before的传递性,构成JMM对开发者的整个承诺。承诺以外的部分,程序都可以被重排序,都需要开发者小心地处理内存

    可见性问题。

      volatile背后的原理:

          开发这层面:volatile、final、synch

          JVM层面:JMM(happen-before规则)

          CPU层面:CPU缓存体系、CPU内存重排序、内存屏障

    无锁编程:

      一写一读的无锁队列:内存屏障

      一写多读的无锁队列:volatile

      多读多写的无锁队列:CAS

        同内存屏障一样,CAS也是CPU提供的一种原子指令。基于CAS和链表,可以实现一个多写多读的队列。具体来说,就是链表有一个头指针head

        和尾指针tail。入队列,通过对tail进行CAS操作完成;出队列,对head进行CAS操作完成。

      无锁栈:无锁栈比无锁队列实现更简单,只需要对head进行CAS操纵,就能实现多线程的入栈和出栈。

      无锁链表:无锁链表要复杂很多,因为无锁链表要在中间插入和删除元素。

  • 相关阅读:
    都说程序员钱多空少,程序员真的忙到没时间回信息了吗?
    C/C++知识分享: 函数指针与指针函数,看完这篇你还能不懂?
    C++的那些事儿:从电饭煲到火箭,C++无处不在
    年薪90万程序员不如月入3800公务员?安稳与高收入,到底如何选择?
    【C++学习笔记】C++ 标准库 std::thread 的简单使用,一文搞定还不简单?
    C语言既有高级语言又有低级语言的特点,但为什么它不是低级语言呢?
    【C++学习笔记】看完这篇,C++ 的链接问题不怕你搞不明白!
    既然C++这么难学,为什么还有人“自讨苦吃”?
    编程语言这么多,为什么就只有C 语言能一直得到 SQLite 的青睐?
    初学者疑惑:C语言中,函数反复调用会有什么问题?
  • 原文地址:https://www.cnblogs.com/jixp/p/13164529.html
Copyright © 2020-2023  润新知