Java实现synchronized锁同步机制

目录
  • synchronized 实现原理
  • 适应性自旋(Adaptive Spinning)
  • 锁升级
    • Java 对象头
    • 偏向锁(Biased Locking)
      • 偏向锁获取
      • 偏向锁释放
      • 关闭偏向锁
    • 轻量级锁(Lightweight Locking)
      • 轻量级锁获取
      • 轻量级锁解锁
    • 重量级锁
  • 锁消除(Lock Elimination)
  • 锁粗化(Lock Coarsening)
  • 文末总结

synchronized 是 java 内置的同步锁实现,一个关键字实现对共享资源的锁定。synchronized 有 3 种使用场景,场景不同,加锁对象也不同:

  • 普通方法:锁对象是当前实例对象
  • 静态方法:锁对象是类的 Class 对象
  • 方法块:锁对象是 synchronized 括号中的对象

synchronized 实现原理

synchronized 是通过进入和退出 Monitor 对象实现锁机制,代码块通过一对 monitorenter/monitorexit 指令实现。在编译后,monitorenter 指令插入到同步代码块的开始位置,monitorexit 指令插入到方法结束和异常处,JVM 要保证 monitorenter 和 monitorexit 成对出现。任何对象都有一个 Monitor 与之关联,当且仅当一个 Monitor 被持有后,它将处于锁状态。

在执行 monitorenter 时,首先尝试获取对象的锁,如果对象没有被锁定或者当前线程持有锁,锁的计数器加 1;相应的,在执行 monitorexit 指令时,将锁的计数器减 1。当计数器减到 0 时,锁释放。如果在 monitorenter 获取锁失败,当前线程会被阻塞,直到对象锁被释放。

在 JDK6 之前,Monitor 的实现是依靠操作系统内部的互斥锁实现(一般使用的是 Mutex Lock 实现),线程阻塞会进行用户态和内核态的切换,所以同步操作是一个无差别的重量级锁。

后来,JDK 对 synchronized 进行升级,为了避免线程阻塞时在用户态与内核态之间切换线程,会在操作系统阻塞线程前,加入自旋操作。然后还实现 3 种不同的 Monitor:偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)、重量级锁。在 JDK6 之后,synchronized 的性能得到很大的提升,相比于 ReentrantLock 而言,性能并不差,只不过 ReentrantLock 使用起来更加灵活。

适应性自旋(Adaptive Spinning)

synchronized 对性能影响最大的是阻塞的实现,挂起线程和恢复线程都需要操作系统帮助完成,需要从用户态转到内核态,状态转换需要耗费很多 CPU 时间。

在我们大多数的应用中,共享数据的锁定状态只会持续很短的一段时间,为了这段时间挂起和回复线程消耗的时间不值得。而且,现在大多数的处理器都是多核处理器,如果让后一个线程再等一会,不释放 CPU,等前一个释放锁,后一个线程立马获取锁执行任务就行。这就是所谓的自旋,让线程执行一个忙循环,自己在原地转一会,每转一圈看看锁释放没有,释放了直接获取锁,没有释放就再转一圈。

自旋锁是在 JDK 1.4.2 引入(使用-XX:+UseSpinning参数打开),JDK 1.6 默认打开。自旋锁不能代替阻塞,因为自旋等待虽然避免了线程切换的开销,但是它要占用 CPU 时间,如果锁占用时间短,自旋等待效果挺好,反之,则是性能浪费。所以在 JDK 1.6 中引入了自适应自旋锁:如果同一个锁对象,自旋等待刚成功,且持有锁的线程正在运行,那本次自旋很有可能成功,会允许自旋等待持续时间长一些。反之,如果对于某个锁,自旋很少成功,那之后很有可能直接省略自旋过程,避免浪费 CPU 资源。

锁升级

Java 对象头

synchronized 用的锁存在于 Java 对象头里,对象头里的 Mark Word 里存储的数据会随标志位的变化而变化,变化如下:

Java 对象头 Mark Word

