Java并发6:等待通知机制
等待-通知机制
上篇文章通过while(!actr.apply(this, target));
来破坏占用且等待条件。
若apply()
操作耗时非常短,且并发冲突量也不大,那这个方案还可行,可能循环上几次或者几十次就能一次性获取转出账户和转入账户。
但若 apply()
操作耗时长,或者并发冲突量大的时候,就不适用了,可能要循环上万次才能获取到锁,代码不断执行太消耗 CPU 。
最好的方案应该是:
如果线程要求的条件(转出账本和转入账本同在文件架上)不满足,则线程阻塞自己,进入等待状态;
当线程要求的条件(转出账本和转入账本同在文件架上)满足后,通知等待的线程重新执行。
使用线程阻塞的方式就能避免循环等待消耗 CPU 的问题。
一个完整的等待 - 通知机制:
线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;
当要求的条件满足时,通知等待的线程,重新获取互斥锁。
synchronized实现等待-通知机制
同一时刻,只允许一个线程进入``` synchronized``` 保护的临界区,当有一个线程进入临界区后,其他线程就只能进入图中左边的等待队列里等待。
**等待队列和互斥锁是一对一的关系,每个互斥锁都有自己独立的等待队列。**
![wait()操作工作原理](https://images-1252818907.cos.ap-chengdu.myqcloud.com/images/wait()操作工作原理.png)
调用`` wait() ``方法,当前线程被阻塞,进入到右边的等待队列,这个等待队列也是互斥锁的等待队列。
线程在进入等待队列的同时,会释放持有的互斥锁,线程释放锁后,其他线程就有机会获得锁,并进入临界区了。
线程要求的条件满足时,通过Java 对象的``` notify() ```和 ```notifyAll() ```方法唤醒阻塞的线程。
当条件满足时调用``` notify()```,会通知等待队列(互斥锁的等待队列)中的线程,告诉它条件曾经满足过。
**notify() 只能保证在通知时间点,条件是满足的。**
而被通知线程的执行时间点和通知的时间点基本上不会重合,所以当线程执行的时候,很可能条件已经不满足了(保不齐有其他线程插队)。
![notify()操作工作原理图](https://images-1252818907.cos.ap-chengdu.myqcloud.com/images/notify()操作工作原理图.png)
**```wait()```、```notify()```、```notifyAll() ```方法操作的等待队列是互斥锁的等待队列。**
所以如果``` synchronized``` 锁定的是``` this```,那么对应的一定是``` this.wait()```、```this.notify()```、```this.notifyAll()```;
如果 ```synchronized ```锁定的是``` target```,那么对应的一定是 ```target.wait()```、```target.notify()```、```target.notifyAll() ```。
**```wait()```、```notify()```、```notifyAll() ```这三个方法能够被调用的前提是已经获取了相应的互斥锁,即 wait()、notify()、notifyAll() 都是在 synchronized{}内部被调用的。**
如果在``` synchronized{}```外部调用,或者锁定的``` this```,而用``` target.wait()``` 调用的话,JVM 会抛出一个运行时异常:```java.lang.IllegalMonitorStateException```。
### 利用等待-通知实现转账
+ 等待-通知机制条件判断范式
这种范式可以解决上面提到的条件曾经满足过这个问题。因为当``` wait()``` 返回时,有可能条件已经发生变化了,曾经条件满足,但是现在已经不满足了,所以要重新检验条件是否满足。
范式,意味着是经典做法,所以没有特殊理由不要尝试换个写法。
```java
while(条件不满足) {
wait();
}
- 代码
class Allocator {
private List<Object> als;
// 一次性申请所有资源
synchronized void apply(Object from, Object to){
// 经典写法
while(als.contains(from) || als.contains(to)){
try{
//集合不为空,说明有其他Account线程已经申请了资源,当前申请资源的Account线程就wait()释放
//实例allocator锁
wait();
}catch(Exception e){}
}
als.add(from);
als.add(to);
}
// 归还资源
synchronized void free(
Object from, Object to){
als.remove(from);
als.remove(to);
notifyAll();
}
}
notify()
是会随机地通知等待队列中的一个线程,而 notifyAll()
会通知等待队列中的所有线程,尽量使用notifyAll()
避免线程被饿死。
思考
+ ```wait()```方法会释放对象的“锁标志”
调用对象的```wait()```方法后,当前线程暂停执行,释放锁标志,并将当前线程放入**对象等待池**中。
调用```notify()```方法后,将从**对象等待池**中移出任意一个线程并放入**锁标志等待池**中,**只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。**
调用```notifyAll()```方法后,**会将对象等待池中的所有线程都移动到该对象的锁标志等待池**。
+ ```sleep()```方法**必须**要指定等待的时间
可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。
但是```sleep()```方法不会释放“锁标志”,如果有```synchronized```同步块,其他线程仍然不能访问共享数据。
+ ```wait()```是```Object```的对象方法,```sleep()```是```Thread```的类方法。
+ ```wait()```需要被唤醒,```wait(1000L)```不需要,```sleep()```不需要。
+ ```wait()```需要获取到监视器,即只能在同步方法和同步快中使用,否则抛异常,```sleep()```不需要。
### 别人的总结
>```java
>public class MyLock {
> // 测试转账的main方法
> public static void main(String[] args) throws InterruptedException {
> Account src = new Account(10000);
> Account target = new Account(10000);
> CountDownLatch countDownLatch = new CountDownLatch(9999);
> for (int i = 0; i < 9999; i++) {
> new Thread(() -> {
> src.transactionToTarget(1, target);
> //放在finally里面?更安全?
> countDownLatch.countDown();
> }).start();
> }
> countDownLatch.await();
> System.out.println("src="+src.getBanalce() );
> System.out.println("target=" + target.getBanalce());
> }
>
> static class Account { //账户类
> public Account(Integer banalce) {
> this.banalce = banalce;
> }
>
> private Integer banalce;
>
> public void transactionToTarget(Integer money, Account target) {//转账方法
> Allocator.getInstance().apply(this, target);
> this.banalce -= money;
> target.setBanalce(target.getBanalce() + money);
> //放在finally里面?更安全?
> Allocator.getInstance().release(this, target);
> }
>
> public Integer getBanalce() {
> return banalce;
> }
>
> public void setBanalce(Integer banalce) {
> this.banalce = banalce;
> }
> }
>
> static class Allocator { //单例锁类
> private Allocator() {
> }
> //优化 list换成set更快
> private List<Account> locks = new ArrayList<>();
>
> public synchronized void apply(Account src, Account tag) {
> while (locks.contains(src) || locks.contains(tag)) {
> try {
> this.wait();
> } catch (InterruptedException e) {
> }
> }
> locks.add(src);
> locks.add(tag);
> }
>
> public synchronized void release(Account src, Account tag) {
> locks.remove(src);
> locks.remove(tag);
> this.notifyAll();
> }
>
> public static Allocator getInstance() {
> return AllocatorSingle.install;
> }
>
> static class AllocatorSingle {
> public static Allocator install = new Allocator();
> }
> }
>}
——wang