【读书笔记】《Java并发编程之美》——notify()唤醒的线程是随机的么?

237 阅读6分钟

第13页
一个线程调用共享对象的notify()方法后会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的

一个并不随机的例子

请考虑代码片段

...
private static volatile Object resourceA = new Object();

public static void main(String[] args) throws InterruptedException {
    ExecutorService pool = Executors.newCachedThreadPool();
    int concurrencyThreadNum = 10;
    CountDownLatch latch = new CountDownLatch(concurrencyThreadNum);
    for (int i = 0; i < 10;) {
        pool.execute(new Task(++i, resourceA, latch));
    }
    Thread.sleep(TimeUnit.SECONDS.toMillis(1));
    synchronized (resourceA) {
        resourceA.notify();
        log.info("release lock");
    }
    latch.await();
    log.info("done!");
}

public static class Task implements Runnable {

    private final int i;
    private final Object lock;
    private final CountDownLatch latch;

    public Task(int i, Object lock, CountDownLatch latch) {
        this.i = i;
        this.lock = lock;
        this.latch = latch;
    }

    @Override
    public void run() {
        synchronized (lock) {
            log.info(String.format("thread-%d get lock", i));
            try {
                log.info(String.format("thread-%d begin wait", i));
                lock.wait();
                log.info(String.format("thread-%d end wait", i));
                Thread.sleep(TimeUnit.SECONDS.toMillis(2));
                lock.notify();
            } catch (InterruptedException e) {
                log.error(e.getMessage());
            }
            log.info(String.format("thread-%d release lock", i));
            latch.countDown();
        }
    }
}
...

执行结果可能如下:

03:38:31.168 [pool-1-thread-1] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-1 get lock
03:38:31.171 [pool-1-thread-1] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-1 begin wait
03:38:31.171 [pool-1-thread-10] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-10 get lock
03:38:31.171 [pool-1-thread-10] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-10 begin wait
03:38:31.171 [pool-1-thread-9] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-9 get lock
03:38:31.172 [pool-1-thread-9] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-9 begin wait
03:38:31.172 [pool-1-thread-8] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-8 get lock
03:38:31.172 [pool-1-thread-8] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-8 begin wait
03:38:31.172 [pool-1-thread-7] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-7 get lock
03:38:31.172 [pool-1-thread-7] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-7 begin wait
03:38:31.172 [pool-1-thread-6] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-6 get lock
03:38:31.172 [pool-1-thread-6] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-6 begin wait
03:38:31.172 [pool-1-thread-5] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-5 get lock
03:38:31.172 [pool-1-thread-5] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-5 begin wait
03:38:31.173 [pool-1-thread-3] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-3 get lock
03:38:31.173 [pool-1-thread-3] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-3 begin wait
03:38:31.173 [pool-1-thread-4] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-4 get lock
03:38:31.173 [pool-1-thread-4] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-4 begin wait
03:38:31.173 [pool-1-thread-2] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-2 get lock
03:38:31.173 [pool-1-thread-2] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-2 begin wait
03:38:32.161 [main] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - release lock
03:38:32.161 [pool-1-thread-1] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-1 end wait
03:38:34.162 [pool-1-thread-1] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-1 release lock
03:38:34.162 [pool-1-thread-10] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-10 end wait
03:38:36.162 [pool-1-thread-10] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-10 release lock
03:38:36.162 [pool-1-thread-9] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-9 end wait
03:38:38.162 [pool-1-thread-9] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-9 release lock
03:38:38.162 [pool-1-thread-8] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-8 end wait
03:38:40.163 [pool-1-thread-8] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-8 release lock
03:38:40.163 [pool-1-thread-7] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-7 end wait
03:38:42.164 [pool-1-thread-7] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-7 release lock
03:38:42.164 [pool-1-thread-6] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-6 end wait
03:38:44.164 [pool-1-thread-6] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-6 release lock
03:38:44.164 [pool-1-thread-5] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-5 end wait
03:38:46.165 [pool-1-thread-5] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-5 release lock
03:38:46.165 [pool-1-thread-3] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-3 end wait
03:38:48.165 [pool-1-thread-3] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-3 release lock
03:38:48.165 [pool-1-thread-4] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-4 end wait
03:38:50.166 [pool-1-thread-4] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-4 release lock
03:38:50.166 [pool-1-thread-2] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-2 end wait
03:38:52.167 [pool-1-thread-2] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - thread-2 release lock
03:38:52.167 [main] INFO love.iris.firestige.beautyofjavaconcurrency.RandomNotify - done!

