Java线程同步内存实现
提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加
例如:第一章 Python 机器学习入门之pandas的使用
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
前言
提示:这里可以添加本文要记录的大概内容:
例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。
锁的作用:
CPU对内存值进行修改的时候,其他CPU不许打断。
加锁主要是实现线程同步,保证资源访问的安全性。
线程同步指的是,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作。
最终实现:
cmpxchg = CAS修改变量值
lock cmpxchg 指令。
硬件:
lock指令在执行后面指令的时候锁定一个北桥信号。(不采用锁总线的方式)
一、对象在内存中的布局
1.1 对象在内存的布局
普通对象在内存中的布局:按照对象头(MarkWord)、类型指针(class pointer)、实例数据(Instance data)和对齐补位(padding)组成。
对象头Mark Word(标记字):对象头中的主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;8个字节。
类型指针(class pointer):是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;4个字节
实例数据:类对象的非静态实例成员所占空间。这个根据类自身的定义而确定。
数组长度length:创建数组对象的时候,会含有一个数组长度的部分,4个字节
内存中对象存储,对象长度按照8的倍数存储,Object对象占有的内存:对象头8字节+类型指针4字节+实例数据0字节=12字节,因此需要增加4个额外字节补位,整体占16个字节。
如果对象自身所占内存刚好是8的倍数,则不需要增加对齐字节了。
对象头MarkDown、类型指针和数组长度,合起来叫做对象头区域。
实例数据有时候叫做对象体区域。
1.2 使用JOL打印对象内存
JOL (Java Object Layout)是个打印Java对象内容的强大工具。工具地址:
https://repo.maven.apache.org/maven2/org/openjdk/jol/jol-cli/maven-metadata.xml
这个是专用于HotSpot平台打印,在其他平台运行,例如OpenJDK将会出错。
Demo实例参考:
https://gitee.com/joedan0104/javademo
/**
* main入口函数
*
* @param args 参数集
*/
public static void main(String[] args) {
Object obj = new Object();
// 打印对象内存
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
Object对象没有成员,因此实例数据部分的长度为空。
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
问:如果是String对象,那么对象内存结构又是什么样的呢?
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
String对象含有value数组和hash两个成员。我们分析一下实例数据的内存:
value指向一个数组对象,占4个字节;
hash是Int成员,占4个字节。
因此占有的内存:对象头8字节+类型指针4字节+value4字节+hash4字节=20字节,补齐占位4个字节,应该占有24字节。
java.lang.String object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80002da
12 4 char[] String.value []
16 4 int String.hash 0
20 4 (object alignment gap)
Instance size: 24 bytes
打印结果和预期的一致。
64位机器,内存只需要4个字节表示,这个是因为JVM开启了内存压缩。
$ java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296
-XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)
1.3 对象头(markDown)
在不同的锁状态下,对象头分段存储内容。
锁状态(lock):2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。biased_lock和lock一起,表达的锁状态含义如下:
是否启用偏向锁标记(biased_lock):对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock共同表示对象处于什么锁状态。
对象年代(age):4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
哈希码(identity_hashcode):31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。
thread:持有偏向锁的线程ID。
epoch:偏向锁的时间戳。
锁记录的指针(ptr_to_lock_record):轻量级锁状态下,指向栈中锁记录的指针。
对象监视器Monitor的指针(ptr_to_heavyweight_monitor):重量级锁状态下,指向对象监视器Monitor的指针。
无锁:初期锁对象刚创建时,还没有任何线程来竞争,对象的Mark Word是下图的第一种情形,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)
创建对象时内存打印:
java.lang.String object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80002da
12 4 char[] String.value []
16 4 int String.hash 0
20 4 (object alignment gap)
Instance size: 24 bytes
可以看到biased_lock为0,锁状态为01.分代年龄:0
轻量锁:当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。
增加Syncnized后,打印对象的值
synchronized (obj) {
System.out.println("加锁情况内存打印");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
此时增加了锁,但是只有一个线程进行竞争使用,因此这个锁为轻量锁状态。
加锁情况内存打印
java.lang.String object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000070000ebd6990 (thin lock: 0x000070000ebd6990)
8 4 (object header: class) 0xf80002da
12 4 char[] String.value []
16 4 int String.hash 0
20 4 (object alignment gap)
Instance size: 24 bytes
打印对象为轻量锁(thin lock),锁状态为00
0x90--》1001 0000 锁状态为00,对应轻量级锁定。biased_lock为0
重量锁:当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。
1.4 偏向锁
JVM默认是开启偏向锁的。可以通过命令查看偏向锁设置。
~/DEV/developer/helloApp$ java -XX:+PrintFlagsFinal -version | grep Biase
intx BiasedLockingBulkRebiasThreshold = 20 {product}
intx BiasedLockingBulkRevokeThreshold = 40 {product}
intx BiasedLockingDecayTime = 25000 {product}
intx BiasedLockingStartupDelay = 4000 {product}
bool TraceBiasedLocking = false {product}
bool UseBiasedLocking = true {product}
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)
属性 | 含义 | 备注 |
UseBiasedLocking | 是否开启偏向锁 | true:开启 |
BiasedLockingBulkRebiasThreshold | 修改批量重偏向阈值 | |
BiasedLockingBulkRevokeThreshold | 修改批量撤销偏向阈值 | |
BiasedLockingStartupDelay | 偏向锁启动延时 | 单位毫秒。默认延时4秒 |
我们通过代码测试偏向锁。
/**
* 测试偏向锁
*/
private static void testBiasedLock() {
System.out.println("===测试偏向锁===");
// 偏向锁默认延时4秒。因此线程延迟5秒后创建对象
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object obj = new Object();
// 打印对象内存
System.out.println("初始化情况内存打印");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println("加锁情况内存打印");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println("锁释放情况内存打印");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
5秒以后,偏向锁已经启动。通过new创建的对象,此时创建的对象,应该是偏向锁状态。
===测试偏向锁===
线程ID:1
初始化情况内存打印
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
打印结果与预期值一致。
使用synchronized同步加锁以后,此时没有新的线程竞争obj资源,因此不需要进行锁升级,仍然保持偏向锁状态。对同步对象obj增加偏向锁,将线程ID通过CAS操作写入markword,变为偏向锁加锁状态。
加锁情况内存打印
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fd846009005 (biased: 0x0000001ff6118024; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
同步处理完成以后,偏向锁进行锁释放,因为没有新的线程竞争锁,markdown不进行刷新,保持不变。
锁释放情况内存打印
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fb45b00c005 (biased: 0x0000001fed16c030; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
1.5 Thread.holdsLock
Thread.holdsLock(Object obj)方法检测一个线程是否拥有锁,同时会刷新obj的对象头markword。
/**
* 测试偏向锁
*/
private static void testBiasedLock() {
System.out.println("===测试偏向锁===");
// 偏向锁默认延时4秒。因此线程延迟5秒后创建对象
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object obj = new Object();
// 打印对象内存
System.out.println("初始化情况内存打印");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println("加锁情况内存打印");
System.out.println("Thread.holdsLock刷新前");
// 刷新obj的对象头,此时synchronized下应该升级为轻量锁
System.out.println("是否持有锁:" + Thread.holdsLock(obj));
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println("锁释放情况内存打印");
// 刷新obj的对象头,此时锁释放应该刷新为无锁态
System.out.println("是否持有锁:" + Thread.holdsLock(obj));
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
增加Thread.holdsLock后,在synchronized中,obj对象变为轻量锁。
加锁情况内存打印
Thread.holdsLock刷新前
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fab58008805 (biased: 0x0000001fead60022; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
是否持有锁:true
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000070000a974940 (thin lock: 0x000070000a974940)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
可以看到,刷新前是偏向锁加锁状态,使用Thread.holdsLock后,变为轻量锁状态。
synchronized以后,轻量锁已经释放,变为无锁态。恢复为无偏向锁状态情况。
是否持有锁:false
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
二、锁管理
2.1 锁升级
JDK对syncnized进行优化升级过程。
无锁态(new)-偏向锁-轻量级锁(无锁、自旋锁、自适应自旋锁)-重量级锁
偏向锁:首先在Java对象头进行markword,记录这个线程id,只有一个线程对这个对象加锁,这个锁叫做偏向锁。
1)默认情况,偏向锁有个时延,默认是4秒。why?因为JVM虚拟机自己有一些默认启动线程,里面有好多的sync代码,这些sync代码启东市会知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
2)打开偏向锁,默认new出来的对象,就是一个可偏向的匿名对象。
3)上偏向锁,就是把markword的线程ID修改为自己线程ID的过程。偏向锁不可重偏向 批量偏向,批量撤销。
4)如果有线程竞争,撤销偏向锁,升级为轻量级锁。线程在自己的线程栈生成lockRecord,用CAS操作将markword设置指向自己这个线程的LR的指针,设置成功者得到锁。
5)可以设置禁用偏向锁 -XX:-UseBiasedLocking
轻量级锁:默认synchronize(o)。偏向锁如果有线程争用,那么升级为自旋锁?一个锁如果被使用,那么另外一个线程打算争抢这把锁,则会陷入while循环,形成自旋。这叫做自旋锁。
重量级锁:自旋10次之后,-XX:PreBlockSpan设置的自选次数,或者自旋线程数超过CPU核数一半,升级为重量级锁。重量级锁以OS为主体。那么重量级锁不会占用CPU。--》向操作系统申请资源,linux mutex,CPU从3级-》0级系统调用,挂起线程,等待操作系统的调度,
自适应自旋:JDK1.6以后,加入了自适应自旋Adaptive Self Spanning,JVM自己控制。JDK11默认打开就是偏向锁,JDK8默认是无锁。
2.2 锁降级
重量级锁的降级发生于STW阶段,降级对象就是那些仅仅能被VMThread访问而没有其他JavaThread访问的对象。STW(stop the world)指的是GC事件发生过程中,会产生应用程序的停顿。
https://zhuanlan.zhihu.com/p/138274427
2.3 锁消除(lock eliminate)
锁消除即删除不必要的加锁操作。JVM在运行时,对一些“在代码上要求同步,但是被检测到不可能存在共享数据竞争情况”的锁进行消除。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么就可以认为这段代码是线程安全的,无需加锁。
/**
* 测试锁消除
*/
private static void testLockEliminate(String str1, String str2) {
StringBuffer buffer = new StringBuffer();
buffer.append(str1).append(str2);
}
StringBuffer是线程安全的。因为他的关键方法都被synchronized修饰,但上面的代码发现,
buffer对象是局部变量,只在此方法中引用,因此buffer是不可共享的资源,JVM会自动消除StringBuffer的对象内部的锁。
2.4 锁粗化(Lock coarsening)
假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。
/**
* 测试锁粗化
*/
private static void testLockCoarsening() {
StringBuffer sb = new StringBuffer();
sb.delete(0, sb.length());
for (int i = 0; i < 100; i++) {
sb.append(((Integer) i).toString());
}
}
总结
参考文献:
1.马士兵2021年最新Java多线程高并发编程详细讲解