深入理解synchronized关键字

上一篇博客中我们通过JOL工具查看了加锁对象的对象头,也大致的了解了偏向锁、轻量锁、重量锁的对象头,但是什么时候是偏向锁?什么时候又是轻量锁?什么时候又是重量锁?

为了说明这个问题,我们假设我们运行的程序中有一个线程A,有一个线程B,还有一个加锁对象MyLock。

  • 偏向锁:当线程A对锁对象MyLock加锁的时候,没有其他的线程,首次加锁的时候就是偏向锁
  • 轻量锁:
    • 当线程A对锁对象MyLock加锁的时候,然后线程A结束的时候,这时候B线程对锁对象MyLock加锁,这个时候就是轻量锁。可能有人会认为这个时候线程A结束了。没有线程和线程B进行资源竞争,那么MyLock对象应该重新偏向B,很可惜不是,下面我会进行证明
    • 当线程A对锁对象MyLock加锁的时候,然后线程A没有结束,这时候线程B对锁对象MyLock进行加锁,前提两个线程没有竞争,这个时候也是轻量锁。
  • 重量锁:当线程A对锁对象MyLock加锁的时候,线程B也对锁对象MyLock进行加锁的时候,存在资源的竞争,这个时候就是重量锁。

说完了JDK对synchronized 关键字的三种锁的优化,同时JDK还有一种机制就是批量重偏向,批量撤销。下面就让我们进行代码的验证吧!至于偏向锁和重量锁的情况,我在我的博客深入理解Java对象头mark word中有详细的介绍。那么就剩下轻量锁的两种情况,以及批量重偏向和批量撤销几种情况,我们还没有详细说明,下面我们就开始我们的编码了。

首先导入查看对象头的工具JOL,至于怎么导入,可以参考我的博客深入理解Java的对象头Mark word。创建MyLock.java,代码如下:

//只是作为一个加锁的对象,我们什么都可以不书写
public class MyLock {
    
}

然后我们创建我们的测试类LightWeightLockDemo.java

import org.openjdk.jol.info.ClassLayout;

public class LightWeightLockDemo {
    public static void main(String[] args) throws InterruptedException {

        MyLock myLock = new MyLock();

        System.out.println("before lock");
        System.out.println(ClassLayout.parseInstance(myLock).toPrintable());

        Thread aThread = new Thread(() -> {
            synchronized (myLock) {
                System.out.println("aThread lock ing");
                System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
            }
        });

        aThread.start();
        //让A线程结束,然后B线程执行
        aThread.join();

        Thread bThread = new Thread(() -> {
            synchronized (myLock) {
                System.out.println("bThread lock ing");
                System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
            }
        });
        
        bThread.start();
    }

}

运行之前我们要关闭偏向锁的延迟,然后就是这种情况就是我们上面提到轻量锁中的第一种情况,线程A死亡的时候,然后我们查看运行结果

#关闭偏向锁的延迟
-XX:BiasedLockingStartupDelay=0

在这里插入图片描述

在这里插入图片描述

我们发现一种诡异的情况,B线程也是偏向的状态,但是你发现偏向的线程的ID都是一样的。难道说线程A在死亡后,创建的线程B和线程A是一样的线程ID,然后导致JVM虚拟机把线程B当成了线程A了,当线程B进行加锁的时候,JVM以为是原来的线程A来重新加锁,所以还是偏向锁。那么存在这种情况吗?我们来验证一下,到底是JVM的优化,还是操作系统的问题。这时候我们需要到Linux系统中来验证我们的猜想了,因为Java中的线程和操作系统的线程是一一对应,我在我的博客通过基于JNI手动模拟Java线程中证明过,可以去查看一下。

我们先在Ubuntu18.04中创建以下的文件,文件名为b.c

#include <pthread.h>
#include <stdio.h>

void* thread_func(void* arg)
{
    //打印线程的id      
    printf("thread_id:%lu\n",pthread_self());
}


int main()
{
    pthread_t t1, t2;
    //调用操作系统的线程创建函数
    pthread_create(&t1, NULL, thread_func, NULL);
    //调用操作系统的线程join函数
    pthread_join(t1, NULL);

    pthread_create(&t2, NULL, thread_func, NULL);
    pthread_join(t2, NULL);
    return 0;
}

我们执行编译的命令如下:

gcc -o b -pthread b.c

然后运行以下的命令

./b

查看运行的结果

在这里插入图片描述

我们通过上面的运行结果,可以看到生成了线程ID是一样的。引用网上的一句话:glibc的pthreads实现实际上把pthread_t作为一个结构体指针,指向一块动态分配的内存,但是这块内存是可以反复使用的,也就是说很容易造成pthread_t的重复。也就是说pthreads只能保证同一进程内,同一时刻的各个线程不同;不能保证同一个进程全程时段每个线程具有不同的id,不能保证线程id的唯一性。

