谁无风暴劲雨时,守得云开见月明
CAS是什么?
CAS全程是CompareAndSet,比较转换。是原子操作类的一种方式。达到预期值才修改。否则不修改
代码调用:
public class Test {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// public final boolean compareAndSet(int expect, int update) {
// 期望、更新
// 如果我期望的值达到了,那么就更新,否则就不更新
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
}
源码分析:
//native关键字是操作内核引擎的关键字,代表操作的是内核系统
//var 1,是当前类
//var 2,位置便宜量
public native int getIntVolatile(Object var1, long var2);
//var1,当前类
//var2,内存位置偏移量
//var4,修改为的值
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
//这里内存加载,速度快。
do {
//从内存中根据偏移量中读取数据
var5 = this.getIntVolatile(var1, var2);
//判断var5为拿到的值,预期值,var5+var4即修改后的值,。
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
总结:
CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作,如果不是就一直循环!
缺点:
- 底层的自旋锁,循环耗时
- 一次性只能保证一个共享变量的原子性,不能锁代码块
- 会存在ABA问题
在CAS中我们比较的时候,如果其他线程在我们读取数据正准备修改时将值更改后会造成ABA的问题。
比如:线程A读取到数据X=5,准备修改为X=6,还没有进行修改,这个时候另一个线程将值修改为了X=8,那么X的变化路径为,5=>8=>6,虽然最后的结果是正确了,但是中间发生了变化,万一有其他的线程拿取到了8的值怎么办。
在原子类当中,即采用乐观锁进行判断,即加上乐观锁。AtomicStampedReference中提供了时间错
public class Test {
public static void main(String[] args) {
// AtomicInteger atomicInteger = new AtomicInteger(2020);
//带时间戳的原子类操作类对象,时间戳设置为1
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);
new Thread(()->{
//拿取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println("a1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//预期值,1,修改后的值为2
System.out.println(atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
//输出版本号
System.out.println("a2=>"+atomicStampedReference.getStamp());
//预期值,2,修改后的值为1
System.out.println(atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
//输出版本号
System.out.println("a3=>"+atomicStampedReference.getStamp());
},"a").start();
// 乐观锁的原理相同
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println("b1=>"+stamp);
try {
//停止2秒等待上面的执行完
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//预期值,1,修改后的值为6,版本后依次加1
System.out.println(atomicStampedReference.compareAndSet(1, 6, stamp, stamp + 1));
System.out.println("b2=>"+atomicStampedReference.getStamp());
},"b").start();
}
}
上面代码我们启用了2个线程。在最开始值都等于1,后面停止一个线程,让另一个线程对时间戳进行修改。执行结果:
我们发现最后一次为false,因为时间戳发生了改变,版本号预期是2,结果是版本号是3.
就和CAS的源代码一样,不放弃内存操作的时间片段,判断是否和预期值相等,如果预预期值不相等则一直占用则资源
代码实例:
public class TestOne {
public static void main(String[] args) throws InterruptedException {
//在外围创建唯一的实例
Test lock = new Test();
new Thread(()->{
//将值写入
lock.mylock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnlock();
}
},"T1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
lock.mylock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnlock();
}
},"T2").start();
}
}
class SpinLockTest {
//创建一个原子操作类
AtomicReference<Thread> atomicReference = new AtomicReference<>();
// 加锁
public void mylock(){
//创建一个线程对象
Thread thread = Thread.currentThread();
// 自旋锁
//将值变为null变为thred
while (!atomicReference.compareAndSet(null,thread)){
}
System.out.println(Thread.currentThread().getName()+"===> mylock");
}
// 解锁
public void myUnlock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"===> myUnlock");
//将原子类的数据值为null
atomicReference.compareAndSet(thread,null);
}
}
在上面代码中我们在TestOne 调用方中创建了一个唯一的实例SpinLockTest,内部有一个原子操作类,上锁时我们将值为thread ,线程B进来发现有值了会不断的遍历,阻止线程进行。我们执行myUnlock解锁时,将原子操作类的值置为null,线程B发现到达预期,null=>thread转换成功,拿到锁。
执行结果:
当拿到外锁后,内部锁其实可以不用拿取
public class Test {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.message();
},"A").start();
new Thread(()->{
phone.message();
},"B").start();
}
}
class Phone{
public synchronized void message(){
System.out.println(Thread.currentThread().getName()+"发短信");
call();
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName()+"打电话");
}
}
上面代表执行call,不会重新去拿synchronize锁。
lock不是可重入锁。
证明文章:https://www.zhihu.com/question/37168009
在JDK早期的时候,synchronized的底层实现是重量级的,所谓重量级,就是它直接去找操作系统去申请锁,它的效率是很低的。
JDK后来对synchronized锁进行了优化,这样才有了锁升级的概念。
锁升级的过程大致是这样的:
「偏向锁」 -> 「轻量级锁 (自旋锁)」-> 「重量级锁」
用markword中最低的三位代表锁状态,其中1位是偏向锁位,最后两位是普通锁位。
1.无锁状态= 0 0 1
这个时候没有锁定资源
2.偏向锁 = 1 0 1
这个时候只有一个线程夺得资源,所以升级为偏向锁
3.轻量级锁 = 0 0
当另一个线程开始进入进行竞争的时候synchronization会动态的进行升级为轻量级(自旋锁)
4.重量级锁 = 1 0
因为自旋需要耗费大量cpu的资源,当自旋到一定的层次后,自旋锁就会进行升级为重量级锁(线程等待wait等待唤醒noticeall)
System.gc();
为什么有自旋锁了还需要重量级锁?
自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗。
重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源
偏向锁是否一定比自旋锁效率高?
不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁。
JVM启动过程,会有很多线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段儿时间再打开。