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获取锁 的调试的主要内容,如果未能解决你的问题,请参考以下文章