通过修改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号相同成对出现的次数不止一次,故加锁的次数也不止一次,所以加的是重量锁。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值