多线程刁钻点并发就该这么学并发

09. 小伙子, 听说你对synchronized理解很深?..

2020-07-13  本文已影响0人  码哥说

大佬: 你这段代码有线程安全问题, 解决下!
我: ……

image

大佬: Synchronized加上, 别锁方法, 锁必要的代码块就好, 这样性能高点.

我: 哦, 好的…………(ps, 迅速百度……)

image

前言

Synchronized(也叫同步)是 Java 中解决并发问题的一种最常用的方法.

在 JDK1.5 之前, synchronized 是一个重量级锁, 相对于j.u.c.Lock,显得很笨重.

随着 Javs SE 1.6 对 synchronized 进行的各种优化, synchronized 再次焕发了生机.

下面, 我们一起探索 synchronized 的基本使用及原理

基本使用

前面我们讲过, synchronized 可以保证线程安全.

它的作用主要有三个

确保线程“互斥”的访问同步代码

保证共享变量的修改能够及时可见,
主要通过Java内存模型中的以下特征来保证

对一个变量unlock操作之前,必须要同步到主内存中;
如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,
在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值

解决重排序问题,
即“一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”

从语法上讲,synchronized 可以把任何一个非 null 对象作为"锁",

在 HotSpot JVM (使用最广泛的java虚拟机)实现中,

锁有个专门的名字: 对象监视器(Object Monitor).

synchronized有三种方式来加锁, 分别是:

其中在方法锁 层面可以有如下3种方式:

注意:

synchronized内置锁是一种对象锁(锁的是对象而非引用变量),作用粒度是对象 ,可以用来实现对临界资源的同步互斥访问,是可重入的, 其可重入最大的作用是避免死锁.

如子类同步方法调用了父类同步方法,如没有可重入的特性,则会发生死锁;

synchronized同步原理

数据同步需要依赖锁,那锁的同步又依赖谁?

synchronized 是在软件层面依赖 JVM(基于JVM的内置锁Monitor实现);而 j.u.c.Lock 是在硬件层面依赖特殊的 CPU 指令.

我们都知道, synchronized的使用遵循以下规则:

当一个线程访问同步代码块时,首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须要释放锁,

那么它是如何来实现这个机制的呢?

对象锁: synchronized (this)

我们先看一段简单的代码:

public class Test {
    public void method() {
        synchronized (this) {
            System.out.println("Hello World!");
        }
    }
}

编译

javac Test.java

反汇编

javap -v Test.class
image.png

下面, 我们针对代码的重要指令进行说明

monitorenter

每个对象都是一个监视器锁(monitor).
当 monitor 被占用时就会处于锁定状态,
线程执行 monitorenter 指令时会尝试获取 monitor 的所有权

过程如下:

  • 如果 monitor 的进入数为 0,则该线程进入 monitor,然后将进入数设置为 1,该线程即为 monitor 的所有者;
  • 如果线程已经占有该 monitor,只是重新进入,则进入 monitor 的进入数加 1;
  • 如果其他线程已经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再重新尝试获取 monitor 的所有权;

monitorexit

执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者.
指令执行时,monitor 的进入数减 1,
如果减 1 后进入数为 0,那线程退出 monitor,
不再是这个 monitor 的所有者.
其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权.

细心的朋友会在截图中发现

不对啊, 指令中出现了两次monitorexit?

其实是这样的:

正常情况下, 同步代码块会调用第一个monitorexit释放锁;
如果同步代码块中出现异常,则调用第二个monitorexit来保证释放锁;

通过上面两段描述,我们应该能很清楚的看出 synchronized 的实现原理,

synchronized 的语义底层是通过一个 monitor 的对象来完成.
其实 wait/notify 等方法也依赖于 monitor 对象,
这就是为什么只有在同步的块或者方法中才能调用 wait/notify 等方法,
否则会抛出java.lang.IllegalMonitorStateException的异常的原因.

方法锁 synchronized void method()

再来看一下同步方法

public class Test {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

反汇编

image.png

从编译的结果来看,

方法的同步 并没有通过指令 monitorentermonitorexit 来完成(理论上也可以通过这两条指令来实现),
不过相对于普通方法, 其常量池中多了 ACC_SYNCHRONIZED标示符.

JVM 就是根据该标示符来实现方法的同步的:

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,
如果设置了,执行线程将先获取 monitor,
获取成功之后才能执行方法体,
方法执行完后再释放 monitor.
在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象.

synchronized(this) 和 synchronized void method() 两种同步方式本质上没有区别,

只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成.

monitorentermonitorexit两个指令的执行是 JVM 通过调用操作系统的互斥原语 mutex 来实现的, 被阻塞的线程会被挂起,等待重新调度, 这样会导致“用户态和内核态”来回切换, 对性能有较大影响.

欢迎关注我

技术公众号 “CTO技术”

上一篇 下一篇

猜你喜欢

热点阅读