该文章是本人学习记录总结的,有误请指出,感谢。
一开始学习Java时,介绍Java的同步机制那就必然是synchronized。但之后又了解到synchronized是一个重量级锁,所以应当尽量使用Lock。
之后又了解到Java1.6对synchronized进行了优化。
所以除非:
的情况下使用Lock,应当尽量使用synchronized。代码更加简洁。
因为是Java语法提供的,也可以称为内置锁。
根据作为锁的对象不同,可分为
从上图可以看出synchronized代码块通过monitorenter和monitorexit指令实现,由JVM保证monitorenter保证有一个配对的monitorexit。
synchronized方法则没有特别的指令,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。
先要了解一下Java对象头和monitor
对象头由三部分组成
Mark Word为一个字大小,即在32位JVM长度为32位,在64位JVM长度为64位。
因为Mark Word用于存储与对象自定义数据无关的数据,为了节省空间,会根据对象的状态不同存放不同的数据。
32位JVM存储格式:
状态(State) | 25bit | 4bit | 1bit | 2bit | |
---|---|---|---|---|---|
23bit | 2bit | 是否偏向锁(biased_lock):1bit | 锁标志位(lock):2bit | ||
无锁(normal) | 对象的散列值(identity_hashcode) | 分代年龄(age) | 0 | 01 | |
偏向锁(Biased) | 线程ID(threadID) | 偏向时间戳(epoch) | 分代年龄(age) | 1 | 01 |
轻量级锁(Lightweight Locked) | 指向栈中记录的指针(ptr_to_lock_record) | 00 | |||
重量级锁(Heavyweight Locked) | 指向管程的指针(ptr_to_heavyweight_monitor) | 10 | |||
GC标记(Marked for GC) | 空(null) | 11 |
JDK1.6之后存在锁升级的概念,JVM对同步锁的处理随着竞争激烈,处理方式从偏向锁到轻量级锁再到重量级锁。
用于存储对象的类型指针,该指针指向它的元数据,大小为一个字。
只有数组对象才有这部分数据
因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
参考:
Monitor Record是线程私有的,每个线程都有一个Monitor Record列表,同时还有一个全局可用列表。每一个作为锁的对象都会与一个Monitor Record关联(对象头的MarkWord中的LockWord指向monitor record的起始地址)。
Owner |
---|
EntryQ |
RcThis |
Nest |
HashCode |
Candidate |
Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。
RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。
Nest:用来实现重入锁的计数。
HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。
也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。
通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销)。
所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
一开始我对于锁的了解就是拿到了就执行任务,拿不到就阻塞。
Java的线程时是映射到操作系统原生线程上的,线程的阻塞和唤醒都需要操作系统的介入,需要在用户态和核心态之间转换当,这种切换会消耗掉大量的系统资源(因为用户态和系统态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作 摘自:Java线程阻塞的代价)。
因此,JVM使用锁会逐步升级:无锁->偏向锁->轻量级锁->重量级锁
锁只能升级,不能降级
? 初始没有线程使用锁,Mark Word为无锁状态
等待全局安全点(此时间点,所有的工作线程都停了字节码的执行),通过ID找到已获得偏向锁的线程,挂起该线程,从该线程的Monitor Record列表获得一个空闲记录,并将锁对象的对象头设为轻量级锁状态,将Lock Record更新为指向该空闲记录的指针。到这里锁撤销完成,被挂起的线程继续运行。
? 偏向锁这个机制很特殊, 别的锁在执行完同步代码块后, 都会有释放锁的操作, 而偏向锁并没有直观意义上的“释放锁”操作。
那么作为开发人员, 很自然会产生的一个问题就是, 如果一个对象先偏向于某个线程, 执行完同步代码后, 另一个线程就不能直接重新获得偏向锁吗? 答案是可以, JVM 提供了批量再偏向机制(Bulk Rebias)机制
该机制的主要工作原理如下:
重量级锁依赖于操作系统的互斥量(mutex) 实现。
偏向锁、轻量级锁、重量级锁适用于不同的并发场景:
另外,如果锁竞争时间短,可以使用自旋锁进一步优化轻量级锁、重量级锁的性能,减少线程切换。
如果锁竞争程度逐渐提高(缓慢),那么从偏向锁逐步膨胀到重量锁,能够提高系统的整体性能。
参考:
【java并发编程实战4】偏向锁-轻量锁-重量锁的那点秘密(synchronize实现原理)
整篇参考:
JVM内部细节之一:synchronized关键字及实现细节(轻量级锁Lightweight Locking)
【死磕Java并发】—–深入分析synchronized的实现原理
原文:https://www.cnblogs.com/rynar/p/11145397.html