22 (续01)偏向锁的重入 以及 线程1获取偏向锁并释放线程2获取锁 的调试

Posted 蓝风9

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了22 (续01)偏向锁的重入 以及 线程1获取偏向锁并释放线程2获取锁 的调试相关的知识,希望对你有一定的参考价值。

前言

呵呵 最近收到了一个 有意思的评论

然后 花了一天的时间看了一下, 确实是非常有意思, 也不枉费 这一天的时间了

好在 有所收获 

问题也挺有意思的, 因此 今晚[2022.01.28]花了一些时间来记录 

该评论来自于 偏向锁的重入 以及 线程1获取偏向锁并释放线程2获取锁 的调试

评论的具体信息如下 

SpectacuLar Now:有进行多次测试吗?我多次测试发现,后面进行的线程有事可以获取偏向锁,有时会膨胀成轻量级锁,请问这是为什么

SpectacuLar Now:
package com.company.concurrent;
 
import org.openjdk.jol.info.ClassLayout;
 
 
public class TestSync3 
    static Object yesLock = new Object();
    public static void main(String[] args) throws InterruptedException 
        Thread.sleep(5000);
 
        yesLock = new Object();
        System.out.println("无锁时对象布局:" + ClassLayout.parseInstance(yesLock).toPrintable());
        getLock("偏向锁");
        Thread.sleep(3000);
        getLock("轻量级锁");
 
    
 
    private static void getLock(final String expectLockLevel) 
        new Thread(() -> 
            try 
                synchronized (yesLock) 
                    System.out.println("线程[" + Thread.currentThread().getName() + "]" +
                            ":" + expectLockLevel + "状态对象布局:" + ClassLayout.parseInstance(yesLock).toPrintable());
                    Thread.sleep(2000);
                
             catch (Exception e) 
                e.printStackTrace();
            
        ).start();
    
 

SpectacuLar Now回复:kdj1.8

SpectacuLar Now回复:可以试试这个,第二个线程有时候是偏向锁,有时候是轻量级锁

蓝风9正在上传…重新上传取消回复:呵呵 这个是一个很好的问题, 这个在 windows, linux 上面很容易复现出来, mac 上面似乎是复现不出来, 主要的问题是 两次 getLock 新建的线程[java语言层面的 java.lang.Thread] 对应的 JavaThread[vm层面] 的地址是相同的, 导致 第二次 getLock 偏向锁 cas 的时候发现 markWord 上面记录的 线程地址就是当前线程地址[逻辑上是第一个线程 和 第二个线程], 之后空了 我记录一篇文章, 如果你还关注这个问题的话, 可以加一个 qq, 后面我发送消息给你

以下代码截图基于 ubuntu 16 + jdk8 

测试用例

测试用例如下, 主要是新建了一个对象, 然后 第一个线程获取 yesLock 的偏向锁之后, 第一个线程挂掉 

然后 第二个线程来获取 yesLock 的锁, 获取到的还是偏向锁 

呵呵 按照常规的逻辑上来说, yesLock 偏向于 线程1, 然后 线程2 来获取 yesLock 的锁的时候两个线程不一致, 按道理应该会升级为 轻量级锁 

import org.openjdk.jol.info.ClassLayout;

/**
 * Test01Sync
 *
 * @author Jerry.X.He <970655147@qq.com>
 * @version 1.0
 * @date 2022-01-26 20:34
 */
public class Test01Sync 
    static Object yesLock = new Object();

    public static void main(String[] args) throws InterruptedException 
        Thread.sleep(5000);

        yesLock = new Object();
        System.out.println("无锁时对象布局:" + ClassLayout.parseInstance(yesLock).toPrintable());
        getLock("偏向锁");
        Thread.sleep(3000);
        getLock("轻量级锁");

    

    private static void getLock(final String expectLockLevel) 
        new Thread(() -> 
            try 
                synchronized (yesLock) 
                    System.out.println("线程[" + Thread.currentThread().getName() + "]" +
                            ":" + expectLockLevel + "状态对象布局:" + ClassLayout.parseInstance(yesLock).toPrintable());
                    Thread.sleep(2000);
                
             catch (Exception e) 
                e.printStackTrace();
            
        ).start();
    

执行结果如下, 这里输出为 小端序 

第一次输出, vm 4秒之后会初始化类型的 InstanceKlass 的 prototype_header, 更新为 "has_bias_pattern" 的对象头, 低3bit 为 0b101, 十进制的 5, 这里低3bit 为 0b101, 其他高位字节为 0, 为偏向锁无锁状态 

第二次输出, 这里低3bit 为 0b101, 其他高位字节为 不为0, 依次包含了 age[4bit], epoch[2bit], biased_locker 

第三次输出, 这里低3bit 为 0b101, 其他高位字节为 不为0, 依次包含了 age[4bit], epoch[2bit], biased_locker 

其中 第二次输出 和 第三次输出的对象头一致 

这里会有很多可能潜在的发散性的一些考虑, 我们这里 仅仅做如下 case3 正向考虑 

1. 是否是第二次 getLoc 里面的 synchronized 被削除了? 

2. 是否是 ClassLayout.parseInstance(yesLock).toPrintable() 存在缓存什么的? 

3. 是否是 第一个线程 和 第二个线程 的标记相同 ?

root@ubuntu:~/Desktop/openJdk/HelloWorld# java -cp .:lib/jol-core-0.8.jar Test01Sync
无锁时对象布局:java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

