Multithreading Basics (7): Proof that the notify method in HotSpot does not have randomness

Get into the habit of writing together! This is the 8th day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

@[toc] When talking about the wait/notify and notifyAll methods earlier, notify said in the comment of the source code that the thread that notify chooses to wake up is arbitrary, but depends on the specific implementation of jvm. The original text is as follows:

 * Wakes up a single thread that is waiting on this object's
 * monitor. If any threads are waiting on this object, one of them
 * is chosen to be awakened. The choice is arbitrary and occurs at
 * the discretion of the implementation. A thread waits on an object's
 * monitor by calling one of the {@code wait} methods.
复制代码

But when talking about notify and notifyAll in many blogs or interviews, many people say notify is random. So is it really random? We now test this situation experimentally.

1. Experiment 1

Define 50 threads, let them wait in turn, and then notify. For obvious comparison, sleep is added after wait, so that threads can wait in sequence by id. There is no lock contention before wait.

package com.dhb.notify;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class NotifyTest{

	private static List<String> before = new LinkedList<>();
	private static List<String> after = new LinkedList<>();

	private static Object lock = new Object();


	public static void main(String[] args) throws InterruptedException{

		for(int i=0;i<50;i++){
			String threadName = Integer.toString(i);
			new Thread(() -> {
				synchronized (lock) {
					String cthreadName = Thread.currentThread().getName();
					System.out.println("Thread ["+cthreadName+"] wait.");
					before.add(cthreadName);
					try {
						lock.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("Thread ["+cthreadName+"] weak up.");
					after.add(cthreadName);
				}
			},threadName).start();
			TimeUnit.MILLISECONDS.sleep(50);
		}

		TimeUnit.SECONDS.sleep(1);

		for(int i=0;i<50;i++){
			synchronized (lock) {
				lock.notify();
				TimeUnit.MILLISECONDS.sleep(10);
			}
		}
		TimeUnit.SECONDS.sleep(1);

		System.out.println(before.toString());
		System.out.println(after.toString());
	}
}
复制代码

The execution result of the above code:

Thread [0] wait.
Thread [1] wait.
Thread [2] wait.
Thread [3] wait.
Thread [4] wait.
Thread [5] wait.
Thread [6] wait.
Thread [7] wait.
Thread [8] wait.
Thread [9] wait.
Thread [10] wait.
Thread [11] wait.
Thread [12] wait.
Thread [13] wait.
Thread [14] wait.
Thread [15] wait.
Thread [16] wait.
Thread [17] wait.
Thread [18] wait.
Thread [19] wait.
Thread [20] wait.
Thread [21] wait.
Thread [22] wait.
Thread [23] wait.
Thread [24] wait.
Thread [25] wait.
Thread [26] wait.
Thread [27] wait.
Thread [28] wait.
Thread [29] wait.
Thread [30] wait.
Thread [31] wait.
Thread [32] wait.
Thread [33] wait.
Thread [34] wait.
Thread [35] wait.
Thread [36] wait.
Thread [37] wait.
Thread [38] wait.
Thread [39] wait.
Thread [40] wait.
Thread [41] wait.
Thread [42] wait.
Thread [43] wait.
Thread [44] wait.
Thread [45] wait.
Thread [46] wait.
Thread [47] wait.
Thread [48] wait.
Thread [49] wait.
Thread [0] weak up.
Thread [19] weak up.
Thread [18] weak up.
Thread [17] weak up.
Thread [16] weak up.
Thread [15] weak up.
Thread [14] weak up.
Thread [13] weak up.
Thread [12] weak up.
Thread [11] weak up.
Thread [10] weak up.
Thread [9] weak up.
Thread [8] weak up.
Thread [7] weak up.
Thread [6] weak up.
Thread [5] weak up.
Thread [4] weak up.
Thread [3] weak up.
Thread [2] weak up.
Thread [1] weak up.
Thread [44] weak up.
Thread [43] weak up.
Thread [42] weak up.
Thread [41] weak up.
Thread [40] weak up.
Thread [39] weak up.
Thread [38] weak up.
Thread [37] weak up.
Thread [36] weak up.
Thread [35] weak up.
Thread [34] weak up.
Thread [33] weak up.
Thread [32] weak up.
Thread [31] weak up.
Thread [30] weak up.
Thread [29] weak up.
Thread [28] weak up.
Thread [27] weak up.
Thread [26] weak up.
Thread [25] weak up.
Thread [24] weak up.
Thread [23] weak up.
Thread [22] weak up.
Thread [21] weak up.
Thread [20] weak up.
Thread [47] weak up.
Thread [46] weak up.
Thread [45] weak up.
Thread [49] weak up.
Thread [48] weak up.
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
[0, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 47, 46, 45, 49, 48]
复制代码

According to the above execution results, it seems that the order of weakup is random. So if we get the results so quickly, there is something wrong with the title of this article. Let's look at the second experiment.

2. Experiment 2

package com.dhb.notify;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class NotifyTest{

	private static List<String> before = new LinkedList<>();
	private static List<String> after = new LinkedList<>();

	private static Object lock = new Object();


	public static void main(String[] args) throws InterruptedException{

		for(int i=0;i<50;i++){
			String threadName = Integer.toString(i);
			new Thread(() -> {
				synchronized (lock) {
					String cthreadName = Thread.currentThread().getName();
					System.out.println("Thread ["+cthreadName+"] wait.");
					before.add(cthreadName);
					try {
						lock.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("Thread ["+cthreadName+"] weak up.");
					after.add(cthreadName);
				}
			},threadName).start();
			TimeUnit.MILLISECONDS.sleep(50);
		}

		TimeUnit.SECONDS.sleep(1);

		for(int i=0;i<50;i++){
			synchronized (lock) {
				lock.notify();
			}
			TimeUnit.MILLISECONDS.sleep(10);
		}
		TimeUnit.SECONDS.sleep(1);

		System.out.println(before.toString());
		System.out.println(after.toString());
	}
}
复制代码

The execution result is as follows:

Thread [0] wait.
Thread [1] wait.
Thread [2] wait.
Thread [3] wait.
Thread [4] wait.
Thread [5] wait.
Thread [6] wait.
Thread [7] wait.
Thread [8] wait.
Thread [9] wait.
Thread [10] wait.
Thread [11] wait.
Thread [12] wait.
Thread [13] wait.
Thread [14] wait.
Thread [15] wait.
Thread [16] wait.
Thread [17] wait.
Thread [18] wait.
Thread [19] wait.
Thread [20] wait.
Thread [21] wait.
Thread [22] wait.
Thread [23] wait.
Thread [24] wait.
Thread [25] wait.
Thread [26] wait.
Thread [27] wait.
Thread [28] wait.
Thread [29] wait.
Thread [30] wait.
Thread [31] wait.
Thread [32] wait.
Thread [33] wait.
Thread [34] wait.
Thread [35] wait.
Thread [36] wait.
Thread [37] wait.
Thread [38] wait.
Thread [39] wait.
Thread [40] wait.
Thread [41] wait.
Thread [42] wait.
Thread [43] wait.
Thread [44] wait.
Thread [45] wait.
Thread [46] wait.
Thread [47] wait.
Thread [48] wait.
Thread [49] wait.
Thread [0] weak up.
Thread [1] weak up.
Thread [2] weak up.
Thread [3] weak up.
Thread [4] weak up.
Thread [5] weak up.
Thread [6] weak up.
Thread [7] weak up.
Thread [8] weak up.
Thread [9] weak up.
Thread [10] weak up.
Thread [11] weak up.
Thread [12] weak up.
Thread [13] weak up.
Thread [14] weak up.
Thread [15] weak up.
Thread [16] weak up.
Thread [17] weak up.
Thread [18] weak up.
Thread [19] weak up.
Thread [20] weak up.
Thread [21] weak up.
Thread [22] weak up.
Thread [23] weak up.
Thread [24] weak up.
Thread [25] weak up.
Thread [26] weak up.
Thread [27] weak up.
Thread [28] weak up.
Thread [29] weak up.
Thread [30] weak up.
Thread [31] weak up.
Thread [32] weak up.
Thread [33] weak up.
Thread [34] weak up.
Thread [35] weak up.
Thread [36] weak up.
Thread [37] weak up.
Thread [38] weak up.
Thread [39] weak up.
Thread [40] weak up.
Thread [41] weak up.
Thread [42] weak up.
Thread [43] weak up.
Thread [44] weak up.
Thread [45] weak up.
Thread [46] weak up.
Thread [47] weak up.
Thread [48] weak up.
Thread [49] weak up.
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
复制代码

The weak up order of this code thread is consistent with the wait order.

3. Problem analysis

Why are the execution results of the above two codes so different, the codes seem to be similar. So where is the problem? Let's see if readers can figure out the problem on their own. This is the same problem that I did before to get this example. Yes, the problem lies in the sleep. Whether the sleep is in the synchronized or the external sleep is very different. Let's look at the code first. experiment one:

for(int i=0;i<50;i++){
    synchronized (lock) {
        lock.notify();
        TimeUnit.MILLISECONDS.sleep(10);
    }
}
复制代码

Experiment 2:

for(int i=0;i<50;i++){
	synchronized (lock) {
		lock.notify();
	}
	TimeUnit.MILLISECONDS.sleep(10);
}
复制代码

就是上述这几行代码,可能造成的结果就不一样。为什么实验一中的结果会出现随机呢?那是因为,当我们执行notify之后,由于sleep在symchronized内部,因此没有释放锁。那么被通知到的线程,就会进入waitSet队列,之后当通知线程进入循环之后,可能会同时竞争synchronized,由于synchronized的BLOCK到RUNNING状态是非公平的。这个通知线程可能再次又获得锁,进行第二次notify。 整个过程复盘如下:

  • 1.初始状态,用N表示通知线程,在notify没执行之前,状态如下;

Before executing notify

  • 2.此后执行notify,假定C1被通知到,进入waitSet队列。

execute notify

  • 3.执行完notify之后会进行sleep,此时仍然由N持有锁,在这之后,sleep结束,N将释放锁。N还会继续执行。当N再次进入循环的时候,此时,N就会进入BLOCK对synchronized的资源进行竞争。那么需要注意的是,这个时候,之前处于BLOCK状态的线程不一定就会执行,因为这是在并发条件下进行的。很大概率的情况下,都会出现同时位于BLOCK队列的情况。

After executing notify

  • 4.那么由于synchronized实际上不是公平锁,其锁竞争的机制具有随机性,那么此时有可能线程N再次获得锁。就是下图这种情况。

N grabs the lock again

  • 5.线程N获得锁之后,会再次notify一个线程到WaitSet队列。

call notify again

  • 6.与第三步类似,可能此时Nsleep完成之后进入了BLOCK而C1、C2也在WaitSet队列等待。

Re-compete after N sleep

  • 7.那么此时,有一种情况就会出现,那就是C2抢到了锁。这样C2就会在C1之前被weak up。

C2 grabs the lock, weakup

通过上述分析,就很容易得出实验一种唤醒次序随机的原因了。我们再来看看实验一的weak up输出:

[0, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 47, 46, 45, 49, 48]
复制代码

According to this output, you can see that the second digit is 19, that is to say, when the thread with thread number 19 is weakup, notify has been called 20 times. For experiment 2, since after each notify, the lock is released, and then sleep is entered, so the notification thread will not compete for the lock with the thread in the WaitSet. Then the order actually obtained in experiment 2 is the order of notify. It is not difficult to see through experiment 2 that notify is not actually random, but sequential.

4. HotSpot source code

In order to further prove the above problem, we can analyze the HotSpot source code. Hotspot source code Click zip to download the source code in zip format:openJDK下载

After this, we know that the synchronized wait and notify are located in ObjectMonitor.cpp. Let's take a look at this specific notify method:

// Consider:
// If the lock is cool (cxq == null && succ == null) and we're on an MP system
// then instead of transferring a thread from the WaitSet to the EntryList
// we might just dequeue a thread from the WaitSet and directly unpark() it.

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") ;
  //重点再这里,是调用的DequeueWaiter方法
  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) {      // append to EntryList
         if (List == NULL) {
             iterator->_next = iterator->_prev = NULL ;
             _EntryList = iterator ;
         } else {
            // CONSIDER:  finding the tail currently requires a linear-time walk of
            // the EntryList.  We can make tail access constant-time by converting to
            // a CDLL instead of using our current DLL.
            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) {      // prepend to cxq
         // prepend to cxq
         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) {      // append to cxq
        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);
     }

     // _WaitSetLock protects the wait queue, not the EntryList.  We could
     // move the add-to-EntryList operation, above, outside the critical section
     // protected by _WaitSetLock.  In practice that's not useful.  With the
     // exception of  wait() timeouts and interrupts the monitor owner
     // is the only thread that grabs _WaitSetLock.  There's almost no contention
     // on _WaitSetLock so it's not profitable to reduce the length of the
     // critical section.
  }

  Thread::SpinRelease (&_WaitSetLock) ;

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

The notify process calls the DequeueWaiter method:

inline ObjectWaiter* ObjectMonitor::DequeueWaiter() {
  // dequeue the very first waiter
  ObjectWaiter* waiter = _WaitSet;
  if (waiter) {//判断_WaitSet是否为空,否则,将第一个元素调用Dequeue方法。
    DequeueSpecificWaiter(waiter);
  }
  return waiter;
}
复制代码

In fact, the first element in _WaitSet is dequeued. This also shows that notify is a sequential operation. be fair.

5. Summary

After the above two experimental analysis and viewing the source code, it can be explained:

  • 1. In HotSpot, notify is executed sequentially, and the first element of the queue is dequeued from the waiting queue. As for other jvms, I haven't touched it for the time being, but this is true for HotSpot. So next time when an interviewer asks the difference between notify and notifyAll, I hope that the answer is no longer random.
  • 2.synchronized is an unfair lock, and this article is the best proof. Although the source code is not analyzed. Due to space limitations, they are not expanded here.
  • 3. The final point is, don't be superstitious in authority, dare to question, and ask why repeatedly if you get a conclusion. No, we can look at the source code. The source code will not lie, and we can also prove it through experiments.

Guess you like

Origin juejin.im/post/7084150894371012644