[Android Interview] The latest interview topic 8 in 2023: Java concurrent programming (3)

11 Deadlock Scenarios and Solutions Tencent

What is this question trying to investigate?

Do you really understand the definition of deadlock? Do you know how to troubleshoot and resolve deadlocks?

Knowledge points of inspection

concurrent programming deadlock

How should candidates answer

Definition of deadlock

Deadlock refers to a phenomenon in which two or more processes are blocked due to competition for resources or communication with each other during execution. If there is no external force, they will not be able to advance. At this time, the system is said to be in a deadlock state or a deadlock has occurred in the system.

harm

1. The thread is not working, but the whole program is still alive

2. There is no abnormal information for us to check.

3. Once the program has a deadlock, there is no way to recover it, only to restart the program. This is a very serious problem for officially released programs.

The occurrence of deadlock must meet the following four necessary conditions.

  1. Mutual exclusion condition: Refers to the exclusive use of the allocated resources by the process, that is, a certain resource is only occupied by one process within a period of time. If there are other processes requesting resources at this time, the requester can only wait until the process that occupies the resources is used up and released.
  2. Request and holding conditions: It means that a process has kept at least one resource, but has made a new resource request, and the resource has been occupied by other processes. At this time, the requesting process is blocked, but it still holds on to other resources it has obtained.
  3. Non-deprivation condition: Refers to the resources that the process has acquired, which cannot be deprived before it is used up, and can only be released by itself when it is used up.
  4. Loop waiting condition: when a deadlock occurs, there must be a process—a circular chain of resources, that is, P0 in the process set {P0, P1, P2,...,Pn} is waiting for a resource occupied by P1; P1 is waiting for resources occupied by P2, ..., Pn is waiting for resources already occupied by P0.

Understanding the causes of deadlocks, especially the four necessary conditions for deadlocks, can avoid, prevent and resolve deadlocks as much as possible. As long as one of the four necessary conditions is broken, deadlock can be effectively prevented.

  1. Break mutual exclusion conditions: transform exclusive resources into virtual resources, and most resources can no longer be transformed.
  2. Break the non-preemption condition: When a process occupies an exclusive resource and then applies for an exclusive resource that cannot be satisfied, the originally occupied resource will be withdrawn.
  3. Break the occupation and application conditions: adopt the resource pre-allocation strategy, that is, apply for all resources before the process runs, run if it is satisfied, or wait, so that there will be no occupation and application.
  4. Break the circular waiting condition: realize the resource allocation strategy in an orderly manner, implement classification and numbering for all devices, and all processes can only apply for resources in the form of increasing serial numbers.

Common algorithms to avoid deadlocks include: ordered resource allocation method , banker's algorithm , etc.

Ordered Resource Allocation

The ordered resource allocation method is an algorithm to prevent deadlocks. All resources in the system are uniformly numbered according to a certain rule, and the application must be in ascending order.

For example, when cooking, salt is 1, soy sauce is 2, and so on. If two chefs A and B are cooking at the same time, the order of using resources is:

A: Application sequence 1->2

B: Application order 2->1

At this time, A wants to use soy sauce while holding the salt, but since the soy sauce is held by B, neither of them will let the other. At this time, a loop condition is formed, resulting in a deadlock. But using the ordered resource allocation method, then:

A: Application sequence 1->2

B: Application sequence 1->2

If A gets the salt first, then B can only wait at this time. This breaks the loop condition and avoids deadlocks.

Summarize

Deadlock is bound to occur in the case of multiple operators (M>=2), and it will only happen when competing for multiple resources (N>=2, and N<=M). Obviously, a single thread will naturally not have a deadlock, only B will go there, not two, and ten will be fine; what about a single resource? Only 13, A and B will only have fierce competition, and the fight will be fierce. Whoever grabs it will be the one, but there will be no deadlock.

12 What are the types of locks?

What is this question trying to investigate?

Do you understand the knowledge of concurrency-related locks?

Knowledge points of inspection

  1. Classification and concept of lock
  2. How to use locks to solve concurrency problems