偏向锁(Biased Locking)

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引入偏向锁。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需简单地测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。

偏向锁获取

  • 当锁对象第一次被线程获取时,对象头的标志位设为 01,偏向模式设为 1,表示进入偏向模式。
  • 测试线程 ID 是否指向当前线程,如果是,执行同步代码块,如果否,进入 3
  • 使用 CAS 操作把获得到的这个锁的线程 ID 记录在对象的 Mark Word 中。如果成功,执行同步代码块,如果失败,说明存在过其他线程持有锁对象的偏向锁,开始尝试当前线程获取偏向锁
  • 当到达全局安全点时(没有字节码正在执行),会暂停拥有偏向锁的线程,检查线程状态。如果线程已经结束,则将对象头设置成无锁状态(标志位为“01”),然后重新偏向新的线程;如果线程仍然活着,撤销偏向锁后升级到轻量级锁状态(标志位为“00”),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。

偏向锁释放

偏向锁的释放采用的是惰性释放机制:只有等到竞争出现,才释放偏向锁。释放过程就是上面说的第 4 步,这里不再赘述。

关闭偏向锁

偏斜锁并不适合所有应用场景,撤销操作(revoke)是比较重的行为,只有当存在较多不会真正竞争的同步块时,才能体现出明显改善。实践中对于偏斜锁的一直是有争议的,有人甚至认为,当你需要大量使用并发类库时,往往意味着你不需要偏斜锁。

所以如果你确定应用程序里的锁通常情况下处于竞争状态,可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

轻量级锁(Lightweight Locking)

轻量级锁不是用来代替重量级锁的,它的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能损耗。

轻量级锁获取

如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如下图所示:

拷贝对象头中的 Mark Word 复制到锁记录(Lock Record)中。

拷贝成功后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,并将 Lock record 里的 owner 指针指向 object mark word。

如果成功,当前线程持有该对象锁,将对象头的 Mark Word 锁标志位设置为“00”,表示对象处于轻量级锁定状态,执行同步代码块。这时候线程堆栈与对象头的状态如下图所示:

如果更新失败,检查对象头的 Mark Word 是否指向当前线程的栈帧,如果是,说明当前线程拥有锁,直接执行同步代码块。

如果否,说明多个线程竞争锁,如果当前只有一个等待线程,通过自旋尝试获取锁。当自旋超过一定次数,或又来一个线程竞争锁,轻量级锁膨胀为重量级锁。重量级锁使除了拥有锁的线程以外的线程都阻塞,防止 CPU 空转,锁标志的状态值变为“10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

轻量级锁解锁

  • 轻量级锁解锁的时机是,当前线程同步块执行完毕。
  • 通过 CAS 操作尝试把线程中复制的 Displaced Mark Word 对象替换当前的 Mark Word。
  • 如果成功,整个同步过程完成
  • 如果失败,说明存在竞争,且锁膨胀为重量级锁。释放锁的同时,会唤醒被挂起的线程。

重量级锁

轻量级锁适应的场景是线程近乎交替执行同步块的情况,如果存在同一时间访问相同锁对象时(第一个线程持有锁,第二个线程自旋超过一定次数),轻量级锁会膨胀为重量级锁,Mark Word 的锁标记位更新为 10,Mark Word 指向互斥量(重量级锁)。

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

下图是偏向锁、轻量级锁、重量级锁之间转换对象头 Mark Word 数据转变:

偏向锁、轻量级锁、重量级锁之间转换

网上有一个比较全的锁升级过程:

锁升级过程

锁消除(Lock Elimination)

锁消除说的是虚拟机即时编译器在运行过程中,对于一些同步代码,如果检测到不可能存在共享数据竞争情况,就会删除锁。也就是说,即时编译器根据情况删除不必要的加锁操作。
锁消除的依据是逃逸分析。简单地说,逃逸分析就是分析对象的动态作用域。分三种情况:

  • 不逃逸:对象的作用域只在本线程本方法
  • 方法逃逸:对象在方法内定义后,被外部方法所引用
  • 线程逃逸:对象在方法内定义后,被外部线程所引用

