简介
本文记录一下自己看b站juc视频的一点疑惑。视频地址
疑惑主要是下面两个问题
重偏向的时候,撤销次数会增加吗? 不会
一个被重偏向完的对象,还能被第二次重偏向吗? 能(本来写的是不能,经评论指正后更改)
在一次批量重偏向中,一个被重偏向的对象,还能被第二次重偏向吗? 不能
注意上面两者的区别,文中描述的是不能的情况。
能的情况可以看评论区的解释。
不清楚的朋友可以往下看一看,应该能有所收获。
实验代码
package cn.itcast.test;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import java.util.Vector;
import java.util.concurrent.locks.LockSupport;
@Slf4j(topic = "c.TestBiased")
public class TestBiased {
static Thread t1,t2,t3;
public static void main(String[] args) throws InterruptedException {
test4();
}
private static void test4() throws InterruptedException {
Vector<Dog> list = new Vector<>();
int loopNumber = 39;
t1 = new Thread(() -> {
for (int i = 0; i < loopNumber; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}
LockSupport.unpark(t2);
}, "t1");
t1.start();
t2 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
LockSupport.unpark(t3);
}, "t2");
t2.start();
t3 = new Thread(() -> {
LockSupport.park();
log.debug("===============> ");
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}, "t3");
t3.start();
t3.join();
log.debug(ClassLayout.parseInstance(new Dog()).toPrintable());
}
}
class Dog {
}
整体逻辑和代码执行顺序:
- 线程t1创建39个Dog对象锁,并且都synchronized一遍,这样它们的Mark Wokd就都写了t1。
- 线程t2对39个对象锁又synchronized一遍,这样前19个(下标0-18)就被撤销成轻量级锁了,synchronized第20个锁时,进入批量重偏向状态,第20-39(下标19-38)重偏向到t2。
- 现在问题来了,线程t3中synchronized前19个没什么好说的,就是无锁变成轻量级锁。关键是20-39个,问一下自己,为什么此时不再是重偏向,而是撤销锁了?
分析
从上一部分的问题为起点开始分析:为什么t3中synchronized第20-39个锁对象时,不再是重偏向了。
如果loopNumber是40的话,可能很多人会觉得是因为t2中执行了40次synchronized,所以达到了批量撤销操作。这么想的朋友可以做个实验:把loopNumber改成40,然后在t3运行前创建一个新的Dog对象,你会发现Dog仍旧是偏向锁(101),而不是无锁(001)。这就说明重偏向是不会增加撤销数量的!!
那现在我们可以肯定t3中synchronized第20个对象时,撤销数刚好是20,也就是批量重偏向状态,并且此时t2已经释放了所有的锁,为什么t3不能执行重偏向呢?
t2 synchronized流程
我们来仔细分析一下t2中第1-39(下标0-38)个Dog锁对象的synchronized过程,就能理解了。
- 第1-19:只说是偏向锁的情况,先进行一系列判断(是不是偏向当前线程,偏向模式关闭的话撤销偏向锁,能不能重偏向),发现上述判断都不成立(偏向t1,偏向模式没有关闭,不能重偏向),进入锁升级。在锁升级这一步里,会把撤销次数+1,执行单次撤销锁操作。
- 第20:跟1中一样进入锁升级,把撤销次数+1,此时撤销次数==20,就执行批量重定向操作。具体如下:1.类Dog的epoch+1(00-01);2.把类Dog的所有处在加锁状态的锁对象的epoch更新为类Dog的epoch;3. 执行重偏向操作。
- 第21-39:先执行1中的一系列判断,此时发现可以进行重偏向,(重偏向的条件就是锁对象的epoch!=类的epoch),那就直接重偏向了,重偏向时会把锁对象epoch更新为类的epoch。不会进入锁升级,也不会+撤销次数了!
t3 synchronized流程
前19个就不用说了,在t2中都变成轻量级锁了。关键看第20-39,此时它们都是偏向锁,那也会执行分析t2时提到的一系列判断,但这时就不满足重偏向条件了,因为在t2的重偏向执行完后,20-38锁对象的epoch和类的epoch就一样了。那就会进入锁升级的流程,也就是20次撤销操作。
到此为止就能解释所有的问题了。最后简单说一下epoch。
epoch
epoch是Mark Word中的一部分,只存在于偏向锁状态,占2位。锁对应的类同样用2位存了epoch。
epoch的作用就是标记偏向的合法性,说通俗点,就是看这个偏向锁有没有其他线程正在用。如果不用epoch,那每次想要重偏向,都得去遍历所有的线程栈看看有没有其他线程在用。
再完整解释一遍:每次撤销数量刚到20的时候,锁的类epoch都会+1,并且更新加锁状态的同类锁对象,那么那些不加锁的锁对象epoch就和类的epoch不一样了,那就可以知道哪些偏向锁是空闲的了。
epoch只有两位,那肯定会循环,但不会影响准确性。