首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >深入Synchronized

深入Synchronized

作者头像
一个架构师
发布2022-06-20 20:02:56
发布2022-06-20 20:02:56
3780
举报

在JAVA的并发编程中Synchronized是最常用的关键字之一.

它一般可以用于修饰代码块, 普通方法以及静态方法.

今天就一起看下它是如何完成锁机制的.

Synchronized代码块处理

以如下方法为例

代码语言:javascript
复制
public  void  test1(){
    synchronized(this){
        System.out.println(1);
    }
}

这是一个非常简单的同步逻辑,我们利用javap命令查看它对应的字节指令是什么样的.

代码语言:javascript
复制
javap -v -p -l -s MyObject.class 

字节指令如下:

代码语言:javascript
复制
public void test1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: iconst_1
         8: invokevirtual #9                  // Method java/io/PrintStream.println:(I)V
        11: aload_1
        12: monitorexit
        13: goto          21
        16: astore_2
        17: aload_1
        18: monitorexit
        19: aload_2
        20: athrow
        21: return

可以发现2个同步相关字节指令monitorenter和monitorexit

monitorenter: 第3个字节指令.

尝试获得锁, 进入同步代码块流程. 后续对象监视器小节中会详细介绍锁获得流程.

monitorexit: 第12和18个字节指令

退出同步代码块指令, 2个字节指令分别表示正常退出释放锁和异步退出释放锁.

Synchronized方法处理

以如下方法为例, 看下同步方法是如何做的.

代码语言:javascript
复制
public synchronized void  test2(){
    System.out.println(1);
}

同样使用用javap命令查看字节指令

代码语言:javascript
复制
javap -v -p -l -s MyObject.class 

指令如下:

代码语言:javascript
复制
  public synchronized void test2();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED

方法级的同步是利用ACC_SYNCHRONIZED标识做的, 当方法调用时, 被设置ACC_SYNCHRONIZED 标识的方法被调用时, 执行线程将先尝试获取monitor, 获取成功之后才能执行方法体, 方法执行完后再释放monitor.

对象监视器(objectMonitor)

上面所提到的计数器, 是不在对象头中的, 而是在对象关联的monitor中.

JVM规范中规定, 每个对象和类都可能有一个关联的对象监视器.

我们先看下对象监视器, 它是由底层C++代码写的, 在HotSpot虚拟机中它的数据结构如下:

代码语言:javascript
复制
objectMonitor() {
  _header       = NULL;
  _count        = 0;
  _waiters      = 0,
  _recursions   = 0;
  _object       = NULL;
  _owner        = NULL;
  _WaitSet      = NULL;
  _WaitSetLock  = 0 ;
  _Responsible  = NULL ;
  _succ         = NULL ;
  _cxq          = NULL ;
  FreeNext      = NULL ;
  _EntryList    = NULL ;
  _SpinFreq     = 0 ;
  _SpinClock    = 0 ;
  OwnerIsThread = 0 ;
  _previous_owner_tid = 0;
}

主要涉及到的变量有5个,

_owner: 指向持有ObjectMonitor对象的线程地址;

_WaitSet: 存放调用wait()方法, 而进入等待状态的线程的队列;

_EntryList: 这里是等待锁block状态的线程的队列;

_recursions: 锁的重入次数;

_count: 线程获取锁的次数;

在多线程执行时, 各部分对应功能如下图

1.如果monitor的所有者(_owner)为null, 则该线程进入monitor, 进入数(_recursions)设置为1, 该线程设置为monitor的所有者(_owner).

2.如果线程已经占有该monitor, 则进入monitor的进入数(_recursions)加1.

3. 如果其他线程已经占用了monitor, 则该线程进入blocked状态, 进入EntryList中, 直到monitor的进入数(_recursions)为0, 再尝试获取monitor的所有权.

4. 当线程调用wait()方法时, 会进入WaitSet中, 并释放相关锁资源.

5. 当其他线程调用notify() 或notifyAll() 方法唤醒时, 会重新尝试获得锁资源.

6. 释放锁时, monitor的进入数(_recursions)减1, 如果此时进入数(_recursions)为0, 那线程退出monitor, 不再是这个monitor的所有者. 其他被阻塞的线程就可以尝试去获取这个monitor的所有权.

对象头: markword

我们了解了monitor相关指令, 也了解了对象监视器(objectMonitor). 那两者又是怎么关联到一起的呢?

之前, 在Java 对象在内存里已经讲过了, 一个对象在内存中的存放结构是什么样的.

这里再详细说下, 与锁密切相关的对象头中的markword部分.

markword在不同的线程竞争情况下, 结构组成以及每个bit所代表的含义是不同的.

在重量级锁的情况下, 前62位指向的就是对象监视器(objectMonitor). 具体结构如下图:

锁升级

无锁

没有线程执行同步方法或代码块时的状态.

偏向锁

对象头中记录线程ID, 下次进入时直接比较线程ID, 相同则直接进入即可.

轻量级锁

在偏向锁的基础上, 有其他线程竞争时, 会升级为轻量级锁. 轻量级锁一般采用自旋锁, 自旋锁又分为固定次数自旋锁和自适应自旋锁.

当线程获取到锁时, 会将对象的markword信息复制到自己线程栈的锁记录空间(DisplacedMarkWord)中. 再次使用会用CAS的方式比较markword信息,判断锁的所有者.

重量级锁

在轻量级锁基础上, 有其他线程以自旋的方式获取锁失败, 则升级为重量级锁, 也就会用到本文介绍的对象监视器(objectMonitor).

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 从码农的全世界路过 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档