Summary of "A Practical Guide to Multithreaded Programming"

For Java concurrency and multi-threaded programming, we recommend "Java Concurrent Programming in Practice" and "Multi-Threaded Programming in Practice". The former is a translation of a very popular foreign book, and the latter is a book written by a Chinese and is in line with the Chinese thinking model.

Processes, Threads and Tasks

Multiple programs will run in the operating system. A running program is a process. For example, a running Idea is a process, and a Java virtual machine is a process. A process is the basic unit for a program to apply for resources from the operating system.

A process can contain multiple threads, and all threads in the same process share resources in the process, such as memory space, file handles, etc. A thread is the smallest unit in a process that can be executed independently.

The calculation to be completed by the thread is called a task. A specific thread always performs a specific task.

A program often needs to complete many independent tasks, that is, it contains multiple threads. The operating system abstracts the changes of processes and threads. In this way, the operating system only needs to manage the process, and the process manages many threads, which can reduce the cost of direct management by the operating system. Thread complexity.

Implementation of threads in Java

Java is an object-oriented design language, so thread is also an object in Java. It is the Java standard class library java.lang.Thread. An instance of the Thread class or its subclasses is a thread.

Two commonly used constructors of the Thread class are: Thread() and Thread(Runnable target). Corresponds to the two ways to create threads in Java.One is to use the first constructor, inherit the Thread class and call the start() method, and the second is to implement the Runnable interface and then pass it to 2nd constructor and calls the start() method.

The processing logic of the thread needs to be written in the run() method, but the thread cannot be started by calling the run() method. Instead, the start() method needs to be called. From the JVM's runtime data area, the thread has a program counter, a virtual machine stack and a local method stack, which need to be allocated by the JVM. The start() method will request the JVM to allocate these resources and apply for a thread from the operating system, and call run () method will only execute processing logic in the current thread. It can be seen that the cost of creating thread objects is higher than that of ordinary objects.

Threads in Java have a hierarchical relationship. Other threads created in the current thread are called child threads of the current thread. Child threads can also have child threads. The default values ​​​​of some attributes of the child thread will inherit the attribute values ​​​​in the parent thread, such as Thread priority, whether it is a daemon thread, etc. Although there is a parent-child hierarchical relationship between threads, there is no necessary connection between the life cycles of the parent thread and the child thread. That is, after the parent thread ends, the child thread can continue to run, and the end of the child thread does not prevent the parent thread from continuing to run. .

Thread class

Attributes

The attributes of the thread include the thread number (ID), name (name), whether it is a daemon thread (Daemon), and priority (Priority).

The thread number is assigned by the JVM. At a point in time when the JVM is running, the thread number is unique, which means that the JVM will reuse the number of the previous thread that has been terminated. Therefore, this attribute cannot be used to uniquely identify a thread.

The name of the thread can be set by the programmer. The default thread name is related to the thread number. The thread definition name is helpful for code debugging and problem location.

Threads are divided into daemon threads and non-daemon threads. The default value of this attribute is the same as the value of this attribute of the parent thread. Non-daemon threads will prevent the termination of the process, but daemon threads will not. That is to say, when only the daemon is left in the process thread, even if the daemon thread is still running, the process will terminate. Daemon threads are suitable for performing tasks that are not very important, such as monitoring the execution of other threads.

Java defines 10 priorities from 1 to 10, and the default is 5. If the thread has a parent thread, the default priority is equal to the priority value of the parent thread. The number of priorities defined in Java is different from that in the operating system, and improperly setting this property may lead to thread starvation, so modifying this property is not recommended.

method

  • static Thread currentThread(): This method returns the current thread
  • void run(): used to define the task processing logic of the thread
  • void start(): Start the corresponding thread
  • void join(): Wait for the corresponding thread to finish running. If thread A calls the join method of thread B, then the running of thread A will be suspended until the running of thread B finishes.
  • static void yield(): Thread courtesy causes the current thread to lose the CPU time slice, but the current thread has the opportunity to reoccupy the CPU time slice, so this method is unreliable
  • static void sleep(long millis): Causes the current thread to pause for a specified time

Thread life cycle