Process finished with exit code 0

可以看到,虽然线程池内的线程随机获得了锁。但是当我们notify()的时候,唤醒顺序并不是随机的,而是和进入wait()的顺序一致。notify()唤醒的线程看起来并不是随机的,那么书说错了么?

notify()做了什么?

先说结论,原文的说法没有问题,这里出现顺序一致是特殊情况。那么为什么会有这种情况呢?先上源码,看看notify()都做了什么。

// openjdk8
//./hotspot/src/share/vm/runtime/objectMonitor.cpp

//如果锁没有竞争,那么可以直接把线程从WaitSet出队,并且直接解除阻塞

void ObjectMonitor::notify(TRAPS) {
  CHECK_OWNER();
  if (_WaitSet == NULL) {
     TEVENT (Empty-Notify) ;
     return ;
  }
  DTRACE_MONITOR_PROBE(notify, this, object(), THREAD);

  //获取入队策略
  int Policy = Knob_MoveNotifyee ;

  Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ;
  ObjectWaiter * iterator = DequeueWaiter() ;
  if (iterator != NULL) {
     TEVENT (Notify1 - Transfer) ;
     guarantee (iterator->TState == ObjectWaiter::TS_WAIT, "invariant") ;
     guarantee (iterator->_notified == 0, "invariant") ;
     if (Policy != 4) {
        iterator->TState = ObjectWaiter::TS_ENTER ;
     }
     iterator->_notified = 1 ;
     Thread * Self = THREAD;
     iterator->_notifier_tid = Self->osthread()->thread_id();

     ObjectWaiter * List = _EntryList ;
     if (List != NULL) {
        assert (List->_prev == NULL, "invariant") ;
        assert (List->TState == ObjectWaiter::TS_ENTER, "invariant") ;
        assert (List != iterator, "invariant") ;
     }

     if (Policy == 0) {       // prepend to EntryList
         if (List == NULL) {
             iterator->_next = iterator->_prev = NULL ;
             _EntryList = iterator ;
         } else {
             List->_prev = iterator ;
             iterator->_next = List ;
             iterator->_prev = NULL ;
             _EntryList = iterator ;
        }
     } else
     if (Policy == 1) {
         if (List == NULL) {
             iterator->_next = iterator->_prev = NULL ;
             _EntryList = iterator ;
         } else {
            ObjectWaiter * Tail ;
            for (Tail = List ; Tail->_next != NULL ; Tail = Tail->_next) ;
            assert (Tail != NULL && Tail->_next == NULL, "invariant") ;
            Tail->_next = iterator ;
            iterator->_prev = Tail ;
            iterator->_next = NULL ;
        }
     } else
     if (Policy == 2) {
         if (List == NULL) {
             iterator->_next = iterator->_prev = NULL ;
             _EntryList = iterator ;
         } else {
            iterator->TState = ObjectWaiter::TS_CXQ ;
            for (;;) {
                ObjectWaiter * Front = _cxq ;
                iterator->_next = Front ;
                if (Atomic::cmpxchg_ptr (iterator, &_cxq, Front) == Front) {
                    break ;
                }
            }
         }
     } else
     if (Policy == 3) {
        iterator->TState = ObjectWaiter::TS_CXQ ;
        for (;;) {
            ObjectWaiter * Tail ;
            Tail = _cxq ;
            if (Tail == NULL) {
                iterator->_next = NULL ;
                if (Atomic::cmpxchg_ptr (iterator, &_cxq, NULL) == NULL) {
                   break ;
                }
            } else {
                while (Tail->_next != NULL) Tail = Tail->_next ;
                Tail->_next = iterator ;
                iterator->_prev = Tail ;
                iterator->_next = NULL ;
                break ;
            }
        }
     } else {
        ParkEvent * ev = iterator->_event ;
        iterator->TState = ObjectWaiter::TS_RUN ;
        OrderAccess::fence() ;
        ev->unpark() ;
     }

     if (Policy < 4) {
       iterator->wait_reenter_begin(this);
     }
  }

  Thread::SpinRelease (&_WaitSetLock) ;

  if (iterator != NULL && ObjectMonitor::_sync_Notifications != NULL) {
     ObjectMonitor::_sync_Notifications->inc() ;
  }
}

