多线程中的各种锁

注意
博主是初学者,此文包含个人理解,谨慎阅读

乐观锁与悲观锁

悲观锁
总是认为临界资源会被多个线程同时争用,于是在使用之前,先对资源加锁,使其它线程阻塞,使用完成之后再释放资源
乐观锁
认为临界资源大多数时间不会被多个线程同时争用,在进行修改操作时,通过某些手段,检测有没有其他线程使用了此共享资源,如果没有,操作成功,如果有,拒绝访问,并重试.

在硬件上,有专门处理器指令来处理这一过程
1.测试并设置(Test-and-Set)
2.获取并增加(Fetch-and-Increment)
3.交换(Swap)
4.比较并交换(Compare-and-Swap) 这就是CAS
5.加载链接/条件储存 (Load-Linked/Stroe-Conditional)

MySQL数据库中,MVCC使用了基于版本号机制的乐观锁

公平锁与非公平锁

公平锁
对于等待统一临界资源的线程来说,资源被释放后,先到先得
非公平锁
无法保证下一个是谁获取临界资源

可重入锁与不可重入锁

可重入锁
一条线程能够反复进入被它只有锁的同步块.
使用一个信号值标志该临界资源有没有被占用,每一次进入时是信号值+1,释放资源时-1,信号值为0则标志当前临界资源可用

不可重入锁
如果一个线程想要再次访问已经被自己占用的临界资源,可能死锁

轻量级,重量级以及偏向锁

重量级锁
在java中,synchronize关键字表现出的语义和重量级锁相同,到来即占用,离开即释放

在对象头中的Mark Word中,如果没有上锁,那么需要存储对象hash码,分代年龄等信息.
无论有没有上锁,Mark Word的最后两位都是标志位,其中,01表示没有上锁或者是偏向锁

如果标志位是10,即表示当前对象被重量级锁了.
此时,对象的hash码等信息不再存储于Mark Word中,代表重量级锁的ObjectMonitor类中,会保存原来Mark Word中的信息,原来的Mark Word存储hash码等信息的bit,用于表示指向重量级锁的指针
注意,在这些变化中,Mark Word的最后两个字节始终表示标志位.

重量级锁的实现方式用到了互斥
对于synchronize关键字来说,JAVAC在编译时,会产生monitorenter和monitorexit两条字节码指令,分别表示访问临界资源和释放临界资源.
执行monitorenter时,会使信号量(锁计数器)加一;执行monitorexit时,会使信号量(锁计数器)减一,当信号量为0时,就表示这临界资源当前没有被锁定
由此可见,重量级锁是可重入锁.

在获取锁时,如果失败了,那么当前线程应该被阻塞,由此可见,重量级锁是悲观锁.

然而,synchronize关键字在JVM中被执行时,并非就一定是重量级锁,实际上,轻量级锁和偏向锁是对其的一种优化

轻量级锁
轻量级锁的标志位是00,它的实现与重量级锁类似,将Mark Word中的部分bit化为指针,指向Lock Record
Lock Record是轻量级锁为Mark Word提供的一个拷贝,其中包含了原来存储在Mark Word的相关信息.

与重量级锁不同的是,轻量级锁并没有使用互斥来实现,而是使用了CAS
这就意味着,轻量级锁实际上是一种乐观锁吗?至少我现在认为,不应该说轻量级锁是乐观锁.因为它也像重量级锁一下,锁住了临界资源,只是,给临界资源上锁时,使用了乐观锁的方式
轻量级锁会尝试使用CAS把对象的Mark Word指向Lock Record,并更新标志位.
如果失败了,当前线程会试图使用自旋(这个稍后说)来获取锁,如果仍然失败,那么此时,轻量级锁就必须膨胀为重量级锁,以减少自旋带来的消耗

轻量级锁比重量级锁性能优化体现在,将互斥转化为了CAS,但是如果在存在多线程争用临界资源的情况下,轻量级锁的消耗要高于重量级锁.

关于解锁,轻量级锁也使用了CAS

JVM在获取轻量级锁是,如果失败了,会检查对象的Mark Word是否指向了当前线程的栈帧,如果是,直接执行代码.
由此可见,轻量级锁也是可重入锁

偏向锁
偏向指的是对线程的偏向,如果一个线程获取了锁,那么在没有其他线程争用的情况下,这个锁就一直归这个线程所有了,不再需要解锁等操作.

偏向锁的标志位是01,和无锁相同,所以需要倒数第三位表示偏向模式,倒数第三位为0表示无锁,倒数第三位为1表示偏向锁

偏向锁的加锁操作也是通过CAS来完成的,比起轻量级锁,它避免了在单个线程中多次使用CAS来锁住统一个对象.
但是一旦有第二个线程争用临界资源,偏向模式立刻结束.

值得注意的是,偏向模式直接修改了Mark Word而没有留下备份,而在java中,JVM需要保证,没有重写hashcode方法的对象,其hash只能被计算一次.
所以,对于没有重写hashcode方法的对象,如果以及计算过hash值,那么它就无法被偏向锁锁定.如果处于偏向锁状态,计算hash值也会直接导致偏向模式的退出

这是《深入理解java虚拟机(第三版)》P482的图
表示的是,32位Mark Word的各种状态

JVM中的其他锁优化

这些优化,并不一定在所有情况下都会减少锁的消耗

自旋锁
如果发现临界资源已经被其他线程占用,先不进入阻塞状态,而是在CPU上执行一个忙循环(自旋),看看能不能等到临界资源被释放
如果在规定时间没有等到,那就老实去阻塞.
自适应的自旋锁就是,根据一些变量,比如自旋成功次数等,动态决定自旋应该等待的时间

锁消除
去掉没有必要加的锁

锁粗化
增大锁的颗粒
一般来说,锁颗粒越小越好,有利于其他线程获取资源,避免过多的阻塞.
但是如果在一个线程中,反复加锁解锁,对性能也有较大影响.

其它

不要使用拥有不可变特性的对象,作为锁的对象,还在临界区修改该对象

posted @ 2020-04-04 14:06  断腿三郎  阅读(990)  评论(0编辑  收藏  举报