关键字 synchronized

Synchronized和lock区别

ReentrantLock可重入锁的使用

一、简述

synchronized 是一把经典的 JVM 级别的锁。在加了它的方法、代码块中,一次只允许一个线程进入特定代码段,从而避免多线程同时修改同一数据。在 JDK6 之前,syncronized 是一把重量级的锁,随着 JDK 的升级,不断的优化,如今它变得不那么重了,甚至在某些场景下,它的性能反而优于轻量级锁。实现原理就是锁升级的过程。

1️⃣synchronized 的作用

  1. 原子性:保证语句块内操作是原子的。
  2. 可见性:保证可见性(通过“在执行 unlock 之前,必须先把此变量同步回主内存”实现)。
  3. 有序性:保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行 lock 操作”)。

2️⃣synchronized 的使用

  1. 修饰实例方法,对当前实例对象加锁。
  2. 修饰静态方法,多当前类的 Class 对象加锁。
  3. 修饰代码块,对 synchronized 括号内的对象加锁。

二、实现原理

JVM 的同步(synchronized)基于进入和退出 Monitor 对象(即对象的监视器,虚拟机规范中用的是管程一词)实现,无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。synchronized 是可重入的,所以不会自己把自己锁死。

1️⃣代码块的同步显示同步
利用 monitorenter 和 monitorexit 这两个字节码指令,配合完成了 synchronized 修饰代码块的互斥访问。

  1. 被 synchronized 修饰的代码块,编译器编译后:在代码开始加入了 monitorenter,在代码后面加入了 monitorexit。
  2. 在虚拟机执行到 monitorenter 指令的时候,会请求获取对象的 monitor 锁,基于 monitor 锁又衍生出一个锁计数器的概念。
  3. 当执行 monitorenter 时,若对象未被锁定时,或者当前线程已经拥有该对象的 monitor 锁,则锁计数器 +1,该线程获取该对象锁。
  4. 当执行 monitorexit 时,锁计数器 -1。当计数器为 0 时,此对象锁就被释放了。此时,其它阻塞的线程可以请求获取该 monitor 锁。
  5. 如果获取 monitor 对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

2️⃣方法级的同步隐式同步
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM 可以从方法常量池中的方法表结构(method_info Structure)中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志,如果设置了,执行线程将先持有 monitor,然后再执行方法,最后在方法(正常/非正常)完成时释放 monitor。

3️⃣关于 monitorenter/monitorexit、ACC_SYNCHRONIZED 指令,可以看下下面的反编译代码:

public class SynchronizedDemo {
    public void explicit() {
        synchronized (this) {//这个是同步代码块
            System.out.println("this method is explicit");
        }
    }
    public synchronized void implicit() {//这个是同步方法
        System.out.println("this method is implicit");
    }
    public static void main(String[] args) {
    }
}

切换到目标类目录执行javac SynchronizedDemo.java命令生成编译后的 .class 文件。执行javap -c -s -v -l SynchronizedDemojavap -verbose SynchronizedDemo反编译后得到:

同步代码块反编译后得到monitorenter和monitorexit指令
同步方法,反编译后得到ACC_SYNCHRONIZED标志

tips:通过javap SynchronizedDemo可以查看其中的内容。

三、JVM 对 synchronized 的锁优化

Java6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。锁的状态总共有四种,级别从低到高依次是:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以升级但不能降级。

1️⃣偏向锁
偏向锁是 Java6 之后加入的新锁,它是一种针对加锁操作的优化手段,目的是消除数据在无竞争情况下的同步原语,进一步提高程序的性能。通常,锁不仅不存在多线程竞争,而且还总是由同一线程多次获得,为了减少同一线程获取锁(涉及到 CAS 操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word 的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提升程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合每次申请锁的线程极有可能都是不相同的,使用偏向锁得不偿失。注意,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

偏向锁在 JDK8 中,默认是轻量级锁。但如果设定了-XX:BiasedLockingStartupDelay=0,那在对一个 Object 做 synchronized 的时候,会立即上一把偏向锁。当处于偏向锁状态时,markwork 会记录当前线程 ID。

  1. 判断是否为可偏向状态。
  2. 如果为可偏向状态,则判断线程 ID 是否是当前线程,如果是进入同步块。
  3. 如果线程 ID 并未指向当前线程,利用 CAS 操作竞争锁,如果竞争成功,将 Mark Word 中线程 ID 更新为当前线程 ID,进入同步块。
  4. 如果竞争失败,等待全局安全点,准备撤销偏向锁,根据线程是否处于活动状态,决定是转换为无锁状态还是升级为轻量级锁。

当锁对象第一次被线程获取的时候,虚拟机会把对象头中的标志位设置为“01”,即偏向模式。同时使用 CAS 操作把获取到这个锁的线程 ID 记录在对象的 Mark Word 中。如果 CAS 操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。

偏向锁的释放:

偏向锁使用了遇到竞争才释放锁的机制。偏向锁的撤销需要等待全局安全点,然后它会首先暂停拥有偏向锁的线程,然后判断线程是否还活着,如果线程还活着,则升级为轻量级锁,否则,将锁设置为无锁状态。

2️⃣轻量级锁
当下一个线程参与到偏向锁竞争时,会先判断 markword 中保存的线程 ID 是否与这个线程 ID 相等,如果不相等,会立即撤销偏向锁,升级为轻量级锁。每个线程在自己的线程栈中生成一个 LockRecord(LR),然后每个线程通过 CAS(自旋) 的操作将锁对象头中的 markwork 设置为指向自己的 LR 的指针,哪个线程设置成功,就意味着获得锁。关于 synchronized 中此时执行的 CAS 操作是通过 native 的调用 HotSpot 中 bytecodeInterpreter.cpp 文件 C++ 代码实现的。

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6 之后加入的),此时 Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

3️⃣自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是 50 或 100 个循环,经过若干次循环后,如果得到锁就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

4️⃣重量级锁
如果锁竞争加剧(如线程自旋次数或者自旋的线程数超过某阈值,JDK1.6 之后,由 JVM 自己控制改规则),就会升级为重量级锁。此时就会向操作系统申请资源,线程挂起,进入到操作系统内核态的等待队列中,等待操作系统调度,然后映射回用户态。在重量级锁中,由于需要做内核态到用户态的转换,而这个过程中需要消耗较多时间,也就是“重”的原因之一。

Synchronized 的重量级锁是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁) 来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁称之为“重量级锁”。

5️⃣锁消除
锁消除是虚拟机另外一种锁的优化,这种优化更彻底,Java 虚拟机在 JIT 编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。如:

public void add() {
    StringBuffer sb = new StringBuffer();
    sb.append("a").append("b");
}

StringBuffer 的 append() 是一个同步方法。代码中 add() 中的局部对象 sb,只在该方法内的作用域有效,不可能被其他线程引用(因为是局部变量,栈私有)。不同线程同时调用 add() 时,都会创建不同的 sb 对象,sb 不可能存在共享资源竞争的情景。因此此时的 append() 若是同步,就是白白浪费的系统资源。JVM 会自动消除 StringBuffer 对象内部的锁。

6️⃣锁粗化
如果虚拟机检测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。如:

public String test() {
    int i = 0;
    StringBuffer sb = new StringBuffer();
    while (i < 100) {
        sb.append("a");
        i++;
    }
    return sb.toString();
}

JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append(),没有锁粗化就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 循环体外),使得这一连串操作只需要加一次锁即可。

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