前言
阅读本文前需要了解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
求证思路
- 修改Linux源码中glibc库中pthread_mutex_lock.c文件中的
pthread_mutex_lock()
方法,增加输出当前os id 语句。 - java代码中使用
synchronized
关键字加锁,打印出加锁前线程id(此线程id会转化为真实os 线程Id),1和2两者相互比较。 - 如果调用os
pthread_mutex_lock()
os-id 与 java thread-id 相同,说明锁真的存在, 并且只出现过一次相同为偏向锁
开始求证
-
环境准备
- linux:centos 7
- Gilbc:Gilbc-2.19
- JDK:Java8
- gcc version 4.8.5
- 百度网盘地址:链接:https://pan.baidu.com/s/1tk1mI18d3kS5-nrFXft-Bw 密码:vjfb
要保证java
和javac
命令可用,以及注意gcc版本和glibc版本。(本人踩坑了好久,因为操作系统版本以及glibc版本),最好采用我提供的系统镜像以及glibc版本。
-
解压glibc:
tar -zxvf glibc-2.19.tar.gz
-
修改glibc的源码: 打开./glibc-2.19/nplt/pthread_mutex_lock.c(如第一张截图目录所示)再第65行添加一行打印语句,修改完之后,以后所有调用
pthread_mutex_lock
函数都会打印出自己的线程id。
-
编译刚才修改完的文件
cd glibc-2.19
-
编译完成后要存放的文件位置
mkdir out
-
编译
cd out
../configure --prefix=/usr --disable-profile --enable-add-ons --with-headers=/usr/include --with-binutils=/usr/bin
-
执行
make && make install
-
以上编译以及执行过程较长,大概会耗费近30分钟时间。上面一切完成后,输入
java
命令测试一下。
只有调用了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文件
执行Java文件!执行前说明一下,如果打印了一次message threadID = xxxxxxxx
那么说明是调用了OS的锁,
按照我们的理解,如果SUN公司对sync做了优化,那么就只会调用一次OS的锁,其余都只会打印我们Java线程的ID,不会去调用OS的系统函数,如果频繁的去调用OS的函数,那无疑就会效率很低了!
分析一下上图:
首先打印了一行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
做了优化!