Files
JavaYouth/docs/java_concurrency/Java并发体系-第二阶段-锁与同步-[2].md
2022-07-24 20:30:09 +08:00

500 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 'Java并发体系-第二阶段-锁与同步-[2]'
tags:
- Java并发
- 原理
- 源码
categories:
- Java并发
- 原理
keywords: Java并发原理源码
description: '万字系列长文讲解-Java并发体系-第二阶段,从C++和硬件方面讲解。'
cover: 'https://npm.elemecdn.com/lql_static@latest/logo/Java_concurrency.png'
abbrlink: '8210870'
date: 2020-10-07 22:10:58
---
# 可见性设计的硬件
<img src="https://npm.elemecdn.com/youthlql@1.0.8/Java_concurrency/Source_code/Second_stage/0016.png">
从硬件的级别来考虑一下可见性的问题
**1、第一个可见性的场景**每个处理器都有自己的寄存器register所以多个处理器各自运行一个线程的时候可能导致某个变量给放到寄存器里去接着就会导致各个线程没法看到其他处理器寄存器里的变量的值修改了就有可能在寄存器的级别导致变量副本的更新无法让其他处理器看到。
**2、第二个可见性的场景**然后一个处理器运行的线程对变量的写操作都是针对写缓冲来的store buffer并不是直接更新主内存所以很可能导致一个线程更新了变量但是仅仅是在写缓冲区里罢了没有更新到主内存里去。这个时候其他处理器的线程是没法读到他的写缓冲区的变量值的所以此时就是会有可见性的问题。
**3、第三个可见性的场景**然后即使这个时候一个处理器的线程更新了写缓冲区之后将更新同步到了自己的高速缓存里cache或者是主内存然后还把这个更新通知给了其他的处理器但是其他处理器可能就是把这个更新放到无效队列里去没有更新他的高速缓存。此时其他处理器的线程从高速缓存里读数据的时候读到的还是过时的旧值。【处理器是优先从自己的高速缓存里取读取变量副本】
可见性发生的问题
如果要实现可见性的话其中一个方法就是通过MESI协议这个MESI协议实际上有很多种不同的时间因为他不过就是一个协议罢了具体的实现机制要靠具体底层的系统如何实现。
根据具体底层硬件的不同MESI协议的实现是有区别的。比如说MESI协议有一种实现就是一个处理器将另外一个处理器的高速缓存中的更新后的数据拿到自己的高速缓存中来更新一下这样大家的缓存不就实现同步了然后各个处理器的线程看到的数据就一样了。
# MESI-缓存一致性协议(简介)
1、为了实现MESI协议有两个配套的专业机制要给大家说一下flush处理器缓存、refresh处理器缓存。
- flush处理器缓存他的意思就是把自己更新的值刷新到高速缓存里去或者是主内存因为必须要刷到高速缓存或者是主内存才有可能在后续通过一些特殊的机制让其他的处理器从自己的高速缓存或者是主内存里读取到更新的值
- 除了flush以外他还会发送一个消息到总线bus通知其他处理器某个变量的值被他给修改了
- refresh处理器缓存他的意思就是说处理器中的线程在读取一个变量的值的时候如果发现其他处理器的线程更新了变量的值必须从其他处理器的高速缓存或者是主内存读取这个最新的值更新到自己的高速缓存中
2、所以说为了保证可见性在底层是通过MESI协议、flush处理器缓存和refresh处理器缓存这一整套机制来保障的
3、要记住flush和refresh这两个操作flush是强制刷新数据到高速缓存主内存不要仅仅停留在写缓冲器里面refresh是从总线嗅探发现某个变量被修改必须强制从其他处理器的高速缓存或者主内存加载变量的最新值到自己的高速缓存里去。【不同的硬件实现可能略有不同】
4、内存屏障的使用在底层硬件级别的原理其实就是在执行flush和refreshMESI协议是如何与内存屏障搭配使用的flush、refresh
```java
volatile boolean isRunning = true;
isRunning = false; => 写volatile变量就会通过执行一个内存屏障在底层会触发flush处理器缓存的操作while(isRunning) {}读volatile变量也会通过执行一个内存屏障在底层触发refresh操作
```
# 内存屏障的相关讲解
> 上面的文章可能已经把读者搞混了,其实可见性和有序性最主要的就是内存屏障,下面来介绍下内存屏障帮读者梳理一下。
内存屏障是被插入两个CPU指令之间的一种指令用来禁止处理器指令发生重排序像屏障一样从而保障有序性的。另外为了达到屏障的效果它也会使处理器写入、读取值之前将写缓冲器的值写入高速缓存清空无效队列实现可见性。
举例:将写缓冲器数据写入高速缓存,能够避免不同处理器之间不能访问写缓冲器而导致的可见性问题,以及有效地避免了存储转发问题;清空无效队列保证该处理器上高速缓存中不存在旧的副本,进而拿到最新数据
## 基本内存屏障
- LoadLoad屏障 对于这样的语句 Load1; LoadLoad; Load2在Load2及后续读取操作要读取的数据被访问前保证Load1要读取的数据被读取完毕。
- StoreStore屏障对于这样的语句 Store1; StoreStore; Store2在Store2及后续写入操作执行前保证Store1的写入操作对其它处理器可见。
- LoadStore屏障对于这样的语句Load1; LoadStore; Store2在Store2及后续写入操作被执行前保证Load1要读取的数据被读取完毕。
- StoreLoad屏障对于这样的语句Store1; StoreLoad; Load2在Load2及后续所有读取操作执行前保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的冲刷写缓冲器清空无效化队列。在大多数处理器的实现中这个屏障是个万能屏障兼具其它三种内存屏障的功能。
以上的四种屏障主要依据不同处理器支持的重排序读写读读写写写读来确定的比如某些处理器只支持写读重排序因此只需要StoreLoad屏障
下面对上述的基本屏障进行利用,以针对不同的目的用相应的屏障。
## 可见性保障
主要分为加载屏障Load Barrier和存储屏障Store Barrier
- 加载屏障StoreLoad屏障作为万能屏障作用是冲刷写缓冲器清空无效化队列这样处理器在读取共享变量时因为本高速缓存中的数据是无效的因此先从主内存或其他处理器的高速缓存中读取相应变量更新到自己的缓存中
- 存储屏障同样使用StoreLoad屏障作用是将写缓冲器内容写入高速缓存中使处理器对共享变量的更新写入高速缓存或者主内存中 ,同时解决存储转发问题,使得写缓冲器中的数据不存在旧值
以上两种屏障解决可见性问题。
## 有序性保障
主要分为获取屏障Acquire Barrier和释放屏障Release Barrier
- 获取屏障相当于LoadLoad屏障和LoadStore屏障的组合它能禁止该屏障之前的任何读操作与该屏障之后的任何读、写操作之间进行重排序
- 释放屏障相当于StoreLoad屏障与StoreStore屏障的组合它能够禁止该屏障之前的任何读、写操作与该屏障之后的任何写操作之间进行重排序
对于其实大家记住volatile修饰的字段和普通修饰的字段同样不可以重排序因此只要存在读写、写写、写读、读读等操作包含了volatile关键字都会在操作指令之间插入屏障的具体插入什么屏障可以根据对应的操作插入。
## synchronized
```
结论:
1原子性加锁和释放锁ObjectMonitor
2可见性加了Load屏障和Store屏障释放锁flush数据加锁会refresh数据
3有序性Acquire屏障和Release屏障保证同步代码块内部的指令可以重排但是同步代码块内部的指令和外面的指令是不能重排的
```
举个例子说明加屏障的顺序:
```java
int b = 0;
int c = 0;
synchronized(this) { -> monitorenter
//Load内存屏障
//Acquire内存屏障
int a = b;
c = 1; // synchronized代码块里面还是可能会发生指令重排
//Release内存屏障
} -> monitorexit
//Store内存屏障
```
1、java的并发技术底层很多都对应了内存屏障的使用包括synchronized他底层也是依托于各种不同的内存屏障来保证可见性和有序性的
2、按照可见性来划分的话内存屏障可以分为Load屏障和Store屏障。
- Load屏障的作用是执行refresh处理器缓存的操作说白了就是对别的处理器更新过的变量从其他处理器的高速缓存或者主内存加载数据到自己的高速缓存来确保自己看到的是最新的数据。
- Store屏障的作用是执行flush处理器缓存的操作说白了就是把自己当前处理器更新的变量的值都刷新到高速缓存或者主内存里去
+ 在monitorexit指令之后会有一个Store屏障让线程把自己在同步代码块里修改的变量的值都执行flush处理器缓存的操作刷到高速缓存或者主内存里去然后在monitorenter指令之后会加一个Load屏障执行refresh处理器缓存的操作把别的处理器修改过的最新值加载到自己高速缓存里来
+ 所以说通过Load屏障和Store屏障就可以让synchronized保证可见性。
3、按照有序性保障来划分的话还可以分为Acquire屏障和Release屏障。
- 在monitorenter指令之后Load屏障之后会加一个Acquire屏障这个屏障的作用是禁止读操作和读写操作之间发生指令重排序。在monitorexit指令之前会加一个Release屏障这个屏障的作用是禁止写操作和读写操作之间发生重排序。
- 所以说,通过 Acquire屏障和Release屏障就可以让synchronzied保证有序性只有synchronized内部的指令可以重排序但是绝对不会跟外部的指令发生重排序。
## volatile
```
前面讲过lock前缀指令相当于一个内存屏障lock前缀指令同时保证可见性和有序性
1可见性加了Load屏障和Store屏障释放锁flush数据加锁会refresh数据
2有序性Acquire屏障和Release屏障保证同步代码块内部的指令可以重排但是同步代码块内部的指令和外面的指令是不能重排的
3不保证原子性
```
1、volatile对原子性的保证真的是非常的有限其实主要就是32位jvm中的long/double类型变量的赋值操作是不具备原子性的加上volatile就可以保证原子性了。但是总体上就说不保证原子性。
2、
```java
volatile boolean isRunning = true;
线程1
Release屏障
isRunning = false;
Store屏障
线程2
Load屏障
while(isRunning) {
Acquire屏障
// 代码逻辑
}
```
- 在volatile变量写操作的前面会加入一个Release屏障然后在之后会加入一个Store屏障这样就可以保证volatile写跟Release屏障之前的任何读写操作都不会指令重排然后Store屏障保证了写完数据之后立马会执行flush处理器缓存的操作
- 在volatile变量读操作的前面会加入一个Load屏障这样就可以保证对这个变量的读取时如果被别的处理器修改过了必须得从其他处理器的高速缓存或者主内存中加载到自己本地高速缓存里保证读到的是最新数据在之后会加入一个Acquire屏障禁止volatile读操作之后的任何读写操作会跟volatile读指令重排序
跟之前讲解的volatie读写内存屏障的知识对比一下其实你看一下是类似的意思的。
## 强调
- 其实不要对内存屏障这个东西太较真因为说句实话不同版本的JVM不同的底层硬件都可能会导致加的内存屏障有一些区别所以这个本来就没完全一致的。你只要知道内存屏障是如何保证volatile的可见性和有序性的就可以了
- 看各种并发相关的书和文章,对内存屏障到底是加的什么屏障,莫衷一是,没有任何一个官方权威的说法,因为这个内存屏障太底层了,底层到了涉及到了硬件,硬件不同对内存屏障的实现是不一样的
- 内存屏障这个东西大概来说其实就是大概的给你说一下这个意思尤其是Release屏障Store屏障和Load屏障还好理解一些比较简单Acqurie屏障莫衷一是我也没法给你一个官方的定论
- 如果你一定 要了解清除,到底加的准确的屏障是什么?到底是如何跟上下的指令避免重排的,你自己去研究吧。【我也看过很多的资料,做过很多的研究,硬件对这个东西的实现和承诺,莫衷一是,没有标准和官方定论。-----这句话是某BAT大佬说的】
内存屏障对应的底层的一些基本的硬件级别的原理,也都讲清楚了
# MESI-缓存一致性协议(进阶)
## MESI-初步
1、处理器高速缓存的底层数据结构实际是一个拉链散列表的结构就是有很多个bucket每个bucket挂了很多的cache entry每个cache entry由三个部分组成tag、cache line和flag其中的cache line【缓存行】就是缓存的数据。tag指向了这个缓存数据在主内存中的数据的地址flag标识了缓存行的状态另外要注意的一点是cache line中可以包含多个变量的值
2、处理器会操作一些变量怎么在高速缓存里定位到这个变量呢
- 那么处理器在读写高速缓存的时候实际上会根据变量名执行一个内存地址解码的操作解析出来3个东西index、tag和offset。index用于定位到拉链散列表中的某个buckettag是用于定位cache entryoffset是用于定位一个变量在cache line中的位置
- 如果说可以成功定位到一个高速缓存中的数据而且flag还标志着有效则缓存命中否则不满足上述条件就是缓存未命中。如果是读数据未命中的话会从主内存重新加载数据到高速缓存中现在处理器一般都有三级高速缓存L1、L2、L3越靠前面的缓存读写速度越快
3、因为有高速缓存的存在所以就导致各个处理器可能对一个变量会在自己的高速缓存里有自己的副本这样一个处理器修改了变量值别的处理器是看不到的所以就是为了这个问题引入了缓存一致性协议MESI协议
4、MESI协议规定对一个共享变量的读操作可以是多个处理器并发执行的但是如果是对一个共享变量的写操作只有一个处理器可以执行其实也会通过排他锁的机制保证就一个处理器能写
之前说过那个cache entry的flag代表了缓存数据的状态MESI协议中划分为
- invalid无效的标记为I这个意思就是当前cache entry无效里面的数据不能使用
- shared共享的标记为S这个意思是当前cache entry有效而且里面的数据在各个处理器中都有各自的副本但是这些副本的值跟主内存的值是一样的各个处理器就是并发的在读而已
- exclusive独占的标记为E这个意思就是当前处理器对这个数据独占了只有他可以有这个副本其他的处理器都不能包含这个副本
- modified修改过的标记为M只能有一个处理器对共享数据更新所以只有更新数据的处理器的cache entry才是exclusive状态表明当前线程更新了这个数据这个副本的数据跟主内存是不一样的
MESI协议规定了一组消息就说各个处理器在操作内存数据的时候都会往总线发送消息而且各个处理器还会不停的从总线嗅探最新的消息通过这个总线的消息传递来保证各个处理器的协作
**下面来详细的图解MESI协议的工作原理**
1、处理器0读取某个变量的数据时首先会根据index、tag和offset从高速缓存的拉链散列表读取数据如果发现状态为I也就是无效的此时就会发送read消息到总线
2、接着主内存会返回对应的数据给处理器0处理器0就会把数据放到高速缓存里同时cache entry的flag状态是S
3、在处理器0对一个数据进行更新的时候如果数据状态是S则此时就需要发送一个invalidate消息到总线尝试让其他的处理器的高速缓存的cache entry全部变为I以获得数据的独占锁。
4、其他的处理器1会从总线嗅探到invalidate消息此时就会把自己的cache entry设置为I也就是过期掉自己本地的缓存然后就是返回invalidate ack消息到总线传递回处理器0处理器0必须收到所有处理器返回的ack消息
5、接着处理器0就会将cache entry先设置为E独占这条数据在独占期间别的处理器就不能修改数据了因为别的处理器此时发出invalidate消息这个处理器0是不会返回invalidate ack消息的除非他先修改完再说
6、接着处理器0就是修改这条数据接着将数据设置为M也有可能是把数据此时强制写回到主内存中具体看底层硬件实现
7、然后其他处理器此时这条数据的状态都是I了那如果要读的话全部都需要重新发送read消息从主内存或者是其他处理器来加载这个具体怎么实现要看底层的硬件了都有可能的
<img src="https://npm.elemecdn.com/youthlql@1.0.8/Java_concurrency/Source_code/Second_stage/0017.jpg">
## MESI-优化
<img src="https://npm.elemecdn.com/youthlql@1.0.8/Java_concurrency/Source_code/Second_stage/0018.png">
MESI协议如果每次写数据的时候都要发送invalidate消息等待所有处理器返回ack然后获取独占锁后才能写数据那可能就会导致性能很差了因为这个对共享变量的写操作实际上在硬件级别变成串行的了。所以为了解决这个问题硬件层面引入了写缓冲器和无效队列
1、
写缓冲器的作用是一个处理器写数据的时候直接把数据写入缓冲器同时发送invalidate消息然后就认为写操作完成了接着就干别的事儿了不会阻塞在这里。接着这个处理器如果之后收到其他处理器的ack消息之后才会把写缓冲器中的写结果拿出来通过对cache entry设置为E加独占锁同时修改数据然后设置为M。
其实写缓冲器的作用就是处理器写数据的时候直接写入缓冲器不需要同步阻塞等待其他处理器的invalidate ack返回这就大大提升了硬件层面的执行效率了
包括查询数据的时候,会先从写缓冲器里查,因为有可能刚修改的值在这里,然后才会从高速缓存里查,这个就是存储转发
2、
引入无效队列就是说其他处理器在接收到了invalidate消息之后不需要立马过期本地缓存直接把消息放入无效队列就返回ack给那个写处理器了这就进一步加速了性能然后之后从无效队列里取出来消息过期本地缓存即可
通过引入写缓冲器和无效队列一个处理器要写数据的话这个性能其实很高的他直接写数据到写缓冲器发送一个validate消息出去就立马返回执行别的操作了其他处理器收到invalidate消息之后直接放入无效队列立马就返回invalidate ack
## 硬件层面的MESI协议为何会引发有序性和可见性的问题
MESI协议在硬件层面的原理其实大家都已经了解的很清晰了。
讲了这么多再来看一下MESI-协议为何会引发可见性和有序性的问题
- 可见性写缓冲器和无效队列导致的写数据不一定立马写入自己的高速缓存或者主内存是因为可能写入了写缓冲器读数据不一定立马从别人的高速缓存或者主内存刷新最新的值过来invalidate消息在无效队列里面
- 有序性:
简单的举两个例子
1StoreLoad重排序
```java
int a = 0;
int c = 1;
线程1
a = 1; //Store操作
int b = c; //因为要读C的值所以这个是load操作
```
这个很简单吧第一个是Store第二个是Load。但是可能处理器对store操作先写入了写缓冲器此时这个写操作相当于没执行。然后就执行了第二行代码第二行代码的b是局部变量那这个操作等于是读取c的值是load操作。
- 第一个store操作写到写缓冲器里去了导致其他的线程是读不到的看不到的好像是第一个写操作没执行一样第二个load操作成功的执行了
- 这就导致好像第二行代码的load先执行了第一行代码的store后执行
StoreLoad重排明明Store先执行Load后执行看起来好像Load先执行Store后执行
2StoreStore重排序
```java
resource = loadResource();
loaded = true;
```
- 两个写操作但是可能第一个写操作写入了写缓冲器然后第二个写操作是直接修改的高速缓存【可能此时第二个数据的状态是m】这个时候不就导致了两个写操作顺序颠倒了诸如此类的重排序都可能会因为MESI的机制发生
- 可见性问题也是一样的写入写缓冲器之后没刷入高速缓存导致别人读不到读数据的时候可能invalidate消息在无效队列里导致没法立马感知到过期的缓存立马加载最新的数据
## 内存屏障在硬件层面的实现原理
1、可见性问题
> Store屏障 + Load屏障
如果加了Store屏障之后就会强制性要求你对一个写操作必须阻塞等待到其他的处理器返回invalidate ack之后对数据加锁然后修改数据到高速缓存中必须在写数据之后强制执行flush操作。
他的效果,要求一个写操作必须刷到高速缓存(或者主内存),不能停留在写缓冲里
如果加了Load屏障之后在从高速缓存中读取数据的时候如果发现无效队列里有一个invalidate消息此时会立马强制根据那个invalidate消息把自己本地高速缓存的数据设置为I过期然后就可以强制从其他处理器的高速缓存中加载最新的值了。这就是refresh操作
2、有序性问题
> 内存屏障Acquire屏障Release屏障但是都是由基础的StoreStore屏障,StoreLoad屏障可以避免指令重排序的效果
StoreStore屏障会强制让写数据的操作全部按照顺序写入写缓冲器里他不会让你第一个写到写缓冲器里去第二个写直接修改高速缓存了。
```java
resource = loadResource();
StoreStore屏障
loaded = true;
```
StoreLoad屏障他会强制先将写缓冲器里的数据写入高速缓存中接着读数据的时候强制清空无效队列对里面的validate消息全部过期掉高速缓存中的条目然后强制从主内存里重新加载数据
a = 1; // 强制要求必须直接写入高速缓存,不能停留在写缓冲器里,清空写缓冲器里的这条数据 store
int b = c; //load
> java内存模型是对底层的硬件模型cpu缓存模型做了大幅度的简化提供一个抽象和统一的模型给java程序员易于理解很多时候如果要理解一些技术的本质还是要深入到底层去研究的。
# 原子操作的实现原理
原子atomic本意是“不能被进一步分割的最小粒子”而原子操作atomic operation意为“不可被中断的一个或一系列操作”。在多处理器上实现原子操作就变得有点复杂。让我们一起来聊一聊在Intel处理器和Java里是如何实现原子操作的。
## 相关术语
<img src="https://npm.elemecdn.com/youthlql@1.0.8/Java_concurrency/Source_code/Second_stage/0019.png">
## 处理器如何实现原子操作
32位IA-32处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操 作。首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写入一个字节是原子的意思是当一个处理器读取一个字节时其他处理器不能访问这个字节的内存地址。Pentium 6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的但是复杂的内存操作处理器是不能自动保证其原子性的比如跨总线宽度、跨多个缓存行和跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
### 使用总线锁保证原子性
**第一个机制是通过总线锁保证原子性。**如果多个处理器同时对共享变量进行读改写操作 i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操 作就不是原子的操作完之后共享变量的值会和期望的不一致。举个例子如果i=1我们进行 两次i++操作我们期望的结果是3但是有可能结果是2如图所示。
<img src="https://npm.elemecdn.com/youthlql@1.0.8/Java_concurrency/Source_code/Second_stage/0020.png">
原因可能是多个处理器同时从各自的缓存中读取变量i分别进行加1操作然后分别写入 系统内存中。那么想要保证读改写共享变量的操作是原子的就必须保证CPU1读改写共享 变量的时候CPU2不能操作缓存了该共享变量内存地址的缓存。
处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK信号当一个处理器在总线上输出此信号时其他处理器的请求将被阻塞住那么该处理器可以独占共享内存。
### 使用缓存锁保证原子性
**第二个机制是通过缓存锁定来保证原子性**。在同一时刻,我们只需保证对某个内存地址 的操作是原子性即可但总线锁定把CPU和内存之间的通信锁住了这使得锁定期间其他处 理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
频繁使用的内存会缓存在处理器的L1、L2和L3高速缓存里那么原子操作就可以直接在处理器内部缓存中进行并不需要声明总线锁在Pentium 6和目前的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存 行中并且在Lock操作期间被锁定那么当它执行锁操作回写到内存时处理器不在总线上声言LOCK信号而是修改内部的内存地址并允许它的缓存一致性机制来保证操作的原子性因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据当其他处理器回写已被锁定的缓存行的数据时会使缓存行无效在如上图所示的例子中当CPU1修改缓存行中的i时使用了缓存锁定那么CPU2就不能同时缓存i的缓存行。
**但是有两种情况下处理器不会使用缓存锁定。**
第一种情况是当操作的数据不能被缓存在处理器内部或操作的数据跨多个缓存行cache line则处理器会调用总线锁定。
第二种情况是有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。针对以上两个机制我们通过Intel处理器提供了很多Lock前缀的指令来实现。例如位测试和修改指令BTS、BTR、BTC交换指令XADD、CMPXCHG以及其他一些操作数和逻辑指令如ADD、OR被这些指令操作的内存区域就会加锁导致其他处理器不能同时访问它。
## Java如何实现原子操作
1使用循环CAS实现原子操作
2使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁JVM实现锁的方式都用了循环CAS即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁当它退出同步块的时候使用循环CAS释放锁。
传统的锁也就是下文要说的重量级锁依赖于系统的同步函数在linux上使用`mutex`互斥锁,最底层实现依赖于`futex`,这些同步函数都涉及到用户态和内核态的切换、进程的上下文切换,成本较高。对于加了`synchronized`关键字但**运行时并没有多线程竞争,或两个线程接近于交替执行的情况**,使用传统锁机制无疑效率是会比较低的。
futex由一个内核层的队列和一个用户空间层的atomic integer构成。当获得锁时尝试cas更改integer如果integer原始值是0则修改成功该线程获得锁否则就将当期线程放入到 wait queue中即操作系统的等待队列。【及其类似于AQS的设计思想可能AQS就参考了futex的思想】