Let's consider the following piece of code:
public static void main(String[] args) throws InterruptedException {
CyclicBarrier cb = new CyclicBarrier(3, () -> {
logger.info("Barrier action starting");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("Barrier action finishing");
});
for (int i = 0; i < 6; i++) {
int counter = i;
Thread.sleep(100);
new Thread(() -> {
try {
logger.info("Try to acquire barrier for {}", counter);
cb.await();
logger.info("barrier acquired for {}", counter);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
I've created barrier with size = 3 and barrirer action which takes 5 seconds.
I see following output:
2019-10-27 15:23:09.393 INFO --- [ Thread-0] my.playground.RemoteServiceFacade : Try to acquire barrier for 0
2019-10-27 15:23:09.492 INFO --- [ Thread-1] my.playground.RemoteServiceFacade : Try to acquire barrier for 1
2019-10-27 15:23:09.593 INFO --- [ Thread-2] my.playground.RemoteServiceFacade : Try to acquire barrier for 2
2019-10-27 15:23:09.594 INFO --- [ Thread-2] my.playground.RemoteServiceFacade : Barrier action starting
2019-10-27 15:23:09.693 INFO --- [ Thread-3] my.playground.RemoteServiceFacade : Try to acquire barrier for 3
2019-10-27 15:23:09.794 INFO --- [ Thread-4] my.playground.RemoteServiceFacade : Try to acquire barrier for 4
2019-10-27 15:23:09.897 INFO --- [ Thread-5] my.playground.RemoteServiceFacade : Try to acquire barrier for 5
2019-10-27 15:23:14.594 INFO --- [ Thread-2] my.playground.RemoteServiceFacade : Barrier action finishing
2019-10-27 15:23:14.595 INFO --- [ Thread-2] my.playground.RemoteServiceFacade : barrier acquired for 2
2019-10-27 15:23:14.595 INFO --- [ Thread-5] my.playground.RemoteServiceFacade : Barrier action starting
2019-10-27 15:23:19.596 INFO --- [ Thread-5] my.playground.RemoteServiceFacade : Barrier action finishing
2019-10-27 15:23:19.597 INFO --- [ Thread-0] my.playground.RemoteServiceFacade : barrier acquired for 0
2019-10-27 15:23:19.597 INFO --- [ Thread-4] my.playground.RemoteServiceFacade : barrier acquired for 4
2019-10-27 15:23:19.597 INFO --- [ Thread-3] my.playground.RemoteServiceFacade : barrier acquired for 3
2019-10-27 15:23:19.597 INFO --- [ Thread-1] my.playground.RemoteServiceFacade : barrier acquired for 1
2019-10-27 15:23:19.597 INFO --- [ Thread-5] my.playground.RemoteServiceFacade : barrier acquired for 5
So we can see that:
- First barrier action lasts 15:23:09 - 15:23:14
- Second barrier action lasts 15:23:14 - 15:23:19
But only one thread was able to log after first barrier action termination:
2019-10-27 15:23:14.595 INFO --- [ Thread-2] my.playground.RemoteServiceFacade : barrier acquired for 2
I expected 3 threads should be able to acquire approximately at 15:23:14 because CyclicBarrier size is 3.
Could you explain this behaviour?
P.S.
I tried to run this code a lot of time and always the similar result.
P.S.2.
I tried to change timings a bit:
public static void main(String[] args) throws InterruptedException {
CyclicBarrier cb = new CyclicBarrier(3, () -> {
logger.info("Barrier action starting");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("Barrier action finishing");
});
for (int i = 0; i < 6; i++) {
int counter = i;
Thread.sleep(1000);
new Thread(() -> {
try {
logger.info("Try to acquire barrier for {}", counter);
cb.await();
logger.info("barrier acquired for {}", counter);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
And I see expected results:
2019-10-27 23:22:14.497 INFO --- [ Thread-0] my.playground.RemoteServiceFacade : Try to acquire barrier for 0
2019-10-27 23:22:15.495 INFO --- [ Thread-1] my.playground.RemoteServiceFacade : Try to acquire barrier for 1
2019-10-27 23:22:16.495 INFO --- [ Thread-2] my.playground.RemoteServiceFacade : Try to acquire barrier for 2
2019-10-27 23:22:16.496 INFO --- [ Thread-2] my.playground.RemoteServiceFacade : Barrier action starting
2019-10-27 23:22:16.998 INFO --- [ Thread-2] my.playground.RemoteServiceFacade : Barrier action finishing
2019-10-27 23:22:16.998 INFO --- [ Thread-0] my.playground.RemoteServiceFacade : barrier acquired for 0
2019-10-27 23:22:16.998 INFO --- [ Thread-2] my.playground.RemoteServiceFacade : barrier acquired for 2
2019-10-27 23:22:16.998 INFO --- [ Thread-1] my.playground.RemoteServiceFacade : barrier acquired for 1
2019-10-27 23:22:17.495 INFO --- [ Thread-3] my.playground.RemoteServiceFacade : Try to acquire barrier for 3
2019-10-27 23:22:18.495 INFO --- [ Thread-4] my.playground.RemoteServiceFacade : Try to acquire barrier for 4
2019-10-27 23:22:19.496 INFO --- [ Thread-5] my.playground.RemoteServiceFacade : Try to acquire barrier for 5
2019-10-27 23:22:19.499 INFO --- [ Thread-5] my.playground.RemoteServiceFacade : Barrier action starting
2019-10-27 23:22:20.002 INFO --- [ Thread-5] my.playground.RemoteServiceFacade : Barrier action finishing
2019-10-27 23:22:20.003 INFO --- [ Thread-5] my.playground.RemoteServiceFacade : barrier acquired for 5
2019-10-27 23:22:20.003 INFO --- [ Thread-3] my.playground.RemoteServiceFacade : barrier acquired for 3
2019-10-27 23:22:20.003 INFO --- [ Thread-4] my.playground.RemoteServiceFacade : barrier acquired for 4
A fascinating question, it's not super trivial but I'll try to explain it as succinctly as I can.
Although multithreading doesn't guarantee any type of execution order, for this answer let's imagine there are two sequences that happen first:
barrier.await()
at the same time.In this case you'd see an output like
The current state of your 6 threads are as follows:
Threads
0
and1
await
on a sharedCondition
since three threads have not reached the barrier "yet"Thread
2
is still runnable as the "tripping" thread for the barrierThreads
3
,4
and5
will wait on alock.lock
invocation of the barrier (the sameLock
instance in which theCondition
was created.When the barrier sees thread
2
it previously recorded0
and1
as reaching the barrier so it knows that this cycle is completed and will release0
and1
. But before it releases the other two threads, it needs to run thebarrierAction
you define as sleeping for 5 seconds, so it does that.You'd then see output
Thread
2
still holds the lock, isRUNNABLE
and is ready to exit the barrier, so it does that and you see this output.But before thread
2
exists, it will signal all other threads waiting on the current barrier. Here is where it gets tricky, theawait
by0
and1
is done on the sharedCondition
. TheCondition
is shared across all barrier "generations". So even though the first two threads wereawait
ing prior to the second 3 threadslock
ing, when asignalAll
is done, the first generation threads still have to wait their turn to wake up.At this point we have 5 threads in a
BLOCKED
(3, 4 & 5) orTIMED_WAITING
(0, 1) state. In this example the timing is important of when they block/wait on theLock
. If they all happen in order the queue for the critical section would be:So that the next threads released will be
Thread-3
, then4
and subsequently5
. The reason the queue looks like this is because all threads reach thelock
at the same time and they all queue up, thread0
and1
obviously reached it first and so make it into the barrier's critical section, but thenawait
while thread2
comes in to wake them up, but now0
and1
get placed on the end of the queue and 3, 4 and 5 will be triggered next.When thread
2
leaves the barrier andsignalAll
threads3
and4
will run and since they are part of the second generation will suspend until thread5
comes through and triggers thebarrier
action. This then prints outFinally, thread
5
willsignalAll
again and the rest of the threads will wake up complete.In this case you will see thread
5
first complete and the rest to follow