条件对象
之前提到了,如果线程持有一个临界资源,那么在线程操作临界资源的过程中对待其他的线程是互斥的,也就是说在某一时刻只有持有临界资源的线程才能访问,读写临界资源, 而其他的线程是不可以的,这样就保证了一定次序从而避免讹误的产生。
那么很显然就有一个问题,对于一个锁来说,几个线程都想持有它,这几个线程都是公平的,就看线程调度器选择哪个,但是如果一个线程持有了一个锁,但是并不是说运行的条件就是一定的满足,有可能会产生一直困死在这的问题,因为这个线程持有锁但是没有能力所以导致程序也完全崩溃了。
那么如何解决这个问题?
理所当然的就能想到,既然这个线程没有能力去执行,为什么不让他先释放掉资源,别占着* * 不* *,先让其他有能力的线程去执行,又不是欺负它不让他执行,等条件合适的时候就叫醒他,让他执行。
1.2.1定义
当线程进入临界区,却发现某一条件满足之后它才能执行,这时就可以使用一个条件对象来管理那些已经获得了一个锁但是却不能运行的线程。
1.2.2例子
在之前的银行模拟程序中,我们理所当然的写出了一些代码:
if (account[from] < amount){
/*账户固有金额小于转账金额,无法转账,退出函数*/
return;
}
这个代码看着没有问题,是因为代码在串行的思维上是没有问题的,这个时候确实需要去检查当前账户的额度是否支持转账,但是存在一个问题,因为我们当前的程序是多线程的,如果一个线程通过了额度检查,但是却被中断,在他进行转账账户额度增减的操作前,线程被中断,其他的线程修改了额度,进而导致,第一个线程因为额度不满足转账需求,系统出错了。
显然这样的问题是不行的,而此时我们也很直接的可以考虑到用一个锁来保证访问账户和修改账户的额度之间的操作的原子性就能保证不会因为中断出这样的问题。
但是我的需求并不是这样的,我的需求是:如果当前的线程操作账户的额度并不足以支持装置,所以线程要一直等待直到这个条件满足的情况再去转账。那么此时应该如何去处理呢?
分析一下需求:
- 1)本线程要等待条件满足
- 2) 操作的账户属于临界资源,要给其他线程机会可以修改这个临界资源,从而能满足条件
- 3)条件满足的时候,线程要能再去转账
1.2.3实现
通过是用条件对象来实现这个操作
常用的方法:
- java.util.concurrent.locks.Lock
- Condition newCondition()
- 返回一个于该锁相关的条件对象
- Condition newCondition()
- java.util.concurrent.locks.Condition
- void await()
- 将线程放到条件对象的等待集中
- void signalAll()
- 解除该条件对象等待集中的所有线程的阻塞状态
- void signal()
- 从该条件的等待集中随机选一个线程,解除器阻塞状态
- void await()
/**
* ReentrantLock 实现了Lock接口
*/
private Lock bankLock = new ReentrantLock();
/**
* 条件对象
*/
private Condition sufficientFunds;
public void transfer(int from,int to,double amount){
bankLock.lock();
try {
/*如果金额小于要转走的金额,那么肯定无法操作*/
while (account[from] < amount){
/*
习惯上给每个条件对象命名方式就是所表示的条件的名字
因为运行的条件不满足,所以要阻塞掉线程
此时,本线程会被阻塞并放弃锁,等待其他线程的操作能满足条件,之后,再运行
这里的等待和等待去获得锁的线程是完全不一样的:
此时线程会进入await 的等待集,当锁可用是它也不能不会去立马解除阻塞,相反它会一直保持阻塞直到其他的线程调用同一条件上的signalAll方法
*/
sufficientFunds.await();
}
System.out.println(Thread.currentThread());
account[from] -= amount;
/*打印转账金额,转出账户和转入账户*/
System.out.printf(" %10.2f from %d to %d",amount,from,to);
account[to] += amount;
System.out.printf(" Total Balance: %10.2f%n",getTotalBalance());
/*
signal all 方法会激活其他等待的所有线程,并从等待集中挪出,
此时这些线程会再次处于可运行的状态,在线程调度器的作用下,等待线程会再次尝试持有锁
当锁可用时,他们的某个从await 中返回,如果判定结果生效则继续执行
如果条件不满足就会再次被阻塞
(signalALl方法其实并不会去保证现在条件一定满足了,只是告诉这些等待的线程
现在发生了他们希望发生的事件,条件可能被满足了,而检测还是交给他们)
*/
sufficientFunds.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bankLock.unlock();
}
}
重点:什么时候调用await?
显然应当在条件不满足的时候调用await?但是根据条件对象的使用,此时的线程在被唤醒后是需要再去检查是否满足条件的,并不是说唤醒之后就能工作下去了。
所以使用的格式应当是:
while(!(ok to proceed)){ condition.await(); }
调用await 后,这个线程就进入了阻塞的状态,并且释放了锁,它无法自己激活自己,而是把希望给到了其他的线程,那么需要注意的就是如果没有线程可以激活它,那么它就陷入了死锁的状态。
重点:核心在于什么时候调用signalAll方法?
显然,如果调用signalAll方法每个线程此时都会去检查余额是否满足条件,如果没有满足条件就会再进入条件对象的等待集中,而满足条件则就会执行下去。
所以signalAll方法的调用必须在此时发生的事件可能会使得条件满足。
也就是上面完成了转账操作后,此时每个线程并不能确定是不是我的条件满足了,但是唤醒其他线程的线程,只是告诉他们,有这个可能,具体谁的条件满足了,调度器调度哪个线程都是不确定的。
重点:signal 和signalAll 的区别
signal 会随机的激活等待集中的某个线程的阻塞状态,这比接触所有的线程的阻塞状态显然更加的有效,但是也存在危险,如果随机选择的线程发现自己仍然不能运行,那么它仍旧会被阻塞,但是此时如果没有其他的线程再次调用signal 方法,那么系统就死锁了。