In Java, the state of a thread is defined in the State enumeration class inside the Thread class. The values ​​contained in the State enumeration class are:

  • NEW: A thread that has been created but not started is in this state. Since a thread instance can only be started once, a thread may be in this state only once.
  • RUNNABLE: Running threads and threads waiting for CPU time slices (ready state) are in this state.
  • BLOCKED: When a thread initiates a blocking IO operation, the thread will be in this state. The thread in this state does not occupy processor resources. When the blocking IO operation is completed, the thread's state can be converted to the RUNNABLE state.
  • WAITING: When a thread executes certain methods, it will be in a state of waiting for other threads to perform other certain operations. Methods that can change its execution thread to the WAITING state include: Object.wait(), Thread.join(), and LockSupport.park(Object). The corresponding methods that can change the corresponding thread from WAITING to RUNNABLE state include: Object.notify() and LockSupport.unpark(Object).
  • TIMED_WAITING: This state is similar to WAITING. The difference is that the thread in this state is not waiting indefinitely for other threads to perform specific operations, but is in a waiting state with a time limit. The thread's state automatically transitions to RUNNABLE when other threads do not perform a specific operation expected by the thread within the specified time.
  • TERMINATED: The thread that has finished executing is in this state. Since a thread instance can only be started once, a thread can only be in this state once.

Thread life cycle flow diagram:
Insert image description here

Thread termination

The Thread class has some methods for thread stopping: stop(), suspend(), but these methods have been deprecated.

The Java platform has a unique Boolean state variable called an interrupt flag for each thread to indicate whether the corresponding thread has received an interrupt. An interrupt flag value of true indicates that the corresponding thread has received an interrupt. The target thread can obtain the interrupt mark value of the thread by calling Thread.currentThread().isInterrupted(), or obtain and reset the interrupt mark value by calling Thread.interrupted(), that is, Thread.interrupted() will return the current thread. The interrupt flag value and resets the current thread interrupt flag to false. Calling interrupt() on a thread is equivalent to setting the thread's interrupt flag to true.

The operations performed by the target thread after checking the interrupt mark are called the target thread's response to the interrupt, or interrupt response for short. Given an initiating thread originator and a target thread target, the target's response to interrupts generally includes:

  • no effect. Calling target.interrupt() by originator will not have any impact on the running of target. This situation can also be referred to as the target thread's failure to respond to the interrupt. Blocking methods/operations such as InputStream.read(), ReentrantLock.lock(), and applying for internal locks fall into this category.
  • Cancel the running of the task. Originator calling target.interrupt() will cause the task executed by the target at the moment the interrupt is detected to be cancelled, but this will not affect the target's continued processing of other tasks.
  • The worker thread is stopped. Originator calling target.interrupt() will cause the target to terminate, that is, the target's life cycle status changes to TERMINATED.

Many blocking methods in the Java standard library, such as Object.wait(), Object.notify(), Thread.sleep(), etc., respond to interruptions by throwing exceptions such as InterruptedException. There are also some blocking methods such as InputStream.read() and Lock.lock() that cannot interrupt exceptions.

The method that can respond to interrupts is usually to check the interrupt mark before blocking. If the interrupt mark value is true, an InterruptedException exception is thrown. By convention, a method that throws an InterruptedException usually resets the current thread's thread interrupt flag to false when it throws the exception.

If it is found that at the moment when the thread sends an interrupt to the target thread, the target thread has been suspended due to executing some blocking methods (the life cycle status is WAITING or BLOCKED), then the JVM may be set to wake up the thread at this time, thereby causing The target thread gets a chance to respond to the interrupt. From this, sending an interrupt to the target thread can also have the effect of waking up the target thread.

The reasons for thread stopping include normal stopping when the run() method ends execution and abnormal stopping when an exception is thrown during running. Therefore, we can set a Boolean thread stop flag for the thread. The target thread detects that the flag is true and causes its run() method to return, thus realizing the termination of the thread.

The thread interruption mark we mentioned before is also of Boolean type. Can it be used as a thread stop mark?

Since the thread interruption mark may be cleared by some methods of the target thread, from the perspective of versatility, the thread interruption mark cannot be used as a thread stop mark! And if you only use a Boolean thread stop flag, the thread stop flag will not be checked when the thread executes some blocking methods, so we need to use the thread stop flag and the thread interruption flag in combination.

When you need to stop the target thread, in addition to modifying the thread stop flag to true, you also need to send a thread interrupt flag to the target thread.

Serial, parallel and concurrent

Serial means executing one task at a time, and multiple tasks are executed in sequence, task1 -> task2 -> task3. Then the execution time is the sum of the time taken by all tasks.

Parallelism refers to executing multiple tasks at one time. If multiple tasks start executing at the same time, the execution time is the execution time of the longest time-consuming task.

Concurrency refers to executing one task at a time. For example, execute task1 first, pause the execution of task1 after executing task1 for a period of time, and then execute task 2. After executing task 2 for a period of time, pause task2 and execute task 3, and so on, until all All tasks are completed.

Concurrency describes the situation when multiple threads run on one CPU. The thread task may not use the processor resources temporarily because the CPU time slice expires or blocking IO occurs. At this time, the thread task has not been completed. In order to fully Utilizing CPU resources to improve program performance, the operating system will not wait for the current thread, but save resources such as the current thread's call stack and current instructions, and then execute another thread task.

