CAS: 全称Compare and swap,字面意思:”比较并交换“,CAS涉及如下操作:
假设内存中的原数据为A,旧的预期值为B ,需要修改的值为C。
我们来写一个CAS的伪代码以帮忙我们更好理解CAS。
boolean Cas(int a,int b,int c){
//进行比较看a是否发生变化
if(a==b){
a=c;
return true;
}
return false;
}
CAS是乐观锁的一种实现方式,当多个线程对一个数据进行操作时,只有一个线程操作成功,其他线程并不会阻塞,会返回操作失败的信号。
真实的 CAS 是一个原子的硬件指令完成的,只有硬件予以支持,软件方面才能实现。
标准库中提供了 java.util.concurrent.atomic 包,里面的类都是基于这种方式来实现的。
典型的就是 AtomicInteger 类, 其中的 getAndIncrement 相当于 i++ 操作。
public static void main(String[] args) {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
AtomicInteger seq = new AtomicInteger(0);
//进行++操作
seq.getAndIncrement();
seq.getAndIncrement();
seq.getAndIncrement();
System.out.println(seq);
}
我们点开自增方法,我们看到它的操作也是通过上述伪代码的那种方式实现的。
也可以使用CAS实现自旋锁
假设存在两个线程 t1 和 t2。 有一个共享变量 num, 初始值为 A。
接下来,线程 t1 想使用 CAS 把 num 值改成 Z,那么就需要
以银行取钱为例:
这样我们的钱就不翼而飞了,所以这种情况是万万不可的。
所以我们引入版本号来解决这个问题。CAS在读取旧值时也要读取版本号,在修改时,如果读到的版本号与当前版本号相同就进行修改,如果当前版本号高于读到的版本号,就修改失败。
偏向锁就是在当前锁对象中标记改锁属于那个线程,没有进行实际加锁,能不加锁就不加锁,减少不必要的开销,只有当其他线程来竞争锁时,才会进行锁升级,由偏向锁变为轻量级锁。
锁升级为轻量级锁之后,通过CAS实现。
如果竞争进一步激烈, 自旋不能快速获取到锁状态,就会膨胀为重量级锁
此处的重量级锁就是指用到内核提供的 mutex。
当多个线程竞争同一把锁,自旋等待的时间过长,无法获取到锁时,JVM会将这把锁升级为重量级锁。这时,线程并不再进行自旋等待,而是进入内核态,通过操作系统提供的mutex实现来管理锁的状态和等待队列。
在内核态中,操作系统判定当前锁是否已经被占用。如果锁没有被占用,则线程成功获取到锁,并切换回用户态继续执行。如果锁已经被占用,则线程加锁失败。此时,线程会进入锁的等待队列,并被操作系统挂起,等待被唤醒。
随着时间的推移和线程的竞争,当其他线程释放了这把锁并且操作系统意识到有线程在等待这个锁时,操作系统会唤醒等待的线程,使其重新启动并尝试重新获取锁。这个过程可能会经历一段时间,之后线程再次尝试获取锁以继续执行。
编译器+JVM 判断锁是否可消除,如果可以,就直接进行消除了。
也就是说我们许多加锁操作在单线程中运行时,那些加锁操作的锁就没必要。
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
例如 StringBuffe中的append操作就会涉及加锁操作,我们在单线程运行中就可以进行锁消除。
一段逻辑中如果出现多次加锁解锁,编译器 + JVM 会自动进行锁的粗化。
用我们上课讲的例子就是:
领导给下面人布置任务呢,一共三个任务,现在有这两种做法:
让我们大家选择,大家肯定选择做法一啊,当然人家jvm也会进行这样的锁粗化。
可以用一个代码理解一下:
//频繁加锁
for (int i = 0; i < 100; i++) {
synchronized (o1){
}
}
//粗化
synchronized (o1){
for (int i = 0; i < 100; i++) {
}
}
把锁粗化,避免频繁申请释放锁。