那么肯定有人说,刚才那个明明是重偏向,不应该是轻量锁,那我们再次修改原来的代码,再次验证它就是轻量锁,而不是重偏向,我们只需要在A线程和创建B线程之间创建一个线程,什么事都不用做,这样线程B创建出来的线程ID就和线程A创建出来的线程ID是不一样的。我们再次修改LightWeightLockDemo.java代码如下:

import org.openjdk.jol.info.ClassLayout;

public class LightWeightLockDemo {
    public static void main(String[] args) throws InterruptedException {

        MyLock myLock = new MyLock();

        System.out.println("before lock");
        System.out.println(ClassLayout.parseInstance(myLock).toPrintable());

        Thread aThread = new Thread(() -> {
            synchronized (myLock) {
                System.out.println("aThread lock ing");
                System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
            }
        });

        aThread.start();
        //让A线程结束,然后B线程执行
        aThread.join();

        //创建一个线程让线程B和线程A的线程ID不一样
        Thread test = new Thread(() -> {
            System.out.println("test ...");
        });
        test.start();

        Thread bThread = new Thread(() -> {
            synchronized (myLock) {
                System.out.println("bThread lock ing");
                System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
            }
        });

        bThread.start();
    }

}

运行结果如下:

在这里插入图片描述

我们可以看到线程ID不一样了,就是轻量了,所以Java的线程是不支持重偏向的

我们再次验证轻量锁的第二种情况,我们先创建LightWeightLockDemo1.java,具体代码如下:

import org.openjdk.jol.info.ClassLayout;

public class LightWeightLockDemo1 {

