1. The background

Recently attended a company’s interview, don’t know why now company like arrange at 2 PM, should be they have just the end of the lunch break, had no choice but to sacrifice his lunch break, very not easy after more than an hour of the subway finally by the target company, the personnel of the little girl led me to the conference room directly to a written examination paper is removed, So let’s do the problem.

2. The title

The first question is about concurrent threads, which immediately confused me:

// Write the final output of this program

public class ThreadJoinTest {
    public static void main(String[] args) throws Exception {
        // TODO Auto-generated method stub
        // Define two lock objects.
        Object lock1 = new Object();
        Object lock2 = new Object();
        
        Thread thread1 = new Thread() {
            @Override
            public void run(a) {
                // Get the lock object 'lock1' before starting the thread body.
                synchronized(lock1) {
                    // Prints information about the start of thread execution.
                    System.out.println("thread1 start");
                    try {
                        // Sleep for one minute to simulate time-consuming tasks.
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    // Get lock object 'lock2', note that the thread still holds lock 'lock1',
                    // The thread attempts to acquire the lock object 'lock2' while holding the lock 'lock1'.
                    synchronized(lock2) {
                        System.out.println("thread1 end");
                    }
                    // Thread releases lock object 'lock2'.
                }
                // Thread releases lock object 'lock1'.}}; Thread thread2 =new Thread() {
            @Override
            public void run(a) {
                // Get the lock object 'lock2' before starting the thread body.
                synchronized(lock2) {
                    // Prints information about the start of thread execution.
                    System.out.println("thread2 start");
                    try {
                        // Sleep for one minute to simulate time-consuming tasks.
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    // Try to acquire lock object 'lock1' while thread holds lock object 'lock2'.
                    synchronized(lock1) {
                        System.out.println("thread2 end");
                    }
                    // Release the lock object 'lock1'.
                }
                // Release the lock object 'lock2'.}};// Start the thread
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        // The main thread sleeps for two minutes to simulate time-consuming tasks.
        Thread.sleep(2000);
        // Prints the end of the main thread.
        System.out.println("main thread end"); }}Copy the code

My first impression of the topic is that it examines the problem of thread concurrency and deadlock. I think the execution process of the program is as follows:

  1. The first threadthread1The execution starts and the lock object is acquiredlock1“Will print directlythread1 start, then slept for 1 minute to simulate time-consuming tasks;
  2. Then the threadthread2The lock object has also been started and acquiredlock2“Will print directlythread2 start, then slept for 1 minute to simulate time-consuming tasks;
  3. When a threadthread1To prepare for the continuation of the hibernation, obtain the lock objectlock2, because this is the threadthread2Hold the locklock2, so the threadthread1The lock object is blocked because it cannot be acquired.
  4. When a threadthread2To prepare for the continuation of the hibernation, obtain the lock objectlock1Since this is a threadthread1Hold the locklock1, so the threadthread2The lock object is blocked because it cannot be acquired.
  5. In the previous process,thread1holdlock1Waiting for thelock2At the same timethread2holdlock2Waiting for thelock1, obviously in a deadlock state, so neither thread can continue to execute;
  6. Due to thethread1andthread2If it’s deadlocked and can’t even proceed, then the main thread gets a chance to execute and printmain thread end.

To sum up, my final result is:

thread1 start
thread2 start
main thread end
Copy the code

When I communicated with the interviewer in person, I specifically mentioned this topic. The interviewer said that this question mainly investigated the use of join. Obviously, I did not have a correct understanding of this, and the final answer was also wrong.

3. Think about

3.1 usage

Thread. Join () : Thread. Join () : Thread. Join () : Thread.

// Thread.java

/**
 * Waits for this thread to die.
 *
 * <p> An invocation of this method behaves in exactly the same
 * way as the invocation
 *
 * <blockquote>
 * {@linkplain #join(long) join}{@code (0)}
 * </blockquote>
 *
 * @throws  InterruptedException
 *          if any thread has interrupted the current thread. The
 *          <i>interrupted status</i> of the current thread is
 *          cleared when this exception is thrown.
 */
public final void join(a) throws InterruptedException {
    join(0);
}

Copy the code

The description of this method in the source code is simply “wait for the thread to die”, which means that a thread calling another thread’s join method has to wait for the thread to die before it can continue to execute, which is equivalent to changing the concurrent threads into serial execution sequence at this point in time.

With this in mind, the analysis of deadlock waits in thread1 and Thread2 is correct. The key is whether the main thread can continue to execute after this point. Since thread1.join() and thread2.join() are called on the main thread, it means that the main thread must wait for the two threads to finish before continuing. However, thread1 and thread2 are deadlocked and cannot die, so the main thread cannot continue. So the final output should be:

thread1 start
thread2 start
Copy the code

I have run this code myself since I came back, and the results are consistent with the analysis, and I have figured out what thread.join () is about.

3.2 Implementation Principles

Thread. Join () : Thread. Of course not, we learned as much as we could about its internal implementation.

In short, there are two questions to know:

  1. How do I get the current thread to callThread.join()Then stop executing until another thread dies?
  2. How does the current thread continue to start execution after another thread dies?

3.2.1 How to Stop a Vm

Let’s look at the declaration and definition of thread.join () :

// Thread.java

/** * Waits for this thread to die. */
public final void join(a) throws InterruptedException {
    Call another overloaded function directly.
    join(0);
}
    
/**
 * Waits at most {@code millis} milliseconds for this thread to
 * die. A timeout of {@code 0} means to wait forever.
 *
 * <p> This implementation uses a loop of {@code this.wait} calls
 * conditioned on {@code this.isAlive}. As a thread terminates the
 * {@code this.notifyAll} method is invoked. It is recommended that
 * applications not use {@code wait}, {@code notify}, or
 * {@code notifyAll} on {@code Thread} instances.
 *
 */
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) {
        while (isAlive()) {
            // Wait while the thread is still alive.
            wait(0); }}else {
        while (isAlive()) {
            // The wait time is not directly used with the specified millis, because it is possible to exit the loop.
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            While the thread is still alive, wait a while.
            wait(delay);
            // Update the now time information to exit the loop due to delay <= 0.now = System.currentTimeMillis() - base; }}}Copy the code

The code of this function is not large and the logic is easy to understand. That is, after calling the join() method of thread B in thread A, thread A will be in the wait state for thread B. Depending on the parameters passed in, thread A can wait for A long time or only for A period of time.

3.2.2 How can I Recover data

Since thread A is in wait state after calling thread B’s join method, when does thread A resume execution? The join method with no parameters is only described here, which is the case of always waiting. Thread A cannot be restored until thread B dies. Thread A cannot be restored until thread B dies.

// Thread.java

/** * This method is called by the system to give a Thread * a chance to clean up before it actually exits. */
private void exit(a) {
    if(group ! =null) {
        // Call the destruction callback
        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;
}
Copy the code

ThreadTerminated () is terminated by calling exit to retrieve data before the thread exits. The code is terminated as empty data except for group.threadterminated (). The recovery logic is probably hidden in the language. The group is an instance of a ThreadGroup, which is created by the thread at initialization, and can simply be interpreted as belonging to that ThreadGroup.

Directly to see ThreadGroup. ThreadTerminated () code:

/**
 * Notifies the group that the thread {@code t} has terminated.
 *
 * <p> Destroy the group if all of the following conditions are
 * true: this is a daemon thread group; there are no more alive
 * or unstarted threads in the group; there are no subgroups in
 * this thread group.
 *
 * @param  t
 *         the Thread that has terminated
 */
void threadTerminated(Thread t) {
    synchronized (this) {
        remove(t);

        if (nthreads == 0) {
            // Wake up all waiting threads.
            notifyAll();
        }
        if (daemon && (nthreads == 0) &&
            (nUnstartedThreads == 0) && (ngroups == 0)) { destroy(); }}}Copy the code

NotifyAll () is obviously called when A thread is destroyed to wake up all waiting threads so that thread A can resume running when thread B dies.

4. Develop

Join (); Thread (); Thread (); join();

/** * A hint to the scheduler that the current thread is willing to yield * its current use of a processor. The scheduler is free to ignore this * hint. * * <p> Yield is a heuristic attempt to improve relative progression * between threads that would otherwise over-utilise a CPU. Its use * should be combined with detailed profiling and benchmarking to * ensure that it actually has the desired effect. * * <p> It is rarely appropriate to use this method. It may be useful * for debugging or testing purposes, where it may help to reproduce * bugs due to race conditions. It may also be useful when designing * concurrency control  constructs such as the ones in the * {@link java.util.concurrent.locks} package.
 */
public static native void yield(a);
Copy the code

This method is a native method, so we can’t directly see its internal implementation, so let’s take a look at its declaration, which mentions two important information:

  1. Function: Tells the thread scheduler that the current thread intends to abandon the use of the processor. Whether the processor will call the thread again based on this information depends on the specific scheduling policy.
  2. Usage scenario: This method is used for debugging, because this method does not guarantee that the current thread calls this method, other threads will get the processor, and therefore cannot be used to control the order of execution between threads.

To give a simple example:

public class ThreadYieldTest {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        YieldThread thread1 = new YieldThread("thread_1");
        YieldThread thread2 = new YieldThread("thread_2");
        thread1.start();
        thread2.start();
    }

    private static class YieldThread extends Thread {
        private String name;
        public YieldThread(String name) {
            super(name);
            this.name = name;
        }
        
        @Override
        public void run(a) {
            for (int i = 0; i < 50; i ++) {
                System.out.println(name + ":" + i);
                if (i == 25) {
                    // Call yield in an attempt to abort execution halfway through the thread.
                    System.out.println(name + ": yield");
                    Thread.yield();
                }
            }
        }
    }
}
Copy the code

If you are interested in running this program several times, you can see that it is not always possible for a thread1 or thread2 to yield to another thread and start executing. Because of this uncertainty, it is not recommended to use this method in code to control execution between threads.

5. To summarize

Through a simple interview question, can learn a little knowledge, is also a kind of improvement on their own, I feel that they usually think about these basic problems too little, in solving bugs, or pay attention to the accumulation of knowledge.