Conceptos básicos de subprocesos múltiples (7): prueba de que el método de notificación en HotSpot no tiene aleatoriedad

¡Acostúmbrate a escribir juntos! Este es el octavo día de mi participación en el "Nuggets Daily New Plan · April Update Challenge", haz clic para ver los detalles del evento .

@[toc] Cuando se habló de los métodos wait/notify y notificarAll anteriormente, notifique dicho en el comentario del código fuente que el subproceso que notifica elige despertarse es arbitrario, pero depende de la implementación específica de jvm. El texto original es el siguiente:

 * 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.
复制代码

Pero cuando se habla de notificar y notificarTodo en muchos blogs o entrevistas, muchas personas dicen que notificar es aleatorio. Entonces, ¿es realmente aleatorio? Probamos ahora esta situación experimentalmente.

1. Experimento 1

Defina 50 subprocesos, déjelos esperar por turno y luego notifique. Para una comparación obvia, la suspensión se agrega después de la espera, de modo que los subprocesos puedan esperar en secuencia por id. No hay contención de bloqueo antes de esperar.

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());
	}
}
复制代码

El resultado de la ejecución del código anterior:

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]
复制代码

Según los resultados de ejecución anteriores, parece que el orden de debilitamiento es aleatorio. Entonces, si obtenemos los resultados tan rápido, hay algo mal con el título de este artículo. Veamos el segundo experimento.

2. Experimento 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());
	}
}
复制代码

El resultado de la ejecución es el siguiente:

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]
复制代码

El orden ascendente débil de este subproceso de código es coherente con el orden de espera.

3. Análisis de problemas

¿Por qué los resultados de ejecución de los dos códigos anteriores son tan diferentes? Los códigos parecen ser similares. ¿Entonces, dónde está el problema? Veamos si los lectores pueden resolver el problema por sí mismos. Este es el mismo problema que hice antes para obtener este ejemplo. Sí, el problema radica en el sueño, si el sueño es sincronizado o externo es muy diferente. Veamos primero el código. experimento uno:

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

Experimento 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没执行之前,状态如下;

Antes de ejecutar notificar

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

ejecutar notificar

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

Después de ejecutar notificar

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

N agarra el candado de nuevo

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

llamar a notificar de nuevo

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

Vuelva a competir después de dormir N

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

C2 agarra el candado, debilitado

通过上述分析,就很容易得出实验一种唤醒次序随机的原因了。我们再来看看实验一的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]
复制代码

De acuerdo con este resultado, se puede ver que el segundo dígito es 19, es decir, cuando el hilo con el número de hilo 19 se debilita, se ha llamado a la notificación 20 veces. Para el experimento 2, dado que después de cada notificación, se libera el bloqueo y luego se ingresa a la suspensión, por lo que el subproceso de notificación no competirá por el bloqueo con el subproceso en WaitSet. Entonces el orden realmente obtenido en el experimento 2 es el orden de notificación. No es difícil ver a través del experimento 2 que la notificación no es en realidad aleatoria, sino secuencial.

4. Código fuente de HotSpot

Para probar aún más el problema anterior, podemos analizar el código fuente de HotSpot. Código fuente del punto de acceso Haga clic en zip para descargar el código fuente en formato zip:openJDK下载

Después de esto, sabemos que la espera y notificación sincronizadas se encuentran en ObjectMonitor.cpp. Echemos un vistazo a este método de notificación específico:

// 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() ;
  }
}
复制代码

El proceso de notificación llama al método DequeueWaiter:

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

De hecho, el primer elemento de _WaitSet se quita de la cola. Esto también muestra que notificar es una operación secuencial. Sé justo.

5. Resumen

Después de los dos análisis experimentales anteriores y de ver el código fuente, se puede explicar:

  • 1. En HotSpot, la notificación se ejecuta secuencialmente y el primer elemento de la cola se retira de la cola de espera. En cuanto a otros jvms, no lo he tocado por el momento, pero esto es cierto para HotSpot. Entonces, la próxima vez que un entrevistador pregunte la diferencia entre notificar y notificar a todos, espero que la respuesta ya no sea aleatoria.
  • 2.synchronized es un bloqueo injusto, y este artículo es la mejor prueba. Aunque no se analiza el código fuente. Debido a limitaciones de espacio, no se amplían aquí.
  • 3. El punto final es, no seas supersticioso en autoridad, atrévete a cuestionar y pregunta por qué repetidamente si obtienes una conclusión. No, podemos ver el código fuente. El código fuente no miente, y también podemos probarlo a través de experimentos.

Supongo que te gusta

Origin juejin.im/post/7084150894371012644
Recomendado
Clasificación