文章目录
- 一、CAS
- 1.什么是CAS
- 2.实现原子类
- 3.实现自旋锁
一、CAS
1.什么是CAS
Compare and swap 比较并交换。
比较交换的是 内存 和 寄存器
比如此时有一个内存 : M。 还有两个寄存器A,B
CAS ( M , A , B ) :如果M和A的值相同的话,就把M和B的值进行交换(交换的目的是为了把B赋值给M,M = B),同时整个操作返回 true。如果M和A的值不同的话,无事发生,同时整个操作返回false。
//伪代码:
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
- CAS其实是一个CPU指令,一个CPU指令,就可以完成上述比较交换的逻辑。
- 单个的CPU指令是原子的。所以可以使用CAS来完成一些操作来代替“加锁”。为编写线程安全的代码提供了线的思路,基于CAS实现线程安全的方式,也叫“无锁编程”。
优点:保证线程安全,同时避免发生阻塞。
缺点:代码可读性差,并且只能适用在特定场景,不如加锁普适。
CAS本质上是CPU提供的指令,被操作系统封装后,提供成为APi。又再次被JVM进行封装,提供成API,最终由程序员调用方法实现操作。
2.实现原子类
之前int count++的操作,就不是原子的(load,add,save)
在Java标准库中,提供了原子类,例如AtomicInteger,基于CAS的方式对int进行封装,++就封装成原子操作了。
public static AtomicInteger count = new AtomicInteger(0);
//对int进行原子化包装
//0作为构造方法的参数
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count.getAndIncrement();//count++; 后置++
// count.incrementAndGet();//++count 前置++
// count.getAndDecrement();//count--; 后置--;
// count.decrementAndGet();//--count 前置--
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
在Java中,有些操作是偏底层的操作,在使用时有着更多的注意事项,稍有不慎就容易写出问题。这些操作就同一放在unsafe这个类中
//伪代码
class AtomicInteger {
private int value;//相当于内存
public int getAndIncrement() {
int oldValue = value;
//oldValue相当于寄存器
while ( CAS(value, oldValue, oldValue+1) != true) {
//CAS(内存,寄存器A,寄存器B)
oldValue = value;
}
return oldValue;
}
}
寄存器A先保存当前内存的值,CAS来判断此时有没有被穿插执行。如果没有穿插执行,内存的值等于寄存器A的值,把寄存器B(B执行+1操作)的值赋值给内存,同时CAS返回的是true,循环不成立,返回是是自增前的值。
如果被穿插执行了,CAS中,内存的值不等于寄存器A的值,CAS直接返回false.此时循环成立,将被穿插执行过的内存值,重新赋值给寄存器A,也就是说要再读一遍内存。再次执行CAS。
线程不安全的本质是进行操作的过程中,穿插执行。
1.加锁是通过阻塞的方式,避免穿插。
2.CAS是通过重试的方式,避免穿插。如果值和之前不相等了,重新更新一下,在进行操作。
3.实现自旋锁
//伪代码:
public class SpinLock {
private Thread owner = null;//记录当前这个锁被哪个线程获取到。 null表示未加锁
public void lock(){//加锁
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){//解锁
this.owner = null;
}
}
- while循环中,CAS判断当前owner是不是未加锁状态。如果是未加锁的,就把owner赋值成 Thread.currentThread())(调用lock方法的线程引用)进行加锁。如果已经被加锁了,CAS返回false,取反之后,while就会反复执行。一旦其他线程释放了这个锁,owner变成null时,因为while不停循环,就会立即拿到当前这把锁。
缺点:while循环在忙等的过程中,不断消耗CPU资源。如果加锁的概率不大,付出的CPU资源就是值得的。如果当前的锁冲突很激烈,可能会拿不到,此时自旋锁就不适用了。
点击移步博客主页,欢迎光临~