锁优化

为了在线程之间更高效的共享数据,以及解决竞争问题,从而提高程序的执行效率,实现了各种锁优化技术。

锁的优化策略

编码过程中可采取的锁优化的思路有以下几种:

1、减少锁持有时间

例如:对一个方法加锁,不如对方法中需要同步的几行代码加锁;

2、减小锁粒度

例如:ConcurrentHashMap采取对segment加锁而不是整个map加锁,后来又对node加锁,提高并发性;

3、锁分离

根据同步操作的性质,把锁划分为的读锁和写锁,读锁之间不互斥,提高了并发性。

4、锁粗化

这看起来与思路1有冲突,其实不然。思路1是针对一个线程中只有个别地方需要同步,所以把锁加在同步的语句上而不是更大的范围,减少线程持有锁的时间;

而锁粗化是指:在一个间隔性地需要执行同步语句的线程中,如果在不连续的同步块间频繁加锁解锁是很耗性能的,因此把加锁范围扩大,把这些不连续的同步语句进行一次性加锁解锁。虽然线程持有锁的时间增加了,但是总体来说是优化了的。

5、锁消除

锁消除是编译器做的事:根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程(即不会影响线程空间外的数据),那么可以认为这段代码是线程安全的,不必要加锁。

Java虚拟机中锁优化
1、自旋锁

由于挂起线程和恢复线程都需要转入内核态完成,给系统带来很大压力。
同时,共享数据的锁定状态只会持续很短的一段时间,因此去挂起和恢复线程很不值得。
因此,我们可以线程执行一个忙循环(自旋),看看持有锁的线程会不会很会释放锁。

自旋等待不能代替阻塞,自旋本身虽然避免了线程切换的开销,但是会占用处理器时间,如果锁被占用时间短,自旋等待效果好;反之,自旋的线程只会白白浪费处理器资源;因此,要限制自旋等待时间,自旋次数默认值是10次,超过次数仍然没有成功获取锁,就挂起线程,进入同步阻塞状态。

2、自适应自旋

自旋时间不在固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。
如果对于某个锁,自旋很少有成功获得过,就不自旋了,避免浪费CPU资源。
如果自旋等待刚刚成功获得过锁,并且持有锁的线程在运行,则VM就认为此次自旋很有可能成功,就允许自旋等待更长的时间,比如100个寻新欢。

3、偏向锁

使用场景
HotSpot的研究人员发现大多数情况下虽然加了锁,但是没有竞争的发生,甚至是同一个线程反复获得这个锁,为了让线程获取锁的代价更低,就引入了偏向锁。

偏向锁特点:
这个锁会偏向第一个获得它的线程,只有当其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

锁是存在对象的对象头里面的。
在HotSpot JVM实现中,锁有个专门的名字:对象监视器。

在JVM1.6中引入了偏向锁,偏向锁主要解决无竞争下的锁性能问题,首先我们看下无竞争下锁存在什么问题:现在几乎所有的锁都是可重入的,也即已经获得锁的线程可以多次锁住/解锁监视对象,按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。

偏向锁获取、撤销
举个例子,一个楼管阿姨管着钥匙,然而每一次都是小明去借,楼管阿姨于是就认识了小明,直接和他说,“行,你直接拿就不用填表格了,我记得你”。

  1. 当一个线程访问同步块并且获得锁的时候,会在对象头和栈帧中的锁记录里存储取得锁偏向的线程ID。
  2. 下次 该线程尝试获取锁的时候,首先检查这个对象头的MarkWord是不是存储着这个线程的ID。
    如果是,那么直接进去同步块,而不需要进行CAS来加锁解锁。
    如果不是,那么分为两种情况:

    1)对象的偏向锁标志位为0(当前不是偏向锁),说明发生了竞争,已经膨胀为轻量级锁,这时使用CAS操作尝试获得锁。

    2)偏向锁标志位为1,说明还是偏向锁不过请求的线程不是原来那个了。这时只需要使用CAS尝试把对象头偏向锁从原来那个线程指向目前求锁的线程。
    这种情况举个例子就是小明准备毕业了,他学弟小小明经常来拿钥匙,于是楼管阿姨认识了小小明,小小明每次来也不用登记注册了。

    这个CAS失败了呢?首先必须明确这个CAS为什么会失败,也就是说发生了竞争,有别的线程和它抢锁并且抢赢了,那么这个情况下,它就会要求撤销偏向锁(因为发生了竞争)。接着它首先暂停拥有偏向锁的线程,检查这个线程是否是个活动线程,如果不是,那么好,你拿了锁但是没在干事,锁还记录着你,那么直接把对象头设置为无锁状态重新来过。如果还是活动线程,先遍历栈帧里面的锁记录,让这个偏向锁变为无锁状态,然后恢复线程。

4、轻量级锁

加锁的过程:
JVM在当前线程的栈帧中创建用于存储锁记录的空间(Lock Record),然后把对象头中的Mark Word复制进去,同时生成一个叫Owner的指针指向那个加锁的对象,同时用CAS尝试把对象头的MarkWord更新为指向锁记录(Lock Record)的指针。成功了就拿到了锁。
那么失败了呢?失败了的说法比较多。主流有《深入理解JVM》的说法和《并发编程的艺术》的说法。

《深入理解JVM》的说法:

失败了,去查看MarkWord的值。有2种可能:1,指向当前线程的指针,2,别的值。

如果是1,那么说明发生了“重入”的情况,直接当做成功获得锁处理。

其实这个有个疑问,为什么获得锁成功了而CAS失败了,这里其实要牵扯到CAS的具体过程:先比较某个值是不是预测的值,是的话就动用原子操作交换(或赋值),否则不操作直接返回失败。在用CAS的时候期待的值是其原本的Mark Word。发生“重入”的时候会发现其值不是期待的原本的Mark Word,而是一个指针,所以当然就返回失败,但是如果这个指针指向这个线程,那么说明其实已经获得了锁,不过是再进入一次。如果不是这个线程,那么情况2:

如果是2,那么发生了竞争,锁会膨胀为一个重量级锁(MutexLock)

《并发编程的艺术》的说法:

失败了直接自旋。期望在自旋的时间内获得锁,如果还是不能获得,那么开始膨胀,修改锁的Mark Word改为重量级锁的指针,并且阻塞自己。

解锁过程:
(那个拿到锁的线程)用CAS把Mark Word换回到原来的对象头,如果成功,那么没有竞争发生,解锁完成。如果失败,表示存在竞争(之前有线程试图通过CAS修改Mark Word),这时要释放锁并同时唤醒被挂起的线程。

偏向锁,轻量级锁,自旋锁总结

上述的锁不是Java语言层面的锁优化方法,是内置在JVM当中的。

首先偏向锁是为了避免某个线程反复获得/释放同一把锁时的性能消耗,如果仍然是同个线程去获得这个锁,尝试偏向锁时会直接进入同步块,不需要再次获得锁。

而轻量级锁和自旋锁都是为了避免直接调用操作系统层面的互斥操作,因为挂起线程是一个很耗资源的操作。

为了尽量避免使用重量级锁(操作系统层面的互斥),首先会尝试轻量级锁,轻量级锁会尝试使用CAS操作来获得锁,如果轻量级锁获得失败,说明存在竞争。但是也许很快就能获得锁,就会尝试自旋锁,将线程做几个空循环,每次循环时都不断尝试获得锁。如果自旋锁也失败,那么只能升级成重量级锁。

可见偏向锁,轻量级锁,自旋锁都是乐观锁。

https://www.cnblogs.com/dsj2016/p/5714921.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值