线程[Thread-0]:偏向锁状态对象布局:java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 98 21 ec (00000101 10011000 00100001 11101100) (-333342715)
      4     4        (object header)                           7e 7f 00 00 (01111110 01111111 00000000 00000000) (32638)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

线程[Thread-1]:轻量级锁状态对象布局:java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 98 21 ec (00000101 10011000 00100001 11101100) (-333342715)
      4     4        (object header)                           7e 7f 00 00 (01111110 01111111 00000000 00000000) (32638)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

偏向锁重入的判断标志 

偏向锁加锁的时候会向 markWord 中写入 currentThread 的地址信息  

然后 重入的时候的判断, 也是基于这个 currentThread 的地址信息 

我们来看一下 这个 currentThread, 对应的是一个 JavaThread 的地址信息 

然后这个 JavaThread 的信息 从 java launguage 层面上来说, 是对应于 该 java.lang.Thread 的 eetop, 这个 eetop 存放的就是 对应的 JavaThread 的地址信息 

java language 层面上来看这个 JavaThread 

在 getLock 中新建的 java.lang.Thread 中业务代码中添加如下代码, 输出当前线程的 eetop 的信息 

                Field etopField = Thread.class.getDeclaredField("eetop");
                etopField.setAccessible(true);
                Object etop = etopField.get(Thread.currentThread());
                System.out.println(etop);

从 java language 层面上来查看两次 getLock 新建的 java.lang.Thread 对应的 JavaThread 的信息, 可以发现 两次新建的 JavaThread 的地址信息是完全一样的

可以证明 我上面的回复的逻辑 

呵呵 这个是一个很好的问题, 这个在 windows, linux 上面很容易复现出来, mac 上面似乎是复现不出来, 主要的问题是 两次 getLock 新建的线程[java语言层面的 java.lang.Thread] 对应的 JavaThread[vm层面] 的地址是相同的, 导致 第二次 getLock 偏向锁 cas 的时候发现 markWord 上面记录的 线程地址就是当前线程地址[逻辑上是第一个线程 和 第二个线程], 之后空了 我记录一篇文章, 如果你还关注这个问题的话, 可以加一个 qq, 后面我发送消息给你

root@ubuntu:~/Desktop/openJdk/HelloWorld# java -cp .:lib/jol-core-0.8.jar Test01Sync
无锁时对象布局:java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

线程[Thread-0]:偏向锁状态对象布局:java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 10 20 10 (00000101 00010000 00100000 00010000) (270536709)
      4     4        (object header)                           b8 7f 00 00 (10111000 01111111 00000000 00000000) (32696)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

140428521246720
线程[Thread-1]:轻量级锁状态对象布局:java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 10 20 10 (00000101 00010000 00100000 00010000) (270536709)
      4     4        (object header)                           b8 7f 00 00 (10111000 01111111 00000000 00000000) (32696)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

140428521246720

HotSpotVM 层面上来看这个 JavaThread 

第一个 getLock, 启动线程的时候 java.lang.Thread 对应的 JavaThread 

第二个 getLock, 启动线程的时候 java.lang.Thread 对应的 JavaThread 

可以看到 和 第一个 getLock 的 java.lang.Thread 对应的 JavaThread 的地址是相同的, 具体的实现在 os::malloc 里面, 里面具体分配空间是委托给 glibc 的 malloc 

具体这两个线程对应的 JavaThread 通过 os::malloc 分配的空间为什么一样?

我这里仅仅是一个猜测, 呵呵 期望以后能有真凭实据能够证明这个原因吧 

第一个 JavaThread, malloc, free 之后, 第二个线程 malloc, free, 只是移动了 brk 的指针

第一个 malloc, free : 向上推进, 向下推进 

第二个 malloc, free : 向上推进, 向下推进 

malloc, free + malloc, free 

来一次 malloc + free, 然后第二次 mallc + free 

//
// Created by Jerry.X.He on 2022/1/28.
//

#include "stdio.h"
#include "mm_malloc.h"

int main(int argc, char** argv) 

    void *p1 = malloc(20);
    printf("p1 : 0x%x\\n", p1);
    free(p1);

    void *p2 = malloc(20);
    printf("p2 : 0x%x\\n", p2);
    free(p2);

    return 0;


运行时效果如下 

root@ubuntu:~/Desktop/openJdk/HelloWorld# ./Test13MallocFreeThenMalloc 
p1 : 0x9b9010
p2 : 0x9b9010
root@ubuntu:~/Desktop/openJdk/HelloWorld# ./Test13MallocFreeThenMalloc 
p1 : 0x202c010
p2 : 0x202c010
root@ubuntu:~/Desktop/openJdk/HelloWorld# ./Test13MallocFreeThenMalloc 
p1 : 0x20fb010
p2 : 0x20fb010

完 

参考 

偏向锁的重入 以及 线程1获取偏向锁并释放线程2获取锁 的调试

以上是关于22 (续01)偏向锁的重入 以及 线程1获取偏向锁并释放线程2获取锁 的调试的主要内容,如果未能解决你的问题,请参考以下文章

Java-锁与实现

偏向锁 10 连问,被问懵圈了。。

偏向锁,轻量级锁与重量级锁的区别以及如何膨胀

偏向锁 10 连问,被问懵圈了。。

开启偏向锁一定性能更好吗?

偏向锁跟可重入性有什么区别