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

大佬: 你这段代码有线程安全问题, 解决下!
我:      ……   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有三种方式来加锁, 分别是:

  • 方法锁 synchronized void method()

  • 对象锁 synchronized(this)

  • 类锁 synchronized(Demo.Class)

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

  • 修饰**非静态方法时,监视器锁便是对象实例(this)**;

  • 修饰**静态方法时,监视器锁便是对象的 Class 实例**,因为 Class 数据存在于永久代,因此静态方法锁相当于该类的一个全局锁;

  • 修饰某一个**对象实例时, 监视器锁便是括号括起来的对象实例**;

注意:

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 来实现的, 被阻塞的线程会被挂起,等待重新调度, 这样会导致“用户态和内核态”来回切换, 对性能有较大影响.

请关注我的订阅号

订阅号.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码哥说

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值