JAVA并发(2)—sync关键字和Monitor管程的关系

  1. sync到底是对什么加锁
  2. Monitor模型
    2.1 为什么sync是非公平锁
    2.2 Monitor和对象头的关系
    2.3 Monitor的JDK源码
  3. sync各种锁与Monitor的关系
    3.1 锁膨胀过程
    3.2 自适应式自旋
    3.3 锁消除
    3.4 锁粗化
  4. wait和notify方法的疑问
    4.1 wait和notify为什么必须synchronized中使用
    4.2 wait和notify为什么是Object的方法

1. sync到底是对什么加锁

synchronized锁静态方法和实例方法有什么区别?

JAVA虚拟机给每个对象和class字节码文件都设置了一个监控器Monitor,用于检测并发代码的重入。同时,Object类中提供notify和wait方法来对Monitor中的线程进行控制。

sync锁是一个可重入的非公平独占锁。sync加锁(此处特指重量级锁)会去获取obj的Monitor,如果Monitor已经被其他线程获取,那么当前线程会进入Entry Set。等待其他线程释放obj的Monitor。

而这里的Monitor可以是类.class的Monitor,也可以是当前对象(this)Monitor。

也就是可以回答上面的问题:锁实例方法是同一个对象互斥,锁静态方法是全局互斥。

  1. 作用于实例方法时,锁住的是this对象为锁的所有代码块;
  2. 作用于静态方法时,锁住的是Class实例,又因为Class相关数据存储在永久代,永久代是全局贡献,因此静态方法相当于类的一个全局锁,会锁住调用该方法的所有线程;
  3. 作用于一个对象实例时,锁住的是所有以该对象为锁的所有代码块;

即sync借助obj中的Monitor完成线程的阻塞。

2. Monitor模型

Monitor模型.png

Monitor实现对临界资源的保护,保证每次只有一个线程能进入代码块进行访问。进入代码块即为持有Monitor,退出代码块即为释放Monitor。

2.1 为什么sync是非公平锁

而未抢占到锁的资源,便会进入Monitor的Entry Set阻塞。当抢占到锁的方法遇到wait()方法后,会释放Monitor资源,并进入到Wait Set阻塞。当Monitor资源被释放后。Entry Set、Wait Set和刚进入Monitor的线程共同争夺Monitor资源。这就是sync是非公平锁的原因。

2.2 Monitor和对象头的关系

sync是借助obj的monitor对象实现当前线程的阻塞。代码块加锁是在前后分别加上monitorentrymonitorexit指令来实现的。obj被加锁后,可以在其对象头的mark word的标记位体现。

2.3 Monitor的JDK源码

请点击获取objectMonitor.hpp源码...