即时编译器会针对对象的不同情况进行优化处理:

  • 对象栈上分配(Stack Allocations,HotSpot 不支持):直接在栈上创建对象。
  • 标量替换(Scalar Replacement):将对象拆散,直接创建被方法使用的成员变量。前提是对象不会逃逸出方法范围。
  • 同步消除(Synchronization Elimination):就是锁消除,前提是对象不会逃逸出线程。

对于锁消除来说,就是逃逸分析中,那些不会逃出线程的加锁对象,就可以直接删除同步锁。

通过代码看一个例子:

public void elimination1() {
    final Object lock = new Object();
    synchronized (lock) {
        System.out.println("lock 对象没有只会作用域本线程,所以会锁消除。");
    }
}

public String elimination2() {
    final StringBuffer sb = new StringBuffer();
    sb.append("Hello, ").append("World!");
    return sb.toString();
}

public StringBuffer notElimination() {
    final StringBuffer sb = new StringBuffer();
    sb.append("Hello, ").append("World!");
    return sb;
}

elimination1()中的锁对象lock作用域只是方法内,没有逃逸出线程,elimination2()中的sb也就这样,所以这两个方法的同步锁都会被消除。但是notElimination()方法中的sb是方法返回值,可能会被其他方法修改或者其他线程修改,所以,单看这个方法,不会消除锁,还得看调用方法。

锁粗化(Lock Coarsening)

原则上,我们在编写代码的时候,要将同步块作用域的作用范围限制的尽量小。使得需要同步的操作数量尽量少,当存在锁竞争时,等待线程尽快获取锁。但是有时候,如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
比如上面例子中的elimination2()方法中,StringBuffer的append是同步方法,频繁操作时,会进行锁粗化,最后结果会类似于(只是类似,不是真实情况):

public String elimination2() {
    final StringBuilder sb = new StringBuilder();
    synchronized (sb) {
        sb.append("Hello, ").append("World!");
        return sb.toString();
    }
}

或者

public synchronized String elimination3() {
    final StringBuilder sb = new StringBuilder();
    sb.append("Hello, ").append("World!");
    return sb.toString();
}

文末总结

  • 同步操作中影响性能的有两点:

    • 加锁解锁过程需要额外操作
    • 用户态与内核态之间转换代价比较大
  • synchronized 在 JDK 1.6 中有大量优化:分级锁(偏向锁、轻量级锁、重量级锁)、锁消除、锁粗化等。
  • synchronized 复用了对象头的 Mark Word 状态位,实现不同等级的锁实现。

