学习过Java的人对synchronized关键字应该都不会陌生。其中一个经典的多线程下,线程安全的例子如下:
public class MySyncDemo {
public static int i = 0;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for(int a=0;a<10000;a++){
i++;
}
});
Thread t2 = new Thread(()->{
for(int a=0;a<10000;a++){
i++;
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i 的值最终为:"+i);
}
}
多次运行上面代码,结果每次都不一样。但是有一个共同点,基本不会出现i 的值最终为:20000的情况。
这就违背了我们的预期。可以认为这就是线程不安全的。
一般情况我们会想到用synchronized关键字加锁来解决;
public class MySyncDemo {
public static int i = 0;
public synchronized void addOnes(){
for(int a=0;a<10000;a++){
i++;
}
}
public void addOnesV2(){
synchronized(MySyncDemo.class){
for(int a=0;a<10000;a++){
i++;
}
}
}
public static synchronized void addOnesV3(){
for(int a=0;a<10000;a++){
i++;
}
}
public static void main(String[] args) {
MySyncDemo d = new MySyncDemo();
Thread t1 = new Thread(()->{
d.addOnes();
});
Thread t2 = new Thread(()->{
d.addOnes();
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i 的值最终为:"+i);
}
}
在addOnes,addOnesV2,addOnesV3中我们用到了synchronized。其中addOnesV2,addOnesV3是等价的。
有时候使用synchronized的时候可能代码是这样的:
public class MySyncDemo {
public static int i = 0;
public synchronized void addOnes(){
for(int a=0;a<10000;a++){
i++;
}
}
public void addOnesV2(){
synchronized(MySyncDemo.class){
for(int a=0;a<10000;a++){
i++;
}
}
}
public static synchronized void addOnesV3(){
for(int a=0;a<10000;a++){
i++;
}
}
public static void main(String[] args) {
MySyncDemo d = new MySyncDemo();
Thread t1 = new Thread(()->{
d.addOnes();
});
// Thread t2 = new Thread(()->{
// d.addOnes();
// });
t1.start();
// t2.start();
try {
t1.join();
// t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i 的值最终为:"+i);
}
}
我们看到这里只有一个线程访问synchronized方法。其实这个时候不需要加锁访问方法。如果加锁反而会降低代码性能。为了解决这类问题,需要引入偏向锁的概念。
通俗的说就是假定将来只有第一个申请锁的线程会使用锁,如果以后同一个线程要访问这个方法可以零成本的直接获得锁。换句话说就是第一次加锁,后面都不需要加锁。
笔者曾经感觉这些理论深奥难懂,而且过了一段时间在来看这些知识还是感觉非常陌生。原因是什么是?
感觉这些内容始终停留在理论层面。一个疑问就是,我怎么知道他真的"偏向了"?
由于java方法调用了底层jvm,而之前分析过java线程和os线程时一一对应的。那么我们可以猜想是不是加锁的时候调用了os的一些方法?
glibc中就有这么一个方法 pthread_mutex_lock,可以用来加锁。那么我们猜测一下,synchronized可能也用到了这个函数。
由于我们需要观察实验结果,所以我们需要修改glibc中pthread_mutex_lock函数的源码,添加fprintf(stderr,"msg tid=%lu\n",pthread_self());然后我们重新编译glibc并去安装到我们的linux系统中。
测试Java代码如下:
public class SyncDemo {
static {
System.loadLibrary("SyncDemoNative");
}
volatile int flag = 0;
int i = 0;
public synchronized void syncMethod(){
i++;
}
public static void main(String[] args) {
SyncDemo sd = new SyncDemo();
Thread t1 = new Thread(()->{
while(sd.flag == 0){
sd.getMyThreadId();
sd.syncMethod();
}
});
t1.start();
try {
Thread.sleep(1000 * 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
sd.flag = 1;
System.out.println("java main jieshu");
}
public native void getMyThreadId();
}
jni的C代码如下:
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include "SyncDemo.h"
JNIEXPORT void JNICALL Java_SyncDemo_getMyThreadId(JNIEnv *env, jobject c1){
printf("current tid:%lu-----\n",pthread_self());
usleep(700);
}
编译命令:
gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -L $JAVA_HOME/lib/amd64/server -pthread -fPIC -shared SyncDemo.c -o libSyncDemoNative.so
实验结果:
msg tid=139775891588864
msg tid=139775822165760
msg tid=139775822165760
msg tid=139775822165760
current tid:139775822165760-----
current tid:139775822165760-----
current tid:139775822165760-----
current tid:139775822165760-----
current tid:139775822165760-----
current tid:139775822165760-----
current tid:139775822165760-----
current tid:139775822165760-----
可以看到能够连续的输出current tid:139775822165760-----并且之间没有msg tid=139775822165760这类信息,说明在执行java代码的时候没有执行pthread_mutex_lock函数。也就证明了Jvm对synchronized做了“偏向”优化。