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操纵,就能实现多线程的入栈和出栈。
无锁链表:无锁链表要复杂很多,因为无锁链表要在中间插入和删除元素。