Hello, I am Xiao Hei, a migrant worker who lives on the Internet.
If you create multiple threads in the main thread, and the threads are started, the main thread needs to wait for the child threads to finish executing before it can execute its own code, how do you do that?
Thread.join()
In Thread class, there is a join() method. This method is a blocking method, and the current Thread will wait for the Thread that invoked the join() method to die before continuing execution.
Let’s go through the code to see the result.
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
Thread t = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " run ~");
});
t.start();
t.join();
}
System.out.println("Main thread completes execution"); }}Copy the code
As you can see from the results, the main thread does not continue executing until all the child threads have finished executing, and each child thread executes in order.
Let’s see how join() blocks the main thread. Look at the source code.
public final void join(a) throws InterruptedException {
// 0 milliseconds is passed by default
join(0);
}
// Synchronized
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
// Tests whether the current thread is still alive
while (isAlive()) {
// Execute wait, and the current thread waits
wait(0); }}else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break; } wait(delay); now = System.currentTimeMillis() - base; }}}Copy the code
The join() method waits for 0 ms by default. The Join (Long Millis) method is a synchronized method; Loop to determine if the current thread is still alive. What does that mean?
- When the main thread calls T’s join() method, it acquires the lock on T.
- The join method calls T’s wait() method to wait, and the wait() method releases the lock, and the main thread blocks after executing its wait().
- Finally, after being woken up by notify, the main thread needs to recirculate to determine if T is still alive, and if it is, wait() is executed again.
After a thread has executed the run() method, the JVM calls the thread’s exit() method to wake up the waiting thread with notifyAll().
private void exit(a) {
if(group ! =null) {
// Terminates the group thread this
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
void threadTerminated(Thread t) {
synchronized (this) {
remove(t);
if (nthreads == 0) {
// Wake up the waiting thread
notifyAll();
}
if (daemon && (nthreads == 0) &&
(nUnstartedThreads == 0) && (ngroups == 0)) { destroy(); }}}Copy the code
If you are careful, you will notice that using thread.join () allows only one Thread to execute, but not multiple threads at the same time. For example, in our code above, Thread 1 can execute before Thread 2 completes, and Thread 1 and Thread 2 cannot execute simultaneously.
CountDownLatch
The utility class CountDownLatch in the JUC package has the same capabilities as the thread.join () method, which can wait for one Thread to complete execution, and supports waiting for multiple threads at the same time. Let’s modify the example above of thread.join ().
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
Thread t = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " run ~");
countDownLatch.countDown();
});
t.start();
}
countDownLatch.await();
System.out.println("Main thread completes execution"); }}Copy the code
CountDownLatch needs to specify a count at creation time, decrement by calling the countDown() method after execution in the child thread, and await() on the main thread until the count is reduced to zero.
From the results of the run, we can see that the 100 child threads are not executed sequentially, but randomly.
Let’s look at CountDownLatch’s source code to see how this works.
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
Copy the code
In CountDownLatch we see a Sync variable. Sync is a subclass of AQS, as we learned from the previous AQS source code analysis.
The count value passed in by the constructor is first assigned as an argument to the state variable in Sync.
Then let’s look at what the CountDownLath. CountDown () method does in the thread.
public void countDown(a) {
// Release the shared lock
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
Copy the code
If you read my previous AQS source code analysis, you must be familiar with it. This code is the unlock process of shared lock, which is essentially state-1.
So how does the main thread implement waiting? CountDown () = countDown(); countDown() = countDown(); countDown() = countDown(); countDown() = countDown(); countDown() = countDown();
Let’s take a look at the source code, is it the same as we guess?
public void await(a) throws InterruptedException {
// The shared lock can be acquired interruptively
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// Try to obtain the shared lock
if (tryAcquireShared(arg) < 0)
// State is not yet 1
doAcquireSharedInterruptibly(arg);
}
// Get the lock status. When state is reduced to 0, return 1
protected int tryAcquireShared(int acquires) {
return (getState() == 0)?1 : -1;
}
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// go to the back of the line
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return; }}// The thread is in the park
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw newInterruptedException(); }}finally {
if(failed) cancelAcquire(node); }}Copy the code
We can see that the await() method is exactly the same as the shared lock unlocking process we saw yesterday, which matches our guess.
Therefore, the underlying implementation of CountDownLatch also relies on AQS, and now you must have a better understanding of AQS.
The difference between
Let’s compare thread.join () with CountDownLatch:
- Thread.join() is a method of the Thread class, and CountDownLatch is a utility class in the JUC package;
- Thread.join() is implemented by Object wait() and notifyAll(), and CountDownLatch is implemented by AQS.
- Thread.join() supports only one Thread waiting, not multiple threads waiting at the same time. CountDownLatch supports this, so CountDownLatch is more efficient.
All right, that’s it for this episode, and I’ll see you next time.