即使大部分同步装置的功能和使用方式不同(锁, semaphores, 阻塞队列等),但它们的内部实现还是相当一致的.换句话说它们都有相同的组成部分.了解这些组成部分有助于我们设计一个同步装置.下文会一个个分析这些组成部分.
大部分同步装置的目的是保证临界区代码在多线程环境下的安全访问.要做到这点,一个同步装置通常需要以下几个部分:
- 状态
- 访问条件
- 更改状态
- 通知策略
- 检查和设置方法
- 设置方法
并不是所有的同步装置都由这些部分组成,可能会有例外.但大部分同步装置都是由它们中的一到多个部分组成的.
状态
同步装置中的状态用于是否允许一个线程取得访问权限的检查条件.在Lock中,状态用来记录一个布尔值,用于表示Lock是否已经被锁住.在BounedSemaphore中,内部状态用来记录一个整型计数器,用于表示已经发送的信号数量以及能够发送的最大上限.阻塞队列中,状态用于记录一个保存有相关队项的列表,用于表示队列中进队的队项,以及能够容纳的最大队项数量.
以下给出Lock和BoundedSemaphore的状态实现的片段代码.
public class Lock{
//state is kept here
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
...
}
复制代码
public class BoundedSemaphore {
//state is kept here
private int signals = 0;
private int bound = 0;
public BoundedSemaphore(int upperBound){
this.bound = upperBound;
}
public synchronized void take() throws InterruptedException{
while(this.signals == bound) wait();
this.signal++;
this.notify();
}
...
}
复制代码
访问条件
访问条件用于决定一个线程在执行检查和设置状态的方法时,是否允许设置状态.同步装置中,访问条件是状态的典型应用.而访问条件的典型应用是解析成true或false,以在一个while循环中防止线程意外唤醒风险的发生.
在Lock中,访问条件简化为检查isLocked成员变量的值.在BoundedSemaphore中,一共有两个访问条件决定了take()和release()方法的调用结果.当一个线程调用take()方法时需要检查signals变量是否已经达到上限.当一个线程调用release()方法时需要同样需要检查signals变量.
以下给出Lock和BoundedSemaphore的访问条件实现的片段代码.我们需要注意访问条件一直在while循环中检查.
public class Lock{
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException{
//access condition
while(isLocked){
wait();
}
isLocked = true;
}
...
}
复制代码
public class BoundedSemaphore {
private int signals = 0;
private int bound = 0;
public BoundedSemaphore(int upperBound){
this.bound = upperBound;
}
public synchronized void take() throws InterruptedException{
//access condition
while(this.signals == bound) wait();
this.signals++;
this.notify();
}
public synchronized void release() throws InterruptedException{
//access condition
while(this.signals == 0) wait();
this.signals--;
this.notify();
}
}
复制代码
更改状态
一旦线程获得访问临界区代码的权限,它需要更改同步装置内部状态来阻塞其他线程进入临界区.换句话说,状态需要反映一个线程当前正在执行临界区代码.这将对其他线程检查访问条件取得访问权限产生影响.
在Lock中,状态更改表现为代码isLocked = true
.在Semaphore中变现为代码signals--
或是signals++
.
以下给出Lock和BoundedSemaphore的更改状态实现的片段代码.
public class Lock{
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException{
while(isLocked){
wait();
}
//state change
isLocked = true;
}
public synchronized void unlock(){
//state change
isLocked = false;
notify();
}
}
复制代码
public class BoundedSemaphore {
private int signals = 0;
private int bound = 0;
public BoundedSemaphore(int upperBound){
this.bound = upperBound;
}
public synchronized void take() throws InterruptedException{
while(this.signals == bound) wait();
//state change
this.signals++;
this.notify();
}
public synchronized void release() throws InterruptedException{
while(this.signals == 0) wait();
//state change
this.signals--;
this.notify();
}
}
复制代码
通知策略
一旦线程更改了同步装置的状态,那么它需要通知到正在等待进入临界区的其他线程.因为这次状态更改可能会将其他线程的访问条件置换为true.
通知策略一共有以下三种类型:
- 通知所有等待线程
- 随机通知多个等待线程中的1个
- 指定通知多个等待线程中的1个
通知所有等待线程比较简单.只要在线程调用wait()方法的对象上调用notify()即可.调用notify()并不能保证多个等待线程中哪一个被通知到.因此称为"随机通知".
有时候你需要指定通知一个特定的等待线程而不是随机通知.比如你需要按照线程调用同步装置的顺序来通知线程或是按照优先级来通知线程.那么我们需要存储每一个线程与调用wait()方法的对象之间的关系.当需要通知特定的等待线程时,只需要调用线程调用wait()方法的对象的notify()方法即可.这样的例子在公平与饥饿一文中有提及.
以下给出了随机通知一个线程的片段代码:
public class Lock{
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException{
while(isLocked){
//wait strategy - related to notification strategy
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify(); //notification strategy
}
}
复制代码
检查和设置方法
同步装置通常包含有两种类型的方法,其中检查和设置是第一种(设置是第二种).检查和设置是指线程调用检查方法来比较访问条件和同步装置内的状态,如果条件符合预期线程则设置同步装置内的状态来反映当前线程已经取得访问临界区的权限.
通常通过置换状态来使访问条件解析为false,以阻塞其他线程取得访问权限.但不总是如此,例如在读写锁中,一个线程通过更改读写锁的内部状态来反映该线程已经取得读取权限,但在没有线程请求进行写操作的情况下,其他线程仍然可以取得读取权限.
检查和设置操作必须是原子,即不允许其他线程在检查和设置状态的期间进行干扰.
下面给出了检查和设置方法在程序中的关键步骤:
-
如有必要可以在检查前设置状态
-
比较访问条件和状态
-
如果访问条件不满足预期则进入阻塞状态
-
如果访问条件满足预期则设置状态,如有必要同时通知其他等待线程
在之前Java中的读写锁文章提及的ReadWriteLock类中的lockWrite()方法就是一个检查和设置的实例方法.线程在调用lockWrite()时,在检查状态前先设置(writeRequests++).然后再通过canGrantWriteAccess()方法来对比内部状态和访问条件.如果符合预期则在退出调用方法前设置状态.我们注意到这个方法并没有通知其他等待线程.
public class ReadWriteLock{
private Map<Thread, Integer> readingThreads = new HashMap();
private int writeAccesses = 0;
private int writeRequests = 0;
private Thread writingThread;
public synchronized void lockWrite() throws InterruptedException{
writeRequests++;
Thread callingThread = Thread.currentThread();
while(! canGrantWriteAccess(callingThread)){
wait();
}
writeRequests--;
writeAccesses++;
writingThread = callingThread;
}
}
复制代码
在BoundedSemaphoer对象中包含了两个检查和设置方法: take()和release().两个方法都会检查和设置内部状态.
public class BoundedSemaphore {
private int signals = 0;
private int bound = 0;
public BoundedSemaphore(int upperBound){
this.bound = upperBound;
}
public synchronized void take() throws InterruptedException{
while(this.signals == bound) wait();
this.signals++;
this.notify();
}
public synchronized void release() throws InterruptedException{
while(this.signals == 0) wait();
this.signals--;
this.notify();
}
}
复制代码
设置方法
设置方法是同步装置中常见的第二种类型的方法.设置方法仅仅是设置同步装置中的内部状态而没有去检查它.一个典型的设置方法实例是Lock对象中的unlock()方法.通常一个线程在释放已持有的锁时不需要检查锁是否已经释放过了.
下面给出了设置方法在程序中的关键步骤:
- 设置内部状态
- 通知等待线程
下面给出unlock方法示例:
public class Lock{
private boolean isLocked = false;
public synchronized void unlock(){
isLocked = false;
notify();
}
}
复制代码
该系列博文为笔者复习基础所著译文或理解后的产物,复习原文来自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial