查看原文
其他

【原创】Java并发编程系列07 | synchronized原理

何适 java进阶架构师 2022-04-29

点击上方“java进阶架构师”,选择右上角“置顶公众号

20大进阶架构专题每日送达

并发编程中用到最多的关键字毫无疑问是synchronized。这篇文章就来探究下synchronized

synchronized如何使用?

synchronized是实现同步加锁的原理?

synchronized解决了并发编程的哪些问题?

1.  synchronized使用

1.1 线程安全问题

并发编程中,当多个线程同时访问同一个资源的时候,就会存在线程安全问题。

由于每个线程执行的过程是不可控的,所以很可能导致最终的结果与实际期望的结果相违背或者直接导致程序出错。

举例:

public classVolatileTest {
public int inc = 0;

public void increase() {
inc++;
}

public static void main(String[] args) {
final VolatileTest test = newVolatileTest();
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
for (int j = 0; j < 1000;j++)
test.increase();
};
}.start();
}

while (Thread.activeCount() > 1)
// 保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}

目的:test.inc = 10000

结果:多次执行得到的结果都小于10000

分析:线程安全问题。

当某个时间test.inc=2,有多个线程同时读取到test.inc=2,并且同时执行加1操作,这些线程的此次操作都执行之后test.inc=3。也就是说执行了多个加1操作,却只将结果增加了1,所以导致最终结果始终小于10000。

基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。
通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。
在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。

Java中用synchronized标记同步块。

  • 同步块在Java中是同步在某个对象上(监视器对象)。

  • 所有同步在一个对象上的同步块在同一时间只能被一个线程进入并执行操作。

  • 所有其他等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出。

1.2 synchronized用法

  • 普通同步方法,锁是当前实例对象

  • 静态同步方法,锁是当前类的class对象

  • 同步方法块,锁是括号里面的对象

举例:

public class MyClass{
int count;

// 1.实例方法
public synchronized void add(int value){
count += value;
}

// 2.实例方法中的同步块 (等价于1)
public void add(int value){
synchronized(this){
count += value;
}
}

// 3.静态方法
public static synchronized void add(intvalue){
count += value;
}

// 4.静态方法中的同步块 (等价于3)
public static void add(int value){
synchronized(MyClass.class){
count += value;
}
}
}

2. 原理探究

如下代码,利用javap工具查看生成的class文件信息来分析Synchronize的实现。

代码:

public class synchronized Test {
// 同步代码块
public void doSth1(){
synchronized (synchronizedTest.class){
System.out.println("HelloWorld");
}
}
// 同步方法
public synchronized void doSth2(){
System.out.println("HelloWorld");
}
}

使用javap对class文件进行反编译后结果:

javap命令:D:\install\java\jdk8\bin\javap.exe -v .\synchronizedTest.class
同步代码块
同步方法

从反编译后的结果中可以看到:对于同步方法,JVM采用ACC_synchronized标记符来实现同步。对于同步代码块。JVM采用monitorenter、monitorexit两个指令来实现同步。

同步代码块

JVM采用monitorenter、monitorexit两个指令来实现同步。
查询JVM规范The Java® Virtual Machine Specification[1]中关于monitorenter和monitorexit的介绍:

大致内容如下:

  1. 可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。

  2. 每个对象维护着一个记录着被锁次数的计数器。

  3. 未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为1,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。

  4. 当计数器为0的时候。锁将被释放,其他线程便可以获得锁。

同步方法

JVM采用ACC_synchronized标记符来实现同步。
查询JVM规范The Java® Virtual Machine Specification[2]中关于方法级同步的介绍:

大致内容如下:

  1. 方法级的同步是隐式的。同步方法的常量池中会有一个ACC_synchronized标志。

  2. 当某个线程要访问某个方法的时候,会检查是否有ACC_synchronized,如果有设置,则需要先获得监视器锁(monitor),然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。

  3. 值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

3. Monitor

无论是同步方法还是同步代码块都是基于监视器Monitor实现的。

Monitor是什么?

所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。

每个对象都存在着一个Monitor与之关联,对象与其Monitor之间的关系有存在多种实现方式,如Monitor可以与对象一起创建销毁。

Moniter如何实现线程的同步?

在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)。

ObjectMonitor中有几个关键属性:

_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于wait状态的线程队列
_EntryList:存放处于等待锁block状态的线程队列
_recursions:锁的重入次数
_count:用来记录该线程获取锁的次数
  • 线程T等待对象锁:_EntryList中加入T。

  • 线程T获取对象锁:_EntryList移除T,_owner置为T,计数器_count加1。

  • 线程T中锁对象调用wait():_owner置为null,计数器_count减1,_WaitSet中加入T等待被唤醒。

  • 持有对象锁的线程T执行完毕:复位变量的值,以便其他线程进入获取monitor。

4. 解决三大问题

保证原子性

在并发编程中的原子性:一段代码,或者一个变量的操作,在一个线程没有执行完之前,不能被其他线程执行。

synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。

即使在执行过程中,CPU时间片用完,线程放弃了CPU,但并没有进行解锁。而由于synchronized的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会由同一个线程继续执行代码,直到所有代码执行完。从而保证synchronized修饰的代码块在同一时间只能被一个线程访问。

保证有序性

如果在本线程内观察,所有操作都是天然有序的。
——《深入理解Java虚拟机》

单线程重排序要遵守as-if-serial语义,不管怎么重排序,单线程程序的执行结果都不能被改变。因为不会改变执行结果,所以无须关心这种重排的干扰,可以认为单线程程序是按照顺序执行的。

synchronized修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性。

保证可见性

加锁的含义不仅仅局限于互斥行为,还包括可见性。
——《Java并发编程实战》

JMM关于synchronized的两条语义规定保证了可见性:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中。

  • 线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。

5. 总结

多并发编程中通过同步互斥访问临界资源来解决线程安全问题,Java中常用synchronized标记同步块达到加锁的目的。

synchronized用法有两种,修饰方法和修饰同步代码块。

synchronized的实现原理:每一个Java对象都会关联一个Monitor,通过Monitor对线程的操作实现synchronized对象锁。

并发编程中synchronized可以保证原子性、可见性、有序性。

参考资料

[1]

The Java® Virtual Machine Specification: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter

[2]

The Java® Virtual Machine Specification: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10

并发系列文章汇总(点击标题可跳转)

【原创】Java并发编程系列01 | 开篇获奖感言
【原创】Java并发编程系列02 | 并发编程三大核心问题
【原创】Java并发编程系列03 | 重排序-可见性和有序性问题根源
【原创】Java并发编程系列04 | Java内存模型详解
【原创】Java并发编程系列05 | 深入理解volatile

【原创】Java并发编程系列06 | 你不知道的final

————  e n d ————

微服务、高并发、JVM调优、面试专栏等20大进阶架构师专题请关注公众号Java进阶架构师后在菜单栏查看

回复【架构】领取架构师视频一套。

原创从来不开赞赏是因为我觉得

你的“在看”,就是给我最好的赞赏^_^

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存