Jvm对synchronized的优化之偏向锁

学习过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做了“偏向”优化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值