Java synchronized之类锁/对象锁

前言

    之前看到过一篇文章,写的就是Java关键字synchronized的类锁和对象锁,今天想重温一下无奈发现文章已经搜索不到,百度之大部分都是重复的那么几篇文章,甚至也仅仅讲了对象锁没有讲解类锁。于是重写一篇博客介绍 synchronized 类锁 对象锁。 

Java原生提供了 synchronized 关键字用于多线程编程,但往往入门使用者在发现使用情况与预期有差别,可阅读此文章。

Java的 synchronized 锁的是对象,也只锁对象: 对象锁是基于对堆内存内对象的头部加锁信息; 类锁是基于对类对应的 java.lang.Class对象加锁信息; 特别的, synchronized(this) 是对this所对应的对象加锁。

Java 提供 synchronized 关键字,在语言层面上做出支持。JDK实现上还有很多其它的实现,例如: ReentrantLock

synchronized的锁粒度介绍

synchronized 的用法举例

synchronized 的锁可作用于Java方法,或者是一个代码块。无论何种用法,所起到的作用仅限于 类锁/对象锁

类锁的场景举例

① 当synchronized 修饰一个使用了 static 关键字修饰的方法时:

public static synchronized void staticA();

② 当synchronized 修饰一个 class 的方法块时:

synchronized(Object.class) {
    // do something
}

对象锁的场景举例

① 当synchronized 修饰一个不使用了 static 关键字修饰的方法时:

public synchronized void noneStaticA();

② 当synchronized 修饰关键字 this 的方法块时:

synchronized(this) {
    // do something
}

③ 当synchronized 修饰一个对象 xxx 的方法块时:

synchronized(xxx) {
    // do something
}

类锁和对象锁在使用方法上的区别

当使用了对象锁之后,除了获得当前对象的对象锁的线程,其它线程对当前对象的所有使用对象锁的语句的访问受到阻塞,但是对非使用对象锁的语句的访问不受影响

当使用了类锁之后,除了当前线程外,其它线程对当前类的所有 类方法的访问受到阻塞,其它的静态方法(没有使用类锁的静态方法)的访问不受影响

特别的,可以使用 synchronized(xxx) 代码块 语法将一个无用的对象xxx作为一把锁。 这个时候的"对象锁"是针对于xxx对象的内部而言, 对于使用对象xxx作为锁的方法块来说,不管是使用的类锁还是对象锁都互不影响。

类锁和对象锁作用域不同,两者互不影响。

类锁和对象锁的Q&A

下述问题都是针对于此段伪代码片段进行:

class Sync {
    public synchronized void noneStaticA();
    public synchronized void noneStaticB();
    public void noneStaticC();

    public static synchronized void staticA();
    public static synchronized void staticB();
    public static void staticC();
}    

Q1: 如代码片段所示,多线程环境中对象 Sync x 和 对象 Sync y 哪些语句可以同时执行:
A. x.noneStaticA() 和 x.noneStaticA();
B. x.noneStaticA() 和 x.noneStaticB();
C. x.noneStaticA() 和 y.noneStaticA();
D. x.noneStaticA() 和 x.noneStaticC();

A1: C、D.
解析: 如上皆为对象锁,单个对象内所有对象锁互互斥。而对象锁的粒度为单个对象, x对象的对象锁不影响y对象的对象锁。对象锁仅针对使用了对象锁的语句生效。


Q2: 如代码片段所示,多线程环境中对象 Sync x类 Sync 哪些语句可以同时执行:
A. x.noneStaticA() 和 Sync.staticA();
B. x.noneStaticA() 和 Sync.staticC();
C. x.noneStaticC() 和 Sync.staticA();
D. Sync.staticA() 和 Sync.staticA();
E. Sync.staticA() 和 Sync.staticB();

A2: A、B、C
解析:类锁与对象锁作用粒度不一,互不影响。对象锁与类静态方法之间无锁冲突。类锁与对象方法也没有锁冲突。类锁的作用域为这个类所有的类锁。


Q3:对于对象 Sync x 和 对象 Sync y哪些语句可以同时执行?

// 在类 Sync 里面增加如下代码:
public void foo1() {
    synchronized(this) {
        // ...
    }
}

// 另一个类 SyncExecute 的如此写法:
public void foo2() {
	// ...
	synchronized(x) {
         // ...
    }
	// ...
}

