通过修改Linux源码证明有偏向锁的存在
通过博客《基于JNI手动模拟Java线程》,我们知道Java线程的创建方式,
本质:就是调用操作系统底层的线程创建函数 , Linux中是pthread_create函数
那么线程加锁在操作系统又是调用什么函数呢?
查了一下是调用操作系统中的pthread_mutex_lock函数。
回到我们今天的主题:
Java对synchronized关键字进行了优化,引入的偏向锁(当线程没有竞争的时候,偏向锁只会加锁一次,后面再去调用该方法的时候,不会再去操作系统调用对应的操作的系统的加锁的函数)
那我们该如何证明呢?提供了两个思路,不知道是否可行,只能先尝试。
- 思路一:在我上次编译好的JDK源码中,找到对应的pthread_mutex_lock函数,为其加一个断点,然后书写对应的Java代码(只启动一个线程),进行调试,看这个断点会进入几次,如果是一次就证明是偏向锁,如果不是一次,证明不是偏向锁。
- 思路二:修改Linux源码对应的函数,为其加上打印语句,打印当前线程的ID,然后在Java中打印对应的Java的线程ID,通过比较如果两个ID是一样的,证明加锁了,找到整个成对个数,如果是一次,就只是加锁了一次。
思路一的尝试:
在Linux下新建SyncDeom.java,书写一下的Java代码,进行调试,至于怎么在OpenJDK源码中调试,可以作者的博客《Ubuntu18.04下编译JDK12》书写的Java代码如下:
public class SyncDemo {
Object o = new Object();
public static void main(String[] args) {
SyncDemo syncDemo = new SyncDemo();
syncDemo.start();
}
public void start() {
Thread thread = new Thread() {
public void run() {
while (true) {
sync();
}
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
while (true) {
sync();
}
}
};
thread.setName("t1");
thread2.setName("t2");
thread.start();
}
public void sync() {
synchronized (o) {
System.out.println("haha");
}
}
}
编译上面代码,然后在OpenJDK中进行调试,按照思路一进行操作,如果断点只进入一次,证明是偏向锁。
打开Clion IDE找到对应方法的断点,我终究还是太年轻了,以为像找创建线程函数一样的好找,但是打开一看,完全不知道是调用那个的方法,具体图如下:
思路一完全是行不通的,我还忽略了一个问题,就是java启动的时候,还有很多其他的线程需要同步,(GC线程也需要同步)故会调用操作系统的pthread_mutex_lock的方法,所以这个思路是完全不行,就算我不创建自己的线程,这个方法也不止调用一次。我真的有点蠢!!!
思路二:我们要在pthread_mutex_lock加一句打印的话,就必须要编译Linux的源码,找到对应的函数加上对应的打印语句,然后下面我会详细的说明编译Linux的源码过程。
笔者的编译环境如下,请尽量保持和笔者的环境一致,不然可能编译通过不了。
操作系统:centos7
Linux源码:Glibc2.19
在编译Linux源码之前要先确保新装的Linux中安装Java环境,运行java和javac命令,看是否可用?
可以看到我们新装的系统javac命令不可用,于是要执行如下的命令进行java的搜索和安装
yum update
yum search java | grep -i --color jdk
选择Jdk1.8安装,具体命令如下:
yum install -y java-1.8.0-openjdk.x86_64 java-1.8.0-openjdk-devel.x86_64
再次验证验证javac和java命令是否可用?
发现都是可用的状态了,前置的准备工作已经准备好了,开始编译Linux源码。
我们要先去http://mirror.hust.edu.cn/gnu/glibc/网站下载glibc2.19版本,如下图
编译前我们要准备编译的环境gcc,具体的命令如下:
yum install -y gcc
安装过后,运行以下的命令,查看gcc是否安装成功
gcc -v
修改glibc的源码,先将下载好的glibc源码解压,具体命令如下:
tar -zxvf
解压过后我们要修改pthread_mutex_lock的代码,pthread_mutex_lock的代码在glibc-2.19/nptl/下,具体指令如下:
cd glibc-2.19/nptl
vim pthread_mutex_lock.c
修改代码如下:
先导入对应的头文件
加入对应的打印语句
接下来就是编译,在编译前,我们先查看当前系统的glibc的版本,具体的指令是:
ldd --version
现在开始编译,编译的指令如下(请在root账户下执行):
mkdir build //在glibc-2.19目录下新建一个build文件夹cd build //进入build目录下../configure --prefix=/usr --disable-profile --enable-add-ons --with-headers=/usr/include --with-binutils=/usr/bin
检测完成,执行编译的命令(请在root账户下执行)
make
执行完make命令,再执行如下的命令(请在root账户下执行)
make install
我们再次验证我们系统的glibc的版本,执行下面的命令:
ldd --version
可以看到我们的glibc的版本变成了2.19,然后线程的编号也打印了。一切都准备好了,开始验证我们的猜想。重新编写刚才的SyncDemo类,具体代码如下:
public class SyncDemo {
Object o = new Object();
static {
System.loadLibrary("SyncDemoNative");
}
public static void main(String[] args) {
System.out.println("---------main thread begin---------------------");
SyncDemo syncDemo = new SyncDemo();
syncDemo.start();
}
public void start() {
Thread thread = new Thread() {
public void run() {
while (true) {
sync();
}
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
while (true) {
sync();
}
}
};
thread.setName("t1");
thread2.setName("t2");
thread.start();
// thread2.start();
}
//书写一个本地的方法来获取对应的线程ID,因为java方法中,获取的线程ID是Java虚拟机分配的,并不是操作系统的线程ID
public native void tid();
public void sync() {
synchronized (o) {
tid();
}
}
}
然后可以用JNI的技术书写本地的方法,具体过程可以参考我的博客《基于JNI手动模拟Java线程》书写的c代码如下所示:
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include"SyncDemo.h"
JNIEXPORT void JNICALL Java_SyncDemo_tid(JNIEnv *env, jobject c1){
printf("Java Thread Id:%lu---------------\n",pthread_self());
usleep(700);
}
然后执行以下的命令
gcc -fPIC -I /usr/lib/jvm/java‐1.8.0‐openjdk/include -I /usr/lib/jvm/java‐1.8.0‐openjdk/include/linux -shared -o libSyncDemoNative.so SyncDemo.c
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/ys
接下来,只需要运行java -XX:BiasedLockingStartupDelay=0 SyncDemo即可,这里要关闭偏向锁的延迟
上面可以看到java的主线程启动了。
上面我们可以看到Java的线程打印的线程ID号和操作系统的pthread_mutex_lock函数打印的线程ID号相同只成对出现了一次,后面都没有成对出现过,只出现了一次,证明只加锁了一次,是偏向锁。这时候我们开启两个线程,让其产生竞争,然后看看是不是重量锁,修改原来的代码,将thread2线程启动起来。再次运行,查看结果
上面打印的语句表示是Java的主线程启动了。
从上面的代码可以看到Java的线程打印的线程ID号和操作系统的pthread_mutex_lock函数打印的线程ID号相同成对出现的次数不止一次,故加锁的次数也不止一次,所以加的是重量锁。