Competition

In multi-threaded programming, for the same input, the output of the program is sometimes correct and sometimes wrong. This phenomenon in which the correctness of a calculation result is time-dependent is called a race condition. Race conditions are the product of multi-threaded programming. Even if the CPU running the program is single-core, race conditions will occur.

Conditions for race conditions to occur

The generation of race conditions is accompanied by access to shared variables under multi-threads. The condition for generation of race conditions isOne thread reads the shared variables and performs operations based on the shared variables. During the calculation, another thread updated the value of the shared variable, resulting in reading dirty data or losing the updated results.

For local variables, different threads access their own copies, and there is no sharing. So the use of local variables will not cause a race condition!

race mode

  • read-modify-write

The scenario described by this mode is that thread A reads a set of shared variables and updates the value of the shared variable. Before synchronizing the modified value back to the main memory, thread B reads the old value of the shared variable from the main memory. value, which results in reading dirty data. Thread B continues to rely on the old value of the shared variable to calculate the result of updating the shared variable. This result is an incorrect result. Then thread B synchronizes the incorrect result to the main memory. Finally, thread A will update Synchronizing to main memory, thread A overwrites thread B's updates, causing lost updates.

The read-modify-write two-dimensional table is as follows:

time/thread Thread A Thread B
t1 Read shared variable var from main memory
t2 Modify shared variable var in CPU
t3 Read shared variable var from main memory
t4 Modify shared variable var in CPU
t5 Synchronize updated shared variables to main memory
t6 Synchronize updated shared variables to main memory
  • check-then-act

The scenario described by this mode is that thread A reads the value of the shared variable and uses the value in the conditional judgment statement to determine the subsequent execution of code block C1 (such as using if conditional judgment). After thread A executes the conditional judgment statement, C1 is executed. Before the code block, another thread B modified the value of the shared variable, causing the judgment based on the shared variable to execute code block C2, but thread A still executed code block C1.

The two-dimensional table of check-then-act is as follows:

time/thread Thread A Thread B
t1 Read shared variable var from main memory
t2 Determine the shared variable var and determine the execution code block C1
t3 Read shared variable var from main memory
t4 Modify the shared variable var in the CPU (code block C2 will be executed based on the new shared value)
t5 Synchronize updated shared variables to main memory
t6 Execute code block C1

Thread safety

In Java multi-threaded programming, if a class can run normally in a single-threaded environment, and can run normally in a multi-threaded environment without its users having to make any changes, then we call it Thread safe. On the other hand, if a class runs normally in a single-threaded environment but fails to run normally in a multi-threaded environment, then the class is not thread-safe.

If a class is not thread-safe, we say that it has thread-safety issues when used directly in a multi-threaded environment. The Java standard library defines thread-safe classes such as Vector, CopyOnWriteArrayList, and HashTable, and also defines non-thread-safe classes such as ArrayList, HashMap, etc.

Thread safety issues are generally expressed in three aspects: atomicity, visibility and orderliness.

atomicity

Atomic literally means indivisible. For operations involving shared variable access,if the operation is indivisiblefrom the perspective of any thread other than its execution thread, then the operation is atomic. . Accordingly we say this operation is atomic.

In the Java language, writing and reading operations on variables are atomic operations, which are guaranteed by the JVM.

Java provides a variety of ways to achieve the atomicity of a set of operations, such as using locks and using CAS, which also illustrates from the side that locks and CAS guarantee the atomicity of operations.

To understand the concept of atomic operations, you need to pay attention to the following two points:

  • Atomic operations are for operations that access shared variables. In other words, it doesn't matter whether operations that only involve local variable access are atomic, or simply treat this type of operations as atomic operations.
  • An atomic operation is described from a thread other than the execution thread of the operation, which means that it only makes sense in a multi-threaded environment.

visibility

In a multi-threaded environment, after a thread updates a shared variable, subsequent threads that access the variable may not be able to read the updated result immediately, or even never be able to read the updated result. This is another manifestation of thread safety issues: visibility.

Since there is an order of magnitude gap between the CPU processing speed and the memory access speed, the CPU does not directly access the main memory, but indirectly accesses it through registers and caches. Variables are first loaded from the main memory to the cache, and the CPU accesses them indirectly from the main memory. Read the value of a variable from the cache. When updating, the CPU will first write the update to the cache and synchronize it to the main memory at some point in the future. The additional cache causes visibility problems between multiple threads.

Java provides the Volatile keyword to ensure thread visibility of shared variables. Variables marked with the Volatile keyword will update the value of the variable from the main memory before the thread reads it from the cache. After the thread writes the variable to the cache, the updated value will be synchronized to the main memory immediately. .

