深入Java虚拟机(六)线程同步
可以在语言级支持多线程是Java语言的一大优势,这种支持主要集中在同步上,或调节多个线程间的活动和共享数据。Java所使用的同步是监视器
。
监视器Monitor
Java中的监视器支持两种线程:互斥和协作
- 虚拟机通过对象锁来实现互斥,允许多个线程在同一个共享数据上独立而不干扰地工作
- 协作则是通过
Object
类的wait
方法和notify
方法来实现,允许多个线程为了同一个目标而共同工作
我们可以把监视器
比作一个建筑,它有一个很特别的房间,房间里有一些数据,而且在同一时间只能被一个线程占据。一个线程从进入这个房间到它离开之前,它可以独占地访问房间中的全部数据。
我们用一些术语来定义这一系列动作:
- 进入建筑叫做
进入监视器
- 进入建筑中的那个特别的房间叫做
获得监视器
- 占据房间叫做
持有监视器
- 离开房间叫做
释放监视器
- 离开建筑叫做
退出监视器
除了与一些数据关联外,监视器还是关联到一些或更多的代码,这样的代码称作监视区域
,对于一个监视器来说,监视区域
是最小的、不可分割的代码块。而监视器
会保证在监视区域
上同一时间只会执行一个线程。一个线程想要进入监视器
的唯一途径就是到达该监视器
所关联的一个监视区域
的开始处,而线程想要继续执行监视区域
的唯一途径就是获得该监视器
。
监视器下的互斥
当一个线程到达了一个监视区域的开始处,它就会被放置到该监视器的入口区。如果没有其他线程在入口区等待,也没有线程持有该监视器,则这个线程就可以获得监视器,并继续执行监视区域中的代码。当这个线程执行完监视区域后,它就会退出(并释放)该监视器。
如果一个线程到达了一个一个监视区域的开始处,犯这个监视区域已经有线程持有该监视器了,则这个刚刚到达的线程必须在入口区等待。当监视器的持有者退出监视器后,新到达的线程必须与其它已经在入口区等待的线程进行一次比赛,最终只会有一个线程获得监视器。
监视器下的协作
当一个线程需要一些特别状态的数据,而由另一个线程负责改变这些数据的状态时,同步就显得特别重要。
举例:一个读线程
会从缓冲区
读取数据,而另一个写线程
会向缓冲区
填充数据。读线程
需要缓冲区
处于一个非空的状态,这样才可以从中读取数据,如果读线程
发现缓冲区
是空的,它就必须等待。写线程
负责向缓冲区
写数据,只有写线程
写入完成,读线程
才能相应的读取。
Java虚拟机使用的这种监视器被称作等待-唤醒
监视器。在这种监视器中,在一个线程(方便区分,叫线程A
)持有监视器的情况下,可以通过执行一个等待命令
,暂停自身的执行。
当线程A
执行了等待命令
后,它就会释放监视器,并进入一个等待区,这个线程A
会一直持续暂停状态,直到一段时间后,这个监视器中的其他线程执行了唤醒命令
。
当一个线程(线程B
)执行了唤醒命令
后,它会继续持有监视器,直到他主动释放监视器(执行完监视区域或执行一个等待命令)。当执行唤醒的线程(线程B
)释放了监视器后,等待线程(线程A
)会苏醒,并重新获得监视器。
等待-唤醒
监视器有时也被称作发信号并继续
(这个翻译没谁了。。。。)监视器,究其原因,就是在一个线程执行唤醒操作后,它还会继续持有监视器并继续执行监视区域,过了段时间之后,唤醒线程释放监视器,等待线程才会苏醒。
所以一次唤醒往往会被等待线程
看作是一次提醒,告诉它“数据已经是你想要的状态了”。当等待线程
苏醒后,它需要再次检查状态,以确认是否可以继续完成工作,如果数据不是它所需要的状态,等待线程
可能会再次执行等待命令或者放弃等待退出监视器
。
还是上面的例子:一个读线程
、一个缓冲区
、一个写线程
。假定缓冲区
是由某个监视器
所保护的,当读线程
进入这个监视器
时,它会检查缓冲区
是否为空:
- 如果不为空,读线程会从中取出一些数据,然后退出监视器。
- 如果是空的,读线程会执行一个等待命令,同时它会暂停执行并进入
等待区
。
这样读线程释放了监视器,让其他线程有机会可以进入。稍后,写线程进入了监视器,向缓冲区写入了一些数据,然后执行唤醒,并退出监视器。当写线程执行了唤醒指令后,读线程被标志为可能苏醒,当写线程退出监视器后,读线程被唤醒并成为监视器的持有者。
监视器模型
Java虚拟机中的监视器模型分成了三个区域。如下图:
虚拟机将监视器分为三个区域:
- 中间大的
监视区域
只允许一个单独的线程,是监视器的持有者; - 左边是
入口区
- 右边是
等待区
等待线程和活动线程使用红色和蓝色区分。
模型中也规定了线程和监视器交互所必须通过的几道门:
- 当一个线程到达监视区域的开始处时,它会从最左边
1号箭头
进入入口区
,当进入入口区
后- 如果没有任何线程持有监视器,也没有任何等待的线程,这个线程就可以通过
2号箭头
,并持有监视器。作为监视器的持有者,它可以继续执行监视区域
中的代码。 - 如果已经有另一个线程正在持有监视器,这个新到达的线程必须在入口区等待,很可能已经有线程已经在等待了,并且这个新线程会被阻塞,不能执行监视区域中的代码。
- 如果没有任何线程持有监视器,也没有任何等待的线程,这个线程就可以通过
- 上图中有三个线程在
等待区
中,这些线程会一直在那里,直到监视区域
中的活动线程
释放监视器 活动线程
会通过两条途径释放监视器:- 如果
活动线程
执行完了监视区域
的代码,它会从5号箭头
退出监视器。 - 如果
活动线程
执行了等待命令,它会通过3号箭头
进入等待区,并释放监视器
- 如果
- 如果
活动线程
在释放监视器前没有执行唤醒命令(同时在此之前没有任何等待区的线程被唤醒并等待苏醒),那么位于入口区的线程们将会竞争获得监视器。 - 如果
活动线程
在释放监视器前执行了唤醒命令,入口区
的线程就不得不和等待区
的线程一起来竞争:- 如果
入口区
的线程获胜,它就会通过2号箭头
进入监视区域
,并获得监视器
- 如果
等待区
的线程获胜,它会通过4号箭头
退出等待区并重新获得监视器。
- 如果
请注意,一个线程只有通过3号箭头
和4号箭头
才能进入或退出等待区。并且一个线程只有在它持有监视器的时候才能执行等待命令,而且它只能通过再次成为监视器的持有者才能离开等待区。
线程唤醒的一些细节
在Java虚拟机中,线程在执行等待命令时可以随意指定一个暂停之间。在暂停时间到了之后,即使没有来自其他线程的明确的唤醒命令,它也会自动苏醒。看下面这段代码:
public class MonitorTest {public static void main(String[] args) {byte[] buffer = new byte[4];MonitorObj monitorObj = new MonitorObj();Thread read00 = new Thread() {@Overridepublic void run() {System.out.println("read00 准备获取锁");synchronized (monitorObj) {System.out.println("read00 = " + buffer[3]);try {Thread.sleep(1000);monitorObj.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("read00 = " + buffer[3]);}System.out.println("read00 释放锁");}};Thread read01 = new Thread() {@Overridepublic void run() {System.out.println("read01 准备获取锁");synchronized (monitorObj) {System.out.println("read01 = " + buffer[3]);try {Thread.sleep(1000);monitorObj.wait(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("read01 = " + buffer[3]);}System.out.println("read01 释放锁");}};Thread write = new Thread() {@Overridepublic void run() {System.out.println("write 准备获取锁");synchronized (monitorObj) {try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}buffer[3] = 99;//monitorObj.notifyAll();try {Thread.sleep(3000);System.out.println("write thread finish");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}};read00.start();read01.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}write.start();}
}
class MonitorObj {}
请注意,read01
线程使用的是wait(2000)
方法;read00
线程使用的是wait()
方法。
然后,我们把write
线程的monitorObj.notifyAll()
唤醒方法注释掉,输出如下:
read00 准备获取锁
read01 准备获取锁
read00 = 0
read01 = 0
write 准备获取锁
write thread finish
read01 = 99
read01 释放锁
因为wait(2000)
加了暂停时间的原因,read01
还是自动唤醒了。而对于read00
仍然并且会一直处于等待,除非调用唤醒指令notify()
或notifyAll()
。
而对于notify()
和notifyAll()
的使用,请注意只有当绝对确认只会有一个线程在等待区中挂起的时候,才可以使用notify
(notifyAll
也可以);只要同时存在有多个线程在等待区中被挂起,就应该使用notifyAll
对象锁
前面讲过,Java虚拟机的一些运行时数据区会被所有线程共享,像方法区
和堆
。所以,Java程序需要为这两种多线程下的数据访问进行协调:
- 保存在堆中的实例变量
- 保存在方法区中的类变量
程序不需要考虑Java栈中的局部变量,因为是线程私有的。
在Java虚拟机中,每个对象和类在逻辑上都有一个监控器与之相关联的。
- 对于对象来说,相关联的监视器保护对象的实例变量。
- 对于类来说,监视器保护它的类变量。
如果一个对象没有实例变量,或者一个类没有类变量,相关联的监视器就什么都不监视。
为了实现监视器的排他性监视能力,Java虚拟机为每一个对象和类都关联了一个锁(有时候被称为互斥体mutex
)。一个锁就像就像一种任何时候只允许一个线程拥有的特权。
- 正常情况下,线程访问实例变量或者类变量不需要获取锁。
- 但是如果线程获取了锁,那么在它释放这个锁之前,就没有其他线程可以获取这个锁了。
锁住一个对象,其实就是获取对象相关联的监视器。而类锁实际上也是用对象锁来实现的。我们前面说过,当虚拟机装载一个class文件时,它会创建一个java.lang.Class
类的实例。当锁住一个类时,实际上锁住的的就是那个类的Class对象。
一个线程可以允许多次对同一个对象上锁(可重入)。对于每一个对象来说,Java虚拟机维护了一个计数器,记录对象被加了多少次锁:
- 没有被锁的对象的计数器是0
- 线程每加锁一次,计数就加1(只有已经拥有了这个对象锁的线程才能对该对象再次加锁)
- 线程每释放一次锁,计数器减1
- 当计数器为0时,锁就被完全释放了。此时其他线程才可以使用它。
对象锁和监视器
监视器能够实现拦截线程,保证监视区域只有一个线程在工作。靠的就是对象锁
在Java虚拟机中,每一个监视区域
都和一个对象引用相关联。所以整个流程差不多是这样子的:
- Java虚拟机中的一个线程进入监视器
入口区
- 线程根据
监视区域
的对象引用,找到对应的数据- 如果数据显示计数器数值为0,表示
监视区域
没有活动线程
,可以(多个线程的话需要竞争)加锁并通过2号箭头
进入监视区域,执行后续代码。 - 如果数值不为0,那么表示
监视区域
正在被占用,线程就要在入口区
等待,等待锁的数值变为0,和其他线程(如果有的话)竞争进入 - 当线程离开
监视区域
后,不管是如何离开的,它都会释放相关对象上的锁。
- 如果数据显示计数器数值为0,表示
虚拟机对于监视区域
的处理
怎么定义上面提到的监视区域
呢?
Java中的关键字synchronized
就是用来定义监视区域
的关键。synchronized
可以用来定义同步语句
和同步方法
同步语句
被synchronized
包裹起来的代码块就是同步语句
,像下面这样:
public class SynchronizeTest {private int[] array = new int[]{1, 2, 3, 4};public void expandArray() {synchronized (this) {for (int i = 0; i < array.length; i++) {array[i] = array[i] * 10;}System.out.println(Arrays.toString(array));}}
}
对于上面的同步代码块来说,虚拟机要保证不管线程以什么样的形式退出,必须要及时释放锁。
怎么保证呢?
假如上面的代码array[i] = array[i] * 10;
不小心写成了array[i] = array[i]/0;
,当执行到这一步的时候就要报java.lang.ArithmeticException
异常了。
对于可能抛出的异常来说,我们会使用try-catch
进行捕获,编译器的做法也是一样的。
我们看下javap -p SynchronizeTest.class
后expandArray()
部分输出:
public void expandArray();descriptor: ()Vflags: ACC_PUBLICCode:stack=4, locals=4, args_size=10: aload_01: dup2: astore_13: monitorenter4: iconst_05: istore_26: iload_27: aload_08: getfield #2 // Field array:[I11: arraylength12: if_icmpge 3515: aload_016: getfield #2 // Field array:[I19: iload_220: aload_021: getfield #2 // Field array:[I24: iload_225: iaload26: iconst_027: idiv28: iastore29: iinc 2, 132: goto 635: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;38: aload_039: getfield #2 // Field array:[I42: invokestatic #4 // Method java/util/Arrays.toString:([I)Ljava/lang/String;45: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V48: aload_149: monitorexit50: goto 5853: astore_354: aload_155: monitorexit56: aload_357: athrow58: returnException table:from to target type4 50 53 any53 56 53 any
请注意Exception table
这个异常表,这个就是编译器细心为我们加上的。它会监听从方法的第4条指令
到第50条指令
执行过程中的any
异常,出现异常就跳到第53条指令
。
我们可以看到53
往后还有一个monitorexit
在等待执行(这个any
说明啥异常也阻止不了释放锁的决心啊)。
是不是感觉编译器真滴很贴心哇,赞!
如果觉得不真实的话我们把synchronized
代码块去掉,再编译一次看下字节码信息,你会发现Exception table
也被清除了。
同步方法
还是上面的类SynchronizeTest
,这次我们把方法改成这样:
public synchronized void expandArray() {for (int i = 0; i < array.length; i++) {array[i] = array[i] / 0;}System.out.println(Arrays.toString(array));}
我们再来看下相关的字节码:
public synchronized void expandArray();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=4, locals=2, args_size=10: iconst_01: istore_12: iload_13: aload_04: getfield #2 // Field array:[I7: arraylength8: if_icmpge 3111: aload_012: getfield #2 // Field array:[I15: iload_116: aload_017: getfield #2 // Field array:[I20: iload_121: iaload22: iconst_023: idiv24: iastore25: iinc 1, 128: goto 231: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;34: aload_035: getfield #2 // Field array:[I38: invokestatic #4 // Method java/util/Arrays.toString:([I)Ljava/lang/String;41: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V44: return
我们看下区别(本人就找到了3条):
monitorenter
和monitorexit
都不见了。- 多了一个
ACC_SYNCHRONIZED
的flag
- 异常表也不见了(真滴不是故意没粘贴哈)
由于编译器在同步语句
里表现那么好,我来揣测一下它这么做的理由哈?
- 核心是
ACC_SYNCHRONIZED
的标记 - 当虚拟机解析到对这个方法的
符号引用
时,它会判断这个方法是否是同步的(根据ACC_SYNCHRONIZED
标记) - 如果是同步的,虚拟机就在调用方法前获取一个锁。
- 对于实例对象来说虚拟机获取的是与当前对象相关联的锁
- 对于类方法来说,虚拟机获取的是
类Class实例
相关联的锁
- 当方法执行完毕时(不管是异常终止还是正常退出),虚拟机都会释放这个锁
简单比较下同步方法和同步语句
从字节码指令上来看:
- 同步方法没有
monitorenter
和motorexit
等指令 - 同步方法没有异常表
- 同步方法少了一些变量保存的指令(用来记录对象锁的)
同步方法字节码更简洁,看上去更高效一些。
但真的是这样吗?
Amdahl 定律了解一下
speed=\frac{1}{F+\frac{1-F}{N}}
N 表示处理器,F 表示必须串行的部分
当N趋近于无穷大时,
speed = \frac{1}{F}
你懂得。。
Object
的协调支持
Object
一些方法我们前面已经用过了,统一整理一下。下次让你介绍Object
中定义的方法就可以把下面这几个说一下了:
方法 | 描述 |
---|---|
void wait() | 进入监视器的等待区,直到被其他线程唤醒 |
void wait(long timeout) | 进入监视器的等待区,直到被其他线程唤醒。或者经过timeout指定的毫秒后,自动苏醒 |
void wait(long timeout,int nanos) | 进入监视器的等待区,直到被其他线程唤醒。或者经过timeout指定的毫秒加上nanos指定的纳秒后,自动苏醒 |
void notify() | 唤醒监视器的等待区中的一个等待线程(如果等待区中没有线程,那就什么也不敢) |
void notifyAll() | 唤醒监视器的等待区中的所有等待线程(如果等待区中没有线程,那就什么也不敢) |
上面的5个方法,请在同步语句或同步方法中使用,不然会报错哟!
就是这种java.lang.IllegalMonitorStateException
附上Object
类的代码:
public class Object {private static native void registerNatives();static {registerNatives();}public final native Class<?> getClass();public native int hashCode();public boolean equals(Object obj) {return (this == obj);}protected native Object clone() throws CloneNotSupportedException;public String toString() {return getClass().getName() + "@" + Integer.toHexString(hashCode());}public final native void notify();public final native void notifyAll();public final native void wait(long timeout) throws InterruptedException;public final void wait(long timeout, int nanos) throws InterruptedException {if (timeout < 0) {throw new IllegalArgumentException("timeout value is negative");}if (nanos < 0 || nanos > 999999) {throw new IllegalArgumentException("nanosecond timeout value out of range");}if (nanos > 0) {timeout++;}wait(timeout);}public final void wait() throws InterruptedException {wait(0);}protected void finalize() throws Throwable { }
}
很简洁有没有。。。。。。
有个疑问哈,这么多native方法,咋没看到在哪里加载的lib呢?
真滴是个疑问,权当挖个坑,他日必来回复。。。。。
结语
好滴,到这里虚拟机的线程同步就结束啦,深入Java虚拟机
也到了尾声,收获很多。下一篇好好总结一下吧。
深入Java虚拟机(六)线程同步相关推荐
- Java多线程之线程同步机制(锁,线程池等等)
Java多线程之线程同步机制 一.概念 1.并发 2.起因 3.缺点 二.三大不安全案例 1.样例一(模拟买票场景) 2.样例二(模拟取钱场景) 3.样例三(模拟集合) 三.同步方法及同步块 1.同步 ...
- java多线程之线程同步问题
1.线程不安全的问题分析 当多线程并发访问同一个资源对象的时候,可能出现线程不安全的问题.但是,我们分析打印的结果,发现没有问题: 为了让问题更明显: Thread.sleep(10);//当 ...
- java如何实现线程同步
一.什么是线程的同步 线程有自己的私有数据,比如栈和寄存器,同时与其它线程共享相同的虚拟内存和全局变量等资源. 在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程.但是当多个线 ...
- java笔记--关于线程同步(7种同步方式)
关于线程同步(7种方式) --如果朋友您想转载本文章请注明转载地址"http://www.cnblogs.com/XHJT/p/3897440.html"谢谢-- 为何要使用同步? ...
- Java多线程(线程同步)
多线程编程很容易出现"错误情况",这是由系统的线程调度具有一定的随机性造成的,不过即使程序偶然出现问题,那也是由于编程不当引起.使用多个线程访问同一个数据时很容易出现此类状况,因此 ...
- 深入理解java虚拟机(六)GC垃圾回收-低延迟垃圾收集器(Shenandoah、ZGC)
文章目录 前言 一.Shenandoah收集器 1.Shenandoah介绍 2.Shenandoah与G1对比 3.Shenandoah工作原理 4.Shenandoah并行整理的核心概念-Broo ...
- Java高级之线程同步
本文来自刘兆贤的博客_CSDN博客-Java高级,Android旅行,Android基础领域博主 ,引用必须注明出处! 关于实现多线程的意义,"从业四年看并发"一文已经讲述,而本篇 ...
- jvm(java虚拟机)线程堆栈jstack(2)
jstack是java虚拟机自带的一种堆栈查看工具.主要目的是定位线程出现长时间停顿的原因,如线程间死锁.死循环.请求外部资源导致的长时间等待等. jstack -help Usage:jstack ...
- java中的线程同步机制讲解
文章目录 锁 锁的概述 锁的作用 保证原子性 保证可见性 保证有序性 锁的分类 内部锁 synchronized 概念 原理 使用 可重入性 显示锁 使用 内存屏障 概念 分类 按可见性划分 按有序性 ...
- Java 多线程和线程同步总结
转载:JAVA多线程实现和线程同步总结 1.JAVA多线程实现方式 JAVA多线程实现方式主要有三种:继承Thread类.实现Runnable接口.使用ExecutorService.Callable ...
最新文章
- 一说“并发”就想到“多线程”,那就局限了
- UITextView中的占位符
- 【Paper】2009_Controllability of Multi-Agent Systems from a Graph-Theoretic Perspective 精炼版
- 动态添加XtraTabControl
- html5 loaded,How do you check if a HTML5 audio element is loaded?
- ASP.NET架构分析
- vsftpd出现500 OOPS: cannot change directory的解决办法
- 【图像处理】MATLAB:彩色图像处理
- pdf内容怎么复制到word文档里_PDF转Word要收费?这4种免费实用的方法送你,一分钟统统能搞定...
- ios 手游SDK 开发教程
- macOS Big Sur安装Mojave动态桌面壁纸
- 第25版 OpenStack Yoga 已发布:稳定性与创新并重
- 第一代程序员王小波,最会编程的作家
- The Backrooms - Level 0.2 - 我爱杏仁水
- linux usb lpt,Linux安装Composer
- 计算机word中的行间距在哪里设置,word怎么把所有行间距设置成22磅
- 万字长文:编写 Dockerfiles 最佳实践
- 生僻字用计算机怎么弹歌曲,抖音生僻字计算器乐谱 计算器弹歌曲音乐乐谱大全...
- 单样本的t检验(t-test)是什么?
- zr 10联测 day1
热门文章
- iOS开发UI篇—ios应用数据存储方式(偏好设置)
- 傻白探索Chiplet,关于EPYC Zen2 的一些理解记录(五)
- mysql group by 和 having 使用注意事项
- 用inno Setup做应用程序安装包的示例脚本(.iss文件)(
- 使用a标签时不用href=““调转页面
- FM ALV和OO ALV的区别
- JUC第六讲:ThreadLocal/InheritableThreadLocal详解/TTL-MDC日志上下文实践
- 10驾校科目一考试系统——窗口交互
- Labview上位机与单片机系统的开发
- 艾美捷双链RNA定量试剂盒试验方案