前言
上一篇中我们说的主要问题是死锁发生的条件,以及应对死锁的一些解决方法,它们分别:加锁顺序、加锁时限以及死锁检测,其中死锁检测是最优的解决方法,它通过检查与目标锁相关的数据结构,查询在整个锁的关系图中,是否有某个线程正在等待当前线程持有的锁,以判断死锁是否发生。
1. 饥饿
##1.1 定义
所有CPU时间片被其它线程占据导致其中一个线程无法分配到CPU时间片的状态被称为饥饿(线程可能因为饥饿致死)。
1.2 饥饿状态的原因
- 高优先级的线程吞噬了所有低优先级线程的CPU时间;
- 线程被永久阻塞在一个等待进入同步块的状态;
- 线程在等待一个自身处于永久等待完成的对象(比如调用这个对象的
wait()
方法)。
1.2.1 线程优先级导致的饥饿
Java的线程类允许我们为每个线程对象设置优先级, public final void setPriority(int newPriority)
,优先级的范围是1-10,不过我们需要认识到:优先级并不保证我们的程序的执行逻辑,优先级表示的行为的准确解释依赖于程序的运行平台(我暂且理解为操作系统),因此,如果没有必要,我们最好不要改动优先级。
1.2.2 永久阻塞
Java的同步代码区对于哪个线程能够允许被进入的次序并没有任何保证,这就意味着存在着某种风险即某个线程永远处在阻塞状态,因为其它线程总是先于它进入同步区。
1.2.3 等待着等待
另一种线程处于饥饿状态的原因就是,线程在等待着永远处于等待状态的对象。如果多个线程持有了某个处于wait()
状态的对象,调用该对象的notify()
方法并不能保证某个线程被唤醒,任何线程都可能继续保持在等待状态。因此存在这样的风险:某个线程从未被唤醒,因为其它线程总是能够被唤醒。
2. 公平
2.1 定义
这是解决饥饿问题的对策,保证所有线程都能够公平获得CPU运行时间的策略。
2.2 公平性解决办法
- 使用锁,而不是同步块;
- 公平锁;
2.2.1 使用锁代替同步块
我们来实现一个锁:
package thread;
public class Lock {
private boolean isLocked=false;
private Thread lockingThread=null;
public synchronized void lock() throws InterruptedException {
while (!isLocked){
wait();
}
isLocked=true;
lockingThread=Thread.currentThread();
}
public synchronized void unLock(){
if (this.lockingThread!=Thread.currentThread()){
throw new IllegalMonitorStateException("Calling thread has not locked this lock");
}
isLocked=false;
lockingThread=null;
notify();
}
}
我们尝试使用上述锁:
public class Synchronizer{
Lock lock = new Lock();
public void doSynchronized() throws InterruptedException{
this.lock.lock();
//critical section, do a lot of work which takes a long time
this.lock.unlock();
}
}
在这里我们可以看到doSynchronized()
方法不再使用synchronized
修饰,而是在方法中的业务逻辑前后使用lock()
和unlock()
来包围。
在上述Lock实现中,如果存在多线程并发访问lock()
,这些线程将阻塞在对lock()
的访问上,如果锁已经锁上,那么这些线程将阻塞在while循环中的wait()
调用上。我们要记住的是:当线程正在等待进入lock()
时,可以调用wait()释放其锁实例对应的同步锁,使得其它多个线程可以进入lock()
方法调用wait()
。
我们再看一下doSynchronized()
,我们可以看到lock()
与unlock()
之间的注释:这里将会消耗很长时间——和进入lock()
并调用wait()
方法来比较的话。这意味着大部分用于进入锁和临界区的时间被消耗在wait()
上,而并非是阻塞在进入lock()
方法中。
该版本的锁并不能为线程的公平性提供什么保障。
2.2.2 公平锁
下面我们来将上述的锁变为公平锁,我们可以发现新的实现与之前Lock类中的同步和wait()
/notify()
稍有不同。
准确地说,从原来的Lock类到公平锁是一个循序渐进的过程,每一步都建立在解决前面问题的基础上:Nested Monitor Lockout、Slipped Conditions、Missed Signal。重要的是,每调用一次lock()
方法,都会进入一个队列,当解锁后,只有队列中的第一个线程能够被允许锁上FairLock实例,其他所有线程都将处于等待状态,直到它们到达队列的头部。
#####实现:
package concurrent;
import concurrent.entity.QueueObject;
import java.util.ArrayList;
import java.util.List;
public class FairLock {
private boolean isLocked=false;
private Thread lockingThread=null;
private List<QueueObject> waitThreads=new ArrayList<QueueObject>();
public void lock() throws InterruptedException {
QueueObject queueObject=new QueueObject();
boolean isLockedForThisThread=true;
synchronized (this){
this.waitThreads.add(queueObject);
}
while (isLockedForThisThread){
synchronized (this){
//这里判断是否已经加锁或者并非是队列头部,
//出现这两种情况都导致isLockedForThisThread=true
isLockedForThisThread=isLocked||waitThreads.get(0)!=queueObject;
if (!isLockedForThisThread){
isLocked=true;
waitThreads.remove(queueObject);
lockingThread=Thread.currentThread();
return;
}
}
try{
queueObject.doWait();
} catch (InterruptedException e) {
synchronized (this){
waitThreads.remove(queueObject);
}
throw e;
}
}
}
public synchronized void unlock(){
if (this.lockingThread!=Thread.currentThread()){
throw new IllegalMonitorStateException("calling thread has not locked this lock");
}
isLocked=false;
lockingThread=null;
if (waitThreads.size()>0){
waitThreads.get(0).notify();
}
}
}
public class QueueObject {
private boolean isNotified = false;
public synchronized void doWait() throws InterruptedException {
while (!isNotified) {
this.wait();
}
this.isNotified = false;
}
public synchronized void doNotify(){
this.isNotified=true;
this.notify();
}
@Override
public boolean equals(Object obj) {
return this==obj;
}
}
从FairLock的实现中我们注意到lock()
方法并没有声明为synchronized
,而是对需要被同步的代码,在方法中用synchronized
修饰。
下面这段不是很懂
**
FairLock
在lock()中创建了一个QueueObject
对象,每个调用lock()的线程就会被加入队列。而调用unlock()
方法,则会取得队列头部的QueueObject
实例,调用它的doNotify()方法,唤醒在该对象上等待的线程。**通过这种方式,在同一时间只有一个等待线程会被唤醒,而不是所有等待线程,这就是公平锁的核心所在。
还需注意到,QueueObject
实际是一个semaphore
。doWait()
和doNotify()
方法在QueueObject
中保存着信号。这样做避免一个线程在调用queueObject.doWait()
之前被另一个调用unlock()
并随之调用queueObject.doNotify()
的线程重入,从而导致信号丢失。queueObject.doWait()
调用放置在synchronized(this)
块之外,以避免被monitor
嵌套锁死,所以另外的线程可以解锁,只要当没有线程在lock ()
方法的synchronized(this)
块中执行即可。
2.3 注意性能开销
通过比较Lock
与FairLock
,我们可以发现FairLock的lock()
与unLock()
上有许多深入的地方,这些差异性导致了FairLock
执行起来更慢一些,但是究竟有多大影响是取决于临界区的执行时长,长一些则FairLock
的影响小一些。