public void foo3() {
	// ...
	synchronized(Sync.class) {
         // ...
    }
	// ...
}

A3: 上述是一个复杂环境, 已知对象锁与类锁之间互不影响, 因此单独分析对象锁和类锁即可。

x.noneStaticA() 与 x.noneStaticB() 与 x.foo1() 三者使用的都是对象锁, 对同一对象 x而言是互斥的,而在SyncExecute 中的 foo2() 中也使用了对对象x的对象锁,因此 foo2() 与x里面的三个使用了对象锁的方法都是互斥的。(但是当使用的Sync的对象不是x而是其它的例如y/z的时候,他们之间完全是可以正常运行的)。

Sync.staticA() 与 Sync.staticB() 与 SyncExecute 中的 foo3() 都是使用的类锁,因此无论 new 了多少个 Sync对象他们都是互斥的,会竞争类锁。

synchronized对于类锁和对象锁的解析

对象锁互斥原理

可优先阅读该篇文章田守枝:2.1 对象的内存布局

一个被JVM创建的对象存在于JVM中,不仅仅包含了对象的实例数据,还包含对象头(Header)对齐填充(Padding)。 其中对象头(Header)里面就包含了当前对象是否有锁的信息。

如果对磁盘文件系统了解的同学就会知道,磁盘上储存的文件数据依赖于文件系统,不同的文件系统对于文件的存储数据结构可能不一样,但是大都包含如下特点:文件数据块单独储存,其它内容(如文件名、所有权信息、创建时间等)储存位置是与数据块逻辑隔离的。

因此,不管是 synchronized修饰的实例方法,还是synchronized代码块修饰的this关键字, 还是synchronized修饰的一个具体的对象,语言层面访问该对象时都会检查头部的锁信息,发现有锁了之后就会开始互斥逻辑。

同时,这也解释了为什么不同对象的对象锁之间为何互不影响: 因为对象锁的原理是基于单个对象的头部的锁信息。

synchronized 在锁的实现上相对复杂,存在着不同锁类型的切换升级。如有有兴趣可以阅读这篇文章:http://www.jianshu.com/p/5dbb07c8d5d5 。(*至于synchronized在锁的抢占上目前暂未发现一篇详细介绍的文章。例如 ReentrantLock是基于Java关键字volatile和CPU的CAS机制来实现的,若有知晓可在留言区告知一二 *)

类锁原理及为何类锁完全互斥

可优先阅读该篇文章图解Java类加载机制

想获得一个Java的对象,则需要先获得Java的一个类,这便是Java的类加载。类加载完毕之后的类代码储存在JVM的一块单独区域。一个类可能被加载或者卸载多次,但是任意一个时刻JVM里面只存在一个类的数据区域。阅读谈谈Java虚拟机——Class文件结构知晓这篇数据区域的数据结构

同时,JVM在装载完毕一个类的时候,还会给该类生成一个 java.lang.Class 的对象,由 类数据区里面的该类的this_class字段指向这个Class对象。 从而,类锁的实现原理可以转化为对象锁的原理 — 在对应的Class对象上加对象锁即可

synchronized 的内存可见性

需要特别注意的事是, 根据JMM的规范, synchronized 块里面的对象, 具有内存可见性。即:

  1. A线程在释放synchronized锁之前,会将线程内存中的共享变量回写回主存。
  2. B线程在获取synchronized锁之后,会清空线程内部涉及到的共享变量, 再由主存中读取。

参考文章: https://www.ibm.com/developerworks/library/j-jtp03304/index.html
When a thread exits a synchronized block as part of releasing the associated monitor, the JMM requires that the local processor cache be flushed to main memory.

synchronized 的各类锁实现和优化

synchronized 锁实现

如上文, synchronized 在对象上打标记。 而从源码角度呢?
反编译字节码可以发现:

javap -v -p -s -sysinfo -constants XXXX.class

它依赖的是monitorentermonitorexit指令, 他们在JVM里的实现:

  • monitorenter(参考:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter)
    • Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
    • 每个对象有一个监视器锁(monitor)。当monitor被占用时就处于锁定状态,线程执行monitorcenter指令时尝试获取monitor的所有权
    • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
    • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
    • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
    • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
    • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.
    • 如果其他线程已经占用monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
  • monitorexit(参考:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit)
    • The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
    • 执行monitorexit的线程必须是object所对应的monitor的所有者。
    • The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
    • monitor的退出数减1,如果减1后进入数为0,则线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

