偏向锁真的存在吗

Itachi 2020年05月31日 74次浏览

前言

阅读本文前需要了解Java线程与操作系统线程的关系

关于偏向锁的知识,网上一大片文章,各种理论讲的一个比一个好。但是你知道偏向锁是否真的存在呢?有相关的源码或者代码证明过吗? 那么这篇文章不会讲什么是偏向锁以及偏向锁的优缺点,只是为了证明偏向锁是否真的存在,它为什么要比重量级锁快?

在有了上篇文章的知识铺垫后知道每当java线程创建的时候相对应的操作系统的 pthread_create也会创建一个线程。

再继续阅读跟踪源码后了解到使用synchronized关键字就必然会调用OS的 pthread_mutex_lock 函数,也就是加锁。

众所周知偏向锁一定会保证线程安全 ,但是实际情况不一定有互斥,偏向锁是synchronized锁的对象没有资源竞争的情况下,不会调用OS的pthread_mutex_lock() 函数。但是第一次初始化使用锁的时候确实会调用一次pthread_mutex_lock进行偏向锁。

如果有兴趣想查看操作系统是如何给线程加锁(synchronized)的话可以下载Linux的glibc的库.

源码位置在./glibc-2.19/nplt/pthread_mutex_lock.c

image.png

求证思路

  1. 修改Linux源码中glibc库中pthread_mutex_lock.c文件中的pthread_mutex_lock() 方法,增加输出当前os id 语句。
  2. java代码中使用synchronized关键字加锁,打印出加锁前线程id(此线程id会转化为真实os 线程Id),1和2两者相互比较。
  3. 如果调用os pthread_mutex_lock() os-id 与 java thread-id 相同,说明锁真的存在, 并且只出现过一次相同为偏向锁

开始求证

  1. 环境准备

    • linux:centos 7
  • Gilbc:Gilbc-2.19
    • JDK:Java8
    • gcc version 4.8.5
    • 百度网盘地址:链接:https://pan.baidu.com/s/1tk1mI18d3kS5-nrFXft-Bw 密码:vjfb

要保证javajavac命令可用,以及注意gcc版本和glibc版本。(本人踩坑了好久,因为操作系统版本以及glibc版本),最好采用我提供的系统镜像以及glibc版本。

  1. 解压glibc: tar -zxvf glibc-2.19.tar.gz

  2. 修改glibc的源码: 打开./glibc-2.19/nplt/pthread_mutex_lock.c(如第一张截图目录所示)再第65行添加一行打印语句,修改完之后,以后所有调用pthread_mutex_lock函数都会打印出自己的线程id。

    注意引入头文件(相当于Java里的包)

  3. 编译刚才修改完的文件 cd glibc-2.19

  4. 编译完成后要存放的文件位置 mkdir out

  5. 编译 cd out ../configure --prefix=/usr --disable-profile --enable-add-ons --with-headers=/usr/include --with-binutils=/usr/bin

  6. 执行 make && make install

  7. 以上编译以及执行过程较长,大概会耗费近30分钟时间。上面一切完成后,输入java命令测试一下。
    image.png

只有调用了pthread_mutex_lock()会打印出自己的线程id

表示此处修改linux源码glibc库下的pthread_mutex_lock()成功;

接下来就该验证偏向锁到底是不是真实存在的:

java代码:

public class Demo2 {
    Object o = new Object();

    //.c 文件打印出java threaid 对应的os threadid
    public native void tid();

    static {
        System.loadLibrary("TestThreadNative");
    }

    public static void main(String[] args) {
        //打印出主线程
        System.out.println("java---java---java---java---java---java---java---java---java---");
        Demo2 example4Start = new Demo2();
        example4Start.start();
    }

    public void start() {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    sync();
                }
            }
        };

        Thread t2 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    sync();
                }
            }
        };

        t1.setName("t1");
        t2.setName("t2");
        t1.start();
    }



    public void sync() {
        synchronized (o) {
            // java threadid 是jvm给的线程id 并不是真是的os 对应的线程id
            // System.out.println(Thread.currentThread().getId());

            //获取java thread 对应的真实的os thread 打印出id
            tid();
        }
    }
}

然后编译并且生成H文件和SO文件,可以查看Java线程与操作系统线程的关系 有说明怎么生成H文件。

查看一下生成的H文件

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Demo2 */
#ifndef _Included_Demo2
#define _Included_Demo2
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Demo2
 * Method:    tid
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_Demo2_tid
  (JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif

查看C文件

// 头文件
#include <pthread.h>
#include <stdio.h>
// 记得导入刚刚编译的那个.h文件
#include "Demo2.h"
// 这个方法要参考.h文件的15行代码
JNIEXPORT void JNICALL Java_Demo2_tid(JNIEnv *env, jobject c1){
        printf("current tid:%lu-----\n",pthread_self());
        usleep(1000);
}
// main方法,程序入口,main和java的main一样会产生一个进程,继而产生一个main线程
int main()
{
        return 0;
}

然后编译java文件,生成h文件,编译so文件
image.png

执行Java文件!执行前说明一下,如果打印了一次message threadID = xxxxxxxx那么说明是调用了OS的锁,

按照我们的理解,如果SUN公司对sync做了优化,那么就只会调用一次OS的锁,其余都只会打印我们Java线程的ID,不会去调用OS的系统函数,如果频繁的去调用OS的函数,那无疑就会效率很低了!
image.png

分析一下上图:

首先打印了一行java---java---java---java---java---java---java---java---java---,说明我们的JVM已经获得了执行权,

在我箭头指向的哪一行打印了message threadID = 139718931015424,这是在调用OS的函数开始加锁,

注意看下一行的ID和上一行的是一样的ID,这就说明,synchronized关键字在没有互斥的情况下,

第一次调用了OS的锁后,就不会再调用OS的pthread_mutex_lock函数,

说明了SUN公司再JDK1.5以后真正的对synchronized做了优化!