目录
锁的分类
synchd锁有四种状态:无锁、偏向锁、轻量锁、重量锁。使用synchd后,随着线程的不断竞争锁会升级但不会降级。但偏向锁可以被置为无锁状态(锁撤销)。
没有开启偏向锁的情况下:
- 一个对象没有被作为锁对象,处于无锁状态
- 一个对象被一个线程获取作为锁对象,处于轻量级锁状态
- 一个线程已经持有了该锁对象,其他线程来争用,处于重量级锁状态。
开启偏向锁的情况下:
- 一个对象没有被作为锁对象,处于无锁可偏向状态。(对象头中没有记录线程ID)
- 一个对象被一个线程作为锁对象,处于轻量级锁状态。(对象头中记录了线程ID)
- 一个对象被一个线程作为锁对象,释放锁对象后(但是该线程没有消亡时),其他线 程再获取该锁对象,则处于轻量级锁状态。
- 一个对象被一个线程作为锁对象,没有释放锁,其他线程也要获取该锁对象,就处 于重量级锁状态。
无锁
一个对象如果没有被任何线程当作锁去使用,就是无锁状态。
无锁——>偏向锁
jvm启动偏向锁默认打开,但是偏向锁启动是有延时的(4000ms)
jvm参数-XX:BiasedLockingStartupDelay=4000
将数值设置为0则取消延迟
public static void main(String[] args) throws InterruptedException {
A a = new A();
// ClassLayout pi = ClassLayout.parseInstance(obj);
// String str = pi.toPrintable();
String str = ClassLayout.parseInstance(a).toPrintable();
System.out.println(str);//01 无锁状态
}
偏向锁
大多数时候是不存在锁竞争的,经常是一个线程在持有锁,由于每次竞争锁会有很大的性能开销,为了降低锁的代价,从而引入了偏向锁。
偏向锁——>轻量锁
A线程访问代码块获取到锁对象之后,会在java对象头和栈帧记录偏向锁的请求线程threadid,因为偏向锁不会主动释放,所以之后A线程再次获取锁的时候,需要比较A线程的threadid是否和对象头中的线程id—致。
- —致:锁重入A线程继续获取锁,无需使用CAS加锁、解锁
- 不一致:说明有其它线程B来竞争锁对象,由于偏向锁不会主动释放锁对象,对象头存储的是线程A的threadid,那么需要对象头记录线程A是否存活
- 如果没有存活,那么锁对象被重置为无锁状态(锁撤销),线程B设置偏向锁
- 如果存活,立即查找线程A的栈帧信息,如果线程A还需要继续持有锁,那么暂停线程A (stw),撤销A的偏向锁升级为轻量级锁,如果不需要继续持有锁,锁对象重置为无锁状态,锁偏向线程B
public static void main(String[] args) throws InterruptedException {
A a = new A();
String str = ClassLayout.parseInstance(a).toPrintable();
System.out.println(str);//101 无锁可偏向
new Thread() {
public void run() {
synchronized(a) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/*
* 显示的让当前线程不结束
* 如果结束了,下一个线程的id和刚才这个线程的id是一样的。
*/
try {
Thread.sleep(100000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
Thread.sleep(100);
str = ClassLayout.parseInstance(a).toPrintable();
System.out.println(str);//101 无锁可偏向
Thread.sleep(1000);//保证上一个线程已经把任务执行完毕了
new Thread() {
public void run() {
synchronized(a) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
Thread.sleep(100);
str = ClassLayout.parseInstance(a).toPrintable();
System.out.println(str);
}
轻量锁
轻量锁考虑的是竞争锁的线程不是很多,且持有锁的时间短的场景。因为线程阻塞需要CPU从用户态切换到内核态,如果线程刚阻塞不久,锁就释放了,那这个代价有点大。所以干脆不阻塞这个线程,让它进行自旋等待锁释放。
轻量锁——>重量锁
A线程获得轻量级锁之后,会把锁对象的对象头Markword复制一份到线程A的栈帧中创建用于存储锁记录的空间(DisplacedMarkword),然后使用CAS自旋把对象头中的内容替换为A线程存储锁记录的地址。
如果A线程在复制对象头的过程中(cas替换前),C线程也获取锁,复制了锁对象头到C线程的锁记录空间,在执行CAS自旋做对象头内容替换的时候,发现A线程已经更改了对象头,则C线程替换对象头内容失败,尝试使用自旋锁等待A线程释放锁。
自旋要消耗CPU会影响性能,所以不能无限制的自旋下去,因此自旋次数有限制,如果自旋次数到了,A线程还没有释放锁,C线程在自旋等待,D线程又进来竞争锁,这个时候轻量级锁会升级为重量级锁。重量级锁把除A线程之外的,前来竞争锁的线程全部阻塞,防止CPU空转。
new Thread(){
public void run() {
//把a作为锁对象
synchronized(a) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
Thread.sleep(10);
str = ClassLayout.parseInstance(a).toPrintable();
System.out.println(str);//00 轻量级锁
重量锁
重量级锁就是一个悲观锁了,但是其实不是最坏的锁,因为升级到重量级锁,是因为线程占用锁的时间长(自旋获取失败),锁竞争激烈的场景,在这种情况下,让线程进入阻塞状态,进入阻塞队列,能减少cpu消耗。所以说在不同的场景使用最佳的解决方案才是最好的技术。
new Thread(){
public void run() {
//把a作为锁对象
synchronized(a) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
Thread.sleep(10);
//这个时候第一个线程正在使用a这个锁,第二个线程也要去获取a这个锁
str = ClassLayout.parseInstance(a).toPrintable();
System.out.println(str);//10 重量级锁
不同锁的优缺点
锁状态 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁、解锁没有额外消耗。和执行非同步方法仅有纳秒级的差距 | 如果线程间存在锁竞争,会有额外锁撤销的性能消耗 | 基本没有线程竞争,一个线程访问同步的场景 |
轻量锁 | 竞争线程不会阻塞,适用cas自旋,提高了程序的响应速度 | 如果无法获取到锁,长时间自旋会消耗CPU性能 | 锁持有时间短,追求响应速度 |
重量锁 | 线程竞争不使用自旋,不会导致cpu空转 | 线程阻塞,响应慢 | 追求吞吐量,锁持有时间长 |