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操纵,就能实现多线程的入栈和出栈。

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

 

posted @ 2020-06-19 18:30  青青子衿J  阅读(215)  评论(0编辑  收藏  举报