到此这篇关于Java实现synchronized锁同步机制的文章就介绍到这了,更多相关Java synchronized锁同步 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • java中synchronized锁的升级过程

    目录 synchronized锁的升级(偏向锁.轻量级锁及重量级锁) java同步锁前置知识点 synchronized同步锁 java对象头 偏向锁 轻量级锁 重量级锁 关于自旋锁 打印偏向锁的参数 synchronized原理解析 一:synchronized原理解析 1:对象头 2:Synchronized在JVM中的实现原理 三.锁的优化 1.锁升级 2.锁粗化 3.锁消除 synchronized锁的升级(偏向锁.轻量级锁及重量级锁) java同步锁前置知识点 1.编码中如果使用锁可以

  • java synchronized加载加锁-线程可重入详解及实例代码

    java synchronized加载加锁-线程可重入 实例代码: public class ReGetLock implements Runnable { @Override public void run() { get(); } public synchronized void get() { System.out.println(Thread.currentThread().getId()); set(); } public synchronized void set() { Syste

  • Java Synchronized锁失败案例及解决方案

    synchronized关键字,一般称之为"同步锁",用它来修饰需要同步的方法和需要同步代码块,默认是当前对象作为锁的对象. 同步锁锁的是同一个对象,如果对象发生改变,则锁会不生效. 锁失败的代码: public class IntegerSynTest { //线程实现Runnable接口 private static class Worker implements Runnable{ private Integer num; public Worker(Integer num){

  • Java线程安全和锁Synchronized知识点详解

    一.进程与线程的概念 (1)在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单位都是进程. 在未配置 OS 的系统中,程序的执行方式是顺序执行,即必须在一个程序执行完后,才允许另一个程序执行:在多道程序环境下,则允许多个程序并发执行.程序的这两种执行方式间有着显著的不同.也正是程序并发执行时的这种特征,才导致了在操作系统中引入进程的概念. 自从在 20 世纪 60 年代人们提出了进程的概念后,在 OS 中一直都是以进程作为能拥有资源和独立运行的基本单位的.直到 20 世纪 8

  • 详解Java中的锁Lock和synchronized

    一.Lock接口 1.Lock接口和synchronized内置锁 a)synchronized:Java提供的内置锁机制,Java中的每个对象都可以用作一个实现同步的锁(内置锁或者监视器Monitor),线程在进入同步代码块之前需要或者这把锁,在退出同步代码块会释放锁.而synchronized这种内置锁实际上是互斥的,即没把锁最多只能由一个线程持有. b)Lock接口:Lock接口提供了与synchronized相似的同步功能,和synchronized(隐式的获取和释放锁,主要体现在线程进

  • 详解Java并发编程之内置锁(synchronized)

    简介 synchronized在JDK5.0的早期版本中是重量级锁,效率很低,但从JDK6.0开始,JDK在关键字synchronized上做了大量的优化,如偏向锁.轻量级锁等,使它的效率有了很大的提升. synchronized的作用是实现线程间的同步,当多个线程都需要访问共享代码区域时,对共享代码区域进行加锁,使得每一次只能有一个线程访问共享代码区域,从而保证线程间的安全性. 因为没有显式的加锁和解锁过程,所以称之为隐式锁,也叫作内置锁.监视器锁. 如下实例,在没有使用synchronize

  • java synchronized 锁机制原理详解

    目录 前言: 1.synchronized 的作用: 2.synchronized 底层语义原理: 3. synchronized 的显式同步与隐式同步: 3.1.synchronized 代码块底层原理: 3.2.synchronized 方法底层原理: 4.JVM 对 synchronized 锁的优化: 4.1.锁升级:偏向锁->轻量级锁->自旋锁->重量级锁 4.1.1.synchronized 的 Mark word 标志位: 4.1.2.锁升级过程: 4.2.锁消除: 4.3

  • java中synchronized Lock(本地同步)锁的8种情况

    目录 lock1 lock2 lock3 lock4 lock5 lock6 lock7 lock8 Lock(本地同步)锁的8种情况总结与说明: * 题目: * 1.标准访问,请问是先打印邮件还是短信 Email * 2.email方法新增暂停4秒钟,请问是先打印邮件还是短信 Email * 3.新增普通的hello方法,请问先打印邮件还是hello hello * 4.两部手机,请问先打印邮件还是短信 SMS * 5.两个静态同步方法,1部手机,请问先打印邮件还是短信 Email * 6.两

  • Java并发 synchronized锁住的内容解析

    synchronized用在方法上锁住的是什么? 锁住的是当前对象的当前方法,会使得其他线程访问该对象的synchronized方法或者代码块阻塞,但并不会阻塞非synchronized方法. 脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过的.注意这里 局部变量是不存在脏读的情况 public class ThreadDomain13 {

  • Java 同步锁(synchronized)详解及实例

    Java 同步锁(synchronized)详解及实例 Java中cpu分给每个线程的时间片是随机的并且在Java中好多都是多个线程共用一个资源,比如火车卖票,火车票是一定的,但卖火车票的窗口到处都有,每个窗口就相当于一个线程,这么多的线程共用所有的火车票这个资源.如果在一个时间点上,两个线程同时使用这个资源,那他们取出的火车票是一样的(座位号一样),这样就会给乘客造成麻烦.比如下面程序: package com.pakage.ThreadAndRunnable; public class Ru

随机推荐