oh,老哥,是码友就来看这篇多线程,保证有意外的惊喜

8: return
LineNumberTable:
line 14: 0
line 15: 8

public void testRunning();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/design/model/singleton/SynchronizeDetail
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String Roller Running!
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
LineNumberTable:
line 17: 0
line 18: 5
line 19: 13
line 20: 23
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame /
offset_delta = 18
locals = [ class com/design/model/singleton/SynchronizeDetail, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /
chop */
offset_delta = 4
}

观察一下编译后的代码,在testRoller()方法中有这样一行描述flags: ACC_PUBLIC, ACC_SYNCHRONIZED,表示着当前方法的访问权限为SYNCHRONIZED的状态,而这个标志就是编译后由JVM根据Synchronized加锁的位置增加的锁标识,也称作类锁,凡是要执行该方法的线程,都需要先获取Monitor对象,直到锁被释放以后才允许其他线程持有Monitor对象。以HotSport虚拟机为例Monitor的底层又是基于C++ 实现的ObjectMonitor,我不懂C++,通过查资(百)料(度)查到了这个ObjectMonitor的结构如下:

ObjectMonitor::ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; //线程重入次数
_object = NULL;
_owner = NULL; //标识拥有该monitor的线程
_WaitSet = NULL; //由等待线程组成的双向循环链表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多线程竞争锁进入时的单向链表
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程的队列,也是一个双向链表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

那么接下来就用一张图说明一下多线程并发情况下获取testRoller()方法锁的过程

上文中提到了MutexLock,而图中加解锁获取Monitor对象就是基于它实现的互斥操作,再次强调,在加解锁过程中线程会存在内核态与用户态的切换,因此牺牲了一部分性能。

再来说一下testRunning()方法,很显然,在编译后的class中出现了一对monitorenter/monitorexit,其实就是对象监视器的另一种形态,本质上是一样的,不过区别是,对象在锁实例方法或者实例对象时称作内置锁。而上面的testRoller()是对类(对象的class)的权限控制,两者互不影响。

到这里就解释Synchronized的基本概念,接下来要说一说它到底跟对象在对空间的内存布局有什么关系。

Synchronized与对象堆空间布局

还是以64位操作系统下HotSport版本的JVM为例,看一张全网都搜的到的图

图中展示了MarkWord占用的64位在不同锁状态下记录的信息,主要有对象的HashCode、偏向锁线程ID、GC年龄以及指向锁的指针等,记住这里的GC标志记录的位置,将来的JVM文章也会用到它,逃不掉的。在上篇例子中查看内存布局的基础上稍微改动一下,代码如下:

/**

  • FileName: JavaObjectMode
  • Author: RollerRunning
  • Date: 2020/12/01 20:12 PM
  • Description:查看加锁对象在内存中的布局
    */
    public class JavaObjectMode {
    public static void main(String[] args) {
    //创建对象
    Student student = new Student();
    synchronized(student){
    // 获得加锁后的对象布局内容
    String s = ClassLayout.parseInstance(student).toPrintable();
    // 打印对象布局
    System.out.println(s);
    }
    }
    }

class Student{
private String name;
private String address;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}
}

第一张图是上篇文章的也就是没加锁时对象的内存布局,第二张图是加锁后的内存布局,观察一下VALUE的值

其实加锁后,就是修改了对象头中MarkWord的值用来记录当前锁状态,所以可以看到加锁前后VALUE发生了变化。 从第一张图的第一行VALUE值可以看出当前的锁标记为001(这里面涉及到一个大端序和小端序的问题,可以自己学习一下:https://blog.csdn.net/limingliang_/article/details/80815393 ),对应的表中恰好是无锁状态,实际代码也是无锁状态。而图二可以看出当前锁标记为000(提示:在上图001同样的位置),对应表中状态为轻量级锁,那么代码中的的Synchronized怎么成了轻量级锁了呢?因为在JDK1.6以后对锁进行了优化,Synchronized会在竞争逐渐激烈的过程中慢慢升级为重量级互斥锁。

但是还有问题,为啥加锁了,上来就是轻量级锁而不是偏向锁呢,原因是在初始化锁标记时JVM中默认延迟4s创建偏向锁,由-XX:BiaseedLockingStartupDelay=xxx控制。一旦创建偏向锁,在没有线程使用当前偏向锁时,叫做匿名偏向锁,即上表中偏向线程ID的值为空,当有一个线程过来加锁时,就进化成了偏向锁。

到这里,是不是已经能看明白天天说的锁也不过是一堆标志位实现的,让我写几个if-else就给你写出来了

Synchronized锁升级过程

锁的升级过程为:偏向锁–>偏向锁–>轻量级锁–>重量级锁。这个过程是随着线程竞争的激烈程度而逐渐变化的。

偏向锁

其中匿名偏向锁前面已经说过了,偏向锁的作用就是当同一线程多次访问同步代码时,这一线程只需要获取MarkWord中是否为偏向锁,再判断偏向的线程ID是不是自己,就是俩if-else搞定,Doug Lee先生不过如此嘛。如果发现偏向的线程ID是自己的线程ID就去执行代码,不是就要通过CAS来尝试获取锁,一旦CAS获取失败,就要执行偏向锁撤销的操作。而这个过程在高并发的场景会造成代码很大的性能开销,慎重使用偏向锁。图为偏向锁的内存布局

轻量级锁

轻量级锁是一种基于CAS操作的,适用于竞争不是很激烈的场景。轻量级锁又分为自旋锁和自适应自旋锁。自旋锁:因为轻量锁是基于CAS理论实现的,因此当资源被占用,其他线程抢锁失败时,会被挂起进入阻塞状态,当资源就绪时,再次被唤醒,这样频繁的阻塞唤醒申请资源,十分低效,因此产生了自旋锁。JDK1.6中,JVM可以设置-XX:+UseSpinning参数来开启自旋锁,使用-XX:PreBlockSpin来设置自旋锁次数。不过到了JDK1.7及以后,取消自旋锁参数,JVM不再支持由用户配置自旋锁,因此出现了自适应自旋锁。自适应自旋锁:JVM会根据前一线程持有自旋锁的时间以及锁的拥有者的状态进行动态决策获取锁失败线程的自旋次数,进而优化因为过多线程自旋导致的大量CAS状态的线程占用资源。下图为轻量级锁内存布局:

随着线程的增多,竞争更加激烈以后,CAS等待已经不能满足需求,因此轻量级锁又要向重量级锁迈进了。在JDK1.6之前升级的关键条件是超过了自旋等待的次数。在JDK1.7后,由于参数不可控,JVM会自行决定升级的时机,其中有几个比较重要的因素:单个线程持有锁的时间、线程在用户态与内核态之间切换的时间、挂起阻塞时间、唤醒时间、重新申请资源时间等

重量级锁

而当升级为重量级锁的时候,就没啥好说的了,锁标记位为10,所有线程都要排队顺序执行10标记的代码,前面提到的每一种锁以及锁升级的过程,其实都伴随着MarkWord中锁标记位的变化。相信看到这,大家应该都理解了不同时期的锁对应着对象在堆空间中头部不同的标志信息。重量级锁的内存布局我模拟了半天也没出效果,有兴趣的大佬可以讲一下。

最后附上一张图,展示一下锁升级的过程,画图不易,还请观众老爷们关注啊:

锁优化

1.动态编译实现锁消除

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!**

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值