对于加锁在方法上, 即对象锁,Java 字节码用一个ACCSYNCHRONIZED指令进行标记:
(参考:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.8 2.11.10 synchronized章节)
实质上与synchronized(class)一致,在语义底层都是通过monitorenter、monitorexit指令实现对monitor对象的获取和释放。

有没有觉得这个逻辑和 Java中的公平锁和非公平锁实现详解 中, ReentrantLock的可从重入性原理很像?

上文是也是可重入的原理。那这指令又是如何实现的呢?在这些上面, 分别衍生出了偏向锁、轻量级锁、重量级锁。

锁名称大致场景使用方式
偏向锁没有锁竞争时直接对比对象头Mark Word线程Id信息
轻量级锁已有线程持有对象锁,且为偏向锁。持有线程释放锁,两个线程转化为轻量级锁,原持有者获取锁竞争者自旋,多次自旋中尝试获取锁
重量级锁自旋完毕尚未获得锁,或者被另一竞争线程占据挂起线程,等待通知

偏向锁

当一个synchronized块或者对象锁方法执行时,不存在锁竞争,则获取偏向锁。

  • 无锁竞争时,A线程获得锁,同时锁对象的对象头Mark Word里存储A线程的线程id。后续的重入则通过判断id进行。
  • 偏向锁的释放,要么有线程竞争、要么代码块执行完毕。
  • 偏向锁的释放,如果不存在竞争,将对象头设置成无锁状态(代码块执行完毕自动释放/线程完毕);
    • 如果存在竞争,释放对象头Mark Word锁信息;所有竞争线程转轻量级锁; 之前拥有偏向锁的栈会被执行。

轻量级锁

在偏向锁升级为轻量级锁后。 竞争失败的锁可能采用自旋的方式, 在N次自旋中尝试获取锁,此时所有的竞争线程都平等。因此synchronized是非公平锁。

  • 锁竞争的情况下,竞争的线程都会复制锁对象的Mark Word信息。
  • A线程获得轻量级锁,会在A线程的栈帧里创建lock record,让lock record的指针指向锁对象的对象头中的Mark Word, 同时让Mark Word 指向lock record
  • 同样通过对比指针信息, 来实现锁的重入。
  • 轻量级锁,在于锁竞争失败的线程,首先不进入内核态,而是采用自旋,空循环的方式等待A线程释放锁。
  • 当完成自旋策略还是发现没有释放锁,或者让其他线程占用了。则轻量级锁升级为重量级锁。

重量级锁

重量级锁耗费资源, 在于线程的挂起和用户态和内核态的切换。重量级锁处理逻辑也是一个抢占、挂起、唤醒的过程。
参考monitorentermonitorexit在JVM中的实现。点我下载, 点我线上浏览

在自旋获取锁失败时, 尝试将自己挂起:

        // park self
        if (_Responsible == Self || (SyncFlags & 1)) {
            TEVENT (Inflated enter - park TIMED) ;
            Self->_ParkEvent->park ((jlong) RecheckInterval) ;
            // Increase the RecheckInterval, but clamp the value.
            RecheckInterval *= 8 ;
            if (RecheckInterval > 1000) RecheckInterval = 1000 ;
        } else {
            TEVENT (Inflated enter - park UNTIMED) ;
            Self->_ParkEvent->park() ;
        }

而挂起以及之后的唤醒, 则涉及到用户态和内核态的切换、数据的暂存等操作。将导致大量的资源消耗。

锁实现流程源码解析

主要参考这三份源码

  • src/share/vm/interpreter/interpreterRuntime.cpp
    • InterpreterRuntime::monitorenter
    • InterpreterRuntime::monitorexit
  • src/share/vm/runtime/synchronizer.cpp
  • src/share/vm/runtime/objectMonitor.cpp

