轻量级锁
使用场景:一个对象有多线程访问,但时间是错开的(如果多线程同时访问,也就是有竞争的,会升级为重量级锁)
轻量级锁对使用者是透明的,语法仍是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¬ify不能先notify
每个线程都有一个park对象,由三部分组成_counter(值只能为1或0),_cond,_mutex
调用park,会检查counter的值,为0则停止(获得_mutex互斥锁,进入_cond条件变量阻塞),为1则运行。并把counter值设为0
调用unpark,设counter值为1,如果线程停止了,让它继续运行并把_counter设为0(唤醒了_cond条件变量的线程)
总结:
线程为对象加上轻量级锁后,有竞争,这时会进行锁膨胀,轻量级锁变为重量级锁。竞争重量级锁时,可以用自旋来优化,线程自旋过程中如果持锁线程退出同步块,释放了锁,线程就自旋成功,线程就避免了阻塞。偏向锁有其他线程访问会变为轻量级锁,有竞争(多线程同时访问)会锁膨胀变为重量级锁可以加。