并发编程的两个问题:线程间通信和线程同步
程序员层面:happens-before
JMM层面:内存屏障和monitor(使用cas实现)
处理器层面:CAS和lock等指令实现内存屏障
线程间通信:共享内存隐式通信和消息传递显示通信
同步:调节线程的执行顺序和并发性
在共享内存中,同步是显示的,必须控制代码块在线程间互斥执行(方法原子性调用)。
在消息传递中,因为需要发送消息和接收处理结果,所以同步是隐式的。
JMM抽象结构
java的线程间通信是隐式的,线程A通过修改共享变量,线程B读取共享变量,从而实现通信。
从源代码到指令序列的重排序
编译器(1)编译器优化重排序
处理器(2)指令集的并行流水线技术的重排序
处理器(3)内存系统的重排序
java的编译器通过给生成指令序列时插入特定的内存屏障来保证顺序的可预测性
内存屏障:写屏障和读屏障是jmm的,而具体实现是通过特定指令比如lock实现
Java内存模型(JMM)规定了如何通过内存屏障来实现线程间的内存可见性和操作顺序。Java提供了几个关键字和类来实现这些内存屏障:
volatile 关键字:使用volatile声明的变量会在每次读写时插入内存屏障,在指令层面是通过lock实现的
对volatile变量的写操作会在写之后插入一个写屏障(store barrier),确保所有前面的写操作在该屏障之前完成。
对volatile变量的读操作会在读之前插入一个读屏障(load barrier),确保所有后面的读操作在该屏障之后进行。
synchronized 关键字:synchronized块的进入和退出会分别插入进入屏障和退出屏障,在指令层面是通过monitor实现的
进入 synchronized块时,会插入一个加载屏障,确保该块内的所有读取操作在进入屏障之后。
退出 synchronized块时,会插入一个存储屏障,确保该块内的所有写入操作在退出屏障之前。
java.util.concurrent 包:Java的并发包提供了更高层次的并发控制机制,如Atomic类、Locks和Executors,这些类内部使用了内存屏障来保证线程安全。
内存屏障的种类
内存屏障可以分为以下几种类型:
LoadLoad 屏障:
确保在屏障前的加载操作在屏障后的加载操作之前完成。
示例:Load1; LoadLoad; Load2
StoreStore 屏障:
确保在屏障前的存储操作在屏障后的存储操作之前完成。
示例:Store1; StoreStore; Store2
LoadStore 屏障:
确保在屏障前的加载操作在屏障后的存储操作之前完成。
示例:Load1; LoadStore; Store2
StoreLoad 屏障:
确保在屏障前的存储操作在屏障后的加载操作之前完成。
示例:Store1; StoreLoad; Load2
这种屏障开销最大,因为它通常会导致处理器刷新所有写缓冲区。
happens-before:一个操作需要在另一个操作之前执行
同步:就是为了让一个线程能感知到另一个线程的执行顺序,从而协调
已同步:各个线程的全局执行顺序已被规划
临界区内的代码可以重排序,
volatile变量的读写可以实现线程间的通信
volatile的内存语义:
写:写入内存本地内存后立刻刷入主内存,并且让其他本地内存中的变量失效。
读:每次读,都从主内存读。
volatile通过插入内存屏障,保证读写不会重排序。
具体来说:在每个volatile变量使用前或者后插入屏障。
锁的内存语义
临界区需要做同步控制,保证同一时期只有一个线程能够执行。
锁也相当于释放锁的线程给获取锁的线程发送信号。
进入临界区后,临界区中的共享变量都需要从主内存中重新获得。
出临界区时,会将共享变量写入主内存。
ReenterLock
加公平锁:
先判断如果没锁,判断加锁的线程是不是队首,如果是加锁成功。
判断线程是不是持有锁的线程,如果是加锁成功。
否则加锁失败。
protect final boolean tryAcquire(int acquires){
final Thread current=Thread.currentThread();
int c=getState();
if(c==0){
if(isFirst(current) &&compareAndSetState(0,acquires)){
setExclusiveOwnerThread(current);
return true;
}
}
else if(current=getExclusiveOwnerThread()){
int nextc = c+acquires;
if(nextc < 0){
throw new Error("Maximum lock count exceeded");
}
setState(nextc);
return true;
}
return false;
}
释放锁:
protected final boolean tryRelease(int releases){
int c=getState - releases;
if(Thread.currentThread()!=getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free=false;
if(c==0){
free=true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
可以看出锁的实现基于:
(1)在jmm上依赖内存屏障。
(2)核心是一个volatile变量state。
(3)使用cas更新volatile变量。
并发的核心就是使用volatile和CAS,其所有的包都是依赖这两个
final的内存语义
final的初始化的内存语义就是final变量在构造函数的初始化不会重排序出构造函数,在其他线程访问的永远都是已经初始化后的final变量。
如果对象在构造完成之前就被其他线程可见,从而可能导致其他线程可能看到一个部分初始化或者未初始化的对象。
不安全发布可能导致构造函数中的某些字段初始化在其他线程访问之后,从而导致其他线程访问未初始化或部分初始化的字段。
普通变量在构造函数中的初始化可能被重排出构造函数中
只要对象正确构造(没有重排序出构造函数),那么就不需要同步(volatile和synch)就可以保证其他线程看到的是初始化后的值
使用volatile或者synch或者线程安全的容器也可以
双重检查锁和延迟初始化(懒加载)
class A{
private static volatile A a;
private A(){}
public A getInstance(){
if(a==null){
synchronized(A.class){
if(a==null)
a=new A();
}
}
return a;
}
a=new A();可以分解为:
memory=allocate();//分配空间
ctorInstance(memory);//初始化
a=memory;//指向
害怕将初始化ctorInstance(memory);//初始化重排序到其他线程使用之后,所以加一个volatile。