How should candidates answer

Types of Java locks

  • Optimistic lock/pessimistic lock
  • Exclusive lock/shared lock
  • mutex / read-write lock
  • reentrant lock
  • fair lock/unfair lock
  • segment lock
  • Bias lock/Lightweight lock/Heavyweight lock
  • spin lock

The above are some nouns for locks. These classifications do not all refer to the state of the lock, some refer to the characteristics of the lock, and some refer to the design of the lock.

Optimistic lock/pessimistic lock

Optimistic locks and pessimistic locks do not specifically refer to certain two types of locks. They are concepts or ideas defined by people, mainly referring to the perspective of concurrent synchronization.

  • Optimistic lock: When acquiring data, it is considered that it will not be modified by other threads, so it will not be locked, but when updating, it will judge whether other threads modify the data. If it is modified by other threads, it will spin.
  • Pessimistic lock: Always assume the worst case. When acquiring data, it is assumed that other threads will modify it, so it will be locked when acquiring data, so that other threads need to wait for the thread acquiring the lock to complete and release the lock.

Optimistic locking is suitable for scenarios with frequent reading, because it will not be locked, so it can improve throughput. The atomic variable classes under the java.util.concurrent.atomic package in Java are implemented based on CAS (Compare and Swap), an implementation of optimistic locking.

Pessimistic locking is suitable for scenarios with many write operations, and the implementation of the synchronized keyword is pessimistic locking.

Exclusive lock/shared lock

  • An exclusive lock means that the lock can only be held by one thread at a time.
  • A shared lock means that the lock can be held by multiple threads.

ReentrantLock is an exclusive lock. But for another implementation of Lock, ReadWriteLock, the read lock is a shared lock, while the write lock is an exclusive lock.

mutex / read-write lock

The exclusive lock/shared lock mentioned above is a broad term, and the mutex lock/read-write lock is the specific implementation.

  • The specific implementation of mutex in Java is ReentrantLock.

  • The specific implementation of read-write lock in Java is ReadWriteLock.

reentrant lock

Reentrant lock, also known as recursive lock, means that when the same thread acquires the lock in the outer method, it will automatically acquire the lock when entering the inner method. Both synchronized and ReetrantLock are reentrant locks. One of the benefits of reentrant locks is that deadlocks can be avoided to a certain extent:

synchronized void setA() throws Exception{
    
    
	Thread.sleep(1000);
	setB();
}
    
synchronized void setB() throws Exception{
    
    
	Thread.sleep(1000);
}

In the above code, if synchronized is not a reentrant lock, setAfirst acquire the lock. If the method has not released the lock, the call setBalso needs to acquire the same object lock, which will cause a deadlock.

fair lock/unfair lock

A fair lock means that multiple threads acquire locks in the order in which they apply for locks. An unfair lock means that the order in which multiple threads acquire locks is not in the order in which they apply for locks. It is possible that threads that apply later acquire locks first than threads that apply first . The advantage of unfair locks is that the throughput is greater than that of fair locks, but it may also cause priority inversion or starvation.

ReetrantLock in Java can specify whether the lock is a fair lock through the constructor, and the default is an unfair lock. And synchronized is an unfair lock.

segment lock

Segmented lock is actually a lock design, not a specific lock. For example ConcurrentHashMap, its concurrency implementation is to achieve efficient concurrent operations in the form of segment locks.

The segment lock in ConcurrentHashMap is encapsulated as Segment, which itself is also a structure similar to HashMap. It has an Entry array inside, and each element in the array is a linked list; it is also a ReentrantLock (Segment inherits ReentrantLock).

When it is necessary to put an element, instead of locking the entire HashMap, first use the hashcode to know which segment it will be placed in, and then lock the segment, so when multi-threaded put, As long as it is not placed in a segment, true parallel insertion is achieved.

The design purpose of the segmented lock is to refine the granularity of the lock. When the operation does not need to update the entire array, only one item in the array is locked.

Bias lock/Lightweight lock/Heavyweight lock