首先进入monitorenter指令(代码中有中文注释):

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  if (PrintBiasedLockingStatistics) {
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
  // 在Java命令中控制是否启用偏向锁。+UseBiasedLocking使用, -UseBiasedLocking禁用
  if (UseBiasedLocking) {
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    // 未使用偏向锁的实现
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
         "must be NULL or an object");
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

然后偏向锁的实现(代码中有中文注释):
里面的获取和释放代码较长,归纳如下:

获取偏向锁(BiasedLocking::revoke_and_rebias):

  1. 获取 Mark Word
  2. 判断 Mark Word 是否为可偏向状态,偏向锁标志位为 1,锁标志位为 01;
  3. 判断 Mark Word 中JavaThread的状态:如果指向当前线程,重入;
  4. Mark WordJavaThread为空, 通过CAS抢占, 成功即获取偏向锁。
  5. 如果CAS抢占失败,或者最初判断的时候JavaThread不为空,获得偏向锁的线程被挂起,撤销偏向锁,并升级为轻量级,原拥有者优先;
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
 if (UseBiasedLocking) {
    if (!SafepointSynchronize::is_at_safepoint()) {
      // 获取偏向锁
      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
        return;
      }
    } else {
      assert(!attempt_rebias, "can not rebias toward VM thread");
      BiasedLocking::revoke_at_safepoint(obj);
    }
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
 }
// 轻量级锁
 slow_enter (obj, lock, THREAD) ;
}

然后轻量级锁的实现(代码中有中文注释):

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  // 这就是上文说到的 Mark Word
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");
  // 尚未加锁
  if (mark->is_neutral()) {
    // Anticipate successful CAS -- the ST of the displaced mark must
    // be visible <= the ST performed by the CAS.
    // 保存锁对象的 Mark Word 到线程的锁记录中。
    lock->set_displaced_header(mark);
    // 尝试用CAS在 Mark Word 里面记录下当前线程的信息。 
    // 不同于偏向锁的是, 偏向锁是让Mark Word记录偏向锁,偏向标记1, 同时CAS记录线程信息。  锁标记为偏向锁01
    // 轻量级锁Mark Word中包含HashCode/分代年龄/偏向标记 等这骗区域, 将直接指向 线程的锁记录。  锁标记为轻量级锁00
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
    // Fall through to inflate() ...
  } else
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    // 轻量级锁的重入的实现, _displaced_header 记录的 锁对象的 Mark Word 信息, 将其清除。
    // 没看懂置空是几个意思, 既然此处要置空, 那获取轻量级锁的时候塞这个值的意义就不明。
    lock->set_displaced_header(NULL);
    return;
  }

#if 0
  // 无用的代码
#endif

  // The object header will never be displaced to this lock,
  // so it does not matter what the value is, except that it
  // must be non-zero to avoid looking like a re-entrant lock,
  // and must not look locked either.
  // 锁再升级, 升级为重量级锁。 这个设置的值是临时值, 表示正在升级为重量级锁。
  lock->set_displaced_header(markOopDesc::unused_mark());
  // 先调用inflate膨胀为重量级锁, 使用ObjectMonitor对象enter方法实现。
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}

然后重量级锁的实现:
代码很长, 这儿介绍很详细, 直接看 死磕Synchronized底层实现–重量级锁, 以及深入分析synchronized的JVM实现

如下摘录原文。

膨胀
inflate中是一个for循环,主要是为了处理多线程同时调用inflate的情况。然后会根据锁对象的状态进行不同的处理:

  1. 已经是重量级状态,说明膨胀已经完成,直接返回
  2. 如果是轻量级锁则需要进行膨胀操作
  3. 如果是膨胀中状态,则进行忙等待
  4. 如果是无锁状态则需要进行膨胀操作

其中轻量级锁和无锁状态需要进行膨胀操作,轻量级锁膨胀流程如下:

  1. 调用omAlloc分配一个ObjectMonitor对象(以下简称monitor),在omAlloc方法中会先从线程私有的monitor集合omFreeList中分配对象,如果omFreeList中已经没有monitor对象,则从JVM全局的gFreeList中分配一批monitor到omFreeList中。
  2. 初始化monitor对象
  3. 将状态设置为膨胀中(INFLATING)状态
  4. 设置monitor的header字段为displaced mark word,owner字段为Lock Record,obj字段为锁对象
  5. 设置锁对象头的mark word为重量级锁状态,指向第一步分配的monitor对象

无锁状态下的膨胀流程如下:

  1. 调用omAlloc分配一个ObjectMonitor对象(以下简称monitor)
  2. 初始化monitor对象
  3. 设置monitor的header字段为 mark word,owner字段为null,obj字段为锁对象
  4. 设置锁对象头的mark word为重量级锁状态,指向第一步分配的monitor对象.

如上摘录。
在重量级锁中, 重入的实现则是_recursions 的累增, 原理与 ReentrantLock基本一致。 对比方式也是线程对象的对比。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值