CAS(CompareAndSwap)
什么是CAS?
在Java中调用的是Unsafe
的如下方法来CAS修改对象int属性的值(借助C来调用CPU底层指令实现的):
/**
*
* @param o 对象所在类本身的对象(一般这里是对一个对象的属性做修改,才会出现并发)
* @param offset 属性在对象中的相对偏移量位置(获取偏移量也是通过unsafe的⼀个⽅法: objectFieldOffset(Fieldfield)来获取属性在对象中的偏移量;)
* @param expected 修改前期待的值,原来的值和修改之前的值需要一致
* @param x 修改的目标值
* @return 修改是否成功
*/
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
对象的引用进行比较后交换,交换成功则返回true,交换失败返回false
为什么需要CAS?
CAS能够保证性能的同时保证数据的可见性,可以说是一个非阻塞的轻量级锁,性能高于synchronized
CAS 原理
执行逻辑类似:
if (this == expect) {
this = update
return true;
} else {
return false;
}
交换过程完全是原子的,基本流程如下:
- 在CPU上计算完结果后,都会对比内存的结果是否还是原先的值
- 如果不是原先的值,则认为已被其它线程修改,不能替换
- 如果是原先的值,则认为没有其它线程去修改,则可以修改,因为变量是volatile类型,所以最终写入的数据会被其它线程看到,所以一个线程修改成功后,其它线程就发现自己修改失败了重新尝试修改
这明明是好几步的操作,怎么会是原子操作呢?
整体过程:Java通过JNI来调用native方法,而native方法的实现是C,C调用的是CPU底层指令实现。
在CPU底层指令中,是通过加锁的方式实现,实现方式有如下三种:
- 处理器自动保证基本内存操作的原子性:一个处理器访问某一个字节时,其它处理器不能访问这个字节的内存地址
- 使用总线锁保证原子性:当一个处理器在总线上输出LOCK#信号时,其它处理器的请求将被阻塞,那么该处理器可以独占使用共享内存
- 缓存锁保证原子性:频繁使用的内存会缓存在处理器的L1、L2、L3高速缓存中,那么原子操作可以直接在处理器内部缓存中进行,不需要声总线锁。当一个处理器修改缓存行中的i变量使用缓存锁定,那么其它处理器就不能同时缓存i变量的缓存行
CAS 优缺点
优点:
- 高性能:无锁的方式实现原子操作
- 使用简单:调用compareAndSwapxx()方法即可实现
缺点:
- ABA问题:由于CAS是检测值是否被改变,但一个变量的值原来是A,然后修改成B,然后又修改成A,这是实际上变量已被修改,但CAS检查的时候会发现它的值没有发生变化,还是能够被正常的CAS。解决思路就是通过对变量增加版本号,没修改一次版本号加1,所有原本ABA就是1A-2B-3A。在atomic包提供了一个类
AtomicStampedReference
就是采用类似的思路解决ABA问题 - 循环时间长开销大:在CAS更新失败会进入自旋,一旦长时间更新失败,就会占用较多的CPU
- 只能保证一个共享变量的原子操作:在只有一个变量的时候,可以使用CAS来保证原子操作,但如果有多个变量,CAS无法保证操作的原子性。解决方案就是用锁来保证多个变量的原子性;也可以使用位的方式,将多个变量合并成一个变量进行CAS操作,如x&F0|y&0F
CAS 应用
我们拿java.util.concurrent.atomic.AtomicInteger
举例:
public class AtomicInteger extends Number implements java.io.Serializable {
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 属性在这个对象⾥⾯的相对偏移量位置
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
// 要更新的变量
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* 如果当前值是期望值expect,则原子地将值设置为给定的更新值update
*
* @param expect 期望值
* @param update 更新值
* @return 如果true表示更新成功,false表示实际值不等于期望值
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
/**
* 将当前值原子的加1
*
* @return the 加1之前的值
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
...
}
java.util.concurrent.atomic
包中Atomic*基本都是使用相同的思路来实现原子操作的
volatile
volatile的主要作用是:
- 可见性:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其它线程来说是立即可见的
- 有序性:禁止指令重排序
volatile 可见性
可见性表示一个变量的值被更新后,是否能够在每个线程中可见
public class ExampleVisibility {
private boolean stop = false;
private void stop() {
this.stop = true;
}
private void run() {
while (!stop) {
// run
}
System.out.println("stop!!");
}
public static void main(String[] args) throws InterruptedException {
ExampleVisibility self = new ExampleVisibility();
new Thread(() -> {
self.run();
}).start();
Thread.sleep(1000 * 4);
new Thread(() -> {
// 停止while
self.stop();
}).start();
}
}
执行多次,你会发现有的时候无法停止while,也就是stop一直为false
问题原因:
在cpu中有多级cache,如果每次在内存中取效率不高,所有cpu中有自己的cache以提高效率(在缓存中,每个变量在缓存行上都有一个2bit的状态,那就是CPU EMSI协议)
有如下两种可能原因:
- 线程将变量stop修改后没有更新到主存中
- stop在cpu一级缓存或二级缓存有缓存,当线程要读取stop变量的时候发现已存在缓存中,没有再去主存中重新读取stop变量
解决方案:
在stop变量中增加volatile关键字保证可见性
private volatile boolean stop = false;
volatile 有序性
volatile在一定程度上保证有序性。其中主要就是通过禁止指令重排的方式实现有序性
指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果是正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。
代码转换为机器执行的指令所经过的步骤:
流水线优化问题:
在cpu内部不同的操作是由不同的硬件来做,比如专门ADD操作的硬件、INC的硬件、MOV的硬件,它们之间同时执行多个指令,上一级处理完成流到下一级,所以叫做流水线
在未排序之前需要阻塞等待ADD指令完成写入后INC才能继续执行,INC执行后才能MOV
在重排序之后,先执行了MOV,然后INC,指令之间没有阻塞(一般的重排序不会对结果产生影响)
public class ExampleReordering {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int count = 0;
while (true) {
count++;
x = 0;
y = 0;
a = 0;
b = 0;//clear
Thread one = new Thread(new Runnable() {
public void run() {
shortWait(100000);
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
one.start();
other.start();
one.join();
other.join();
String result = "第" + count + "次 (" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}
}
public static void shortWait(long interval) {
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
} while (start + interval >= end);
}
}
代码正确执行情况下,x和y不会同时等于0,在指令重排序的情况下,就可能发生x和y都等于0的异常情况
解决方案:
在每个变量中增加volatile
关键字禁止指令重排序
private volatile static int x = 0, y = 0;
private volatile static int a = 0, b = 0;
volatile 原理
在对比加入volatile关键字与未加入volatile关键字所生成的汇编代码发现,加入volatile关键字时会多出一个lock前缀指令,lock前缀指令相当于一个内存屏障(或内存栅栏),内存屏障主要提供如下功能:
- 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障之后;
- volatile会强制将对缓存的修改操作立即写入主内存
- 如果是写操作的时候,volatile会导致其它CPU中对应的此缓存行失效
volatile 应用
synchronized关键字是防止多个线程执行同一段代码,而volatile作用却不同,使用前需要具备如下两个条件:
- 对变量的写操作不依赖于当前变量的值
- 该变量没有包含在具有其它变量的不变式中
volatile能保证原子性吗?
不能,volatile关键字保证了操作的可见性,但无法保证变量操作的原子性,原因如下:
在多个线程对i变量进行i++操作时,实际上需要做如上4步,在线程A还没有将add后的i值putfield,上下文就切换到了线程B,线程B执行了add操作。
两个线程同时对一个老的i进行add,所有原来应该+2的变成只有+1,最终导致数据的偏差。
解决方案:在incr()方法中增加synchronized关键字
private synchronized void incr() {
i++;
}
synchronized
什么是synchronized?
是一种同步锁,通过synchronized
对方法或代码块修饰,确保多个线程在同一个时刻,只能有一个线程处于方法/代码块中,它确保了线程对变量访问的可见性和排他性
synchronized 实现原理
JVM是通过进入、退出对象监视器(Monitor)来实现对方法、同步块的同步的;而对象监视器的本质依赖于底层操作系统的互斥锁(Mutex Lock
)实现。
基本过程:
- 代码编译之后会在同步方法调用前加入一条
monitor.enter
指令,在退出方法和异常处插入monitor.exit
指令 - 当有一个线程进入后会获取锁(Mutex Lock),如果有另一个线程调用方法,则会阻塞在方法入口处,直到获取锁的线程
monitor.exit
之后才能继续尝试获取锁
synchronized 锁升级过程
在JDK1.6之前,synchronized是一个重量级锁,开销很大,所以经常被建议说少用点。
但在JDK1.6之后,该关键字进行了很多的优化,主要就是锁升级,会自动根据不同的资源竞争情况升级锁(锁膨胀)。
锁升级过程:
- 偏向锁:是指一段同步代码一直被一个线程所访问,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。降低获取锁的代价。
- 轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能(比如自旋锁)。
- 重量级锁:是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞(对象监视器Monitor),性能降低。
synchronized 应用
synchronized 有如下四种方式修饰对象来实现代码同步:
- 修饰一个代码块,对象锁,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,对象锁,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修饰一个静态的方法,类锁,其作用的对象是这个类的所有对象;
- 修饰一个类,类锁,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象;
- 示例代码:
package com.muse.thread;
import java.util.concurrent.TimeUnit;
public class SynchronizedDemo {
public static void main(String[] args) {
/** case1:无Synchronized,乱序输出 */
// NoneSyncDemo noneSyncDemo = new NoneSyncDemo();
// Thread thread1 = new Thread(noneSyncDemo);
// Thread thread2 = new Thread(noneSyncDemo);
/** case2:synchronized修饰代码块, 对象锁 */
// 加锁有效
// SyncBlockDemo syncBlockDemo = new SyncBlockDemo();
// Thread thread1 = new Thread(syncBlockDemo);
// Thread thread2 = new Thread(syncBlockDemo);
// 加锁无效
// SyncBlockDemo syncBlockDemo1 = new SyncBlockDemo();
// SyncBlockDemo syncBlockDemo2 = new SyncBlockDemo();
// Thread thread1 = new Thread(syncBlockDemo1);
// Thread thread2 = new Thread(syncBlockDemo2);
/** case3:synchronized修饰方法,对象锁 */
// 加锁有效
// SyncMethodDemo syncMethodDemo = new SyncMethodDemo();
// Thread thread1 = new Thread(syncMethodDemo);
// Thread thread2 = new Thread(syncMethodDemo);
// 加锁无效
// SyncMethodDemo syncMethodDemo1 = new SyncMethodDemo();
// SyncMethodDemo syncMethodDemo2 = new SyncMethodDemo();
// Thread thread1 = new Thread(syncMethodDemo1);
// Thread thread2 = new Thread(syncMethodDemo2);
/** case4:synchronized修饰静态方法,类锁 */
// 加锁有效
// SyncStaticMethodDemo syncStaticMethodDemo = new SyncStaticMethodDemo();
// Thread thread1 = new Thread(syncStaticMethodDemo);
// Thread thread2 = new Thread(syncStaticMethodDemo);
// 加锁有效
// SyncStaticMethodDemo syncStaticMethodDemo1 = new SyncStaticMethodDemo();
// SyncStaticMethodDemo syncStaticMethodDemo2 = new SyncStaticMethodDemo();
// Thread thread1 = new Thread(syncStaticMethodDemo1);
// Thread thread2 = new Thread(syncStaticMethodDemo2);
/** case5:synchronized修饰类,类锁 */
// 加锁有效
// SyncClassDemo syncClassDemo = new SyncClassDemo();
// Thread thread1 = new Thread(syncClassDemo);
// Thread thread2 = new Thread(syncClassDemo);
// 加锁有效
SyncClassDemo syncClassDemo1 = new SyncClassDemo();
SyncClassDemo syncClassDemo2 = new SyncClassDemo();
Thread thread1 = new Thread(syncClassDemo1);
Thread thread2 = new Thread(syncClassDemo2);
thread1.start();
thread2.start();
}
}
/**
* [case1:无Synchronized]
*/
class NoneSyncDemo implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(100);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
/**
* [case2:synchronized修饰代码块]
* 一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
**/
class SyncBlockDemo implements Runnable {
@Override
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(200);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
}
/**
* [case3:synchronized修饰方法]
* 一个线程访问一个对象中的synchronized修饰的方法时,其他试图访问该对象的线程将被阻塞。
**/
class SyncMethodDemo implements Runnable {
@Override
public synchronized void run() {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(200);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
/**
* [case4:synchronized修饰静态方法]
* 修饰一个静态的方法,类锁,其作用的对象是这个类的所有对象;
**/
class SyncStaticMethodDemo implements Runnable {
@Override
public void run() {
method();
}
public synchronized static void method() {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(200);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
/**
* [case5:synchronized修饰类]
* 修饰一个类,类锁,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象;
**/
class SyncClassDemo implements Runnable {
@Override
public void run() {
synchronized(SyncClassDemo.class) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(200);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
}
思考
volatile与synchronized的区别
区别点 | volatile | synchronized |
---|---|---|
修饰 | 只能用于修饰变量 | 可以用于修饰方法、代码块、类 |
线程阻塞 | 不会发生线程阻塞 | 发生阻塞 |
原子性 | 不能保证变量的原子性 | 可以保证变量原子性 |
可见性 | 可以保证变量在线程之间访问资源的可见性 | 可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步 |
同步性 | 能保证变量在私有内存和主内存间的同步 | synchronize是多线程之间访问资源的同步性(同时只能有一个线程访问) |
编译器优化 | 不会被编译器优化 | 标记的变量可以被编译器优化 |
synchronized与ReentrantLock的区别
区别点 | Synchronized | ReentrantLock |
---|---|---|
使用方式 | 关键字 | 实现类 |
实现方式 | JVM实现控制 | AQS实现控制 |
是否自动 | yes | no |
锁的获取 | 如果资源被锁,会一直等待 | 如果资源被锁,可以有多种处理方式 详情参见“面试题2” |
锁的释放 | 被锁的代码执行完or发生异常 | finally中手动编程释放 |
锁的状态 | 无法判断 | 可以判断,isLocked() |
锁的类型 | 可重入,不可中断,非公平锁 | 可重入,**可中断(lockInterruptibly),公平锁or非公平锁 |
总结
对CAS、volatile、synchronized原理分析,并对各自的特点、应用场景、使用方式做了举例。在最后比较了这几种保证线程安全机制的区别。希望通过本文能够帮你们揭开它们神秘的面纱。
扩展
CPU EMSI协议
MESI的四个字⺟分别代表了四个可以被标记在缓存⾏上的独⽴状态。(也就是⽤2bit来编码)
- Modified (M)
当缓存⾏处于Modified状态时,它表明该缓存⾏只存在于当前缓存中。并且这个缓存⾏中的数
据是脏数据,也就是说这个缓存⾏中的数据与主存中的数据不⼀致。缓存被要求在未来将缓存
⾏的数据写于主存中,但不⽤⽴即写⼊。但如果别的缓存向该缓存请求这个数据,那必须保证
该数据写⼊主存已经完成。当回写回主存完成后,缓存⾏状态会有Modified变为Shared状
态。 - Exclusive (E)
当缓存⾏处于Exclusive状态时,它表明该缓存⾏只存在于当前缓存中,不过其中的数据与主存
中的数据是⼀致的。当别的缓存向该缓存请求read当前缓存⾏时,缓存⾏状态会变成
Shared。或者当有write操作时,他会变成Modified状态。 - Shared (S)
当缓存⾏处于Shared状态时,它表明该缓存⾏可能同时存在与别的缓存中,并且其中的数据与
主存中⼀致。这个缓存⾏随时可能被丢弃(改变为Invalid状态)。 - Invalid (I)
当缓存⾏处于Invalid 状态时,表明该缓存⾏是⽆效的。
基本过程:
- CPU1从内存读取变量x。 CPU1会向总线发送⼀条读的消息, CPU1读取到数据之后,会将该缓
存⾏cache1的状态设置为E(独享,在底层汇编中加lock信号,保证缓存的⼀致性)。 - CPU2读取变量x。再CPU1还没有将x会写⼊内存的时候, CPU2也发送了⼀条读x变量的信号,
通过总线嗅探机制, CPU1会嗅探到CPU2要读取CPU1中的缓存⾏对应于内存的区域,那么 - CPU1的缓存⾏的状态将会由E转换为S(共享状态),并且CPU2对应的缓存⾏也是S状态。
CPU1修改数据。 CPU1⼜向总线发送消息要求修改x变量,那么CPU1将会锁住该缓存⾏,并将
状态由S改为M(修改状态); CPU2嗅探到CPU1要修改变量x,那么CPU2会将相应的缓存⾏
的状态由S改为I(⽆效)。 - 同步数据。当CPU1将数据x写回内存后,其对应缓存⾏状态由M转换为E,当CPU2再次发送读
消息时, CPU1状态由E改为S, CPU2的状态由I改为S。
参考
- Java 并发编程ReentrantLock–源码解析(底层AQS实现)
- Java 并发编程概念深入理解
- 各种锁的介绍、锁之间的区别
- https://blog.csdn.net/Hsuxu/article/details/9467651
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/17833.html