    public static void main(String[] args) {

        MyLock myLock = new MyLock();

        System.out.println("before lock");
        System.out.println(ClassLayout.parseInstance(myLock).toPrintable());

        Thread aThread = new Thread(() -> {
            try {
                //为了让它们没有竞争,我们先让线程A睡眠7秒
                Thread.sleep(7000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (myLock) {
                System.out.println("aThread lock ing");
                System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
            }
        });

        aThread.start();

        Thread bThread = new Thread(() -> {
            synchronized (myLock) {
                System.out.println("bThread lock ing");
                System.out.println(ClassLayout.parseInstance(myLock).toPrintable());
            }
            try {
                Thread.sleep(8000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        bThread.start();
    }
}

运行的同时,我们关闭JVM的偏向锁延迟,运行的结果如下

在这里插入图片描述

可以看到两个线程不是竞争的情况下,同时线程B没有死亡的时候是轻量锁。

说完了轻量锁的两种情况,那我们来说说线程的批量重偏向,批量撤销。

批量重偏向:线程A创建了100个对象的MyLock的集合myLocks,分别对每次创建的MyLock对象进行加锁,这个时候MyLock就是偏向锁,当线程A结束后。创建线程B对集合myLocks每个MyLock对象进行加锁,这个时候前20个MyLock对象就是轻量锁,当达到20的时候(默认批量重偏向的阈值是20,下面会给你看),这个时候JVM会对后面的80个对象进行批量重偏向,就是让后面的80个对象偏向线程B。

这时候我们先看看这些默认的阈值参数,我们只需要加上如下的JVM参数即可

-XX:+PrintFlagsInitial

在这里插入图片描述
运行结果如下:

在这里插入图片描述

我们可以得到批量重偏向的阈值是:20,批量撤销的阈值是:40,偏向锁的延迟的开启时间是:4000ms

下面我们来验证批量重偏向,创建BiasedLockingDemo.java代码如下:

import org.openjdk.jol.info.ClassLayout;

import java.util.ArrayList;
import java.util.List;

public class BiasedLockingDemo1 {

    public static void main(String[] args) throws InterruptedException {
        List<MyLock> myLocks = new ArrayList<>();

        Thread aThread = new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                //创建30个锁对象
                MyLock myLock = new MyLock();
                //分别加锁
                synchronized (myLock) {
                    myLocks.add(myLock);
                }
            }
        });

        aThread.start();
        aThread.join();

        //创建一个线程,为了让线程A死亡后,线程B创建的线程ID和A不一样,这样就可以避免JVM把线程B当成了线程A
        Thread test = new Thread(() -> {
            System.out.println("test ...");
        });
        test.start();

        Thread bThread = new Thread(() -> {
            //我们查看所有30个的锁对象的对象头
            for (int i = 0; i < myLocks.size(); i++) {
                synchronized (myLocks.get(i)) {
                    System.out.println("myLock" + (i + 1) + "lock ing");
                    System.out.println(ClassLayout.parseInstance(myLocks.get(i)).toPrintable());
                    //当进行了批量重偏向后,再看看后面的有没有直接偏向线程B
                    if (i == 20){
                        System.out.println("myLock" + (25) + "lock ing");
                        System.out.println(ClassLayout.parseInstance(myLocks.get(25)).toPrintable());
                    }
                }
            }
            //当进行了批量重偏向后,我们再看看20以前的对象是否是偏向锁。
            synchronized (myLocks.get(14)){
                System.out.println("after for() myLock15");
                System.out.println(ClassLayout.parseInstance(myLocks.get(14)).toPrintable());
            }

        });
        bThread.start();
    }
}

关闭JVM的偏向锁延迟,运行结果如下:

在这里插入图片描述

我们可以看到当达到阈值20的时候,20~30的都变成了偏向锁。那么20之前的是不是偏向锁呢?

在这里插入图片描述

我们看到15还是轻量锁,所以重偏向的是20以后的锁对象。

简单说明原理:以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作的时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark word中也有该字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向的时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段值改为新值。下次获取锁的时候,发现当前的对象的epoch的值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行批量撤销操作。

批量撤销:

理论:假如线程A创建100对象MyLock的集合MyLocks。分别对每个MyLock对象进行加锁,这个时候就是偏向锁,然后线程B首次对集合Mylocks中每个对象MyLock进行加锁,前20次都是轻量锁,当执行到20以后都是偏向锁,这个时候JVM会直接使用if条件判断有没有撤销,如果有直接偏向线程B。然后线程C过来,首先撤销偏向线程B升级为轻量锁,当再次达到20的时候,不会再重新偏向线程C了。而是将剩余的直接膨胀成轻量锁

import org.openjdk.jol.info.ClassLayout;

import java.util.ArrayList;
import java.util.List;

public class BiasedLockingDemo {

    public static void main(String[] args) throws InterruptedException {
        List<MyLock> myLocks = new ArrayList<>();

        Thread aThread = new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                //创建50个锁对象
                MyLock myLock = new MyLock();
                //分别加锁
                synchronized (myLock) {
                    myLocks.add(myLock);
                }
            }
        });

        aThread.start();
        aThread.join();

        //创建一个线程,为了让线程A死亡后,线程B创建的线程ID和A不一样,这样就可以避免JVM把线程B当成了线程A
        Thread test = new Thread(() -> {
            System.out.println("test ...");
        });
        test.start();

        Thread bThread = new Thread(() -> {
            //我们查看第20个的锁对象的对象头
            for (int i = 0; i < myLocks.size(); i++) {
                synchronized (myLocks.get(i)) {
                    if (i == 19){
                        System.out.println("bThread myLock" + (i + 1) + "lock ing");
                        System.out.println(ClassLayout.parseInstance(myLocks.get(i)).toPrintable());
                    }
                }
            }

        });
        bThread.start();
        bThread.join();

        //创建一个线程,为了让线程B死亡后,线程C创建的线程ID和B不一样,这样就可以避免JVM把线程C当成了线程B
        Thread test1 = new Thread(() -> {
            System.out.println("test1 ...");
            try {
                Thread.sleep(15000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        test1.start();

        Thread cThread = new Thread(() -> {
            //我们查看所有20~50的锁对象的对象头
            for (int i = 20; i < myLocks.size(); i++) {
                synchronized (myLocks.get(i)) {
                    System.out.println("cThread myLock" + (i + 1) + "lock ing");
                    System.out.println(ClassLayout.parseInstance(myLocks.get(i)).toPrintable());
                }
            }      
        });
        cThread.start();
    }
}

关闭偏向锁延迟,运行结果如下:

在这里插入图片描述

在这里插入图片描述
我们可以看到达到阈值20的时候,没有进行批量重偏向,而是进行批量撤销。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
synchronized关键字是用于实现线程同步的重要关键字。它的底层原理涉及到对象的内部锁机制。当一个线程进入一个synchronized代码块或方法时,它会尝试获取该对象的内部锁。如果没有其他线程持有该锁,那么该线程就会成功获取到锁并执行代码块或方法。如果有其他线程已经持有了该锁,那么该线程就会被阻塞,直到其他线程释放了锁。这样可以确保在同一时刻只有一个线程执行被synchronized修饰的代码块或方法,从而保证了并发安全性。 在底层,Java虚拟机使用了monitor对象来实现synchronized关键字。每个对象都有一个与之关联的monitor对象,用于实现锁的机制。当线程获取到一个对象的锁后,它会进入monitor对象的锁定状态,其他线程将无法进入该对象的synchronized代码块或方法,直到拥有锁的线程释放了锁。当线程释放锁时,它会将锁的状态设置为可用,然后通知等待的线程有机会获取锁。这种基于monitor对象的锁机制确保了线程之间的互斥和同步。 <span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [深入理解synchronized底层原理,一篇文章就够了!](https://blog.csdn.net/qq_36934826/article/details/95978700)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [Synchronized的底层实现原理(原理解析,面试必备)](https://blog.csdn.net/weixin_42460087/article/details/126474481)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值