These three kinds of locks refer to the state of the lock, and the biased lock means that a piece of synchronization code has been accessed by a thread, then the thread will automatically acquire the lock. Reduce the cost of acquiring locks.

Lightweight lock means that when the lock is a biased lock and is accessed by another thread, the biased lock will be upgraded to a lightweight lock, and other threads will try to acquire the lock in the form of spin, which will not block and improve performance .

Heavyweight lock means that when the lock is a lightweight lock, although another thread is spinning, the spinning will not continue forever. When the lock is not acquired after spinning for a certain number of times, it will enter blocking , the lock expands to a heavyweight lock. Heavyweight locks will cause the thread he applied for to block and reduce performance.

spin lock

In Java, a spin lock means that the thread trying to acquire the lock will not block immediately, but uses a loop to try to acquire the lock. The advantage of this is to reduce the consumption of thread context switching. The disadvantage is that the loop will consume CPU.

13 What is ThreadLocal?

What is this question trying to investigate?

Do you know how to use ThreadLocal in real scenarios? Are you familiar with ThreadLocal?

Knowledge points of inspection

The concept of ThreadLocal is used in the project and basic knowledge

How should candidates answer

ThreadLocal provides thread local variables, which can ensure that the accessed variables belong to the current thread, each thread saves a copy of the variable, and the variables of each thread are different.

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("享学");
System.out.println("主线程获取变量:"+threadLocal.get());
Thread thread = new Thread() {
    
    
    @Override
    public void run() {
    
    
        super.run();
        System.out.println("子线程获取变量:"+ threadLocal.get());
        threadLocal.set("教育");
        System.out.println("子线程获取变量:"+ threadLocal.get());
    }
};

In the above code, the main thread outputs: enjoy learning, the sub-thread outputs: null for the first time, and education for the second time. ThreadLocal is equivalent to providing a thread isolation, binding variables to threads.

set

By ThreadLocal#setsetting thread-local variables, the implementation of set is:

public void set(T value) {
    
    
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null)
		map.set(this, value);
	else
		createMap(t, value);
}

The current thread reference is obtained through the Thread.currentThread() method, and passed to the getMap(Thread) method to obtain an instance of ThreadLocalMap.

ThreadLocalMap getMap(Thread t) {
    
    
	return t.threadLocals;
}

You can see that the getMap(Thread) method directly returns the member variable threadLocals of the Thread instance. Its definition is inside Thread:

public class Thread implements Runnable {
    
    
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

Each Thread has a ThreadLocal.ThreadLocalMap member variable, which means that each thread is bound to ThreadLocal through ThreadLocal.ThreadLocalMap, so that it can ensure that each thread accesses the variable itself.

After obtaining the ThreadLocalMap instance, if it is not empty, call the set method of ThreadLocalMap.ThreadLocalMap to set the value; if it is empty, call the createMap method of ThreadLocal to create a new ThreadLocalMap instance and assign it to Thread.threadLocals.

void createMap(Thread t, T firstValue) {
    
    
    // this = ThreadLocal
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}

get

The source code of ThreadLocal's get method is as follows:

public T get() {
    
    
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null) {
    
    
		ThreadLocalMap.Entry e = map.getEntry(this);
		if (e != null)
			return (T)e.value;
	}
	return setInitialValue();
}

The current thread reference is also obtained through the Thread.currentThread() method, and passed to the getMap(Thread) method to obtain an instance of ThreadLocalMap. And returns if the current thread's variable cannot be found from the ThreadLocalMap setInitialValue.

private T setInitialValue() {
    
    
	T value = initialValue();
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null)
		map.set(this, value);
	else
		createMap(t, value);
	return value;
}

In setInitialValue, first call the initialValue() method to obtain a value, then perform ThreadLocal#setthe same processing and return the value, which means that the initialValue can be obtained by using get before setting the variable value by rewriting the initialValue method of ThreadLocal the result returned.

ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
    
    
	@Nullable
	@Override
	protected String initialValue() {
    
    
		return "享学";
	}
};
// 享学
String value = threadLocal.get();

