CAS与锁优化

CAS与原子类

CAS概述

CAS即compare and swap,它体现一种**乐观锁(假设不会有其他线程影响当前线程操作,如果发现其他线程影响了当前线程的操作,就回滚当前线程,重新执行)**的思想,比如多个线程要对一个共享整型变量执行+1操作:

while(true){
	int 旧值 = 共享变量;
	int 结果 = 旧值+1;

	/*
		此时如果别的线程把共享变量修改了,本线程的正确结果1旧作废了,这时候compareAndSwap返回false,
		重新尝试,知道compareAndSwap返回true,标识我本线程做修改的同时,没有其他线程干扰
	*/
	if(compareAndSwpap(旧值,结果)){
		// 成功就退出循环
	}
}

获取共享变量时,为了保证变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无所并发,适用于竞争不激烈、多核CPU的场景下

  • 因为没有使用synchronized,所以线程不会阻塞,这是效率提升的因素之一
  • 但是如果竞争激烈,会不断发生重试,反而没有悲观锁(synchronized)效率高

CAS底层

CAS底层是使用的Unsafe对象,调用操作系统的compareAndSwap函数实现的
在这里插入图片描述
他会从函数传入旧值和新值,如果旧值和主内存中的值一样,就写入新值,不一样就回滚

乐观锁和悲观锁

  • CAS是基于乐观锁的思想,最乐观的估计,不怕别的线程来修改共享变量,如果改了就重试
  • synchronized是基于悲观锁的思想,最悲观的估计,得防着其他线程来修改共享变量,一个线程上了锁其他线程就只能等着,等当前线程操作结束解锁了,才有机会操作共享变量

原子操作类

JUC中提供了原子操作类,可以提供线程安全的操作,例如AtomicInteger、AtomicBoolean等,他们底层就是采用CAS技术+volatile来实现的

public class Main{
    private static AtomicInteger num = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException, FileNotFoundException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                // 获取并且自增
                num.getAndIncrement();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                // 获取并自减
                num.getAndDecrement();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(num);

    }

}

synchronized优化

Java HotSpot虚拟机中,每个对象都有对象头(包括class指针和Mark Word)。Mark Word平时存储这个对象的哈希码、分代年龄,当加锁时,这些信息就根据情况被替换为标记位、线程锁记录指针、重量级锁指针、线程ID等
在这里插入图片描述

轻量级锁

如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有锁竞争),那么可以使用轻量级锁来优化

每个线程的栈帧都会包含一个锁记录的结构(这个结构是一个栈,可以有多个锁记录入栈,后加的锁要先解锁),内部可以存储锁定对象的Mark Word

轻量级锁执行流程:

static Object obj = new Object();
public static void m1(){
	synchronized (obj){
		// 同步块A
		m2();
	}
}
public static void m2(){
	synchronized (obj) {
		//同步块B
	}
}
线程1对象Mark Word线程2
访问同步块A,将Mark赋值到线程1的锁记录01(无锁)-
CAS将Mark修改为线程1的锁记录地址01(无锁)-
修改成功00(轻量级锁)线程1锁记录地址-
执行同步块A00(轻量级锁)线程1锁记录地址-
访问同步块B,把Mark复制到锁记录00(轻量级锁)线程1锁记录地址-
CAS将Mark修改为线程1的锁记录地址00(轻量级锁)线程1锁记录地址-
修改失败(发现是自己的锁)00(轻量级锁)线程1锁记录地址-
锁重入00(轻量级锁)线程1锁记录地址-
执行同步块B00(轻量级锁)线程1锁记录地址-
同步块B执行完毕00(轻量级锁)线程1锁记录地址-
同步块A执行完毕00(轻量级锁)线程1锁记录地址-
解锁01(无锁)-
-01(无锁)访问同步块A,把Mark复制到线程的锁记录

锁膨胀

以上是没有出现锁竞争的情况,如果出现了锁竞争,即超过两个以上的线程竞争同一个锁,轻量级锁就会膨胀成重量级锁,锁标志状态变成10;后面的线程也必须进入阻塞状态

static Object obj = new Object();
public staic void m1(){
	synchronized(obj){
		// 同步块
	}
}
线程1对象Mark Word线程2
访问同步块,将Mark赋值到线程1的锁记录01(无锁)-
CAS将Mark修改为线程1的锁记录地址01(无锁)-
修改成功00(轻量级锁)线程1锁记录地址-
执行同步块00(轻量级锁)线程1锁记录地址-
执行同步块00(轻量级锁)线程1锁记录地址访问同步块,将Mark赋值到线程2的锁记录
执行同步块00(轻量级锁)线程1锁记录地址CAS将Mark修改为线程2的锁记录地址
执行同步块00(轻量级锁)线程1锁记录地址修改失败(发现别人已经占了锁)
执行同步块00(轻量级锁)线程1锁记录地址CAS修改Mark为重量级锁
执行同步块00(轻量级锁)线程1锁记录地址阻塞
执行完毕10(重量级锁)重量锁指针阻塞
解锁失败(发现变成了重量级锁)10(重量级锁)重量锁指针阻塞
释放重量锁,根据重量锁指针,唤起阻塞线程10(重量锁)阻塞
-10(重量锁)竞争重量锁
-10(重量锁)加锁成功

锁自旋

  • 重量锁中的互斥同步对性能影响最大的是阻塞,挂起线程和恢复线程都需要转入内核态中完成
  • 重量锁竞争的时候,在多核CPU中,还可以使用自旋来进行优化
  • 锁自旋:一个处理器核心正在处理线程时,新的线程不会进入阻塞,而是又另一个cpu核心来让他执行一个忙循环(忙循环就是用一个循环让线程等待,它不像阻塞一样会放弃CPU,而是又CPU控制进入死循环) ,如果当前线程自旋成功(即这个时候持有锁的线程已经退出了同步块,释放了锁),这个自旋的线程就可以接手锁
  • 自适应自旋:因为自旋会一直占用CPU,如果锁一直得不到释放,这样可能会得不偿失;可以通过-XX:PreBlockSpin来指定默认自旋次数;在jdk6的自旋是自适应的,上一次自旋次数,决定了下一次自旋次数,如果上一次成功,就认为这一次成功可能性高,就多自旋几次;失败就少自旋几次或者直接进入阻塞
  • Java7开始不能控制是否开启自旋功能,默认自旋

自旋成功的情况

线程1(cpu1)对象Mark线程2(cpu2)
访问同步块,获取monitor10(重量锁)重量锁指针-
成功(加锁)10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针访问同步块,获取monitor
执行同步块10(重量锁)重量锁指针自旋重试
执行完毕10(重量锁)重量锁指针自旋重试
成功(解锁)01(无锁)自旋重试
访问同步块10(重量锁)重量锁指针成功(加锁)

自旋失败的情况

线程1(cpu1)对象Mark线程2(cpu2)
访问同步块,获取monitor10(重量锁)重量锁指针-
成功(加锁)10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针访问同步块,获取monitor
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针阻塞

偏向锁

轻量级锁在没有竞争时,每次重入仍然需要进行CAS操作。Java6中引入了偏向锁来做优化,即偏向第一次加锁的线程,只有第一次使用CAS将线程id写入到对象的Mark Word,之后没有其他线程写入,线程第二次需要对这个对象加锁时发现线程ID是自己的,就不需要重新CAS加锁

  • 如果出现了锁竞争,撤销偏向需要将持锁线程升级为轻量级锁,这个过程需要stop the world
  • 访问对象的hashCode也会撤销偏向锁
  • 如果对象虽然被多个线程访问,但没有竞争,这是偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID
  • 撤销偏向和重偏向都市批量进行的,以类为单位
  • 如果撤销偏向达到某个阈值,整个类所有对象都会变成不可偏向的

其他优化

  1. 减少上锁时间,同步代码块尽量短
  2. 减少锁的粒度,将一个锁拆分为多个锁提高并发度
  3. 锁粗化,多次循环进入同步块,不如在同步块内多次循环;另外JVM可能会做如下优化,new StringBuffer().append("a").append("b")把多次append的加锁粗化为一次,因为都是对一个对象的频繁加锁,没必要重入多次
  4. 锁消除,JVM会进行代码的逃逸分析,例如某个加锁对象是方法内部局部变量,不会被其他线程所访问到,被即使编译期忽略所有同步操作
  5. 读写分离,入CopyOnWriteArrayList、CopyOnWriteSet
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值