Java的对象锁

内置锁

Java提供了一种内置的锁机制来支持原子性可见性同步代码块(Synchronized Block)。同步代码块包括两部分:一个是作为锁的对象引用,一个是锁保护的代码块。每一个Java对象都可以用做一个实现同步的锁,这种锁被称为内置锁(Intrinsic Lock)或者监视器锁(Monitor Lock)。线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时(正常返回,或者是异常退出)会自动释放锁。
同步代码块以关键字synchronized修饰,例如:

synchronized(锁对象引用){
//锁保护的代码块
}

如果synchronized修饰的是对象的方法,那么被修饰的方法体就是同步代码块,锁的对象引用就是被修饰的方法所在的对象。

public class SyncTest {
    public synchronized void method() {
    //方法体就是同步代码块
    } 
}

如果synchronized修饰的是静态方法,那么被修饰的方法体就是同步代码块,锁的对象引用就是被修饰的方法所在的Class对象。

public class SyncTest {
    public static synchronized void method() {
    //方法体就是同步代码块
    } 
}

内置锁的特性

  • 互斥:同一时间最多只有一个线程能够持有这种锁。
    线程尝试获取一个被其它线程持有的内置锁,线程必须等待(自旋)或者阻塞(自旋策略失效),并且因为请求内置锁被阻塞的线程不能被中断
  • 可重入:如果某个线程试图获取一个已经由它持有的内置锁,那么这个请求就会成功。
    实现原理:为每个所关联一个获取计数值和一个所有者线程。当计数值为0,表示这个锁没有被任何线程持有。当线程请求一个未被持有的锁,JVM将所有者线程设置为请求线程,并且将计数值置为1。如果同一个线程再次请求这个锁,计数值递增,退出同步代码块计数值将递减。如果计数值为0,这个锁将被释放。

synchronized的原理

当声明 synchronized 代码块时,编译而成的字节码将包含 monitorenter 和 monitorexit 指令。这两种指令均会消耗操作数栈上的一个引用类型的元素(也就是 synchronized 关键字括号里的引用),作为所要加锁和解锁的锁对象。

自旋

因为监视器锁实现的同步是互斥同步,互斥导致的Java 线程的阻塞以及唤醒,都是依靠操作系统来完成的。举例来说,对于符合 posix 接口的操作系统(如 macOS 和绝大部分的 Linux),上述操作是通过 pthread 的互斥锁(mutex)来实现的。这些操作将涉及系统调用,需要从操作系统的用户态切换至内核态,这些操作给操作系统的并发性能带来了很大的开销。
自旋的实现原理就是,如果线程请求获取监视器锁失败,并不立刻阻塞线程,而是让线程执行一个忙循环(自旋)。自旋之后再次尝试获取锁。如果获取锁失败,这个过程会循环一定次数,超过某个阀值,如果还是获取不到锁,才阻塞线程。自旋可以通过-XX:+UserSpinning参数来开启,自旋的次数通过-XX:PreBlockSpin来更改(默认是10)。
自旋虽然避免了线程切换的损耗,但是需要占用处理器时间。自旋的效果取决于锁被占用的时间,如果锁被占用的时间很短,自旋等待的效果就会很好,反之,自旋只会白白消耗处理器资源,带来性能上的损耗。
JDK1.6引入了自适应的自旋锁。

锁优化

自旋锁

见自旋小节

重量级锁

重量级锁是 JVM中传统的锁实现。在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。

轻量级锁

轻量级锁的目标是在没有多线程的竞争下,减少重量级锁使用的操作系统互斥量产生的性能消耗。

原理

轻量级锁的实现依赖对象头的标记字段。Java的对象头被设计为能够根据对象的状态复用自己的储存空间,对象头的标记字段有2bit用于储存锁状态,不同的锁状态对应的对象头的内容及状态之间转换如下图:


对象头标记字段的锁状态的转化
加锁过程