JDK源码如下图所示,可以看到Wait Set、Entry Set、count(重入次数)、owner(持有锁的线程)等属性。

  ObjectMonitor() {
    _header       = NULL;
   //获取管程锁的次数
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    //持有该ObjectMonitor线程的指针
    _owner        = NULL;     
    //管程的条件变量的资源等待队列
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    //管程的入口线程队列
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

3. sync各种锁与Monitor的关系

只有重量级锁才会借助Monitor,而偏向锁和轻量级锁均借助的是对象头中mark word的标识来实现的。

偏向锁和轻量级锁的目的:偏向锁、轻量级锁并不是来取代重量级锁的。而是在不同的场景下的相互补充。偏向锁和轻量级锁解决的是当临界资源没有被争夺访问的场景下,如何优化性能。

偏向锁和轻量级锁的实现:通过对象头mark word的标记位,通过CAS来实现访问。无需借助Monitor。

锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀。

偏向锁

只有一个线程访问sync保护的临界资源。而偏向锁的目的是某个线程获取锁后,消除这个线程重入的开销。偏向锁只需要在置换ThreadId的时候依赖一次CAS。

当两个线程交替访问(即线程A释放锁,线程B便去申请锁)时,线程B使用CAS是可以获取锁的。此时对象头的Markword中线程ID设置的是线程B的线程ID

轻量级锁

两个线程竞争同一把锁,线程B竞争锁失败,表明锁对象存在竞争,则会先撤销偏向锁模式,进入轻量级锁。刚开始CAS竞争轻量级锁失败时,不会立刻膨胀为重量级锁,而是采用自旋的方式,不断重试,尝试抢锁。JDK6中,默认开启自旋,自旋10次,JDK6引入自适应的自旋锁,对于只能指定固定次数的自旋进行优化,重试机制更加智能。

重量级锁

只有通过自旋依然获取不到锁的情况下,表明锁竞争比较激烈,不再适合额外的CAS操作消耗CPU资源。则直接膨胀为重量级锁。在此状态下,所有等待锁的线程必须进入阻塞状态。

借助obj的Monitor对象完成线程的互斥访问。而线程的阻塞借助操作系统完成。这个过程是比较耗费事件的。

在JDK1.6之后,sync关键字进行了优化,使用了自旋锁,让并发的线程先不借助monitor进行同步操作。而是自旋一段时间,等待临界区线程执行完毕。

3.1 锁膨胀过程

对象头中的MarkWord用于存储对象本身的运行时数据,记录了对象的Hash、锁、GC标记等相关信息。当使用synchronized关键字加锁时,围绕同步锁的一系列过程均和MarkWord相关。

JAVA并发(1)—java对象布局

锁升级过程,可以总结为:无锁->偏向锁->轻量级锁(自旋锁,自适应锁)->重量级锁。且只能正向锁膨胀,不存在降级。

  1. 对象初始化,处于无锁状态;
  2. 存在线程A来获取锁,锁对象第一次被获取使用,进入偏向锁模式,且可重入。若另外一个线程B来获取锁,偏向锁可以被线程B的CAS获取到,那么替换markword中线程ID的相关信息即可。
  3. 当线程BCAS获取不到锁,则会升级为轻量级锁。在升级过程中也采用了CAS操作。若首次CAS获取或者竞争轻量级锁失败,则会采用spin(快速旋转)自旋的方式,旋转N次,重复尝试。JDK1.6默认开启自旋锁,自旋的次数默认是10次,也采用自适应式自旋的方式;
  4. 若经过自旋,依旧无法获取到锁,表明锁竞争比较激烈,CAS自旋较为消耗CPU资源,直接膨胀为重量级锁

3.2 自适应式自旋

JDK1.6中引入了自适应的自旋锁。 自适应意味着自旋的时间不再是固定的, 而是由前一次在同一个锁上的自旋时间以及锁拥有者的状态来决定。如果在同一个锁对象上, 自旋等待刚好成功获得锁, 并且在持有锁的线程在运行中, 那么虚拟机就会认为这次自旋也是很有可能获得锁, 进而它将允许自旋等待相对更长的时间。

3.3 锁消除

锁消除是一种更为彻底的优化,在JIT编译时,对运行上下文进行扫描,去除不可能存在共享资源竞争的锁。

3.4 锁粗化

原则上,我们知道在加同步锁的时候,尽可能的将同步块作用范围限制在尽量小的范围。但是如果存在一连串的操作都是对同一个对象进行反复的加锁和解锁,甚至加锁的操作出现在循环体中,那么即使没有线程竞争共享资源,频繁的进行加锁操作也会导致性能的损耗。

锁粗化就是将加锁的范围粗化到这一连串的操作的外部(比如while循环体外)。使得这一连串操作只需要加一次锁即可。

4. wait和notify方法的疑问

有没有疑问:为什么wait和notify是Object的方法,而不是Thread的方法。且wait和notify方法必须在sync关键字中使用?

4.1 wait和notify为什么必须synchronized中使用

只因为synchronized关键字(重量级锁,开启了管程),使得对象指向了ObjectMonitor对象,所以调用对象的wait()和notify等方法才会将线程阻塞(加入到_WaitSet中)。

4.2 wait和notify为什么是Object的方法

又因为wait()和notify()是ObjectMonitor的方法。而Object对象头中保存了ObjectMonitor的指针,所以是Object便可操作wait()方法。

推荐阅读

Java并发基石——所谓“阻塞”:Object Monitor和AQS(1)

简书—信号量与管程

百度百科—管程

Java精通并发-通过openjdk源码分析ObjectMonitor底层实现

Java 中的 Monitor 机制

深入理解Java并发之synchronized实现原理

synchronized 锁优化(一):自适应自旋锁、锁消除、锁粗化

相关阅读

JAVA并发(1)—java对象布局
JAVA并发(2)—sync关键字和Monitor管程的关系
JAVA并发(3)—线程运行时发生GC,会回收ThreadLocal弱引用的key吗?
JAVA并发(4)— ThreadLocal源码角度分析是否真正能造成内存溢出!
JAVA并发(5)— 多线程顺序的打印出A,B,C(线程间的协作)
JAVA并发(6)— AQS源码解析(独占锁-加锁过程)
JAVA并发(7)—AQS源码解析(独占锁-解锁过程)
JAVA并发(8)—AQS公平锁为什么会比非公平锁效率低(源码分析)
JAVA并发(9)— 共享锁的获取与释放
JAVA并发(10)—interrupt唤醒挂起线程
JAVA并发(11)—AQS源码Condition阻塞和唤醒
JAVA并发(12)— Lock实现生产者消费者

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269