java多线程(并发)夯实之路-synchronized锁升级深入浅出

发布时间:2024年01月13日

轻量级锁

使用场景:一个对象有多线程访问,但时间是错开的(如果多线程同时访问,也就是有竞争的,会升级为重量级锁)

轻量级锁对使用者是透明的,语法仍是synchronized

例:

以上的代码运行会先在方法产生的栈帧内创建锁记录(Lock? Record)对象,每个线程的栈帧都会包含一个锁记录的结构。锁记录中有锁对象指针(Object reference)和锁对象Mark Word记录

然后会尝试用cas(CompareAndSwap)替换Object的Mark? Word

如果对象Mark Word最后两位是01,就交换成功,否则会失败

cas失败有两种情况:

其他线程持有该Object的轻量级锁,这表明有竞争,进入锁膨胀过程

自己执行了synchronized锁重入,将再添加一条Lock Record作为重入的计数

第二次加锁,为锁重入,创建一个新的锁记录,里面会存一个null和Object reference,可以根据锁记录条数知道加了多少次锁

解锁时,如果有取值为null的锁记录,表示有重入,将清除值为null的锁记录,表示重入计数减一

最后一次解锁时,将用cas把Mark Word的对象头还原成功,则解锁成功

失败,说明轻量级锁经过锁膨胀变为重量级锁,进入重量级锁解锁流程

锁膨胀

线程为对象加上轻量级锁后,有竞争,这时会进行锁膨胀,轻量级锁变为重量级锁

将为Object对象申请Monitor锁,让Object指向重量级锁地址,后两位变为10
然后自己进入Monitor的EntryList? BLOCKED

当Thread-0退出同步块解锁时,使用cas将Mark? Word的值恢复给对象头会失败,这时进入重量级锁解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒Monitor的EntryList线程

自旋优化

竞争重量级锁时,可以用自旋来优化,线程自旋过程中如果持锁线程退出同步块,释放了锁,线程就自旋成功,线程就避免了阻塞。

自旋成功:

自旋失败:

java6之后自旋是自适应的

自旋会占用CPU时间,多核CPU才能发挥自旋优势(单核CPU自旋只会失败,还浪费了时间)? java7之后不能控制是否开启自旋

偏向锁

轻量级锁重入时仍然需要CAS操作??????????????????????

java6中引入偏向锁进行优化:第一次CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争(锁重入),不用重新CAS,之后只要不发生竞争,锁对象就归该线程所有。

一个对象创建时会开启偏向锁(默认),但偏向锁默认是延迟的(避免延迟可以加VM参数- XX:BiasedLockingStartupDelay=0)。创建后,markword的值为0x05即后三位为101,这时它的 thread,epoch,age都为0;如果没有开启偏向锁,对象创建后,markword值为0x01即后三位为001,这时它的hashcode,age都为0,第一次使用到hashcode时才会赋值。(锁释放后markword后三位为 001)

使用场景:没有竞争

偏向锁有其他线程访问会变为轻量级锁,有竞争(多线程同时访问)会锁膨胀变为重量级锁可以加VM参数-XX:-UseBiasedLocking禁用偏向锁

调用哈希码会禁用偏向锁(因为偏向锁的markword中没空间存哈希码,所以撤销偏向锁,轻量级锁调用哈希码,会存在线程栈帧的锁记录里,重量级锁调用哈希码,会存在Monitor里)

调用wait/notify(升级为重量级锁)会禁用偏向锁

批量重偏向

对象被另一线程访问,偏向锁被撤销变为轻量级锁

没有竞争的情况下,如果一个线程有同类多个偏向锁对象,偏向锁被另一线程访问,偏向锁撤销次数过多,将进入批量重偏向状态,第20次以及之后偏向锁会重新偏向到另一线程(重偏向会重置对象的Thread ID)

批量撤销

偏向锁撤销次数到40,会将整个类的对象设为不可偏向(normal),新建对象也是不可偏向

锁消除

JIT即时编译器优化过程中会进行锁消除

如下不会执行加锁操作

原理之wait/notify

Owner线程发现条件不满足,调用wait方法,线程会释放锁,进入WaitSet变为WAITING状态,然后唤醒BLOCKED状态的线程来竞争,新的Owner线程调用notify/notifyAll方法会唤醒WaitSet中的线程,唤醒后的线程仍需进入EntryList重新竞争

WAITING和BLOCKED都是阻塞状态,不占用CPU时间片

这些都属于object对象的方法,只有取得了对象的锁才能调用这些方法 wait方法无参为一直等待,有参为有时限的等待

与sleep区别:??????????

1)sleep是Thread方法,wait是Object方法

2)sleep不需要与synchronized配合使用,wait需要

3)sleep不会释放对象锁,wait会

如果它们都是有参的,都是进入TIME_WAITING状态锁对象加final

用while循环可以解决虚假唤醒的问题(等待时间结束,没有满足条件却继续往下执行)

Park&Unpark

它们是LockSuppport类中的方法

与wait/notify相比:

wait/notify需要配合Object Monitor一起使用,而park,unpark不用 notify唤醒是随机的,notifyAll唤醒全部,而unpark可以准确地唤醒线程 park&unpark可以先unpark,wait&notify不能先notify

每个线程都有一个park对象,由三部分组成_counter(值只能为1或0),_cond,_mutex

调用park,会检查counter的值,为0则停止(获得_mutex互斥锁,进入_cond条件变量阻塞),为1则运行。并把counter值设为0

调用unpark,设counter值为1,如果线程停止了,让它继续运行并把_counter设为0(唤醒了_cond条件变量的线程)

总结:

线程为对象加上轻量级锁后,有竞争,这时会进行锁膨胀,轻量级锁变为重量级锁。竞争重量级锁时,可以用自旋来优化,线程自旋过程中如果持锁线程退出同步块,释放了锁,线程就自旋成功,线程就避免了阻塞。偏向锁有其他线程访问会变为轻量级锁,有竞争(多线程同时访问)会锁膨胀变为重量级锁可以加。

文章来源:https://blog.csdn.net/qq_61282807/article/details/135536260
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。