In set/get, it is actually the binding and acquisition of threads and local variables with the help of ThreadLocalMap. Each thread has its own ThreadLocalMap, which is a collection of mappings, with ThreadLocal as the key. [External link image transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the image and upload it directly (img-Mln0Pz7y-1688439193379)(images\threadlocalmap.png)]

The simplified pseudocode of ThreadLocal is:

class Thread extends Thread {
    
    
     ThreadLocalMap threadLocals;
}

class ThreadLocal<T> {
    
    
	public void set(T t) {
    
    
		Thread thread = Thread.currentThread();
		thread.threadLocals.put(this, t);
	}

	public T get() {
    
    
		Thread thread = Thread.currentThread();
		thread.threadLocals.get(this);
	}
}

14 Java multithreading operates on the same object (byte beating)

What is this question trying to investigate?

Do you understand how Java multithreading operates on the same object and use it in real scenarios? Are you familiar with Java multithreading operating on the same object?

Knowledge points of inspection

The concept of Java multithreading operating on the same object is used in the project and basic knowledge

How should candidates answer

In a multi-threaded environment, multiple threads operate on the same object, which is essentially a thread safety issue. Therefore, in order to deal with thread safety, it is necessary to lock objects operated by multithreading.

For example, when we meet the demand: realize three windows to sell 20 tickets at the same time.

Program analysis:

1. The number of votes should use a static value.

2. In order to ensure that the same ticket will not be sold, a synchronization lock must be used.

3. Design ideas: Create a station class Station, inherit Thread, rewrite the run method, and execute ticket sales operations inside the run method.

Synchronization lock is used for ticket sales: that is, when one platform sells this ticket, other platforms have to wait for this ticket to be sold out before continuing to sell tickets!

package com.multi_thread;

//站台类
public class Station extends Thread {
    
    
    // 通过构造方法给线程名字赋值
    public Station(String name) {
    
    
        super(name);// 给线程起名字
    }

    // 为了保持票数的一直,票数要静态
    static int tick = 20;
    // 创建一个静态钥匙
    static Object ob = "aa";// 值是任意的

    @Override
    public void run() {
    
    
        while (tick > 0) {
    
    
            // 这个很重要,必须使用一个锁,进去的人会把钥匙拿在手上,出来后把钥匙让出来
            synchronized (ob) {
    
    
                if (tick > 0) {
    
    
                    System.out.println(getName() + "卖出了第" + tick + "张票");
                    tick--;
                } else {
    
    
                    System.out.println("票卖完了");
                }
            }
            try {
    
    
                // 休息一秒钟
                sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}
package com.multi_thread;

public class MainClass {
    
    
    // java多线程同步所的使用
    // 三个售票窗口同时出售10张票
    public static void main(String[] args) {
    
    
        // 实例化站台对象,并为每一个站台取名字
        Station station1 = new Station("窗口1");
        Station station2 = new Station("窗口2");
        Station station3 = new Station("窗口3");
        // 让每一个站台对象各自开始工作
        station1.start();
        station2.start();
        station3.start();
    }
}
程序运行结果:

窗口1卖出了第20张票
窗口3卖出了第19张票
窗口2卖出了第18张票
窗口2卖出了第17张票
窗口3卖出了第16张票
窗口1卖出了第15张票
窗口1卖出了第14张票
窗口3卖出了第13张票
窗口2卖出了第12张票
窗口1卖出了第11张票
窗口3卖出了第10张票
窗口2卖出了第9张票
窗口1卖出了第8张票
窗口3卖出了第7张票
窗口2卖出了第6张票
窗口1卖出了第5张票
窗口3卖出了第4张票
窗口2卖出了第3张票
窗口3卖出了第2张票
窗口1卖出了第1张票

15 Thread life cycle, can a thread call start multiple times? What can go wrong? Why can't start be called multiple times?

What is this question trying to investigate?

Do you know the relevant knowledge of Java concurrent threads?

Knowledge points of inspection

Thread life cycle and changes

How should candidates answer

Important states in the thread life cycle

  • Create New;
  • Ready Runnable
  • running running
  • Blocked Blocked
  • death dead

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-wYnmnG7B-1688439193381)(images/lifecycle.webp)]

new new

public class CThread extends Thread{
    
    
    @Override
        public void run() {
    
    
            
        }
}
//新建就是new出对象
CThread thread = new CThread();

When the program uses the new keyword to create a thread, the thread is in a new state (initial state). At this time, it is the same as other Java objects, only memory is allocated for it by the Java virtual machine, and its member variables are initialized. value. At this time, the thread object does not show any dynamic characteristics of the thread, and the program will not execute the thread execution body of the thread.

Ready Runnable

When the thread object calls the Thread.start() method, the thread is in the ready state, and the Java virtual machine creates a method call stack and program counter for it. The thread in this state does not start running, it just means that the thread can run . It can be seen from the source code of start() that it is added to the thread list after start, and then added to the VM in the native layer. As for when the thread starts running, it depends on the scheduling of the thread scheduler in the JVM (if OS scheduling is selected, will enter the running state). Look back at the source code of the start method below:

public synchronized void start() {
    
    
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        // Android-changed: throw if 'started' is true
        if (threadStatus != 0 || started)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
         //通知组此线程即将启动,以便将其添加到组的线程列表中,并且可以减少组的未启动计数。
        group.add(this);
        started = false;
        try {
    
    
            nativeCreate(this, stackSize, daemon);
            started = true;
        } finally {
    
    
            try {
    
    
                if (!started) {
    
    
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
    
    
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

Source code of nativeCreate in C/C++

static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, 
                                  jlong stack_size, jboolean daemon) {
    
    
  Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);
}

CreateNativeThread in C/C++

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
    
    
  Thread* self = static_cast<JNIEnvExt*>(env)->self;
  Runtime* runtime = Runtime::Current();

  ...
  Thread* child_thread = new Thread(is_daemon);
  child_thread->tlsPtr_.jpeer = env->NewGlobalRef(java_peer);
  stack_size = FixStackSize(stack_size);

  env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer,
                    reinterpret_cast<jlong>(child_thread));

  std::unique_ptr<JNIEnvExt> child_jni_env_ext(
      JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM()));

  int pthread_create_result = 0;
  if (child_jni_env_ext.get() != nullptr) {
    
    
    pthread_t new_pthread;
    pthread_attr_t attr;
    child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get();
    //创建线程
    pthread_create_result = pthread_create(&new_pthread,
                         &attr, Thread::CreateCallback, child_thread);

    if (pthread_create_result == 0) {
    
    
      child_jni_env_ext.release();
      return;
    }
  }
  
  ...
}

