Java多线程 - 锁
三性
-
可见性
指的是线程之间的可见性,一个线程对状态的修改,对其他线程是可见的。在
Java
中volatile
、synchronized
和final
实现可见性。 -
原子性
如果一个操作是不可分割的,我们则称之为原子操作,也就是有原子性。比如
i++
,就不是原子操作。在Java
中synchronized
和在lock
、unlock
中操作保证原子性 -
有序性
一系列操作是按照规定的顺序发生的。如果在本线程之内观察,所有的操作都是有序的,如果在其他线程观察,所有的操作都是无序的;前半句指“线程内表现为串行语义”后半句指“指令重排序”和“工作内存和主存同步延迟”。
Java
语言提供了volatile
和synchronized
两个关键字来保证线程之间操作的有序性。volatile
是因为其本身包含“禁止指令重排序”的语义,
synchronized
是由“一个变量在同一个时刻只允许一条线程对其进行lock
操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
Volatile
作用
- 保证了可见性。
- 防止指令重排序(有序性)。
- 半个原子性: 对任意单个volatile变量的读/写具有原子性
读-写时内存语义
-
写的内存语义
-
当写一个
volatile
变量时,JMM
会把该线程对应的本地内存中共享变量值刷新会共享内存 -
读的内存语义
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
Volatile的内存屏障实现
是否可以重排序 | 第二个操作 | 第二个操作 | 第二个操作 |
---|---|---|---|
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | YES | YES | NO |
volatile读 | NO | NO | NO |
volatile写 | YES | NO | NO |
编译器生成字节码文件时会在指令序列中插入内存屏障来禁止特定类型的处理器排序。但是,在实际执行过程中,只要不改变volatile
的内存语义,
编译器可以根据实际情况省略部分不必要的内存屏障。
放置的内存屏障
在每个volatile写操作前面插入StoreStore屏障
在每个volatile写操作后面插入StoreLoad屏障
在每个volatile读操作后面插入LoadLoad屏障
在每个volatile读操作后面插入LoadStore屏障
什么是指令重排序
执行任务的时候,为了提高编译器和处理器的执行性能,编译器和处理器(包括内存系统,内存在行为没有重排但是存储的时候是有变化的)会对指令重排序。编译器优化的重排序是在编译时期完成的,指令重排序和内存重排序是处理器重排序
编译器优化的重排序
在不改变单线程语义的情况下重新安排语句的执行顺序。比如,在第10行创建了临时变量,在使用前才进行初始化。或者,下面的情况。
int a = 3;
a = 5;
int a = 5
指令级并行重排序
处理器的指令级并行技术将多条指令重叠执行,如果不存在数据的依赖性,为了使 CPU 内部的运算单元能够尽量被充分利用,处理器可能会对输入的字节码指令进行重排序处理,也就是处理器优化。
现在的CPU一般采用流水线来执行指令。一个指令的执行被分成:取址、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。指令流水线并不是串行的,多个指令可以同时处于同一个阶段,只要CPU内部相应的处理部件未被占满即可。
比如说CPU有一个加法器和一个除法器,那么一条加法指令和一条除法指令就可能同时处于“执行”阶段, 而两条加法指令在“执行”阶段就只能串行工作。
a++;
b=f(a); //会阻塞直到a++有结果
c--; //没有因果关系,所以,可以先计算。但是,这个先计算是发生在CPU执行时
像这样有依赖关系的指令如果挨得很近,后一条指令必定会因为等待前一条执行的结果,而在流水线中阻塞很久,占用流水线的资源。而编译器的乱序,作为编译优化的一种手段,则试图通过指令重排将这样的两条指令拉开距离, 以至于后一条指令进入CPU的时候,前一条指令结果已经得到了,那么也就不再需要阻塞等待了。比如将指令重排为:
a++; c--; b=f(a);
相比于CPU的乱序,编译器的乱序才是真正对指令顺序做了调整。但是编译器的乱序也必须保证程序上下文的因果关系不发生改变。
内存系统的重排序
因为使用了读写缓存区,使得看起来并不是顺序执行的。
内存重排序实际上并不是真的相关操作被排序了,而是因为CPU引入高速缓存还没来得及刷新导致;
寄存器是什么和高速缓存什么区别:
CPU要取数据,处理数据,都要放到寄存器处理。一般寄存器不用太大,它只要存放指令一次操作的数据就够了。
高速缓存是内存的部分拷贝,因为高速缓存速度快,把常用的数据放这里可以提高速度。
每个CPU都有自己的缓存,为了提高共享变量的写操作,CPU把整个操作变成异步的了,如果写入操作还没来的及同步到其它CPU,就有可能发生其它CPU读取到的是旧的值,因此看起来这条指令还没执行一样。
多CPU架构中,多个CPU高速缓冲区数据同步,依赖于缓存一致性协议
缓存一致性协议
在多处理器的情况下,每个处理器总是嗅探总线上传播的数据来检查自己的缓存是否过期,当处理器发现自己对应的缓存对应的地址被修改,
就会将当前处理器的缓