前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 并发篇-04.synchronized

Java 并发篇-04.synchronized

作者头像
haoming1100
发布2019-07-30 15:54:26
4020
发布2019-07-30 15:54:26
举报
文章被收录于专栏:步履前行

大家好,缓缓来迟的第 5 篇 并发内容,其实准备了好久了,因为想写点不一样的内容,结果导致托了一个月才准备好。在开始正文之前,继续来我们的几个灵魂问题:

  1. 你是否还在经常看知乎学习知识
  2. 是否每天还会关注朋友圈的各种时事政治
  3. 是否还在知识星球继续学习
  4. 密切关注大佬们的言谈举止
  5. 是否仍然执着的自我催眠着利用碎片化时间学习
  6. 是否仍在空闲时间小说、游戏、水群
  7. 请问你多久没有好好阅读过一本书了

今天是第五篇- synchronized 的原理讲解。

首先,在多线程编程中,我们一般处理的就是两个关键问题:

  1. 线程通信
  2. 线程同步

其中线程通信指的是线程之间共享程序的公共状态,通过写 - 读这个公共状态来隐式通信。

线程同步则是不同线程间的操作发生的相对顺序。

而 Java 的并发采用的是 共享内存模型,Java 线程通信总是 隐式 进行的。当然,各种面试问题虽然没有直面的问 内存模型的问题,但是我们知道了这个,对我们了解整个并发模型的帮助是很大的。这里我们不再叙述。

说到 synchronized,大家的第一反应就是 锁 , 它可以修饰类的实例方法,静态方法,和代码块,我们将分别介绍下。


修饰方法

代码语言:javascript
复制
public class CountTest {

    private int count;

    public synchronized void addCount(){
        count ++;
    }

    public synchronized int getCount() {
        return count;
    }

}

这是一个简单的计数器类,其中 addCount 方法和 getCount 方法 分别加了 synchronized 做修饰。我们来看它的 class 文件

代码语言:javascript
复制
 public synchronized void addCount();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field count:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field count:I
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 10

  public synchronized int getCount();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field count:I
         4: ireturn
      LineNumberTable:
        line 10: 0

这里我主要节选了被 synchronized 修饰的两个方法,我们可以看出,每个方法都额外的添加了一个 flags:

ACC_SYNCHRONIZED。这个 flags 在 未修饰的方法中是没有的,如下:

代码语言:javascript
复制
    public void addCount() {
        count++;
    }
--- class 文件 ----
    public void addCount();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field count:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field count:I
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 10

ACC_SYNCHRONIZED:方法是否为 synchronized

也就是说,jvm 是通过看这个 标志来加入同步功能的,

修饰静态方法
代码语言:javascript
复制
public class CountTest {

    private static int count;

    public static synchronized void addCount(){
        count ++;
    }

    public static synchronized int getCount() {
        return count;
    }

}
代码语言:javascript
复制
  public static synchronized void addCount();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field count:I
         3: iconst_1
         4: iadd
         5: putstatic     #2                  // Field count:I
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8

  public static synchronized int getCount();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #2                  // Field count:I
         3: ireturn
      LineNumberTable:
        line 10: 0
}

可以看出,synchronized 修饰静态方法,只是在 flags 加了个 ACC_STATIC 表面是静态的,并没有其他区别。

在 Java 8 的 jvm描述中,有以下内容:

The Java Virtual Machine supports synchronization of both methods and sequences of instructions within a method by a single synchronization construct: the monitor. Method-level synchronization is performed implicitly, as part of method invocation and return (§2.11.8). A synchronized method is distinguished in the run-time constant pool's method_info structure (§4.6) by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.

其中主要表述了,jvm 是通过监视器 monitor 来做同步的,方法级别的同步是隐式执行的,synchronized 方法是在运行时间常量池中的区分 method_info 结构,ACC_SYNCHRONIZED标志 由方法调用指令进行检查。调用方法时 如果 ACC_SYNCHRONIZED 被设置,则执行的线程进入监视器,调用方法本身,然后再退出监视器,在执行线程有监视器期间,没有线程可以进入,如果在调用 synchronized 方法期间抛出异常并且synchronized方法未处理异常,则在从方法重新抛出异常之前,将自动退出该方法的监视器synchronized。 这里的监视器也就是 我们计算机结构中的 - 管程。

synchronized 修饰方法,不管是静态方法还是非静态方法,锁的都是对象,只不过对于实例方法,修饰的是当前实例对象 this,对于静态方法则是修饰的是当前 class 对象,即 CountTest.class。注意:静态和非静态修饰的是不同的对象,不同的两个线程,可以一个执行 静态方法,另一个执行实例方法。

修饰代码块

代码语言:javascript
复制
public class CountTest {

    private int count;

    public synchronized void addCount() {
        synchronized (this) {
            count++;
        }
    }

    public synchronized int getCount() {
        synchronized (this) {
            return count;
        }
    }
}
代码语言:javascript
复制
 public synchronized void addCount();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         ############################  monitor   enter
         3: monitorenter
         4: aload_0
         5: dup
         6: getfield      #2                  // Field count:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field count:I
        14: aload_1
         ############################  monitor   exit
        15: monitorexit
        
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             4    16    19   any
            19    22    19   any
      LineNumberTable:
        line 6: 0
        line 7: 4
        line 8: 14
        line 9: 24
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class CountTest, class java/lang/Object ] 锁对象
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

在修饰代码块的时候,其实就是加了一个监控锁,我们在class 文件中已经标出,即:

代码语言:javascript
复制
3: monitorenter
{其他操作}
15: monitorexit

