Java synchronized 使用与原理

Synchronized 是 Java 中解决并发问题的一种最常用的方法。Synchronized 的作用主要有三个:1. 确保线程互斥的访问同步代码;2. 保证共享变量的修改能够及时可见;3. 有效解决重排序问题。

synchronize 使用

synchronized 可以修饰的对象有四种:

  1. 修饰代码块,作用于调用的对象;
  2. 修饰方法,作用于调用的对象;
  3. 修饰静态方法,作用于所有对象;
  4. 修饰类,作用于所有对象。

被该关键字修饰的域,同一时刻只能有一个线程进行操作。

synchronize 底层原理:

synchronized 关键字编译后会在同步块的前后添加上 montorentermonitorexit 两个字节码指令,这两个字节码指令都需要一个指向锁定和解锁对象的 reference,如果指定了同步的对象就指向这个对象,如果修饰的是是类方法就指向Class对象,如果是实例方法就指向这个实例。

Java 对象头

在 JVM 中,对象在内存中的布局分为三块区域:对象头实例变量填充数据

  • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

  • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

  • 对象头:Hotspot 主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中 Klass Point 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word 用于存储对象自身的运行时数据(hashCode、锁信息或分代年龄或GC标志等信息),它是实现轻量级锁和偏向锁的关键。

Monitor

可以把它理解为一个同步工具,也可以描述为一种同步机制,通常被描述为一个对象。所有的 Java 对象是天生的 Monitor,每一个 Java 对象都有一把看不见的锁,它叫做内部锁或者 Monitor 锁。

Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个 monitor 关联(对象头的 MarkWord 中的 LockWord 指向 monitor 的起始地址),同时 monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;      // 计数器
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;   // 持有者
    _WaitSet      = NULL;   // wait 队列
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // block 队列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

在 HotSpot JVM 中,monitor 由 ObjectMonitor 实现(如上所示),有两个队列,_WaitSet(处于 wait 状态的线程)和 _EntryList(处于等待锁 block 状态的线程),用来保存 ObjectWaiter 对象列表( 每个等待锁的线程都会被封装成 ObjectWaiter 对象),_owner 指向持有 ObjectMonitor 对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList,当线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor owner 变量设置为当前线程,同时 monitor 中的计数器加1。若线程调用 wait() 方法,将释放当前持有的 monitor,owner 恢复为null,计数器减1,同时该线程进入 _WaitSet 等待被唤醒。

monitor 对象存在于每个 Java 对象的对象头中(指针),synchronized 便是通过这种方式获取锁的,也是为什么 Java 中任意对象可以作为锁的原因,同时也是 notify/notifyAll/wait 等方法存在于顶级对象 Object 中的原因。

synchronized 字节码分析

对 synchronized 修饰的代码部分进行反编译(javap),观察其字节码

synchronized 修饰代码块

synchronized (this) {
    i++;
}
3: monitorenter     // 进入同步方法
// ...
15: monitorexit     // 退出同步方法
16: goto          24
// ...
21: monitorexit     // 退出同步方法
// ...
24: return

同步语句块的实现使用的是 monitorentermonitorexit 指令,monitorenter 指向同步代码块的开始位置,monitorexit 指明同步代码块的结束位置。

当执行 monitorenter 指令时,当前线程将试图获取 对象锁对应的 monitor 的持有权(当 monitor 计数器为0,那线程可以成功取得 monitor,并将计数器值设置为 1)

如果当前线程已经拥有 monitor 的持有权,那它可以重入这个 monitor,重入时计数器也会加 1。

如果其他线程已经拥有 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕(monitorexit 指令被执行,执行线程将释放 monitor,并设置计数器值为0),才有机会持有 monitor 。

synchronized 修饰方法

public synchronized void syncTask(){
   i++;
}
public synchronized void syncTask();
    descriptor: ()V
    // ACC_SYNCHRONIZED 指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
        // ...
        10: return

方法级的同步是隐式,无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法中的 ACC_SYNCHRONIZED 标志区分一个方法是否同步方法。

当同步方法被调用时,执行线程将先持有 monitor, 然后再执行方法,最后再方法完成(正常或非正常完成)时释放 monitor。在方法执行期间,执行线程持有了 monitor,其他任何线程都无法再获得同一个monitor。

其他特性

可重入性

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功。

Java 中 synchronized 是基于原子性的内部锁机制,是可重入的,因此在一个线程调用 synchronized 方法的同时在其方法体内部调用该对象另一个 synchronized 方法,是允许的。

线程中断

当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,使用 Thread.interrupt() 可以中断该线程,此时将会抛出一个 InterruptedException 异常。但是非阻塞状态下的线程无法中断。

对于 synchronized 来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,中断线程的方法不会生效。

内存可见性

synchronized 不仅仅实现互斥行为,同时还有另外一个重要的作用:内存可见性。不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且还希望确保当一个线程修改了对象状态后,其他线程能够看到该变化。而线程的同步恰恰也能够实现这一点。


References:

https://blog.csdn.net/javazejian/article/details/72828483

https://www.cnblogs.com/mingyao123/p/7424911.html

Add a Comment

电子邮件地址不会被公开。 必填项已用*标注

6 + 14 =