预备知识:

  • 监视器队列:
    • cxq,持有最近尝试进入监视器的线程(Cxq points to the the set of Recently Arrived Threads attempting entry)
    • EntryList,持有准备进入监视器的线程,通常用一个双向链表或者双向环形链表实现
    • WaitSet,持有调用wait()而阻塞的线程,双向环形链表实现
  • 一个线程任何时间最多只能出现在一个监视器队列

简而言之,notify()的任务就是把WaitSet中的线程移入EntryList或者cxq,在这之后就是竞争监视器锁的过程。

能竞争或者说能给监视器响应的线程只有cxq和EntryList中的线程(Only threads on cxq|EntryList may be responsible for a monitor)。

转移的过程:

抛开特殊情况,如WaitSet为空,或者cxq为空。从WaitSet到EntryList有两个过程:

  • WaitSet出队
  • EntryList入队

WaitSet出队,使用DequeueWaiter()方法直接按照FIFO出队

void ObjectMonitor::notify(TRAPS) {
    ...
    ObjectWaiter * iterator = DequeueWaiter() ;
    ...
}

inline ObjectWaiter* ObjectMonitor::DequeueWaiter() {
  ObjectWaiter* waiter = _WaitSet;
  if (waiter) {
    DequeueSpecificWaiter(waiter);
  }
  return waiter;
}

EntryList入队则分四种策略,由Knob_MoveNotifyee控制。

  1. Knob_MoveNotifyee = 0,头插到EntryList
     if (Policy == 0) {
         if (List == NULL) {
             iterator->_next = iterator->_prev = NULL ;
             _EntryList = iterator ;
         } else {
             List->_prev = iterator ;
             iterator->_next = List ;
             iterator->_prev = NULL ;
             _EntryList = iterator ;
        }
     }
  1. Knob_MoveNotifyee = 1,尾插到EntryList
     if (Policy == 1) {
         if (List == NULL) {
             iterator->_next = iterator->_prev = NULL ;
             _EntryList = iterator ;
         } else {
            ObjectWaiter * Tail ;
            for (Tail = List ; Tail->_next != NULL ; Tail = Tail->_next) ;
            assert (Tail != NULL && Tail->_next == NULL, "invariant") ;
            Tail->_next = iterator ;
            iterator->_prev = Tail ;
            iterator->_next = NULL ;
        }
     }
  1. Knob_MoveNotifyee = 2,头插到cxq
     if (Policy == 2) {
         if (List == NULL) {
             iterator->_next = iterator->_prev = NULL ;
             _EntryList = iterator ;
         } else {
            iterator->TState = ObjectWaiter::TS_CXQ ;
            for (;;) {
                ObjectWaiter * Front = _cxq ;
                iterator->_next = Front ;
                if (Atomic::cmpxchg_ptr (iterator, &_cxq, Front) == Front) {
                    break ;
                }
            }
         }
     }
  1. Knob_MoveNotifyee = 3, 尾插到cxq
     if (Policy == 3) {
        iterator->TState = ObjectWaiter::TS_CXQ ;
        for (;;) {
            ObjectWaiter * Tail ;
            Tail = _cxq ;
            if (Tail == NULL) {
                iterator->_next = NULL ;
                if (Atomic::cmpxchg_ptr (iterator, &_cxq, NULL) == NULL) {
                   break ;
                }
            } else {
                while (Tail->_next != NULL) Tail = Tail->_next ;
                Tail->_next = iterator ;
                iterator->_prev = Tail ;
                iterator->_next = NULL ;
                break ;
            }
        }
     }

回到开头

梳理流程过后我们可以看到,notify()只是将线程从WaitSet向EntryList或者cxq转移,虽然入队的顺序根据jvm中Knob_MoveNotifyee的值来决定,但是在一个运行的JVM中,入队顺序是一致的。真正造成随机性的原因在于被唤醒后的锁竞争过程。

开头的示例代码将全部10个线程加入WaitSet之后,每次只唤醒一个线程,且没有新的线程竞争监视器锁。由于WaitSet是个FIFO的队列,所以日志上表现出notify()的顺序和wait()的顺序一致。