上一篇博客中我们通过JOL工具查看了加锁对象的对象头,也大致的了解了偏向锁、轻量锁、重量锁的对象头,但是什么时候是偏向锁?什么时候又是轻量锁?什么时候又是重量锁?
为了说明这个问题,我们假设我们运行的程序中有一个线程A,有一个线程B,还有一个加锁对象MyLock。
说完了JDK对synchronized 关键字的三种锁的优化,同时JDK还有一种机制就是批量重偏向,批量撤销。下面就让我们进行代码的验证吧!至于偏向锁和重量锁的情况,我在我的博客深入理解Java对象头mark word中有详细的介绍。那么就剩下轻量锁的两种情况,以及批量重偏向和批量撤销几种情况,我们还没有详细说明,下面我们就开始我们的编码了。
首先导入查看对象头的工具JOL,至于怎么导入,可以参考我的博客深入理解Java的对象头Mark word。创建MyLock.java,代码如下:
//只是作为一个加锁的对象,我们什么都可以不书写
public class MyLock {
}
然后我们创建我们的测试类LightWeightLockDemo.java
import org.openjdk.jol.info.ClassLayout;
public class LightWeightLockDemo {
public static void main(String[] args) throws InterruptedException {
MyLock myLock = new MyLock();
System.out.println("before lock");
System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
Thread aThread = new Thread(() -> {
synchronized (myLock) {
System.out.println("aThread lock ing");
System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
}
});
aThread.start();
//让A线程结束,然后B线程执行
aThread.join();
Thread bThread = new Thread(() -> {
synchronized (myLock) {
System.out.println("bThread lock ing");
System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
}
});
bThread.start();
}
}
运行之前我们要关闭偏向锁的延迟,然后就是这种情况就是我们上面提到轻量锁中的第一种情况,线程A死亡的时候,然后我们查看运行结果
#关闭偏向锁的延迟
-XX:BiasedLockingStartupDelay=0
我们发现一种诡异的情况,B线程也是偏向的状态,但是你发现偏向的线程的ID都是一样的。难道说线程A在死亡后,创建的线程B和线程A是一样的线程ID,然后导致JVM虚拟机把线程B当成了线程A了,当线程B进行加锁的时候,JVM以为是原来的线程A来重新加锁,所以还是偏向锁。那么存在这种情况吗?我们来验证一下,到底是JVM的优化,还是操作系统的问题。这时候我们需要到Linux系统中来验证我们的猜想了,因为Java中的线程和操作系统的线程是一一对应,我在我的博客通过基于JNI手动模拟Java线程中证明过,可以去查看一下。
我们先在Ubuntu18.04中创建以下的文件,文件名为b.c
#include
#include
void* thread_func(void* arg)
{
//打印线程的id
printf("thread_id:%lu\n",pthread_self());
}
int main()
{
pthread_t t1, t2;
//调用操作系统的线程创建函数
pthread_create(&t1, NULL, thread_func, NULL);
//调用操作系统的线程join函数
pthread_join(t1, NULL);
pthread_create(&t2, NULL, thread_func, NULL);
pthread_join(t2, NULL);
return 0;
}
我们执行编译的命令如下:
gcc -o b -pthread b.c
然后运行以下的命令
./b
查看运行的结果
我们通过上面的运行结果,可以看到生成了线程ID是一样的。引用网上的一句话:glibc的pthreads实现实际上把pthread_t作为一个结构体指针,指向一块动态分配的内存,但是这块内存是可以反复使用的,也就是说很容易造成pthread_t的重复。也就是说pthreads只能保证同一进程内,同一时刻的各个线程不同;不能保证同一个进程全程时段每个线程具有不同的id,不能保证线程id的唯一性。
那么肯定有人说,刚才那个明明是重偏向,不应该是轻量锁,那我们再次修改原来的代码,再次验证它就是轻量锁,而不是重偏向,我们只需要在A线程和创建B线程之间创建一个线程,什么事都不用做,这样线程B创建出来的线程ID就和线程A创建出来的线程ID是不一样的。我们再次修改LightWeightLockDemo.java代码如下:
import org.openjdk.jol.info.ClassLayout;
public class LightWeightLockDemo {
public static void main(String[] args) throws InterruptedException {
MyLock myLock = new MyLock();
System.out.println("before lock");
System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
Thread aThread = new Thread(() -> {
synchronized (myLock) {
System.out.println("aThread lock ing");
System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
}
});
aThread.start();
//让A线程结束,然后B线程执行
aThread.join();
//创建一个线程让线程B和线程A的线程ID不一样
Thread test = new Thread(() -> {
System.out.println("test ...");
});
test.start();
Thread bThread = new Thread(() -> {
synchronized (myLock) {
System.out.println("bThread lock ing");
System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
}
});
bThread.start();
}
}
运行结果如下:
我们可以看到线程ID不一样了,就是轻量了,所以Java的线程是不支持重偏向的。
我们再次验证轻量锁的第二种情况,我们先创建LightWeightLockDemo1.java,具体代码如下:
import org.openjdk.jol.info.ClassLayout;
public class LightWeightLockDemo1 {
public static void main(String[] args) {
MyLock myLock = new MyLock();
System.out.println("before lock");
System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
Thread aThread = new Thread(() -> {
try {
//为了让它们没有竞争,我们先让线程A睡眠7秒
Thread.sleep(7000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (myLock) {
System.out.println("aThread lock ing");
System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
}
});
aThread.start();
Thread bThread = new Thread(() -> {
synchronized (myLock) {
System.out.println("bThread lock ing");
System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
}
try {
Thread.sleep(8000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
bThread.start();
}
}
运行的同时,我们关闭JVM的偏向锁延迟,运行的结果如下
可以看到两个线程不是竞争的情况下,同时线程B没有死亡的时候是轻量锁。
说完了轻量锁的两种情况,那我们来说说线程的批量重偏向,批量撤销。
批量重偏向:线程A创建了100个对象的MyLock的集合myLocks,分别对每次创建的MyLock对象进行加锁,这个时候MyLock就是偏向锁,当线程A结束后。创建线程B对集合myLocks每个MyLock对象进行加锁,这个时候前20个MyLock对象就是轻量锁,当达到20的时候(默认批量重偏向的阈值是20,下面会给你看),这个时候JVM会对后面的80个对象进行批量重偏向,就是让后面的80个对象偏向线程B。
这时候我们先看看这些默认的阈值参数,我们只需要加上如下的JVM参数即可
-XX:+PrintFlagsInitial
我们可以得到批量重偏向的阈值是:20,批量撤销的阈值是:40,偏向锁的延迟的开启时间是:4000ms
下面我们来验证批量重偏向,创建BiasedLockingDemo.java代码如下:
import org.openjdk.jol.info.ClassLayout;
import java.util.ArrayList;
import java.util.List;
public class BiasedLockingDemo1 {
public static void main(String[] args) throws InterruptedException {
List<MyLock> myLocks = new ArrayList<>();
Thread aThread = new Thread(() -> {
for (int i = 0; i < 30; i++) {
//创建30个锁对象
MyLock myLock = new MyLock();
//分别加锁
synchronized (myLock) {
myLocks.add(myLock);
}
}
});
aThread.start();
aThread.join();
//创建一个线程,为了让线程A死亡后,线程B创建的线程ID和A不一样,这样就可以避免JVM把线程B当成了线程A
Thread test = new Thread(() -> {
System.out.println("test ...");
});
test.start();
Thread bThread = new Thread(() -> {
//我们查看所有30个的锁对象的对象头
for (int i = 0; i < myLocks.size(); i++) {
synchronized (myLocks.get(i)) {
System.out.println("myLock" + (i + 1) + "lock ing");
System.out.println(ClassLayout.parseInstance(myLocks.get(i)).toPrintable());
//当进行了批量重偏向后,再看看后面的有没有直接偏向线程B
if (i == 20){
System.out.println("myLock" + (25) + "lock ing");
System.out.println(ClassLayout.parseInstance(myLocks.get(25)).toPrintable());
}
}
}
//当进行了批量重偏向后,我们再看看20以前的对象是否是偏向锁。
synchronized (myLocks.get(14)){
System.out.println("after for() myLock15");
System.out.println(ClassLayout.parseInstance(myLocks.get(14)).toPrintable());
}
});
bThread.start();
}
}
关闭JVM的偏向锁延迟,运行结果如下:
我们可以看到当达到阈值20的时候,20~30的都变成了偏向锁。那么20之前的是不是偏向锁呢?
我们看到15还是轻量锁,所以重偏向的是20以后的锁对象。
简单说明原理:以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作的时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark word中也有该字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向的时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段值改为新值。下次获取锁的时候,发现当前的对象的epoch的值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行批量撤销操作。
批量撤销:
理论:假如线程A创建100对象MyLock的集合MyLocks。分别对每个MyLock对象进行加锁,这个时候就是偏向锁,然后线程B首次对集合Mylocks中每个对象MyLock进行加锁,前20次都是轻量锁,当执行到20以后都是偏向锁,这个时候JVM会直接使用if条件判断有没有撤销,如果有直接偏向线程B。然后线程C过来,首先撤销偏向线程B升级为轻量锁,当再次达到20的时候,不会再重新偏向线程C了。而是将剩余的直接膨胀成轻量锁
import org.openjdk.jol.info.ClassLayout;
import java.util.ArrayList;
import java.util.List;
public class BiasedLockingDemo {
public static void main(String[] args) throws InterruptedException {
List<MyLock> myLocks = new ArrayList<>();
Thread aThread = new Thread(() -> {
for (int i = 0; i < 50; i++) {
//创建50个锁对象
MyLock myLock = new MyLock();
//分别加锁
synchronized (myLock) {
myLocks.add(myLock);
}
}
});
aThread.start();
aThread.join();
//创建一个线程,为了让线程A死亡后,线程B创建的线程ID和A不一样,这样就可以避免JVM把线程B当成了线程A
Thread test = new Thread(() -> {
System.out.println("test ...");
});
test.start();
Thread bThread = new Thread(() -> {
//我们查看第20个的锁对象的对象头
for (int i = 0; i < myLocks.size(); i++) {
synchronized (myLocks.get(i)) {
if (i == 19){
System.out.println("bThread myLock" + (i + 1) + "lock ing");
System.out.println(ClassLayout.parseInstance(myLocks.get(i)).toPrintable());
}
}
}
});
bThread.start();
bThread.join();
//创建一个线程,为了让线程B死亡后,线程C创建的线程ID和B不一样,这样就可以避免JVM把线程C当成了线程B
Thread test1 = new Thread(() -> {
System.out.println("test1 ...");
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
test1.start();
Thread cThread = new Thread(() -> {
//我们查看所有20~50的锁对象的对象头
for (int i = 20; i < myLocks.size(); i++) {
synchronized (myLocks.get(i)) {
System.out.println("cThread myLock" + (i + 1) + "lock ing");
System.out.println(ClassLayout.parseInstance(myLocks.get(i)).toPrintable());
}
}
});
cThread.start();
}
}
关闭偏向锁延迟,运行结果如下:
我们可以看到达到阈值20的时候,没有进行批量重偏向,而是进行批量撤销。