严格来说,和Java无关。包括jvm中关于cas的api,都放在unsafe包里。
一个特殊的cpu指令,完成的工作是“比较和交换”。
比较address内存地址中的值,是否和expected寄存器中的值相同。如果相同,就把swap寄存器的值和address内存中的值进行交换,返回true。如果不相同,则无事发生,返回false。
一、原子类
基于cas指令,给编写线程安全的代码,打开了一个新世界的大门。
之前线程安全都是靠加锁,加锁 => 阻塞 => 性能降低
使用CAS不涉及加锁,不会阻塞,合理使用也可保证线程安全 => 无锁编程
CAS本身是cpu指令,操作系统对指令进行了封装,jvm又对操作系统提供的api封装了一层。
Java中CAS的api放到了unsafe包中,这样的操作,涉及到一些系统底层的内容,使用不当的话可能会带来一些风险,不建议直接使用CAS。
Java的标准库,对于CAS进行了进一步封装,提供了一个工具类,以供使用。
最主要的工具,叫做“原子类” java.util.concurrent.atomic
AtomicInteger,AtomicLong对Integer和Long进行了封装,针对这样的对象进行了多线程编程,就是线程安全的。
通过CAS的方式实现的。这里的内容不加锁,也能保证线程安全。并且这个代码更高效,不涉及阻塞等待。
count ++ 是三个指令,会相互穿插执行,引起线程不安全。
getAndIncrement 是对变量的修改,是CAS指令(原子的)
二、实现自旋锁
原子的自增:
这里其实oldValue期望是一个放到寄存器里的值,这个值就是初始化成AtomicInteger里保存的整数值value。
发现value和oldValue不同,意味着在CAS之前,另一个线程修改了value。通过这种方式,能识别出是否有人修改。发现value被修改了,就重新读取新的value到oldValue中。
之前线程不安全,内存变了,但是寄存器中的值没有跟着变,所以接下来修改出错
但使用CAS这种方式,就能确保识别出内存的值是不是变了,不变才会进行修改。变了,就会重新读取内存的值。
出现穿插执行就会出现问题。
t2 load的值,不是t1 save的最新值。基于错误的值来进行后续修改,最后结果也就是错了。
使用CAS没有上述问题
确保t2 load到的一定是内存中最新的值,代价“自旋”,消耗更多的cpu
基于CAS实现更灵活的锁,获取到更多的控制权。
一般是用在锁竞争不激烈的场景下。
通过CAS看当前锁是否被某个线程持有,如果这个锁已经被别的线程持有,那么就自旋等待。
如果这个锁没有被别的线程持有,那么就把owner设为当前尝试加锁的线程
当owner不为null的时候,循环就会一致执行下去,通过这样的“忙等”来完成等待效果。阻塞式的等,让线程不参与cpu调度了。此处自旋式的等,没有放弃cpu,不会参与到调度,也就没有调度开销了,但缺点是要消耗更多的cpu资源。
三、ABA问题
CAS在使用时,关键要点是要判定当前的内存的值是否是和寄存器里的值是一样的,是一样的就进行修改,不一样就啥也不做。 => 本质上是判定,当前这个代码执行过程中,是否有其他线程穿插进来了。
可能存在的情况,数值本来是0,执行CAS之前,另一个线程把这个值从0->100,又从100->0,不是没有别的线程穿插进来,而是穿插中又把值改回去了。
例子:
假设去银行取钱,初始情况下账户余额1000,要取500,取钱的时候ATM卡了,按了一下没有反应,有按了一下。
但如果t2取完之后,t3又存了500再执行t1
CAS(balance,oldBalance,oldBalance-500)则balance=500
对于ABA问题的解决方案:
1)约定数据变化是单向的(只能增加/只能减少),不能是双向的(即增加有减少)
2)对于本身就必须双向变化的数据,可以给他引入一个版本号(这个数字只能增加不能减少)
CAS本质上是JVM帮我们封装好的,没法直接感知到
JUC java.util.concurrent 放了一些进行多线程编程时有用的类。