1. Monitor
Java的监视器支持两种线程同步:
- 互斥【mutual exclusion】:Java虚拟机通过对象锁来支持互斥,以允许多个线程独立地操作共享数据,而不会相互干扰。
- 协作【cooperation】:在Java虚拟机中,通过Object类的wait和notify方法支持协作,这使线程能够一起工作,以实现一个共同的目标。
互斥
监视器【Monitor】就像一座建筑物,里面有一个特殊的房间,同一时间有且仅能被以备线程独占。这个房间中通常包含一些数据。从线程进入【Enter】这个房间到离开【Leave】这个房间前,该线程对房间中的任何数据都具有独占访问权:
- 进入这座建筑被称为“进入监视器【entering the monitor】”
- 进入该建筑中的这个特殊的房间被称为“获取监视器【acquiring the monitor】”
- 占用这个房间被称为“持有监视器【owning the monitor】”
- 离开这个房间被称为“释放监视器【releasing the monitor】”
- 离开这座建筑被称为“退出监视器【exiting the monitor】”
监视器【Monitor】除了与一些数据相关联外,还与一些或多些代码相关联,这些代码也被称为监视区【monitor regions】。对于一个监视器【Monitor】,监视区需要作为一个不可分割的操作来执行。换句话说,一个线程必须能够从头到尾执行一个监视区,且不会有另一个线程同时执行同一监视器【Monitor】的一个监视区。
监视器【Monitor】会保证在监视区上,同一时间只会同时被一个线程执行,即使有多个并发的线程。
注意:
- 线程进入监视器【entering the monitor】的唯一方式,就是到达与该监视器相关联的一个监视区的开始处
- 线程想继续执行监视区的惟一方式,就是获取监视器【acquiring the monito】
当一个线程到达某个监视器的监视区的开始处时,他就会被放到与该监视器关联的入口集【entry set】中等待,入口集【entry set】类似于监视器建筑前的走廊。如果没有其他线程在入口集【entry set】中等待,而且也没有线程当前正持有监视器【owning the monitor】,则这个线程就可以获取监视器【acquiring the monito】,并开始执行安全区。当该线程结束安全区的执行,他会释放【releasing the monitor】并退出监视器【exiting the monitor】
当一个线程到达某个监视器的监视区的开始处时,假设监视器已经由其他线程持有了,那么该线程必须在entry set中等待。当监视器的持有者线程释放退出监视器,新到达的线程必须与entry set中其他等待的线程竞争,最终只有一个线程获取监视器【acquiring the monito】。
协作
互斥有助于防止线程在操作共享数据时相互干扰,而协作则有助于线程为实现某个共同目标而共同协作。
当一个线程需要某些数据处于特定的状态,而另一个线程负责将数据置于该状态时,协作就显得格外重要。
Java虚拟机中所使用的协作监视器被称为“Wait and Notify”监视器,在这种监视器中,当前持有监视器【owning the monitor】的线程可以通过执行wait命令,在监视器中挂起自己。当一个线程执行wait时,它会释放监视器并进入一个等待集【wait set】。该线程将在等待集【wait set】中保持挂起状态,直到一段时间后,另一个线程在监视器中执行notify命令。当一个线程执行notify时,它将继续拥有这个监视器,直到它自动释放这个监视器,或者执行一个wait,或者执行完成这个监视区。在执行唤醒【notify】的线程释放监视器后,等待的线程也会苏醒,并可以重新获得监视器【acquiring the monito】。
Java虚拟机中这种监视器也被称为“ Signal and Continue ”监视器,原因在于:当线程执行了唤醒(Signal )【notify】后,并不会释放监视器,而是会持有监视器【owning the monitor】的所有权并继续执行监视区(Continue )。一段时间之后,执行缓醒【notify】的线程释放监视器,一个等待的线程苏醒了过来。So:等待线程因为监视器保护的数据没有处于允许线程继续执行有用工作的状态 ,将自己挂起,同样,缓醒线程在将监视器保护的数据置为等待线程想要的状态后执行缓醒【notify】命令。但是因为缓醒线程并不会释放监视器,而是继续执行监视区,它可能在缓醒后又修改了监视器保护的数据,这可能会使得等待线程依然无法工作。另一种情况是,存在第三个线程可能在缓醒【notify】线程释放监视器之后,并在等待的线程获取监视器【acquiring the monitor】之前,抢先获得了监视器【acquiring the monitor】,而且这个线程很有可能会更改监视器保护的数据的状态。因此,等待线程通常只是将缓醒【notify】看做是一次提示----期望的状态可能存在了。每次等待的线程苏醒时,它都需要再次检查状态,以确定它是否可以继续执行有用的工作。如果发现数据仍未处于期望的状态,则线程可以执行另一次wait,或者直接放弃并退出监视器。
上图展示了线程与监视器交互必须“pass”的几道门。
- 当一个线程到达监区的开始处时,它会通过①号门进入监视器【entering the monitor】,他会发现自己身处于监视器的入口集【entry set】中了。
- 如果没有任何线程在入口集【entry set】中等待,并且没有其他线程持有监视器【owning the monitor】,那么该线程就可以立即通过【pass】②号门,并称为该监视器的持有者。并继续执行监视区代码
- 还存着另一种情况,监视器被其他线程持有,且入口集中有其他线程也在等待,那么新抵达的线程就必须在入口集【entry set】中等待,而无法执行监视区代码
- 上图中入口集【enrtry set】中有三个挂起的线程,等待集【wait set】中有四个挂起的线程。这些线程将一直保持等待,直到监视器的持有者线程释放监视器为止。
- 释放监视器有两种方式:执行完成监视区或者调用wait。如果执行完成了监视区,那么它会通过⑤号门退出监视器,而如果该线程执行了wait命令,那么它会通过③号门进入等待集【wait set】,并释放监视器
- 如果监视器的持有者释放监视器时,没有notify缓醒等待的线程,那么监视器释放后,只有入口区【entry set】的线程会参与竞争,并通过②号门持有监视器。而如果监视器的持有者调用了notify(),那么入口区【entry set】的线程不得不与等待集【wait set】中的一个或者多个线程竞争,如果等待区的线程赢得了竞争,它将退出等待集【wait set】,并在通过④号门后重新获得并持有监视器。③和④号门是线程可以进入或退出等待集【wait set】的唯一方式。线程只能在持有监视器的情况下才可以执行wait()命令,并且只有在再次获得监视器的访问权后,才可以离开等待区【wait set】.
在Java虚拟机中,线程可以选择在执行wait命令时指定超时时间。如果线程指定了超时时间,并且在超时过期之前没有被其他线程唤醒,那么该等待线程将从虚拟机接收自到唤醒命令。通俗而言,在暂停时间到了之后,即使没有来自其他线程的明确的缓醒,他也会自动苏醒。
Java虚拟机提供了两种唤醒命令:
- notify:随意从等待集【wait set】中挑选一个线程缓醒
- notifyall:缓醒等待集【wait set】中所有的等待线程
Java虚拟机如何从等待集【wait set】或入口集【entry set】中选择下一个线程的方式,在很大程度上取决于虚拟机实现设计人员的决策。
2.对象锁
因为堆和方法区是所有线程共享的,因此Java程序需要协调多线程访问如下两类数据::
- 保存在堆中的实例变量
- 保存在方法区中的类变量
Java不需要协调保存在栈中的局部变量,因为他们是线程私有的~
在Java虚拟机中,每个对象和类在逻辑上都与监视器相关联:
- 对于对象,关联的监视器保护对象的实例变量。
- 对于类,关联的监视器保护类的类变量。
如果一个对象没有实例变量,或者一个类没有类变量,则关联的监视器不保护任何数据。
为了实现监视器的互斥功能,Java虚拟机将锁(有时称为互斥锁)与每个对象和类关联起来。锁就像一个在任何时候只有一个线程可以“拥有”的特权。线程访问类变量或者实例变量时,并不需要获得锁。但是,如果一个线程确实获得了锁,那么在持有锁的线程释放锁之前,任何其他线程都无法获得相同数据上的锁。(“锁定一个对象”就是获取与该对象相关联的监视器)
类锁【Class Lock】实际上是用对象锁实现的。当Java虚拟机加载类文件时,它会创建类的Java.lang.class的实例。当您锁定一个类时,实际上是锁定了该类的Class对象。
允许一个线程多次锁定同一个对象。对于每个对象,Java虚拟机维护一个计数器,记录对象被加了几次锁。没有被锁对象的计数为0。当线程第一次获得锁时,计数再次增加到1。每次线程获得同一对象上的锁时,计数再次递增(只有已经拥有对象锁的线程才允许再次锁住它)。每次线程释放锁时,计数都会递减。当计数为0时,锁被完全释放,其他线程可以使用它了。
当Java虚拟机中的线程到达监视区的开始处时,它请求一把锁。
在Java中,有两种监视区:
- synchronized语句
- synchronized方法
Java程序中的每个监视区都与一个对象引用相关联。当一个线程到达监视区中的第一条指令时,该线程必须获得被引用的对象的锁。在获得锁之前,线程不允许执行其中的代码。一旦获得锁,线程就可以进入受保护的代码块了。当线程离开代码块时,无论它如何离开块,它都会释放相关联的对象上的锁。
注意,作为Java程序员,您永远不会显式地锁定对象。对象锁是Java虚拟机的内部锁。在Java程序中,通过编写synchronized语句和synchronized方法来标识程序中的监视区。当Java虚拟机运行您的程序时,每当遇到一个监视区,虚拟机会自动的锁定一个对象或类。
3. 指令集的同步支持
同步语句【Synchronized Statements】
要创建一个同步语句,在几个计算对对象的引用的表达式上加上synchronized关键字即可。
public void sync1(){
synchronized (this){
index++;
}
}
方法内的同步语句【Synchronized Statements】会使用monitorenter和monitorexit两个操作码来实现。
字节码 | 操作数 | 解释 |
---|---|---|
monitorenter | none | 弹出objectref,获取与objectref关联的锁 |
monitorexit | none | 弹出objectref,释放与objectref关联的锁 |
当虚拟机遇到monitorenter时,他会获得栈中objectref所引用的对象的锁。如果线程已持拥有了该对象的锁,那么锁的计数器加1。相对应的,线程中的monitorexit 会引起计数器减1,。当计数器变为0时,锁被完全释放了。
需要注意的是,在字节码中,Java为了保证锁一定会被释放,使用了异常表来保证从monitorenter 到 monitorexit 之间的指令被一个异常处理器处理,该异常处理器的主要目的,就是在出现异常等情况下,可以将锁释放掉。
同步方法【Synchronized Methods】
要同步方法,只需在方法修饰符中添加synchronized关键字:
public synchronized void sync0(){
index++;
}
Java虚拟机不使用任何特殊的操作码,来调用同步方法【Synchronized Methods】或从同步方法【Synchronized Methods】返回。
当虚拟机解析对方法的符号引用时,虚拟机将确定该方法是否是同步方法【Synchronized Methods】。如果是同步方法【Synchronized Methods】,虚拟机在调用方法之前需要获得一个锁:
- 对于实例方法,虚拟机获取与调用方法的对象相关联的锁。
- 对于类方法,它获取与方法所属的类相关联的锁(它锁定该类的Class对象)。
同步方法【Synchronized Methods】要比同步语句【Synchronized Statements更高效一点。
同步方法【Synchronized Methods】不需要异常表,所有的处理均由虚拟机实现。
5. Object类中的协调支持
方法 | 描述 |
---|---|
void wait(); | 进入等待集【wait set】直到被其他线程唤醒 |
void wait(long timeout); | 进入等待集【wait set】,直到被其他线程缓醒或者过了timeout |
void wait(long timeout, int nanos); | 同上 |
void notify(); | 在监视器的等待集【wait set】中唤醒一个正在等待的线程(如果没有线程在等待,则什么也不做)。 |
void notifyAll(); | 唤醒在监视器的等待集中等待的所有线程(如果没有线程在等待,则什么也不做)。 |