At the same time, the JVM ensures the visibility of the following scenarios:

  • Updates to shared variables by the parent thread before starting the child thread are visible to the child thread.
  • Updates to shared variables by a thread after it terminates are visible to the thread that calls the thread's join method.

But these two scenarios are most likely not used in work, because whoever uses new to create threads now uses thread pools (dog head.

Orderliness

Orderliness refers to the situation under which memory access operations performed by one thread running on one processor appear out of order to other threads running on another processor.

Reordering is an optimization of operations related to memory access, which can improve program performance without affecting single-thread correctness. However, it may have an impact on the correctness of multi-threaded programs, i.e. it may cause thread safety issues.

Reordering occurs in two places in a Java program, when javac is used to compile the source code and when the JIT compiler translates the bytecode.

For example, the operation of instantiating an object in Java:Object obj = new Object, which corresponds to three operations in the processor, namely:

  1. Allocate the memory space required by the Object instance and obtain a reference ref pointing to the space
  2. Call the Object constructor to initialize the Object instance
  3. Copies the Object instance reference ref to the instance variable obj

These three operations may not be executed sequentially in the processor, and operation 3 may be executed before operation 2, so the instance pointed to by obj may not have been initialized yet, and using this uninitialized instance will cause unpredictable errors.

Instruction reordering can improve performance, so instruction reordering cannot be completely prohibited. However, the operating system provides instructions to partially prohibit instruction reordering, that is, code fragments that allow multiple threads to access shared variables prohibit instruction reordering.

The ways to ensure orderliness in Java are: locks and volatile keywords.

Locks divide the code into three areas: before acquiring the lock, critical section and after acquiring the lock. Instruction reordering is still allowed within these three areas, but instruction reordering between areas is not allowed.

The volatile keyword prevents reordering of region instructions before accessing a shared variable by using a memory barrier.

Thread synchronization mechanism

The thread synchronization mechanisms provided by the Java platform include locks, volatile keywords, CAS, final keywords, static keys, and some related APIs, such as Object.wait()/Object.notify(), etc.

Lock

Locks in the Java platform are divided into internal locks and explicit locks. Internal locks are implemented through the synchronized keyword, and explicit locks are implemented through the implementation class of the java.concurrent.locks.Lock interface.

The code executed by the lock during the period after the thread acquires the lock and before releasing the lock is calledcritical section.

Locks protect shared data to achieve thread safety, including ensuring atomicity, visibility, and ordering.

Locks guarantee atomicity through mutual exclusion. The so-called mutual exclusion means that a lock can only be held by one thread at a time.

We know that visibility is guaranteed by two actions: the writing thread flushes the processor cache and the read thread flushes the processor cache. The acquisition of the lock implies the action of flushing the processor cache, and the release of the lock implies the action of flushing the processor cache. Therefore, locks guarantee visibility.

Locks can also guarantee orderliness. Locks divide the code into three areas before acquiring the lock, critical area, and after releasing the lock. Instructions are allowed to be reordered within the area, but instruction reordering is prohibited between areas.

Reentrant lock

A thread can acquire a lock again while holding it.

read-write lock

Read-write locks allow multiple threads to read shared variables at the same time, but only allow one thread to update shared variables at a time.

volatile keyword

The volatile keyword ensures the visibility and ordering of shared variables.

CAS

CAS (Compare and Swap) is the name of a processor instruction. CAS is an atomic if-then-act operation.

That is, CAS guarantees atomicity.

static keyword and final keyword

The initialization of classes in Java actually adopts the basis of lazy loading. That is, after a class is loaded by the JVM, the values ​​​​of all static variables of the class remain at their default values ​​until a thread accesses any static variable of the class for the first time. Variables enable this class to be initialized - the static initialization block ("static{}") of the class is executed, and all static variables of the class are assigned initial values.

public class ClassLazyInitDemo {
    
    
    public staic void main(String[] args) {
    
    
        System.in.out.println(Collaborator.class.hashCode());	//语句1
        System.in.out.println(Collaborator.number);				//语句2
        System.in.out.println(Collaborator.flag);
    }
    
    static class Collaborator {
    
    
        static int number = 1;
        static boolean flag = true;
        static {
    
    
            System.in.out.println("Collaborator initializing...");
        }
    }
}

In the above Demo, statement 1 only causes the class to be loaded by the JVM, but does not initialize it (that is, the static block is executed). It is only initialized when statement 2 is executed.

The static keyword has a special meaning in a multi-threaded environment. It ensures that a thread can always read the initial value of a class's static variable instead of the default value even if no other synchronization mechanism is used. But this visibility guarantee is limited to the first time the thread reads the variable. In this scenario, the static keyword guarantees visibility.

For reference static variables, the static keyword can also ensure that when a thread reads the initial value of the variable, the object pointed to by this value has been initialized. In this scenario, the static keyword guarantees orderliness.

The final keyword ensures that when other threads access shared variables, they can always read the initial value of the variable instead of the default value. In this scenario the final keyword guarantees visibility.

For reference final fields, the final keyword further ensures that the object referenced by the field has been initialized. In this scenario, the final keyword guarantees orderliness.

Discover possible concurrency points

To achieve the goal of multi-threaded programming - concurrent computing, we first need to find out which processes in the program can be concurrent, that is, change from serial to concurrent. These concurrent processes are called concurrency points.

Concurrency based on data segmentation

If the size of the original input data of the program is relatively large, data-based segmentation can be used. The basic idea is to decompose the original input data into several smaller sub-inputs according to certain rules, and use worker threads to process these sub-inputs. Each worker thread will form sub-outputs after processing. Finally, we will all The sub-outputs are combined together to become the output of the entire concurrent task.

In the data-based segmentation method, the main thread divides the original input into small sub-outputs according to certain rules, and then creates a worker thread to receive each sub-output. The worker thread will complete all processing steps independently, and then obtain part of the To output the result, the main thread waits for all worker threads to complete processing, and then merges all sub-results to get the total output result. The main thread may cause additional performance losses and program complexity in the process of splitting input and merging output.

The worker threads generated by data-based segmentation arehomogeneous worker threads, that is, threads with the same task processing logic.

The relationship between input, output and worker threads:
Insert image description here

Concurrency based on task segmentation

The basic idea based on task segmentation is to decompose the original task into several subtasks according to certain rules, and use dedicated worker threads to execute these subtasks. At this time, there are dependencies between the worker threads, and the latter worker thread The input is often the output of the previous worker thread, which introduces data interaction between threads and may add additional complexity.

Task-based segmentation producesheterogeneous worker threads, that is, threads with different task processing logic.

The relationship between output, output and worker threads:
Insert image description here

Java thread synchronization utility class

Object.wait()/Object.notify()

In the Java platform, Object.wait()/Object.wait(long) and Object.notify()/Object.notifyAll() can be used to implement waiting and notification. Object.wait() can make the thread enter the WAITING state. Object. notify() can wake up a thread that has entered the WAITING state. Correspondingly, the execution thread of Object.wait() is called the waiting thread; the execution thread of Object.notify() is called the notification thread. Since the Object class is the parent class of any object in Java, waiting and notification can be implemented using any object in Java.

The template code for using Object.wait() to implement thread waiting is as follows:

// 在调用 wait 方法前需获得相应对象的内部锁
synchronized(someObject) {
    
    
    while(保护条件不成立) {
    
    
        // 调用 Object.wait() 暂停当前线程
        someObject.wait();
    }
    
    // 代码执行到这里说明保护条件已经满足
    // 执行目标动作
    doAction();
}

The guard condition is a Boolean expression containing shared variables. When the shared variables are updated by other threads (notification threads) and the corresponding protection conditions are met, these threads will notify the waiting threads. Since a thread can call the wait method of an object only if it holds the internal lock of the object, Object.wait() calls are always placed in the critical section guided by the corresponding object.

Note: During the period when the waiting thread is awakened and continues to run until it again holds the internal lock of the corresponding object, other threads may preemptively obtain the corresponding internal lock and update the relevant shared variables, resulting in the protection required by the thread. The condition is not met. Therefore, the judgment of the protection condition and the call to Object.wait() should be placed in the loop statement to ensure that the target action can only be executed when the protection condition is true!

The execution flow of the above template code is as follows:

  1. The current thread acquires the internal lock of someObject and enters the synchronized code block.
  2. Determine whether the protection conditions are met
  3. If the protection condition is not established, call the someObject.wait() method to suspend the current thread. Because someObject.notify() needs to be executed in a synchronized block, the current thread will release the someObject internal lock held after the suspension. At this time, the wait() method is still no return
  4. Other threads update the protection conditions in the synchronized code block and call someObject.notify()/someObject.notifyAll() to notify the wake-up waiting thread
  5. Since the same method of the same object can be executed by multiple threads, there may be multiple waiting threads on the someObject object. Therefore, after the current thread is awakened, it will first try to acquire the internal lock of someObject. If the internal lock is acquired, the while statement body someObject.wait() will return
  6. Determine again whether the protection condition is true, and if so, execute the target action doAction()
  7. Finally exit the synchronized code block and release the internal lock of someObject object

Use Object.notify() to implement notification. The code template is as follows:

// 在调用 notify() 方法前需获得相应对象的内部锁
synchronized(someObject) {
    
    
    // 更新等待线程的保护条件涉及的共享变量
    updateSharedState();
    // 唤醒等待线程
    someObject.notify();
}

The method containing the above template code is called a notification method, which contains two elements: updating shared variables and waking up waiting threads. Since a thread can only execute the notify method of an object if it holds the internal lock of the object, the Object.notify() call is always placed in the critical section guided by the internal lock of the corresponding object. Therefore, Object.wait() must release the corresponding internal lock while suspending its execution thread; otherwise, the notification thread cannot obtain the corresponding internal lock, and it cannot execute the notify method of the corresponding object to notify the waiting thread!

The notify() method call should be placed as close to the end of the critical section as possible so that the waiting thread can obtain the corresponding internal lock again as soon as possible after it is awakened.

Because wait()/notify() may cause problems such as premature wake-up, signal loss, and deceptive wake-up, it is not often used in daily work to achieve thread synchronization. The Java standard class library provides more advanced thread synchronization. Utility classes, we use these to solve thread synchronization problems encountered in our work.

CountDownLatch

CountDownLatch can be used to implement one or more threads to wait for other threads to complete a specific set of operations before continuing to run. This set of operations is called prerequisite operations.

CountDownLatch internally maintains a counter that represents the number of outstanding prerequisite operations. CountDownLatch.countDown() decrements the counter value of the corresponding instance by 1 each time it is executed. When the counter is not 1, the execution thread of CountDownLatch.await() will be suspended, and these threads are called waiting threads on the corresponding CountDownLatch. CountDownLatch.countDown() is equivalent to a notification method that wakes up all waiting threads on the corresponding instance when the counter value reaches 0. The initial value of the counter is specified in the construction parameter of CountDownLatch, as shown in the following declaration: public CountDownLatch(int count).

The use of CountDownLatch is one-time. After the counter value reaches 0, the counter value will no longer change.

CyclicBarrier

Sometimes multiple threads may need to wait for each other to execute somewhere in the code before these threads can continue executing.

Threads that use CyclicBarrier to implement waiting are called participants. Execution of CyclicBarrier.await() by any party other than the last party will cause the thread to be suspended. The last thread executing CyclicBarrier.await() will wake up all other parties using the corresponding CyclicBarrier instance, but the last thread itself will not be suspended.

CyclicBarrier makes it reusable.

blocking queue

The interface java.util.concurrent.BlockingQueue introduced in JDK 1.5 defines a thread-safe queue - blocking queue. Blocking queue can be used to transfer data between threads. The classic use case is as a producer-consumer model. Transmission channel between threads.

Blocking queues are divided according to whether the capacity of their storage space is limited, and can be divided into bounded queues and unbounded queues. The storage capacity limit of bounded queues is specified by the application, and the maximum storage capacity of unbounded queues is Integer.MAX_VALUE.

Generally speaking, if a method or operation can cause its execution thread to be suspended, then we call the corresponding method or operation a blocking method or blocking operation. In the blocking queue, there are both blocking methods and non-blocking methods. The take()/put() method pair is the blocking method, and the poll()/offer() method pair is the non-blocking method.

Semaphore

The standard library class java.util.concurrent.Semaphore introduced in JDK 1.5 is called a semaphore.

Semaphore.acquire()/release() are used to apply for quotas and return quotas respectively. Semaphore.acquire() will return immediately after successfully acquiring a quota. If the current available quota is insufficient, Semaphore.acquire() will suspend the current thread until other threads return the quota through Semaphore.release().

Semaphore.acquire() and Semaphore.release() are always used in pairs, and the Semaphore.release() call should always be placed in a finally block to avoid that the quota acquired by the current thread cannot be returned. The template code is as follows:

public void template(){
    
    
    semaphore.acqiure();
    try{
    
    
        doSomething();
    } finally {
    
    
        semaphore.release();
    }
}

PipedInputStream and PipedOutputStream

PipedInputStream and PipedOutputStream are subclasses of InputStream and OutputStream. They can be used to implement direct input and output between threads without having to borrow other data exchange intermediaries such as files, databases, and network connections.

PipedInputStream and PipedOutputStream are suitable for use between two threads, that is, suitable for single producer-single consumer situations.

Exchanger

The standard library class Java.util.concurrent.Exchanger introduced in JDK 1.5 can be used to implement double buffering. Exchanger is equivalent to a CyclicBarrier with only two participants.

When the producer thread executes Exchanger.exchange(V), it specifies parameter x as a filled buffer. When the consumer thread executes Exchanger.exchange(V), it specifies parameter x as an empty buffer that has been used. district. After executing Exchanger.exchange(V), the producer thread will enter the waiting state until a consumer thread executes the Exchanger.exchange(V) method.

Thread liveness failure

deadlock

The phenomenon of two or more threads being suspended forever while waiting for each other to release the required locks is called a deadlock.

Methods to avoid deadlock include: reducing the granularity of the lock and specifying the order in which multiple locks are acquired.

Locked

The phenomenon that the wake-up thread terminates for some reason, causing the thread that needs to be woken up to be in a waiting state forever, is called deadlock.

hunger

It refers to the phenomenon that threads have the opportunity to obtain the required resources, but due to too much concurrency and too many threads waiting to obtain the same resources, they do not grab the right to hold the resources each time, resulting in consistent failure to obtain resources. This phenomenon is called thread starvation. .

Thread-safe object design

The reason for thread safety problems in multi-threaded programming is that multiple threads access shared variables of the same object. In order to ensure thread safety, we need to add locks in the code area that accesses shared variables. Adding locks ensures that multiple threads can concurrently access shared variables. When downgrading from concurrent execution to serial execution, this also reduces the throughput of the system. By designing some classes, we can ensure thread safety even when multiple threads access objects without using any thread synchronization mechanism. These objects are called thread-safe objects. These include stateless objects, immutable objects, and thread-specific objects.

stateless object

A stateless object does not contain any instance variables and does not contain any static variables or the static variables it contains are read-only.

immutable objects

An immutable object is an object whose state remains unchanged once created. Immutable objects are inherently thread-safe.

A strictly immutable object must satisfy all of the following conditions:

  • The class itself is decorated with final to prevent subclassing from changing its defined behavior.
  • All fields are modified with final, because final modification not only semantically indicates that the value of the modified field cannot be changed, but more importantly, this semantics ensures the initialization safety of the modified field in a multi-threaded environment, that is, final modification When a field is visible to other threads, it must be initialized.
  • The object does not escape during the initialization process, preventing other classes from modifying its state during the object initialization process.
  • If any field refers to other mutable objects, such as collections, arrays, etc., these fields must be privately decorated, and the values ​​of these fields cannot be exposed to the outside world. If there are relevant methods that return these field values, defensive copying should be performed.

thread-specific objects

Each thread creates its own instance. An object that can only be accessed by one thread is called a thread-specific object. In Java, we use ThreadLocal to implement thread-specific objects.

concurrent collection

The java.util.concurrent package of JDK 1.5 introduces some thread-safe collection objects, which are called concurrent collections. These objects are usually used as replacements for synchronized collections. Their correspondence with commonly used non-thread-safe collection objects As shown in the following table:

non-thread-safe object Concurrent collection class common interface Traversal implementation
ArrayList CopyOnWriteArrayList List Snapshot
HashSet CopyOnWriteArraySet Set Snapshot
LinkedList ConcurrentLinkedQueue Queue Quasi real time
HashMap ConcurrentHashMap Map Quasi real time
TreeMap ConcurrentSkipListMap SortedMap Quasi real time
TreeSet ConcurrentSkipListSet SortedSet Quasi real time

Thread management

Uncaught exception from thread

If the thread's run method throws an uncaught exception, the corresponding thread will terminate early as the run method exits. For this abnormal termination of threads, JDK 1.5 introduced the UncaughtExceptionHandler interface (uncaught exception handler) to solve this problem. This interface is defined inside the Thread class. It only contains one method, so it is a functional interface:

@FunctionalInterface
public interface UncaughtExceptionHandler {
    
    
    /**
         * Method invoked when the given thread terminates due to the
         * given uncaught exception.
         * <p>Any exception thrown by this method will be ignored by the
         * Java Virtual Machine.
         * @param t the thread
         * @param e the exception
         */
    void uncaughtException(Thread t, Throwable e);
}

Two UncaughtExceptionHandlers are defined in the Thread class, one is an instance variable and the other is a static variable:

// null unless explicitly set
private volatile UncaughtExceptionHandler uncaughtExceptionHandler;

// null unless explicitly set
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

The UncaughtExceptionHandler referenced by the instance variable is unique to each thread. The UncaughtExceptionHandler referenced by the static variable is common to all threads. Thread will first use the UncaughtExceptionHandler referenced by the instance variable after throwing an uncaught exception. If the instance variable is null (not defined) , the UncaughtExceptionHandler of the static variable will be used. Both variables define getter/setter methods, and users can customize the thread's uncaught exception handler.

Before the thread's run method throws an uncaught exception and terminates the thread, the JVM will run the UncaughtExceptionHandler.uncaughtException() method. We can do some meaningful things in this method, such as recording information about the thread's abnormal termination to the log file. , even create and start a replacement thread for the abnormally terminated thread.

Thread Pool

Threads are an expensive resource, and their overhead mainly includes the following aspects.

  • The overhead of thread creation and startup. Compared with ordinary objects, Java threads also occupy additional storage space - stack space. Moreover, the startup of threads will generate corresponding thread scheduling overhead.
  • Thread destruction.
  • Thread scheduling overhead. Scheduling of threads will lead to context switching, thereby increasing the consumption of processor resources and reducing the processor resources that the application itself can use.

In Java, we will define an object pool for large objects to avoid frequent creation of large objects. Thread is also an object. We can also use the object pool to maintain a certain number of Threads. This is the thread pool, but it The implementation method is different from the ordinary object pool. A certain number of worker threads can be pre-created inside the thread pool. The client code does not need to borrow threads from the thread pool but submits the task it needs to perform as an object to the thread pool. The thread pool may cache these tasks in the work queue, and each worker thread inside the thread pool continuously removes tasks from the queue and executes them. Therefore, the thread pool can be regarded as a service based on the producer-consumer model. The worker thread maintained internally in the service is equivalent to the consumer, the client thread of the thread pool is equivalent to the producer thread, and the client The tasks submitted by the code to the thread pool are equivalent to "products", and the queue used to cache tasks inside the thread pool is equivalent to the transmission channel.
Insert image description here
The top-level interface of the thread pool in Java is java.util.concurrent.Executor, and the commonly used implementation class is java.util.concurrent.ThreadPoolExecutor. Creating a thread pool requires defining seven parameters, which are reflected in The code is the constructor of ThreadPoolExecutor containing seven parameters:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    
    
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
        null :
    AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
  • corePollSize: the number of core threads in the thread pool
  • maximumPoolSize: the maximum number of threads in the thread pool
  • keepAliveTime: the survival time of idle threads
  • unit: time unit
  • workQueue: work queue
  • threadFactory: thread factory
  • handler: rejection policy

After submitting a task to the thread pool, the life cycle of the task is as follows:

  1. If the current number of threads in the thread is less than the number of core threads when submitting a task, threads will be created directly to execute the task.
  2. If the current number of threads in the thread is greater than the number of core threads when submitting a task, the task is added to the work queue.
  3. If the work queue is full, create a thread directly to perform the task
  4. If the work queue is full and the current number of threads is equal to the maximum number of threads, the configured reject policy is executed
submit 和 execute

Two methods for submitting tasks are defined in the thread pool, namely submit and execute, which are defined as follows:

void execute(Runnable command);

<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

Future<?> submit(Runnable task);

The execute method has no return value. The task submitted using the execute method will execute the configured uncaught exception handler after the thread throws an uncaught exception. The submit method returns a Future object, which represents the abstraction of the result returned after executing the task. Use the submit method. The submitted task will not execute the configured uncaught exception handler after the thread throws an uncaught exception, because the thread pool passes the uncaught exception to Future, and the task result can be obtained by calling the Future.get() method. Getting an uncaught exception. If the Future.get() method is called when the worker thread has not finished executing, the calling thread will be blocked, and Future.get() will not return until the worker thread finishes executing. The Future.isDone() method can be used to determine that the execution of the worker thread has ended. The Future.cancel(boolean mayInterruptIfRunning) method can cancel the execution of the task. When the task is still in the waiting queue, it will be removed and no longer executed. For this task, when mayInterruptIfRunning is true, an interrupt request will be sent to the worker thread.

Utility tools

The JDK standard class library defines the thread pool utility class java.util.concurrent.Executors, which can be used to quickly create a thread pool.

  • Executors.newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
    
    
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

That is, the number of core threads is 0, the maximum number of threads is Integer.MAX_VALUE, the maximum idle space allowed for worker threads is 60 seconds, and SynchronousQueue is used internally as a thread pool for the work queue.

From the above definition, we can see that the tasks in the thread pool will not enter the waiting queue but directly create a thread pool for execution, and the number of threads in the thread pool can be regarded as unlimited. The thread will be idle after 60 seconds. Pool recycling.

  • Executors.newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads) {
    
    
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

That is, a thread pool with an unbounded queue as the work queue, the number of core threads equals the maximum number of threads equal to nThreads, and idle worker threads will not be automatically cleaned up. This is a fixed-size thread pool that neither increases nor decreases worker threads once it reaches its core thread pool size. Therefore, once such a thread pool instance is no longer needed, we must actively close it.

  • Executors.newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() {
    
    
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

This thread pool is similar to the thread pool returned by Executors.newFixedThreadPoll(1). This thread pool facilitates us to implement a single producer-single consumer model.

Debugging and testing of Java multi-threaded programs

A real Java system often has hundreds of threads running. If there are no corresponding tools to monitor these threads, then these threads are black boxes for us. The main way to monitor threads is to obtain and view the program's thread dump (Thread Dump). A thread dump contains thread information for the program at the moment the thread dump was taken. This information includes which threads are in the program and specific information about these threads.

Here's how to get a thread dump:

  • Execute command: jstack -l PID
  • jvisualvm tools
  • JMC tools

Guess you like

Origin blog.csdn.net/imonkeyi/article/details/132650692