该文章是本人学习记录总结的,有误请指出,感谢。java
一开始学习Java时,介绍Java的同步机制那就必然是synchronized。但以后又了解到synchronized是一个重量级锁,因此应当尽可能使用Lock。编程
以后又了解到Java1.6对synchronized进行了优化。数组
因此除非:安全
的状况下使用Lock,应当尽可能使用synchronized。代码更加简洁。并发
由于是Java语法提供的,也能够称为内置锁。ide
根据做为锁的对象不一样,可分为性能
从上图能够看出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实现原理)
整篇参考: