Self-cultivation of Mianba: Java thread topic

Wang Youzhi , a mutual gold fisherman who shares hard-core Java technology,
joins the group of Java people: Java people who are rich together

When I usually surf the Internet, I have collected a lot of stereotyped essays and interview essays. Although there are many contents, the quality is mixed. Therefore, I summarized and sorted out the collected content, reviewed the information and re-answered, and gave each eight-legged essay a score.

Well, let’s not talk nonsense and let’s get down to business. Today’s topic is  stereotyped essays related to threads in Java interviews , which mainly involve the following:

  • Basic concepts of concurrent programming
  • Basic concepts of threads
  • Java 中的 Thread ,Runnable,Callable

Due to my limited level, it is inevitable that there will be mistakes in the answering process. Please give priority to criticism and correction, and try not to spray~~

Tips

  • Stereotype essays usually appear in the first and second rounds of interviews, which are the stepping stones of the interview. The focus of the third round is how to explain the projects you have done;
  • The main sources of stereotyped essays are the documents of various institutions (the father and stepfather of Java, Mou Ling, Mo Pao, Mo Ke, etc.) and various bloggers;
  • A small number of stereotyped essays come from the real interview experience of myself and my friends, and the titles will be marked with "true" ;
  • This article has completed the production of PDF documents, and extracted keywords [Mianba's self-cultivation].

Concept

This part is the basic concept and theoretical basis of concurrent programming. The overall difficulty is relatively low, and when you have a certain number of working years, you will rarely involve such issues. Everyone should understand.

Concurrency and Parallelism

Concurrency , in the operating system, refers to several programs in a period of time between being started and running and running to completion, and these programs are all running on the same processor, but there is only one at any point in time Programs run on processors.

Parallel , in the operating system, means that a group of programs are executed at an independent and asynchronous speed, and the programs are executed together no matter from the micro or the macro. In contrast, concurrency means: in the same period of time, two or more programs are executed with time overlap (simultaneous macroscopically, and sequential execution microscopically)

Concurrency is executed at the same time at the macro level, but it is executed alternately at the micro level, while parallelism is executed at the same time whether it is macro or micro.

Tips : For example, parallelism is like turning on two lights, and they are on at the same time; while concurrent is a light, which seems to be "steady on" to the naked eye, but actually under the action of alternating current, the light is always on. It flickers again, but it cannot be observed by the naked eye.

References : Concurrency (Baidu Encyclopedia) , Parallel (Baidu Encyclopedia)


synchronous and asynchronous

Synchronization : Synchronization can be understood as whether the status of the sender and receiver, the caller and the caller, etc. can be kept in time in scenarios such as communication, function call, and protocol interaction at adjacent layers of the protocol stack. unanimous. If one party completes an action, the other party immediately modifies its state, which is synchronization.

Asynchronous : It means that the caller sends a request and returns immediately. The request may not even reach the receiver. Or the caller polls to get it.

Reference : Synchronization (Wikipedia)


blocking and non-blocking

Blocking and non-blocking refer to the state of the program while waiting for the result of the call.

Blocking : When called, the thread will be suspended/suspended/blocked until the operation is completed, and the subsequent operation will be performed after returning the result. At this point, the program cannot perform other operations and will wait until the call result returns.

Non-blocking (Non-blocking) : When called, even if the operation has not been completed and the result has not been obtained, the thread will not be suspended/suspended/blocked , and the program can continue to perform subsequent operations.


Threads and Processes

Process (process) , used to be the basic unit of operation of the time-sharing system. In a process-oriented design system, it is the basic execution entity of the program; in a thread-oriented design system, the process itself is not the basic execution unit, but the thread container .

thread (thread) , in computer science, is the division of a process into two or more threads (instances) or subprocesses that are executed concurrently by a single processor (single thread) or multiple processors (multithreaded) or multi-core processing system .

The difference between process and thread:

process thread
A process has its own memory space, file handles, system signals and environment variables, etc. All threads share the resources of the process, including memory space, file handles, system signals, etc.
A process is an independent execution unit , has its own stack space, and needs to use an inter-process communication mechanism for data exchange A thread is an execution unit inside a process , shares the address space of the process, and can directly access the global variables and heap space of the process
Switching between processes is expensive . Switching between processes is much more time-consuming and expensive than switching between threads, because process switching needs to save and restore more state information, such as memory images, file handles, system signals, etc. The overhead of switching between threads is small . Thread switching only needs to save and restore a small number of registers and stack information
Resource isolation between processes is obvious, and the security between processes is high Sharing resources between threads is likely to cause race conditions, and the security between threads is low

References : Process (Wikipedia) , Thread (Wikipedia)


3 elements of concurrent programming

Atomicity : Refers to the indivisibility of a transaction. All operations of a transaction are either executed continuously or none of them are executed; Visibility:
In software engineering, it refers to the visibility between objects, which means that an object can see The ability to see or be able to refer to another object;
ordering : ordering refers to the execution order of operations performed by multiple threads or processes, and the order of execution is consistent with the order in the program code or conforms to expected rules.

Tips : No orderly explanation was found in Wikipedia and Baidu Encyclopedia, and the explanation of ChatGPT is used here.

References : 8 Questions You Must Know About Threads (Part 1) , Atomicity (Baidu Encyclopedia) , Visibility (Baidu Encyclopedia)


thread starvation

Thread Starvation refers to the state that in a multi-threaded competitive environment, a thread cannot obtain the required resources for a long time, or cannot be scheduled for a long time, resulting in the state that the task cannot be completed.

Common causes of thread starvation are as follows:

  • Resource competition , multiple threads compete for necessary resources, and a thread cannot obtain resources for a long time;
  • Thread priority , improper setting of thread priority, resulting in threads with lower priority not being scheduled for a long time;
  • Lock competition , competition with resources, but at this time the competition is the lock that protects the resource.

context switch

The process of switching CPU time from one thread to another when multiple threads share the same CPU. In this process, it is necessary to save the context information of the thread (such as: program counter, register state, stack pointer, etc.), and load the online context information of another thread at the same time, so that the system can execute correctly.

Tips

Reference : Context switching (Wikipedia)


True: deadlock and deadlock resolution

Interview companies : Suning, Prime Finance, NetEase

Deadlock , when two or more computing units are waiting for the other to stop executing to obtain system resources, but neither party exits early, it is called a deadlock.

Four conditions are required to form a deadlock:

  • Mutual Exclusion : A resource can only be occupied by one process/thread. When a process/thread acquires a resource, other processes/threads cannot access the resource and can only wait for the resource to be released;
  • Request and Hold Conditions (Hold and Wait) : While the process/thread is holding resources, it continues to request other resources and does not release the held resources;
  • No Preemption : Resources already held cannot be forcibly preempted, and can only be acquired by other processes/threads after they are actively released by the process/thread;
  • Circular Wait condition (Circular Wait) : There is a group of processes/threads that request each other for resources held by each other, and the threads form a loop of circular waiting.

image

The core of solving the deadlock problem is to break one of the four conditions:

  • Destruction of mutual exclusion conditions : allowing resources to be accessed at the same time;
  • Destroy request and hold conditions : Before applying for other resources, the process/thread needs to release the current resource to avoid blocking other threads;
  • Destroy the inalienable condition : Allow processes/threads with higher priority to forcibly deprive resources held by other threads;
  • Destroy the circular waiting condition : number the resources, and force them to be acquired in the order of numbers when acquiring resources.

References : Deadlock (Wikipedia) , 8 questions you must know about threads (Part 2)


true: thread communication

Interview company : Lili.com

Two common inter-thread communication models in concurrent programming: shared memory and message passing .

Shared memory : refers to multiple threads running on different cores. After the data on any core cache is modified and refreshed to the main memory, other cores update their own caches.

image

Message passing : Multiple threads can communicate through a message queue, threads can send messages to the queue, and other threads can get messages from the queue and process them.

Traditional object-oriented programming languages ​​usually use shared memory to communicate between threads, such as Java, C++, etc. But Java can   implement message passing of Actor model through Akka . Golang is a loyal fan of message passing. The first sentence in " Go Proverbs " is:

Don’t communicate by sharing memory, share memory by communicating.

References : shared memory (Wikipedia) , message passing (Wikipedia) , pipes ( Wikipedia) , shared memory (Baidu Baike) , message passing (Baidu Baike)


True: Advantages and Challenges of Multithreading

Interview company : Suning

The fundamental reason for using multithreading is to **"squeeze" hardware performance** and improve program efficiency .

  • Take advantage of multi-core CPUs : under multi-core CPUs, a single-threaded program only uses one core at a time, and the rest of the cores are idle, resulting in a waste of resources;
  • Improve the utilization of system resources : Even in a single-core scenario, the CPU is idle for a long time when the program is waiting for a response or file IO operation. At this time, multi-threading can make full use of the CPU.

But the introduction of multithreading also brings some challenges:

  • Context switching : Please refer to the above for details;
  • Deadlock : Please refer to the above for details;
  • Resource constraints : Too many threads will consume a lot of memory and create more CPU competition, which will affect the performance of the program. The number of threads should be reasonably controlled according to the hardware and program during design;
  • Thread-safety issues : Concurrent access to shared resources may cause data consistency issues or other unexpected results.
  • Increased programming difficulty : Because threads are executed concurrently, there are uncertain behaviors, such as: the execution order of threads leads to differences in results, which will cause difficulties in development and debugging.

References: 8 Questions You Must Know About Threads (Part 2) , The Art of Java Concurrent Programming (Douban)


thread safety

Thread safety : refers to when a function or function library is called in a multi-threaded environment, it can correctly handle the common variables between multiple threads, so that the program function can be completed correctly.

In layman's terms, it can be understood that the execution results of the program in a multi-threaded environment are consistent with those in a single-threaded environment.

Tips : This is slightly different from the ultimate goal of as-if-serial semantics mentioned in JMM. As-if-serial semantics emphasizes that no matter how reordering is done, the semantics in a single-threaded scenario cannot be changed (or the execution result remains unchanged) .

References : Thread Safety (Wikipedia) , Thread Safety (Baidu Encyclopedia) , In-depth understanding of JMM and Happens-Before

Principles

Next is the Java application chapter, mainly about threads in Java, Thread class, Runnable interface, Callable interface and Future interface.


Threads in Java

Early Linux systems did not support threads, but "threads" could be simulated through programming languages, but their essence was still a process. At this time, we considered threads in Java to be user threads. In 2003, RedHat preliminarily completed the NPTL (Native POSIX Thread Library) project, and realized the service number POSIX standard threads through lightweight processes. At this time, threads in Java are kernel threads. Therefore, Java programs running on modern servers  will map the Java threads used to a kernel thread .

So we can get such a formula: Java thread ≈ operating system kernel thread ≈ operating system lightweight process Java thread \approx operating system kernel thread \approx operating system lightweight processJava thread _ _ _   operating system kernel thread  Operating system lightweight process .

Then for the thread scheduling method, we can get: Java thread scheduling method ≈ operating system process scheduling method Java thread scheduling method \approx operating system process scheduling methodScheduling method of J a v a thread  How the operating system processes are scheduled .

It just so happens that preemptive process scheduling is used in Linux . Therefore, it is not that the preemptive thread scheduling method is implemented in the JVM, but Java uses the Linux process scheduling method, and Linux chooses the preemptive process scheduling method .

Reference materials : 8 questions you must know about threads (below)


true: how the thread was created

Interview company : Suning

There is only one way to create threads in Java . From the perspective of Java, it can be considered that executing Thread thread = new Thread() creates a thread; and calling Thread#start means that the thread at the operating system level is created and started.

// 创建Java层面的线程
Thread thread = new Thread();
// 创建系统层面的线程
thread.start();

Tips : Usually there are at least 4 ways to create threads on the Internet:

  • Inherit the Thread class
  • Implement the Runnable interface
  • Implement the Callable interface
  • Created by thread pool

But this is a wrong conclusion. The main purpose of implementing the Runnable interface or the Callable interface is to rewrite Runnable#runthe method to implement business logic. To actually create and start a Java thread, you still need to create a Thread object and call Thread#start method.

Tips : There are 6 ways to create threads in other materials~~

Reference materials : 8 questions you must know about threads (Part 1)


true: thread state and state transition

Interview companies : Jingdong, Baidu

There are 6 thread states defined in Java:

  • **NEW (new): **The thread has not been started after it is created (the Thread.start method is not called);
  • RUNNABLE (runnable) : The thread in the runnable state waits in the Java virtual machine for the scheduling thread to be selected to obtain the CPU time slice;
  • BLOCKED (blocked) : The state of the thread blocked waiting for the monitor lock. The thread in the blocked state is waiting for the monitor lock to enter the synchronized code block/method, or re-enter the synchronized code block/method after calling Object.wait;
  • WAITING (waiting) : The thread is in the waiting state, and the thread in the waiting state is waiting for a specific operation (notification or interrupt) performed by another thread;
  • TIMED_WAITING (timeout waiting) : The thread is in the timeout waiting state. Unlike the waiting state, the thread will be automatically woken up after the specified time;
  • TERMINATED : Thread execution ends.

The thread state is defined as the inner class state of Thread:

public enum State {
  NEW,
  RUNNABLE,
  BLOCKED,
  WAITING,
  TIMED_WAITING,
  TERMINATED;
}

Please refer to the following figure for the transition of thread state:

image

Reference materials : 8 questions you must know about threads (Part 1)


True: what the Object#wait method does

Interview company : Suning

Object#wait Make the thread wait while releasing the lock, and the thread enters the WAITING or TIMED_WAITING state . Object#waitThere are 3 overloaded methods:

public final void wait() throws InterruptedException;
    
public final native void wait(long timeoutMillis) throws InterruptedException;
    
public final void wait(long timeoutMillis, int nanos) throws InterruptedException;

Since Object#waitthe lock is released, it needs to be called in a synchronized block (synchronized block), because only when the lock is acquired first can it be released.

Tips : Often used in interviews Thread#sleepto compare with.

References : 8 Questions You Must Know About Threads (Part 2)


The role of Object#notify/Object#notifyAll method

Object#notifyBoth l and Object#notifyAll are used to wake up the thread. Object#notifyRandomly wakes up a waiting thread , Object#notifyAllwakes up all waiting threads. The thread awakened by Object#notify Object#notifyAllwill not be executed immediately, but will join the queue competing for the built-in lock, and only the thread that successfully acquires the lock will continue to execute.

References : 8 Questions You Must Know About Threads (Part 2)


Why call the Object#wait method in a loop?

If the wait condition is not checked in the loop, the thread in the wait state may be awakened by mistake, and skipping the check of the wait condition at this time may cause unexpected problems. For example: the scenario of producers and consumers.

    public static void main(String[] args) {
      Product product = new Product(0);
      new Thread(() -> {
        for (int i = 0; i < 3; i++) {
          try {
            product.decrement();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          }
        }
        System.out.println(Thread.currentThread().getName() + ",状态:" + Thread.currentThread().getState());
      }, "consumer-1").start();
      
      new Thread(() -> {
        for (int i = 0; i < 3; i++) {
          try {
            product.decrement();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          }
        }
      }, "consumer-2").start();
      
      new Thread(() -> {
        for (int i = 0; i < 3; i++) {
          try {
            product.decrement();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          }
        }
      }, "consumer-3").start();
      
      new Thread(() -> {
        for (int i = 0; i < 9; i++) {
          try {
            product.increment();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          }
        }
        , "producer").start();
    }
    
    static class Product {
        private int count;
        private Product(int count) {
            this.count = count;
        }
        /**
         * 生产
         */
        private synchronized void increment() throws InterruptedException {
            if (this.count > 0) {
                this.wait();
            }
            count++;
            System.out.println("[" + Thread.currentThread().getName() + "]生产产品,当前总数:" + this.count);
            this.notifyAll();
        }
        /**
         * 消费
         */
        private synchronized void decrement() throws InterruptedException {
            if (this.count == 0) {
                this.wait();
            }
            count--;
            System.out.println("[" + Thread.currentThread().getName() + "]消费产品,当前总数:" + this.count);
            this.notifyAll();
        }
    }

The modification scheme is very simple. You only need to check the waiting conditions in the loop . The modified code is as follows:

    static class Product {
    
      private synchronized void increment() throws InterruptedException {
        while (this.count > 0) {
          this.wait();
        }
        count++;
        System.out.println("[" + Thread.currentThread().getName() + "]生产产品,当前总数:" + this.count);
        this.notifyAll();
      }
      
      private synchronized void decrement() throws InterruptedException {
        while (this.count == 0) {
          this.wait();
        }
        count--;
        System.out.println("[" + Thread.currentThread().getName() + "]消费产品,当前总数:" + this.count);
        this.notifyAll();
      }
    }

References : 8 Questions You Must Know About Threads (Part 2)


Why Object#wait, Object#notify and Object#notifyAll methods are not placed in Thread class?

The built-in lock (ObjectMonitor) provided by Java is at the object level, that is, each object has a built-in lock. However Obejct#wait, Obejct#notifyas with Obejct#notifyAlloperations involving built-in locks, this has nothing to do with threads, only objects, so put them in the parent class Object of all objects.

References : 8 Questions You Must Know About Threads (Part 2)


Why are Object#wait, Object#notify and Object#notifyAll methods called in a synchronized block?

Because these three methods all involve the operation of built-in locks.

Object#waitThe method releases the lock, Object#notifyand Object#notifyAllis used to notify other threads that the current lock is available, and those who perform these operations hold the lock, or know the status of the lock, so they must be called in synchronized.

References : 8 Questions You Must Know About Threads (Part 2)


Difference between Thread#start and Thread#run

Thread#startA thread at the operating system level is created and a thread call is started Thread#run. It Thread#runis only the implementation of the Runnable interface, and does not create and start threads.

    public synchronized void start() {
      if (threadStatus != 0)
        throw new IllegalThreadStateException();
      group.add(this);
      boolean started = false;
      try {
        // 调用JNI方法,创建系统层面线程
        start0();
        started = true;
      } finally {
        try {
          if (!started) {
            group.threadStartFailed(this);
          }
        } catch (Throwable ignore) {
        }
      }
    }

Line 8 calls the JNI method private native void start0(), which creates a thread at the operating system level in the implementation of the JVM.

    private Runnable target;
    
    @Override
    public void run() {
      if (target != null) {
        target.run();
      }
    }

Thread#runIt is just an implementation of the Runnable interface, and calls the run method of the member variable target.

References : 8 Questions You Must Know About Threads (Part 2)


True: What happens when Thread#start is called multiple times?

Interview company : Suning

Calling Thread#startthe method multiple times will throw an IllegalThreadStateException.

Thread#startThe source code of the method:

    public synchronized void start() {
      if (threadStatus != 0)
        throw new IllegalThreadStateException();
      group.add(this);
      boolean started = false;
      try {
        start0();
        started = true;
      } finally {
        try {
          if (!started) {
            group.threadStartFailed(this);
          }
        } catch (Throwable ignore) {
        }
      }
    }

When calling Thread#startthe method, the threadStatus will be judged first, and only threadStatus == 0then Thread#startcan it be executed normally, otherwise an IllegalThreadStateException will be thrown.

threadStatus is actually a mapping of the state of the Thread internal class. The following involves the relevant codes of the java.lang.Thread class and the jdk.internal.misc.VM class:

    public class Thread implements Runnable {
      public State getState() {
        return jdk.internal.misc.VM.toThreadState(threadStatus);
      }
      
      public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
      }
    }
    
    public class VM {
      private static final int JVMTI_THREAD_STATE_ALIVE = 0x0001;                    // 1   , 0000 0000 0001
      private static final int JVMTI_THREAD_STATE_TERMINATED = 0x0002;               // 2   , 0000 0000 0010
      private static final int JVMTI_THREAD_STATE_RUNNABLE = 0x0004;                 // 4   , 0000 0000 0100
      private static final int JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER = 0x0400; // 1024, 0100 0000 0000
      private static final int JVMTI_THREAD_STATE_WAITING_INDEFINITELY = 0x0010;     // 16  , 0000 0001 0000
      private static final int JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT = 0x0020;     // 32  , 0000 0010 0000
    
      public static Thread.State toThreadState(int threadStatus) {
        if ((threadStatus & JVMTI_THREAD_STATE_RUNNABLE) != 0) {
          return RUNNABLE;
        } else if ((threadStatus & JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER) != 0) {
          return BLOCKED;
        } else if ((threadStatus & JVMTI_THREAD_STATE_WAITING_INDEFINITELY) != 0) {
          return WAITING;
        } else if ((threadStatus & JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT) != 0) {
          return TIMED_WAITING;
        } else if ((threadStatus & JVMTI_THREAD_STATE_TERMINATED) != 0) {
          return TERMINATED;
        } else if ((threadStatus & JVMTI_THREAD_STATE_ALIVE) == 0) {
          return NEW;
        } else {
          return RUNNABLE;
        }
      }
    }

References : 8 Questions You Must Know About Threads (Part 2)


Can the Thread#run method be called directly?

You can call Thread#runthe method directly, just like calling a normal method, it will be executed in the calling thread, for example:

    public static void main(String[] args) {
      Thread t1 = new Thread(()-> System.out.println("直接调用run方法"));
      t1.run();
    }

In the above code, Thread#runthe method is executed by the main thread and does not start a new thread, because Thread#runit is only the implementation of the interface method Runnable#run:

    public class Thread implements Runnable {
      private Runnable target;
      
      @Override
      public void run() {
        if (target != null) {
          target.run();
        }
      }
    }

References : 8 Questions You Must Know About Threads (Part 2)


true: what the Thread#sleep method does

Interview company : Suning

Thread#sleepMake the thread go to sleep, but will not release the lock (the lock refers to the Java built-in error used in synchronized, namely ObjectMonitor), and the thread enters the TIMED_WATING state. To be precise , Thread#sleepin the implementation of the JVM, the logic of lock-related operations is not executed, so it is impossible to talk about whether to release or not in practice.

Thread#sleepThere are 2 overloaded methods:

    public static native void sleep(long millis) throws InterruptedException;
    
    public static void sleep(long millis, int nanos) throws InterruptedException;

Object#waitThe difference with Thread#sleep:

Object#wait Thread#sleep
effect The thread enters the suspended state (WAITING/TIMED_WAITING) The thread goes to sleep (TIMED_WATING)
CPU resources freed freed
Built-in lock (ObjectMonitor) freed not release

Just focus on the differences in CPU resources and the holding and releasing of built-in locks.

References : 8 Questions You Must Know About Threads (Part 2)


What does Thread#sleep(0) do?

The call Thread#sleep(0)will actually give up CPU time , thus triggering competition for CPU time slices.

jvm.cpp  source code:

    JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
      HOTSPOT_THREAD_SLEEP_BEGIN(millis);
      EventThreadSleep event;
      if (millis == 0) {
        os::naked_yield();
      } else {
        ThreadState old_state = thread->osthread()->get_state();
        thread->osthread()->set_state(SLEEPING);
        if (os::sleep(thread, millis, true) == OS_INTRPT) {
          if (!HAS_PENDING_EXCEPTION) {
            if (event.should_commit()) {
              post_thread_sleep_event(&event, millis);
            }
            HOTSPOT_THREAD_SLEEP_END(1);
          }
        }
        thread->osthread()->set_state(old_state);
      }
      HOTSPOT_THREAD_SLEEP_END(0);
    JVM_END

Line 4 millis == 0will execute os::naked_yield() after the judgment is successful, and the effect is Thread#yieldthe same as in this case.

References : 8 Questions You Must Know About Threads (Part 2)


What the Thread#yield method does

The call Thread#yieldcauses the current thread to relinquish CPU time, thereby triggering competition for CPU time slices . Also, as Thread#sleepwith , Thread#yieldthe lock is not released.

Implementation in JVM:

    JVM_ENTRY(void, JVM_Yield(JNIEnv *env, jclass threadClass))
      if (os::dont_yield()) {
        return;
      }
      os::naked_yield();
    JVM_END

Same implementation as calling Thread#sleep(0).

Tips : Thread#yieldOnly the time slice is given up, but it may be snatched back immediately, for example: all threads must hold the lock to execute, the thread holding the lock calls to Thread#yieldgive up the CPU time slice, but the lock is not released, and other threads cannot execute , can only hold the lock of the thread to continue to acquire the CPU time slice.

References : 8 Questions You Must Know About Threads (Part 2)


The role of the Thread#join method

Thread#joinWait for other threads to finish running, and the thread enters the WAITING or TIMED_WAITING state . If we have the following code:

    Thread t1 = new Thread(()- >{
      // 业务逻辑
    });
    
    Thread t2 = new Thread(()- >{
      t1.join();
      // 业务逻辑
    });
    
    t1.start();
    t2.start();

In the above code, after the main thread starts threads t1 and t2, thread t2 will wait for thread t1 to finish executing before continuing to execute, that is, whoever executes thread t2 executes t1.

References : 8 Questions You Must Know About Threads (Part 2)


What the Thread#interrupt method does

The Thread#interrupt method means to interrupt the thread, but the JVM does not immediately interrupt the thread, but just marks the thread as interrupted, and then tries to wake up the thread in sleep/wait/park. The actual execution of the interrupted thread is acquired by the operating system to the thread's interrupt status flag to begin with.

It should be noted that when the thread calls Object#wait, Thread#joinor Thread#sleepthe method is called after the method Thread#interrupt, an exception InterruptedException will be thrown. This is also explained in the comments of the Java source code:

If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException.

Thread#interrupt0The core source code of the method in the JVM is located  in thread.cpp  :

    void os::interrupt(Thread* thread) {
      OSThread* osthread = thread->osthread();
      if (!osthread->interrupted()) {
        osthread->set_interrupted(true);
        OrderAccess::fence();
        ParkEvent * const slp = thread->_SleepEvent ;
        if (slp != NULL)
          slp->unpark() ;
      }
      if (thread->is_Java_thread())
        ((JavaThread*)thread)->parker()->unpark();
      
      ParkEvent * ev = thread->_ParkEvent ;
      if (ev != NULL) 
        ev->unpark() ;
    }

References : 8 Questions You Must Know About Threads (Part 2) , thread.cpp


How to stop a running thread?

  • Use the 1Thread#interrupt1 method
  • Use the `Thread#stop1 method (this method has been deprecated)

Runnable interface and Callable interface

Both Runnable and Callable are interfaces used to execute business logic methods in threads.

Runnable interface:

    @FunctionalInterface
    public interface Runnable {
      public abstract void run();
    }

Callable 接口:

    public interface Callable<V> {
        V call() throws Exception;
    }

The Runnable interface has no return value, while the Callable interface has a return value. You can use Futur or FutureTask to get the result of thread running. For example:

    public class ByCallable  {
      public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("main的线程:" + Thread.currentThread().getName());
        Callable<String> callable = new MyCallable();
        FutureTask <String> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        System.out.println("MyCallable的执行线程:" + futureTask.get());
      }
      
      static class MyCallable implements Callable<String> {
        @Override
        public String call() {
          System.out.println("MyCallable的线程:" + Thread.currentThread().getName());
          return Thread.currentThread().getName();
        }
      }
    }

Application

This part is to examine the application of thread-related content, and the common use is the use of waiting and waking up threads.

Use 3 threads to alternately print the letters abc 100 times.

Method 1: Introduce Synchronous State (AtomicInteger)

Create a variable synchronization state state, and use the synchronization state to confirm when to print the corresponding letter:

  • When state % 3 == 1, print the letter A and update the synchronization state at the same time;
  • When state % 3 == 2, print the letter B and update the synchronization state at the same time;
  • When state % 3 == 0, print the letter C and update the synchronization state at the same time.

The code is implemented as follows:

    private static final AtomicInteger STATE = new AtomicInteger(1);
    
    private static void useAtomicSyncState() {
      new Thread(() -> {
        for (int i = 0; i < 100; ) {
          if (STATE.get() % 3 == 1) {
            System.out.print("A,");
            STATE.compareAndSet(i * 3 + 1, i * 3 + 2);
            i++;
          }
        }
      }).start();
      
      new Thread(() -> {
        for (int i = 0; i < 100; ) {
          if (STATE.get() % 3 == 2) {
            System.out.print("B,");
            STATE.compareAndSet(i * 3 + 2, i * 3 + 3);
            i++;
          }
        }
      }).start();
      
      new Thread(() -> {
        for (int i = 0; i < 100; ) {
          if (STATE.get() % 3 == 0) {
            System.out.print("C");
            System.out.println();
            STATE.compareAndSet(i * 3 + 3, i * 3 + 4);
            i++;
          }
        }
      }).start();
    }

Tips : The AtomicInteger type is used for the following two considerations:

  • private static int state, this synchronization state can cause visibility problems;
  • private volatile static int state, the operation state++ is not an atomic operation.
Method 2: Synchronous state + synchronized

If you want to use private static int statethe synchronization state of the type, we can introduce synchronized, the code is as follows:

    private static int state = 1;
    
    private static final Object OBJ_LOCK = new Object();
    
    private static void useSynchronized() {
      new Thread(() -> {
        for (int i = 0; i < 100; ) {
          synchronized (OBJ_LOCK) {
            while (state % 3 == 1) {
              System.out.print("A,");
              state++;
              i++;
            }
          }
        }
      }).start();
      
      new Thread(() -> {
        for (int i = 0; i < 100; ) {
          synchronized (OBJ_LOCK) {
            while (state % 3 == 2) {
              System.out.print("B,");
              state++;
              i++;
            }
          }
        }
      }).start();
      
      new Thread(() -> {
        for (int i = 0; i < 100; ) {
          synchronized (OBJ_LOCK) {
            while (state % 3 == 0) {
              System.out.print("C");
              System.out.println();
              state++;
              i++;
            }
          }
        }
      }).start();
    }

Tips : synchronized ensures the visibility and atomicity of the content in the modified code block.

Method 3: Synchronous state + synchronized + Object#wait and Object#notifyAll

The problem with the above method is that each thread will perform a lot of "idling", the state is not satisfied to enter the while loop, and the variable i has not changed, causing the for loop to become an "infinite loop". Reduce the number of "idling" by introducing waiting Object#waitand waking up , the code is as follows:Object#notifyAll

    private static int state = 1;
    
    private static final Object OBJ_LOCK = new Object();
    
    private static void useSynchronizedWithNotify() {
      new Thread(() -> {
        synchronized (OBJ_LOCK) {
          for (int i = 0; i < 100; i++) {
            while (state % 3 != 1) {
              try {
                OBJ_LOCK.wait();
              } catch (InterruptedException e) {
                throw new RuntimeException(e);
              }
            }
            System.out.print("A,");
            state++;
            OBJ_LOCK.notifyAll();
          }
        }
      }).start();
      
      new Thread(() -> {
        synchronized (OBJ_LOCK) {
          for (int i = 0; i < 100; i++) {
            while (state % 3 != 2) {
              try {
                OBJ_LOCK.wait();
              } catch (InterruptedException e) {
                throw new RuntimeException(e);
              }
            }
            System.out.print("B,");
            state++;
            OBJ_LOCK.notifyAll();
          }
        }
      }).start();
      
      new Thread(() -> {
        synchronized (OBJ_LOCK) {
          for (int i = 0; i < 100; i++) {
            while (state % 3 != 0) {
              try {
                OBJ_LOCK.wait();
              } catch (InterruptedException e) {
                throw new RuntimeException(e);
              }
            }
            System.out.print("C");
            System.out.println();
            state++;
            OBJ_LOCK.notifyAll();
          }
        }
      }).start();
    }
Method 4: Synchronous state + ReentrantLock + Condition

Basically consistent with method 3 , ReentrantLock replaces synchronized, Condition#awaitand Condition#signalAllreplaces All, the code is as follows:Object#waitObject#notify

    private static int state = 1;
    
    private static final ReentrantLock REENTRANT_LOCK = new ReentrantLock();
    
    private static final Condition CONDITION = REENTRANT_LOCK.newCondition();
    
    private static void useReentrantLockWithCondition() {
      new Thread(() -> {
        for (int i = 0; i < 100; i++) {
          try {
            REENTRANT_LOCK.lock();
            while (state % 3 != 1) {
              CONDITION.await();
            }
            System.out.print("A,");
            state++;
            CONDITION.signalAll();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          } finally {
            REENTRANT_LOCK.unlock();
          }
        }
      }).start();
      
      new Thread(() -> {
        for (int i = 0; i < 100; i++) {
          try {
            REENTRANT_LOCK.lock();
            while (state % 3 != 1) {
              CONDITION.await();
            }
            System.out.print("B,");
            state++;
            CONDITION.signalAll();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          } finally {
            REENTRANT_LOCK.unlock();
          }
        }
      }).start();
      
      new Thread(() -> {
        for (int i = 0; i < 100; i++) {
          try {
            REENTRANT_LOCK.lock();
            while (state % 3 != 1) {
              CONDITION.await();
            }
            System.out.print("C");
            System.out.println();
            state++;
            CONDITION.signalAll();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          } finally {
            REENTRANT_LOCK.unlock();
          }
        }
      }).start();
    }

Tips : In addition to the above 4 methods, it can also be realized by other synchronization tools, such as: Semphore, CountDownLatch, CyclicBarrier, etc.

Reference materials : 8 questions you must know about threads (middle) , explain the members of the AQS family in detail: Semaphore , explain the members of the AQS family in detail: CountDownLatch , the "outer disciple" of the AQS family: CyclicBarrier


Use 4 threads to print numbers 1~100 alternately.

The reprint of the previous question, I only demonstrate the implementation of ReentrantLock here, the code is as follows:

    static int state = 1;
    
    static ReentrantLock reentrantLock = new ReentrantLock();
    
    static Condition condition = reentrantLock.newCondition();
    
    public static void main(String[] args) {
      new Thread(() -> {
        for (int i = 0; i < 25; i++) {
          try {
            reentrantLock.lock();
            if (state % 4 != 1) {
              condition.await();
            }
            System.out.println("Thread-1 :" + state);
            state++;
            condition.signalAll();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          } finally {
            reentrantLock.unlock();
          }
        }
      }).start();
    
      new Thread(() -> {
        for (int i = 0; i < 25; i++) {
          try {
            reentrantLock.lock();
            if (state % 4 != 2) {
              condition.await();
            }
            System.out.println("Thread-2 :" + state);
            state++;
            condition.signalAll();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          } finally {
            reentrantLock.unlock();
          }
        }
      }).start();
    
      new Thread(() -> {
        for (int i = 0; i < 25; i++) {
          try {
            reentrantLock.lock();
            if (state % 4 != 3) {
              condition.await();
            }
            System.out.println("Thread-3 :" + state);
            state++;
            condition.signalAll();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          } finally {
            reentrantLock.unlock();
          }
        }
      }).start();
    
      new Thread(() -> {
        for (int i = 0; i < 25; i++) {
          try {
            reentrantLock.lock();
            if (state % 4 != 0) {
              condition.await();
            }
            System.out.println("Thread-4 :" + state);
            state++;
            condition.signalAll();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          } finally {
            reentrantLock.unlock();
          }
        }
      }).start();
    }

References: 8 Questions You Must Know About Threads (Part 2)


How to ensure that the 3 threads are executed in the order of execution?

You can use Thread#jointhe method to start another thread in a thread while blocking the current thread, the code is as follows:

    Thread t1 = new Thread(()-> System.out.println("线程[t1]执行!"));
    Thread t2 = new Thread(()-> {
        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("线程[t2]执行!");
    });
    Thread t3 = new Thread(()-> {
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("线程[t3]执行!");
    });
    
    t3.start();
    t2.start();
    t1.start();

Thread#joinThe function of the method is to block the execution thread and wait for the thread calling the join method to finish executing . For example: in the above code, execute in t2 t1.join(), then thread t2 needs to wait for thread t1 to finish executing before executing.

References: 8 Questions You Must Know About Threads (Part 2)


How to execute another thread after one thread finishes executing

Interview company : Meituan

Refer to the answer to "How to ensure that the 3 threads are executed in the order of execution?".

References: 8 Questions You Must Know About Threads (Part 2)


If this article is helpful to you, please give it a lot of praise and support. If there are any mistakes in the article, please criticize and correct. Finally, welcome everyone to pay attention to Wang Youzhi, a financial fisherman , and see you next time!

Guess you like

Origin blog.csdn.net/wyz_1945/article/details/131842496