引入轻量级锁的目的
多线程竞争不激烈的情况下,通过CAS机制竞争减少重量级锁产生的性能消耗.重量级锁使用了操作系统底层的互斥锁,会导致线程在用户态和核心态之间切换.带来性能上的损耗.
轻量级锁的核心原理
轻量级锁存在的目的本身就是为了减少线程从内核态和用户态的切换,从而提高性能.通过经验可以发现好多加锁操作并不会持续很长时间.比如整数的自加操作,在很短的时间阻塞和唤醒线程显然不是很合理.轻量级锁是一种自旋锁,因为JVM本身就是一个应用,希望从应用层面通过自旋解决线程同步问题.
轻量级锁执行过程
抢锁线程进入临界区之前.如果内置锁(临界区的同步对象)没有锁定.JVM首先在抢锁线程的栈帧中建立一个锁记录(Lock Record),用于存储对象目前的Mark Word拷贝.
抢锁线程先处理好栈帧中的轻量级锁记录,然后就是最核心的CAS自旋.抢锁线程通过自旋操作,尝试将内置锁对象头的Mark Word的ptr_lock_record(锁记录指针)更新为抢锁线程中锁记录的地址.如果更新成功了,这个线程就拥有了这个对象锁.然后JVM会把Mark Word的锁记录标记位改为00(轻量级锁标志).
Mard Word的值被更新成功后,包含锁对象信息(如哈希表等)旧值会返回来,抢锁线程会找到一个地方将旧的Mark Word值存起来,所以线程在通过CAS自旋操作更新完Mark Word之后还会有两个善后操作.
1:将含有锁对象信息(如哈希表等)旧的Mark Word值保存在抢锁线程Lock Record的Displaced Mark Word(可以理解为放错的Mark Word)字段中,这一步起到备份作用,以便锁释放以后将旧的值恢复到锁对象头部.
2:抢锁线程将栈帧中的锁记录owner指针指向锁对象.
锁记录是线程私有的,每个线程都有自己的一份锁记录,在创建完锁记录后,会将内置锁对象的Mark Word拷贝到锁记录的Displaced Mark Word字段.这是因为什么?因为内置锁对象的Mark Word的结构会有所变化,Mark Word将会出现一个指向锁记录的指针,而不再存着无锁状态下的锁对象的哈希码等信息,所以必须将这些信息先暂存起来,供后面使用.
轻量级锁演示
public class LightweightLock {
static final int MAX_TURN = 1000;
public static void main(String[] args) throws InterruptedException {
System.out.println(VM.current().details());
//JVM偏向延迟锁.
Thread.sleep(5000);
ObjectLock objectLock = new ObjectLock();
System.out.println("抢占锁前lock的状态:");
objectLock.printObjectStruct();
LockSupport.parkNanos(5000);
CountDownLatch latch = new CountDownLatch(2);
Runnable runnable = () -> {
for (int i = 0; i < MAX_TURN; i++) {
synchronized (objectLock) {
objectLock.increase();
if (i == 1) {
System.out.println("第一个线程占有锁,lock的状态为: ");
objectLock.printObjectStruct();
}
}
}
latch.countDown();
//线程虽然释放锁,但是存在死循环.
for (int j = 0; ; j++) {
//每一次循环等待一毫秒.
LockSupport.parkNanos(1000);
}
};
new Thread(runnable).start();
LockSupport.parkNanos(1000);
Runnable LightweightRunnable = () -> {
for (int i = 0; i < MAX_TURN; i++) {
synchronized (objectLock){
if (i == MAX_TURN / 2) {
System.out.println("第二个线程占有锁,lock的状态为: ");
objectLock.printObjectStruct();
}
LockSupport.parkNanos(1000);
}
}
//循环完毕.
latch.countDown();
};
new Thread(LightweightRunnable).start();
latch.await();
LockSupport.parkNanos(2000);
System.out.println("释放锁后的状态: ");
objectLock.printObjectStruct();
}
}
程序启动五秒以后,ObjectLocal状态为偏向锁.
关于偏向锁的执行结果和上一篇文章相同,就不啰嗦啦.
当第二个抢锁线程抢锁成功后,ObjectLock状态变成了轻量级锁.
可以看到lock标记位改为了00(轻量级锁标志),其中ptr_to_lock_record(锁记录指针)更新为抢锁线程栈帧中Lock Record的地址.
轻量级锁被释放后.ObjectLock状态变成无锁状态,lock标记位01(无锁标志).
轻量级锁的分类
轻量级锁主要有两种.普通自旋锁和自适应自旋锁.
1:普通自旋锁
普通自旋锁就是当线程来竞争锁时,抢锁线程会原地等待,而不是被阻塞.直到那个占有锁的线程释放锁.这个线程才可以获取锁.默认情况下,自旋的次数为10次.可以通过-XX:PreBlockSpin进行更改.
2:自适应自旋锁
自适应自旋锁,就是等待线程空循环次数并非是固定的.而是会动态的根据情况来改变自旋次数.自旋次数由上次获取锁的状态来决定.
原理
1:如果抢锁线程在同一个对象上之前成功的获取过锁,那么JVM会认为这次自旋也很有可能获取到锁,所以自旋的次数会多一些.
2:如果对于某个锁,抢锁线程很少获取到锁,那么JVM就会减少这个线程的自旋次数,甚至省去自旋过程,避免浪费处理器资源.
自适应自旋锁主要解决的是锁竞争时间不确定的问题.根据上一次自旋次数和结果来调整下一次自选的次数.
轻量级锁的膨胀
轻量级锁是为了减少多线程抢锁情况下进入操作系统底层的互斥锁的概率,并不是要去替代操作系统的互斥锁.所以在竞争激烈的情况下,轻量级锁会膨胀为重量级锁.
时间一分一秒的流逝,我们又留下了什么.是一身反骨,还是甘愿被磨平棱角.
如果大家喜欢我的分享的话,可以关注下我的微信公众号
心有九月星辰