Java线程同步内存实现

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多线程高并发编程详细讲解

2.java对象头 MarkWord-CSDN博客

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值