而锁的对象就是

代码语言:javascript
复制
locals = [ class CountTest, class java/lang/Object ]

也就是我们的 this,所以可以看出,synchronized 修饰 代码块的时候 和修饰方法的差别还是蛮大的。我们下面看下 jvm 关于具体监视器的描述。

监视器

### monitorenter A monitorenter instruction may be used with one or more monitorexit instructions (§monitorexit) to implement a synchronized statement in the Java programming language (§3.14). The monitorenter and monitorexit instructions are not used in the implementation of synchronized methods, although they can be used to provide equivalent locking semantics. Monitor entry on invocation of a synchronized method, and monitor exit on its return, are handled implicitly by the Java Virtual Machine's method invocation and return instructions, as if monitorenter and monitorexit were used.

The synchronization constructs of the Java programming language require support for operations on monitors besides entry and exit. These include waiting on a monitor (Object.wait) and notifying other threads waiting on a monitor (Object.notifyAll and Object.notify). These operations are supported in the standard package java.lang supplied with the Java Virtual Machine. No explicit support for these operations appears in the instruction set of the Java Virtual Machine.

一个monitorenter指令可以与一个或多个被用来 monitorexit指令(§ monitorexit)来实现 synchronizedJava编程语言。该monitorenter和 monitorexit指令不执行使用 synchronized方法,但它们可以被用来提供相当于锁定语义。监视方法进入和返回时的监视器退出由Java虚拟机的方法调用和返回指令隐式处理,就像使用了monitorenter和monitorexit一样。 除了进入和退出之外,Java编程语言的同步构造还需要支持监视器上的操作。这些包括等待监视器(Object.wait)并通知在监视器上等待的其他线程(Object.notifyAll 和Object.notify)。java.lang Java虚拟机随附的标准软件包支持这些操作。Java虚拟机的指令集中不显示对这些操作的明确支持。

The objectref must be of type reference. 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:

  • 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.
  • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
  • 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 被拥有之后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor。步骤如下:

  1. 每个monitor维护着一个记录着拥有次数的计数器。未被拥有的monitor的该计数器为0,当一个线程获得monitor(执行monitorenter)后,该计数器自增变为 1 。
    • 当同一个线程再次获得该monitor的时候,计数器再次自增;(这里就有了重入的概念)
    • 当不同线程想要获得该monitor的时候,就会被阻塞。
  2. 当同一个线程释放 monitor(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。monitor将被释放,其他线程便可以获得monitor。

### monitorexit The objectref must be of type reference. The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. 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.

objectref的类型必须是reference 引用。 执行 monitorexit 的线程必须是与 objectref 引用的实例关联的监视器的所有者 。 与objectref关联的监视器的条目计数减一。如果因此条目计数的值为零,则线程退出监视器并且不再是其所有者。其他线程将继续抢占获取监视器。

这个 monitor 对象 指针是一个 ObjectMonitor 对象,所有线程加入它的 entrylist 里面,去 CAS 抢锁,成功后 更改计数 state,执行完毕后再 state 减 1,只是所有线程并不阻塞。

对象头

synchronized 中的锁是存在对象头中的,Hotspot 虚拟机把对象头分为了 Mark Word(标记字段)、Klass Pointer(类型指针)。 Mark Word 用于存储自身的运行时数据,而 Klass Pointer 则是对象指向它的l类元数据的指针。

如果对象是数组类型,则用 3 个字宽 存储对象头,如果是非数组类型,则用 2 个字宽存储对象头,在 32 为 jvm 中,用 1 字宽代表 4 字节,即 32 bit。

Mark Word 默认存储对象的 HashCode、分代年龄和锁 标记位,其中我们关注的 synchronized 中的锁 就是在这个标记位。

32位 jvm 对象头内存结构

25 bit

4 bit

1 bit 是否偏向锁

2 bit 锁标记位

对象的 hashcode

对象分代年龄

0

01

64 位 jvm 对象头内存结构

25 bit

31 bit

1 bit

4 bit

1 bit是否偏向锁

2 bit 锁标记位

unsed

对象的 hashcode

cms_free

对象分代年龄

0

01

其中的锁标记位如下:

锁状态

1 bit是否偏向锁

2 bit 锁标记位

轻量级锁

0

00

重量级锁

0

10

GC 标记

0

11

偏向锁

1

01

最后

看完了上面的关于 monitor 的介绍,是不是对 synchronized 有了更深入的理解呢,我在这里要提几句注意的地方:

虽然 synchronized 可以锁一个 Class ,但是一旦锁上,那么这个类的其他操作也将进行抢占锁处理,比如我们常常说到的用户转账操作,锁住了 用户.class,就像电影院中的包场,只要与用户有关的操作统统串行。所以我们就要处理细粒度的锁了。

synchronized 之所以是可重入的,官方 doc 已经给了我们解释,锁方法的时候是 jvm 做了处理,锁方法块的时候则是监视器做的处理,其中监视器就是 更改 state 来判断重入的。重入我们将在后面的 显示锁中,给大家明确提出。

synchronized 不要锁字符串对象、Integer对象等,因为jvm 在处理这些对象的时候,是有缓存机制的,指不定你锁的你家的锁,隔壁老王拿的你家钥匙。老老实实 new 一个自己的 object 对象多好。

最后,我对 synchronized的理解是这样,不改变CPU 时间片切换,当其他线程要访问某资源时,发现锁还未释放,所以只能在外面等待。

下篇我们将继续锁的研究。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-07-29 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 修饰方法
    • 修饰静态方法
    • 修饰代码块
    • 监视器
    • 对象头
    • 最后
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档