当进行加锁操作时,JVM会判断是否已经是重量级锁。如果不是,它会在当前线程的当前栈桢中划出一块空间,作为该锁的锁记录(Lock Record),并且将锁对象的标记字段复制到该锁记录中。
然后,JVM会尝试用 CAS(compare-and-swap)操作将锁对象的标记字段替换为锁记录的指针。如果操作成功,那么这个线程就获取了这个对象的锁,并且标记字段的锁标志位转变为“00”,表示该锁处于轻量级锁定状态。
如果标记字段替换操作失败,JVM会先检查对象的标记字段是否指向当前线程的栈帧,如果是表明当前线程已经持有了该对象的锁。否则说明这个对象的锁已经被其它线程所持有了,这时,轻量级锁膨胀为重量级锁,锁的标记字段的锁标志位变为“10”,标记字段存储的就是指向重量级锁(互斥量)的指针,后面的线程要进入阻塞状态。

解锁过程

轻量级锁的解锁过程也要通过CAS操作来进行。如果锁对象的标记字段仍然指向线程的锁记录,JVM尝试用CAS操作将锁对象的标记字段替换为锁记录中的复制过来的标记字段。如果CAS操作成功,则释放了锁。否则说明有其他线程尝试获取过该对象的锁,那么在释放锁的同时,唤醒被挂起的线程。

性能分析

轻量级锁提升性能的依据是:对于绝大部分的锁,在整个同步期间都是不存在竞争的。如果没有竞争,轻量级锁使用CAS操作避免了重量级锁使用互斥量的开销。如果存在竞争,除了重量级锁的互斥量开销,还带来了CAS操作的开销,性能反而比重量级锁差。

偏向锁

偏向锁的目的也是消除无竞争条件下的同步原语。偏向锁会偏向于第一个获取到它的线程,如果在获取到锁之后的过程中,没有发生锁竞争,那么持有偏向锁的线程将永远不需要再进行同步。相比轻量级锁,偏向锁能够消除轻量级锁多次加锁的CAS操作。

加锁过程

具体来说,在线程进行加锁时,如果该锁对象支持偏向锁,那么 JVM会通过 CAS 操作,将当前线程的ID记录在锁对象的标记字段之中,并且将标记字段的锁标志位置为“01”,即偏向模式。
如果操作成功,在接下来的运行过程中,每当有线程请求这把锁,Java 虚拟机只需判断锁对象标记字段中:最后三位是否为 101,是否包含当前线程的ID,以及 epoch 值是否和锁对象的类的 epoch 值相同。如果都满足,那么当前线程持有该偏向锁,可以直接返回。
当有另外的线程尝试获取这个锁时,偏向模式宣告结束。如果当前对存于未锁定状态,撤销偏向恢复至未锁定(标记字段的锁标志位为“01”)。如果处于锁定状态,则升级为轻量级锁(标记字段的锁标志位为“00”),后续的同步操作便按照轻量级锁的规则来进行。

偏向锁失效

如果某一类锁对象的总撤销数超过了一个阈值(对应JVM参数-XX:BiasedLockingBulkRebiasThreshold,默认为 20),那么 JVM会宣布这个类的偏向锁失效。
如果总撤销数超过另一个阈值(对应 JVM参数 -XX:BiasedLockingBulkRevokeThreshold,默认值为 40),那么 Java 虚拟机会认为这个类已经不再适合偏向锁。此时,Java 虚拟机会撤销该类实例的偏向锁,并且在之后的加锁过程中直接为该类实例设置轻量级锁。

其它优化手段

  • 锁消除
    JVM通过逃逸分析,如果判断出在一段代码中,堆上的所有数据都不会逃逸出去从而被其它线程访问到,就可以把他们当做栈上数据看待,同步加锁就不需要进行。
  • 锁粗化
    如果一系列的连续操作都是对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中(如StringBuffer的连续多次append操作),JVM会将加锁同步的范围扩展到这个操作系列的外部。

参考

HotSpot-Synchronization

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

推荐阅读更多精彩内容