The analysis of pthread_create in C/C++ and pthread_create will not be analyzed for the time being. It involves in-depth understanding of Linux knowledge generation and then analysis. First, let’s talk about the parameters of pthread_create

  • 原型:int pthread_create((pthread_t thread, pthread_attr_t *attr, void *(start_routine)(void *), void *arg)
  • Header files: #include
  • Input parameters: thread: thread identifier; attr: thread attribute setting; start_routine: starting address of thread function; - arg: parameter passed to start_routine;
  • Return value: Returns 0 on success; returns -1 on error.
  • Function: Create a thread and call the function start_routine pointed to by the thread start address.

running running

If the thread in the ready state obtains the CPU resource, it starts to execute the thread execution body of the run method, and the thread is in the running state. What about the run method? In fact, run is also in the native thread. The source code is as follows:

status_t Thread::run(const char* name, int32_t priority, size_t stack)
{
    
    
    Mutex::Autolock _l(mLock);
    //保证只会启动一次
    if (mRunning) {
    
    
        return INVALID_OPERATION;
    }
    ...
    mRunning = true;

    bool res;
    
    if (mCanCallJava) {
    
    
        //还能调用Java代码的Native线程
        res = createThreadEtc(_threadLoop,
                this, name, priority, stack, &mThread);
    } else {
    
    
        //只能调用C/C++代码的Native线程
        res = androidCreateRawThreadEtc(_threadLoop,
                this, name, priority, stack, &mThread);
    }

    if (res == false) {
    
    
        ...//清理
        return UNKNOWN_ERROR;
    }
    return NO_ERROR;
}

When mCanCallJava is created in the Thread object, mCanCallJava=true is set by default in the constructor.

  • When mCanCallJava=true, it means that the Native thread that can call not only C/C++ code but also Java code is created
  • When mCanCallJava=false, it means that the Native thread that can only call C/C++ code is created.

The createThreadEtc and androidCreateRawThreadEtc methods are not listed one by one. If you are interested, check the source code to understand.

From the start method to nativeCreate, after layer-by-layer calls, it will eventually enter the clone system call, which is a common interface for Linux to create threads or processes. The difference between whether Java code can be executed in the Native thread is that the javaThreadShell() method is used to realize the function of adding hook to the virtual machine and removing the current thread from the virtual machine before and after the execution of _threadLoop(). The calling process, the sequence is:

1.Thread.run
2.createThreadEtc
3.androidCreateThreadEtc
4.javaCreateThreadEtc
5.androidCreateRawThreadEtc
6.javaThreadShell
7.javaAttachThread
8._threadLoop
9.javaDetachThread

Blocked Blocked

The blocked state is that the thread gives up the right to use the CPU for some reason and temporarily stops running. Until the thread enters the ready state, it has the opportunity to go to the running state. There are roughly three types of blocking:

1. Waiting for blocking : The running thread executes the wait() method, and the JVM will put the thread into the waiting pool. (wait will release the held lock)
2. Synchronous blocking : When the running thread acquires the synchronization lock of the object, if the synchronization lock is occupied by other threads, the JVM will put the thread into the lock pool.
3. Other blocking : When a running thread executes the sleep() or join() method, or sends an I/O request, the JVM will put the thread in a blocked state. When the sleep () state times out, the join () waits for the thread to terminate or time out, or when the I/O processing is completed, the thread is transferred to the ready state again. (Note that sleep will not release the held lock).

Thread sleep : The Thread.sleep(long millis) method makes the thread go to the blocked state. The millis parameter sets the time to sleep, in milliseconds. When the sleep is over, it turns to the ready (Runnable) state. sleep() has good platform portability.
Thread waiting : The wait() method in the Object class causes the current thread to wait until other threads call the notify() method or notifyAll() wake-up method of this object. These two wake-up methods are also methods in the Object class, and their behavior is equivalent to calling wait(0). After waking up the thread, it becomes ready (Runnable) state.
Thread yield : The Thread.yield() method suspends the currently executing thread object and gives the execution opportunity to a thread with the same or higher priority.
Thread join : join() method, wait for other threads to terminate. If the join() method of another thread is called in the current thread, the current thread will turn into a blocked state until the other process finishes running, and then the current thread will turn from blocked to ready state.
Thread I/O : The thread performs some IO operations and enters a blocked state because it is waiting for related resources. For example, if you listen to system.in, but you haven’t received any keyboard input, you will enter a blocking state.
Thread wakeup : The notify() method in the Object class wakes up a single thread waiting on the monitor of this object. If all threads are waiting on this object, one of them will be chosen to wake up, the choice is arbitrary and happens at implementation time. A similar method also has a notifyAll(), which wakes up all threads waiting on this object monitor.

death dead

A thread will end in one of the following three ways, and will be in a dead state after the end:

  • The run() method is executed and the thread ends normally.
  • The thread throws an uncaught Exception or Error.
  • Call the thread's stop() method directly to end the thread - this method is prone to deadlock and is generally not recommended.

Thread started multiple times

Java threads are not allowed to start multiple times, and the second call will inevitably throw IllegalThreadStateException. According to the thread life cycle, the initial state of the thread is NEW, which cannot be transformed from other states.

at last

This interview question will continue to be updated, please pay attention! ! !

Scan the QR code below to get the interview questions~

ps: There is also a ChatGPT robot in the group, which can answer your technical problems

Guess you like

Origin blog.csdn.net/datian1234/article/details/131531001