偏向锁的原理与实战

1. 偏向锁的核心原理

如果不存在线程竞争的一个线程获得了锁,那么锁就进入偏向状态,此时Mark Word的结构变为偏向锁结构,锁对象的锁标志位(lock)被改为01,偏向标志位(biased_lock)被改为1,然后线程的ID记录在锁对象的Mark Word中(使用CAS操作完成)。以后该线程获取锁时判断一下线程ID和标志位,就可以直接进入同步块,连CAS操作都不需要,这样就省去了大量有关锁申请的操作,从而也就提升了程序的性能。

关键点:无竞争

缺点:如果锁对象时常被多个线程竞争,偏向锁就是多余的,并且其撤销的过程会带来一些性能开销


2. 偏向锁代码演示

偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数
-XX:BiasedLockingStartupDelay=0 来禁用延迟

package innerlock;

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

public class InnerLockTest {
	

	int a=1;
	
	double b=1.1;
	public static void main(String[] args) {
		System.out.println(VM.current().details());
		Person person=new Person();
		ClassLayout layout=ClassLayout.parseInstance(person);
		new Thread(()->{
			System.out.println("获取偏向锁前:");
			System.out.println(layout.toPrintable());
			synchronized (person) {
				System.out.println("获取偏向锁中:");
				System.out.println(layout.toPrintable());
				
			}
			System.out.println("获取偏向锁结束后:");
			System.out.println(layout.toPrintable());
		}
		,"thread1").start();
	}

}
class Person{
	
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

禁用偏向锁:添加 VM 参数 -XX:-UseBiasedLocking
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


3. 偏向锁的膨胀与撤销

假如有多个线程来竞争偏向锁,此对象锁已经有所偏向,其他的线程发现偏向锁并不是偏向自己,就说明存在了竞争,尝试撤销偏向锁(很可能引入安全点),然后膨胀到轻量级锁

1. 偏向锁的撤销

  1. 在一个安全点停止拥有锁的线程
  2. 遍历线程的栈帧,检查是否存在锁记录。如果存在锁记录,就需要清空锁记录,使其变成无锁状态,并修复锁记录指向的Mark Word,清除其线程ID
  3. 将当前锁升级成轻量级锁
  4. 唤醒当前线程

撤销偏向锁的条件(满足其一即可):

  1. 多个线程竞争偏向锁
  2. 调用偏向锁对象的hashcode()方法或者System.identityHashCode()方法计算对象的HashCode之后,将哈希码放置到Mark Word中,内置锁变成无锁状态,偏向锁将被撤销

2. 批量重偏向与撤销

批量重偏向解决的问题:
一个线程创建了大量对象并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作。这种case下,会导致大量的偏向锁撤销操作。

package innerlock;

import java.util.ArrayList;

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

public class InnerLockTest {
	

	int a=1;
	
	double b=1.1;
	public static void main(String[] args) throws InterruptedException {
		System.out.println(VM.current().details());
		
		ArrayList<Person> list=new ArrayList<Person>();
		new Thread(()->{
			for(int i=0;i<100;i++)
			{
				Person person=new Person();
				synchronized (person) {
					list.add(person);
				}
			}
			try {
				Thread.sleep(10000);
			} catch (InterruptedException e) {
		
				e.printStackTrace();
			}
		}
		,"thread1").start();
		
		Thread.sleep(3000);
		new Thread(()->{
			for(int i=0;i<30;i++)
			{
				Person person=list.get(i);
				synchronized (person) {
					if(i==17||i==18||i==19||i==21)
					{
						System.out.println("第"+(i+1)+"次偏向结果:");
						System.out.println(ClassLayout.parseInstance(person).toPrintable());
					}
				}
			}
			
		}
		,"thread2").start();
	}

}
class Person{
	
}

在这里插入图片描述在这里插入图片描述结果分析:
先用线程1创建了100个对象锁,这些对象锁都偏向于线程1,后面创建线程2去争夺这些锁,前19次线程2都是抢占失败获得轻量级锁(失败过程中阈值增加),第20次抢占时达到阈值20,这时JVM会认为自己是不是不应该偏向线程1,于是之后开始偏向线程2,线程2之后获得的都是偏向锁

  • 第1-19个对象由于线程2在抢占过程中变为轻量级锁,锁释放后变为无锁状态
  • 第20-30个对象触发批量重定向,锁释放后依旧偏向线程2
  • 第31-100个对象依然和开始一样偏向线程1,锁释放后依旧偏向线程1

批量撤销解决的问题:
存在明显多线程竞争的场景下使用偏向锁是不合适的,例如生产者/消费者队列

package innerlock;

import java.util.ArrayList;

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

public class InnerLockTest {
	

	int a=1;
	
	double b=1.1;
	public static void main(String[] args) throws InterruptedException {
		System.out.println(VM.current().details());
		
		ArrayList<Person> list=new ArrayList<Person>();
		new Thread(()->{
			for(int i=0;i<100;i++)
			{
				Person person=new Person();
				synchronized (person) {
					list.add(person);
				}
			}
			try {
			//为了防止JVM线程复用,在创建完对象后,保持线程t1状态为存活
				Thread.sleep(10000);
			} catch (InterruptedException e) {
		
				e.printStackTrace();
			}
		}
		,"thread1").start();
		
		Thread.sleep(3000);
		new Thread(()->{
			for(int i=0;i<40;i++)
			{
				Person person=list.get(i);
				synchronized (person) {
					if(i==18||i==19||i==39||i==41)
					{
						System.out.println("t2  第"+(i+1)+"次偏向结果:");
						System.out.println(ClassLayout.parseInstance(person).toPrintable());
					}
				}
			}
			try {
				Thread.sleep(10000);
			} catch (InterruptedException e) {
		
				e.printStackTrace();
			}
			
		}
		,"thread2").start();
		
		Thread.sleep(3000);
		new Thread(()->{
			for(int i=20;i<40;i++)
			{
				Person person=list.get(i);
				synchronized (person) {
					if(i==20||i==39)
					{
						System.out.println("t3   第"+(i+1)+"次偏向结果:");
						System.out.println(ClassLayout.parseInstance(person).toPrintable());
					}
				}
			}
			
		}
		,"thread3").start();
		Thread.sleep(1000);
		System.out.println("新创建对象:"+ClassLayout.parseInstance(new Person()).toPrintable());
	}
	

}
class Person{
	
}

在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述

做法:
以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值默认20时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id

当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑

总结:
在这里插入图片描述

3. 偏向锁的膨胀
如果偏向锁被占据,一旦有第二个线程争抢这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到内置锁偏向状态,这时表明在这个对象锁上已经存在竞争了。JVM检查原来持有该对象锁的占有线程是否依然存活,如果挂了,就可以将对象变为无锁状态,然后进行重新偏向,偏向抢锁线程。如果JVM检查到原来的线程依然存活,就进一步检查占有线程的调用堆栈是否通过锁记录持有偏向锁。如果存在锁记录,就表明原来的线程还在使用偏向锁,发生锁竞争,撤销原来的偏向锁,将偏向锁膨胀(INFLATING)为轻量级锁


参考:
《Java高并发编程卷2》 尼恩
https://www.cnblogs.com/LemonFive/p/11248248.html

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodePanda@GPF

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值