Public number: byte array, hope to help you 🤣🤣

Today, multithreaded programming is a basic requirement that needs to be implemented on most platforms and applications. This series of articles to the Java platform multithreaded programming knowledge to explain, from the concept of entry, the bottom implementation to the upper application will be involved, is expected to have a total of five articles, hope to help you 😎😎

This article is the first article to introduce the basic concepts of Java multithreading and the challenges that need to be faced, is the stepping stone to the following article

Multithreaded programming

Suppose there are three events (event A, event B, and event C) that need to be completed, and each event contains A certain pre-processing time and waiting time, that is, each event needs to be processed for A certain period of time, and then wait for A period of time after processing, after which the event is regarded as completed. Then, we can accomplish these three events in three different ways:

  • Serial port. Three events are processed in sequence, waiting for one event to be processed and waiting for the next event to be processed. It takes a man to do this
  • Concurrency. After the preprocessing of event A is complete, we move on to event B. After the preprocessing of event B is complete, we move on to event C. Finally, we simply wait for all three events to complete. It takes a man to do this
  • In parallel. Three incidents were handed over to three people for simultaneous processing. It takes three men to do this

Intuitively, serial processing is the least efficient and the most time-consuming, wasting manpower every time it waits for an event to complete. Parallel processing has the highest efficiency and the shortest time. Theoretically, the total time required depends on the event with the longest time, but it also requires the highest labor cost. The processing efficiency and time consumption of concurrency are between serial and parallel, and the labor cost is equal to serial (both lower than parallel).

Map from the hypothetical scenario above to the software world

  • Concurrency is in a period of time to alternate ways to complete multiple tasks, using multiple threads to handle different tasks, even in the case of a single processor can through time switching technology to achieve within a time period to run multiple threads, so even if only one processor can also achieve concurrency
  • Parallelism is to complete multiple tasks in the way of simultaneous processing. Multiple threads are used to process different tasks respectively, and then the multiple threads are transferred to different processors for running. Therefore, parallelism can be realized only with multiple processors
  • In reality, the number of threads required to execute at the same time is often far greater than the number of processors, and concurrency is our primary goal. Therefore, we can understand: the process of realizing multithreaded programming is to change the execution mode of tasks from serial to concurrent process, that is, to achieve concurrency, in order to improve the efficiency of the program and hardware as much as possible

So what are the benefits of using multiple threads?

  • For a computer, its processor is several orders of magnitude faster than the storage and communication subsystem. If only a single thread is used, then when the thread is handling disk I/O, database access and other tasks, the processor will be idle, which causes a great waste of performance. At this point you can use multithreading to make the processor as far as possible in the running state, as far as possible to use its computing power

  • Here, for a server, one of the important criteria to measure its performance is the size of the number of transactions per second (TPS), which represents the average number of requests a server can respond to in a second. A server may receive multiple requests in a very small amount of time, and TPS on the server is closely related to the concurrency (the ability to handle multiple tasks at the same time) of the program

  • For example, in Android application development, the system stipulates that only the main thread can draw and refresh UI. If time-consuming operations (IO reads and writes, network requests, etc.) are not processed by sub-threads, The user’s UI actions to the app (clicking on the screen, swiping a list, etc.) will most likely not be handled by the main thread in time, causing the app to seem stuck and the end user to give up using the app

Therefore, the use of multithreaded programming can maximize the use of the system to provide processing power, improve the program throughput and responsiveness, avoid performance waste. But is using multiple threads necessarily efficient? This is not necessarily

  • After multi-threading is adopted, each thread may compete with each other for system resources, such as processor time slice, exclusive lock, bandwidth, disk read and write, etc. Resources are often limited and can only be used by one thread at a time, and the ultimate benefit of concurrent programming is often limited by the limited allocation of resources. Multiple threads competing for the same exclusive resource can cause issues such as thread context switches and even deadlocks
  • For example, when multiple threads are used to load a network file in segments, you want to reduce download time. Due to bandwidth size is fixed, the use of multiple threads at the same time to download the first of each thread will lower the average size of the available bandwidth, each thread downloaded to a single resource also need through the hard disk read and write merged into one complete file, each section of the consolidation of resources needed in order to write the degree of scheduling, maintenance scheduling sequence is also has the performance of consumption, Multiple threads reading and writing IO also increase the number of thread context switches. Therefore, there are situations where multithreading may not be “worth it” and need to be measured against the actual situation

Processes and threads

  • Program is a static concept describing instructions, data and their organizational form
  • A Process is a running instance of a program. Each started program corresponds to a Process running on the operating system. It is a dynamic concept. Process is the basic unit of the program to apply for resources (memory space, file handles, etc.) from the operating system, and the basic unit of the operating system to schedule and allocate resources. Running a Java program is essentially starting a Java virtual machine process
  • Thread is the smallest unit that can be independently executed in a process and the smallest unit that the operating system can schedule operations. Thread is also known as lightweight process. Each thread is always contained within a specific process, and a process can contain multiple threads and at least one thread, which is the actual running unit in the process. All threads in the same process share resources in that process (memory space, file handles, and so on)
  • A Task is a logical calculation to be performed by a thread. Threads are created to perform specific logical computations, and the work they do is called the thread’s task

Multithreaded programming is a programming paradigm (Praadigm) in which threads are the basic unit of abstraction. Almost all modern computer operating systems support multitasking. There are two different types of multitasking: process-based multitasking and thread-based multitasking

  • Process-based multitasking refers to the operating system’s support for running multiple programs at the same time. A process is the smallest unit of code that a scheduler can schedule. Processes are heavy tasks, each process needs to have its own address space, interprocess communication is expensive and has many restrictions, and switching from one process context to another is also expensive
  • Thread-based multitasking means that a single process can perform multiple tasks at the same time, and a thread is the smallest unit of code a scheduler can schedule. Thread-based multitasking requires much less overhead than process-based multitasking. Threads are lightweight tasks that share resources in the same process. The overhead of inter-thread communication is small, and the overhead of switching between different thread contexts in the same process is much lower than that of switching between different process contexts

Process-based multitasking is implemented and managed by the operating system, which is beyond the reach of normal program development. Thread-based multitasking, on the other hand, can be implemented and managed by the developers themselves. It can be said that one purpose of multithreaded programming is to achieve thread-based multitasking

Java provides built-in support for multithreading. The java.lang.Thread class in the Java standard library is an abstract implementation of the concept of threads, providing unified processing of Thread operations on different hardware and operating system platforms, shielding the differences of different hardware and operating system

Java itself is a multithreaded platform, and even if the developer does not actively create threads, multiple threads are used in the process (for example, there are GC threads). Single-threaded programming often refers to a program in which the developer does not actively create threads

Third, the Thread

The Java standard library java.lang.Thread is the Java platform’s abstract implementation of the concept of threads. An instance of the Thread class or its subclasses is a Thread

1. Thread properties

attribute meaning
Number (ID) Long. Used to identify different threads. Different threads have different numbers that are unique within a single lifetime of the Java virtual machine
Name (Name) String. Threads can be used to distinguish different threads and locate problems during development and debugging. Threads can be repeated during a single Java VM life cycle
Category (Daemon) Boolean. A value of true indicates that the thread is a daemon thread, otherwise it is a user thread
Priority Int. Java defines 10 priorities from 1 to 10, with a default value of 5

Threads in Java are divided into user threads and daemon threads according to whether or not they prevent the Java virtual machine from stopping properly. User threads prevent the Java virtual machine from stopping properly, meaning that a Java virtual machine cannot stop properly until all of its user threads have finished running. Daemon threads do not affect the normal stopping of the Java virtual machine, even if there are daemon threads running in the application. Therefore, daemon threads are suitable for performing tasks of low importance. However, there is nothing the user thread can do to prevent the Java VIRTUAL machine from being stopped if it is forced to stop or if it is stopped due to an exception

Java thread priority attribute essentially just a message to the thread scheduler, so that the thread scheduler decides what thread priority scheduling, the higher priority thread will get more processor time in theory, but the thread scheduler is no guarantee of threads according to the discretion of the thread priority scheduling. In addition, the operating system on which the JVM is running can ignore or even actively modify our thread priority configuration, and if the priority is incorrectly set, threads can never be run, which is thread starvation

If thread B is created in thread A, then thread B is called A child of thread A, and thread A is called the parent of thread B. Since the Main thread created by the Java virtual machine is responsible for executing the main() method, the entry method of the Java program, the threads created in the main method are either child or indirect child threads of the main thread. In addition, whether a thread is a daemon thread is the same as its parent thread by default, and the default value of thread priority is the same as that of its parent thread. It is important to note that although threads have this parent-child relationship, they do not affect each other’s life cycle, and the end of the life cycle of one thread (whether normal or abnormal) does not affect the continuation of the other thread

2. Thread methods

methods instructions
static Thread currentThread() Returns the current thread of execution object
static void yield() Causes the current thread to voluntarily relinquish its processor occupation, but does not necessarily cause the current thread to pause
static void sleep(long) Causes the current thread to sleep for a specified time
void start() Starting a thread
void join() If thread A calls thread B’s join() method, thread A pauses until thread B finishes running
void suspend() Pause thread to go to sleep (deprecated)
void resume() Wake up threads, used in pairs with suspend() (deprecated)
void stop() Stop the thread (obsolete)

Thread.suspend() and thread.resume () are methods provided by the Thread class to suspend and wake up a task when certain running conditions are not met, and to resume the task when certain running conditions are met. These methods are now obsolete

3. Thread state

A thread may be in different states throughout its life and may switch back and forth between states. For a given Thread instance, the Thread State can be obtained using the thread.getState () method, which returns a thread.state enumeration value that identifies the current State of the Thread at the time the method is called. The return value can include the following possibilities:

value state
NEW The thread is new and the start() method has not been called
RUNNABLE The thread is either currently executing or is ready to execute after the processor time slice is acquired
BLOCKED A thread is suspended because it initiated a blocking operation or is waiting for a needed resource
WAITING A thread suspends execution because it is waiting for some action. For example, a thread in this state needs to be woken up by an external thread because it is in this state because the wait() or join() methods were called
TIMED_WAITING A thread is suspended for a specified period of time, and when the specified period of time is reached, the thread becomes RUNNABLE. For example, this state occurs when the sleep(long), wait(long), join(long) and other methods are called
TERMINATED A thread that has finished running is in this state

The life cycle of a thread can be described in more colloquial terms:

  1. NEW state -NEW

    When a thread object is created using the new keyword, the thread is in the new state, as is a thread that has been created but has not been started. Threads remain in this state until thread.start () is called

  2. Ready state -RUNNABLE

    When the thread object calls the start() method, the thread enters the ready state. This state can be thought of as a composite state with two child states: READY and RUNNING. The former indicates that the thread is in the ready queue and can officially run when it is selected by the thread scheduler and becomes RUNNING state. The latter indicates that the thread is running and that the instructions corresponding to its run() method are being executed by the processor. When executing the thread’s yiedId() method, its state may switch from RUNNING to READY

  3. The state is BLOCKED

    This state occurs when a thread initiates a blocking IO operation or requests an exclusive resource held by another thread. Threads that process BLOCKED do not consume CPU resources. When its target behavior or target resource is satisfied, it can switch to the RUNNABLE state

  4. Indefinite WAITING state -WAITING

    When a Thread needs to meet certain execution conditions that are not currently met, it is usually switched to WAITING by having the Thread actively call object.wait (), thread.join (), and similar methods. A thread currently in WAITING is suspended and needs to be woken up by an external thread using object.notify ()

  5. Finite wait state -TIMED_WAITING

    TIMED_WAITING is similar to WAITING. The difference is that the TIMED_WAITING state does not allow the thread itself to wait indefinitely. Its waiting behavior is limited within a specified time range. When the specific operation expected by the thread is not completed within the specified time, the thread will be converted to the RUNNABLE state. A thread can be switched to TIMED_WAITING using the object.wait (long) method

  6. TERMINATED state -TERMINATED

    When a thread completes its task or is forced to terminate for some other reason, it switches to the terminated state, and the entire life cycle of the thread ends

Because a thread can be started only once in its life, it is in the NEW state and TERMINATED state only once. For a multithreaded system, the ideal situation is for all started and unfinished threads to remain in the RUNNING state, but this is not possible. In a real-world scenario, threads switch back and back between multiple states, and a thread switching from a RUNNABLE state to one of BLOCKED, WAITING, or TIMED_WAITING indicates a thread context switch

4, ThreadGroup

ThreadGroup represents a group of associated threads. It is an internal property contained in the Thread class and can be obtained by thread.getThreadGroup (). The relationship between threads and thread groups is similar to the relationship between files and folders in a file system. A thread group can contain multiple threads as well as other thread groups

By default, a thread is grouped under its parent thread group if the thread group is not displayed when it is created. This can be seen in the following methods of the Thread class:

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {···· Thread parent = currentThread(); SecurityManager security = System.getSecurityManager();if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager what to do. */
            if(security ! =null) {
                Thread.currentthread ().getThreadGroup()
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter use the parent thread group. */
            if (g == null) { g = parent.getThreadGroup(); ...}}}Copy the code

The Java virtual machine automatically assigns a thread group to the main thread (the parent thread of all threads) when it is created, so each thread has a thread group associated with it. By default, the parent thread group of a thread group is the thread group of the thread in which the thread group is declared. However, not all thread groups have a parent thread group. The topmost thread group does not contain a parent thread group

/ * * * the author: leavesC * time: 2020/8/12 description: no * * GitHub:https://github.com/leavesC * /
fun main(a) {
    val mainThreadGroup = Thread.currentThread().threadGroup
    println(mainThreadGroup)
    println("mainThreadGroup.parent: " + mainThreadGroup.parent)
    println("mainThreadGroup.parent.parent: " + mainThreadGroup.parent.parent)
    val thread = Thread()
    println(thread.threadGroup)
    val thread2 = Thread(ThreadGroup("otherThreadGroup"), "thread")
    println(thread2.threadGroup)
// java.lang.ThreadGroup[name=main,maxpri=10]
// mainThreadGroup.parent: java.lang.ThreadGroup[name=system,maxpri=10]
// mainThreadGroup.parent.parent: null
// java.lang.ThreadGroup[name=main,maxpri=10]
// java.lang.ThreadGroup[name=otherThreadGroup,maxpri=10]
}
Copy the code

ThreadGroup has its own design flaws and is currently used in limited scenarios, which can be ignored in daily development

5. Thread exception catching

Most of the time, we create a thread pool to execute tasks, and when a task throws an exception that causes its execution thread to terminate abnormally, we need to report the exception for subsequent analysis. In order to achieve this effect, they need to be able to receive event notifications at the termination of the Thread is abnormal, it’s need to use Thread. SetUncaughtExceptionHandler (UncaughtExceptionHandler) method

This method allows us to retrieve Thread objects and Throwable instances when an exception occurs and before the Thread is stopped

/ * * * the author: leavesC * time: * 2020/8/12 hour description: * GitHub:https://github.com/leavesC * /
fun main(a) {
    val runnable = Runnable {
        for (i in 4 downTo 0) {
            println(100 % i)
            Thread.sleep(100)}}val thread = Thread(runnable, "otherName")
    thread.setUncaughtExceptionHandler { t, e ->
        println("threadName: " + t.name)
        println("exc: $e")
    }
    thread.start()
}
Copy the code
0
1
4
2
4
0
0
1
0
0
threadName: otherName
exc: java.lang.ArithmeticException: / by zero
Copy the code

Threadgroups also implement the UncaughtExceptionHandler interface, so if Thread objects do not contain associated UncaughtExceptionHandler instances, The exception is handed over to ThreadGroup for handling

This can be seen in the following logic of the Thread class

public class Thread implements Runnable {

    private ThreadGroup group;

    private volatile UncaughtExceptionHandler uncaughtExceptionHandler;

    private void dispatchUncaughtException(Throwable e) {
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }

    public UncaughtExceptionHandler getUncaughtExceptionHandler(a) {
        returnuncaughtExceptionHandler ! =null? uncaughtExceptionHandler : group; }}Copy the code

ThreadGroup by default will be exceptions to the parent Thread group for processing, and for the Thread does not contain the parent Thread group set of objects (the top Thread group), will be the exception to the Thread class defaultUncaughtExceptionHandler for processing. So, we can through the Thread of the static method setDefaultUncaughtExceptionHandler method for program set up a global default exception handler

public class ThreadGroup implements Thread.UncaughtExceptionHandler {

    public void uncaughtException(Thread t, Throwable e) {
        if(parent ! =null) {
            // When there is a parent thread group, the exception is handled by the parent thread group
            parent.uncaughtException(t, e);
        } else {
            / / when the parent thread group does not exist, will try to abnormal to DefaultUncaughtExceptionHandler to deal with
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if(ueh ! =null) {
                ueh.uncaughtException(t, e);
            } else if(! (einstanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" "); e.printStackTrace(System.err); }}}}public class Thread implements Runnable {

    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
    
    public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(a){
        return defaultUncaughtExceptionHandler;
    }
    
    // Set the global default exception handler
    public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
        SecurityManager sm = System.getSecurityManager();
        if(sm ! =null) {
            sm.checkPermission(
                new RuntimePermission("setDefaultUncaughtExceptionHandler")); } defaultUncaughtExceptionHandler = eh; }}Copy the code

Therefore, when a thread terminates due to an exception, the selection priority of UncaughtExceptionHandler instances goes from high to low:

  1. Thread.uncaughtExceptionHandler
  2. ThreadGroup.uncaughtExceptionHandler
  3. Thread.defaultUncaughtExceptionHandler

6, ThreadFactory

It is common to use multiple threads in a project, and if you simply create threads with new Threads () each time, it is difficult to locate problems when they occur. So the Java standard library also provides a factory method for creating threads, called the ThreadFactory interface

public interface ThreadFactory {

    Thread newThread(Runnable r);
    
}
Copy the code

ThreadFactory provides methods associated with the task Runnable to be executed and the Thread to be created. You can use ThreadFactory to specify what tasks a Thread will perform, give the Thread a meaningful name, set the Thread’s priority, and so on

For example, if the Executors contain a DefaultThreadFactory, set a thread name for each created thread by incresing its threadNumber, ensure that the thread is a user thread, and ensure that the thread priority is normal

    static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private finalString namePrefix; DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s ! =null)? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix ="pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if(t.getPriority() ! = Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY);returnt; }}Copy the code

For the thread pool defined by our project, a meaningful use of ThreadFactory is to set the associated UncaughtExceptionHandler for the thread, which is beneficial in terms of improving the robustness of the system

7. Timing of thread execution

Simply put, running a thread requires the Java virtual machine to call the run() method of the Runnable object so that the thread’s task-processing logic can be executed. However, just because we start a Thread with thread.start () does not mean that the Thread can be executed immediately. The timing of execution is determined by the Thread scheduler, and execution timing is uncertain, and may never run due to Thread activity failure. In addition, because the run() method is public, it can also be invoked externally, but its task is executed by the current running thread, which is mostly moot

Regardless of how a thread is created, when its run() method terminates (either normally or due to an exception), the thread is at the end of its life cycle and its resources are later collected by the Java Virtual Machine garbage. And threads are disposable resources, we can’t again by calling start () method to restart the threads, throw IllegalThreadStateException when multiple calls the method

Four, multithreading security

The realization of multithreaded programming is not simply to declare multiple Thread objects and start it. In the real scene, multiple threads often need to complete various complex operations such as data exchange and behavior interaction, rather than simply “go their own way”. Using multiple threads brings up many issues that do not exist or do not need to be considered in a single thread as opposed to a single thread

1, the race

Let’s start with a simple example. Suppose there is a Shop, Shop, with an initial number of stores of zero. There are forty producers producing goods for them. Each Producer provides one item for the store. Therefore, theoretically, when all Producer finishes producing, the goodsCount of the quantity of goods in Shop should be 40

However, if you run the following code you will find that the actual number is most likely less than forty

/ * * * the author: leavesC * time: 2020/8/3 "* description: * GitHub:https://github.com/leavesC * /
class Shop(var goodsCount: Int) {

    fun produce(a) {
        goodsCount++
    }

}

class Producer(private val shop: Shop) : Thread() {

    override fun run(a) {
        sleep(100)
        if (shop.goodsCount < 40) {
            shop.produce()
        }
        println("over")}}fun main(a) {
    val shop = Shop(0)
    val threads = mutableListOf<Thread>()
    for (i in 1.40.) {
        threads.add(Producer(shop))
    }
    threads.forEach {
        it.start()
    }
    // Ensure that all Producer threads complete execution before executing subsequent statements
    threads.forEach {
        it.join()
    }
    println("shop.goodsCount: " + shop.goodsCount)
}
Copy the code

The above code uses multiple threads. The behavior logic of a single Producer thread is independent, while the behavior logic of multiple Producer threads is interlaced with each other and in uncertain order for Shop, which may lead to the following situation: The two producers simultaneously determine that the current number of goods is ten, and then simultaneously produce an eleventh item for the shop (shop.Goodscount ++). As a result, the eleventh item produced by one Producer is overwritten by another Producer. Both producers produce only one item, i.e. Invalid update/missing update

The shop.goodscount ++ statement, while it looks like an indivisible operation (atomic operation), is actually equivalent to the combination of three instructions represented by the following pseudocode:

load(shop.goodsCount , r1) // Instruction 1 reads the value of the variable shop.goodsCount from memory to register R1
increment(r1) // instruction 2, increment register R1 by 1
store(shop.goodsCount , r1) // write the contents of register R1 to the memory space corresponding to the variable shop.goodscount
Copy the code

Multiple Producer threads may execute these instructions simultaneously. For example, if the current goodsCount is ten, Producer1 and Producer2 execute to instruction 1 at the same time, and both threads read goodsCount into their respective processor registers. That is, each thread has a copy of the data and increments the value of their registers by one. When instruction 3 is executed, the memory space where goodsCount is located is specific, so the two Producer threads will overwrite each other in sending back the value of goodsCount in the memory space. That is, the end result of something that is supposed to increase by two ends up increasing by one

This is a common phenomenon in multithreaded programming, namely races. A race is when the correctness of a calculation depends on relative chronological order or interlocking of threads. A race does not necessarily result in an incorrect calculation, it just does not rule out the possibility that the calculation may be correct and wrong sometimes


From the above code, we can summarize the two modes of race: read-modify-write and check-then-act

The steps of read-modify-write are to read the value of a shared variable, perform some calculation based on the value, and update the value of the shared variable based on the calculation. For example, goodsCount++ in the code above is this pattern, which is equivalent to the combination of three instructions represented by the pseudocode below

load(shop.goodsCount , r1) // Instruction 1 reads the value of the variable shop.goodsCount from memory to register R1
increment(r1) // instruction 2, increment register R1 by 1
store(shop.goodsCount , r1) // write the contents of register R1 to the memory space corresponding to the variable shop.goodscount
Copy the code

Thread B may have finished executing instruction 3 when thread A has finished executing instruction 1, started executing or is executing instruction 2, which makes the shared variable shop.goodsCount currently held by thread A old. When thread A has finished executing instruction 3, this overwrites thread B’s updates to the shared variable. The update is lost


The check-then-act step is to read the value of a shared variable and determine the next action based on the value of the variable. For example, the following code is this pattern

if (shop.goodsCount < 40) { //操作1
    shop.produce() //操作2
}
Copy the code

Before thread A completes operation 1 and starts operation 2, thread B may have updated the value of the shared variable shop.goodsCount, causing the condition in the if statement to be invalid, but thread A still performs operation 2. This is because thread A does not know that the shared variable has been updated and the run condition is no longer valid

From the above analysis we can summarize the general conditions for race generation. Let Q1 and Q2 be operations that concurrently access the shared variable V, not both of which are read operations. If one thread executes Q2 while another thread is executing Q1, either a read or a write will result in a race. From this perspective, races can be seen as a result of interleaving operations performed by multiple threads accessing (reading, updating) the same set of shared variables. The problem of lost updates and reading dirty data in the above code is caused by the existence of races

It is important to note that races occur when multiple threads and shared variables are involved. If the system contains only a single thread, or no shared variables are involved, no races will occur. For local variables (both formal parameters and variables defined in the body of a method), the use of local variables does not cause a race because different threads access their own share of local variables

2. Thread safety

A class is said to be thread-safe if it can run in a single-threaded environment, if it can run in a multi-threaded environment without scheduling or alternate execution in a runtime environment, and if the user doesn’t have to do anything about it. Conversely, if a class works in a single-threaded environment and does not work in a multi-threaded environment, it is not thread-safe. Therefore, only non-thread-safe classes can cause races

If a class is not thread-safe, it is said to have multithreaded safety issues in a multithreaded environment. The above definition also applies to data shared between multiple threads

The multithreading safety problem can be summarized into three aspects: atomicity, visibility and order

atomic

Atom literally means indivisible. An operation involving access to a shared variable is an atomic operation if it is indivisible from any thread other than its executing thread, and the operation is said to be atomic accordingly. By “indivisible”, an operation that accesses (reads, writes) a shared variable is seen by any thread outside of its executing thread as either completed or has not yet occurred, and other threads do not see the intermediate effect of the partial execution of the operation

For example, suppose there is a shared global variable Shop object with an update() method. When thread A executes the update() method, thread B will see the intermediate effect that goodsCount has been incremented by one but CLERK has not been incremented by one, after thread A has executed statement 1 but before thread B has executed statement 2. At this point, we say that the update() method as a whole is not atomic

class Shop(var goodsCount: Int.var clerk: Int) {

    fun update(a) {
        goodsCount++ //语句1
        clerk++ //语句2}}Copy the code

There are two other points to consider to understand the concept of atomic manipulation:

  • Atomic operations refer to operations on shared variables. Operations involving only local variable access do not matter if they are atomic or can be treated as atomic operations
  • Atomic operations are described from the perspective of a thread other than the one executing the operation, that is, they only make sense in a multithreaded environment, so all operations in a single-threaded environment can be treated as atomic operations

In general, there are two ways to provide atomicity in Java:

  • The first is to use locks, “locks.” Locks are exclusionary in that they ensure that shared variables can only be accessed by one thread at a time. This eliminates the possibility of interference and collisions caused by multiple threads accessing the same shared variable at the same time, thus eliminating races
  • The second takes advantage of the CAS instruction provided by the processor. CAS instructions implement atomicity in essentially the same way as locks, except that locks are usually implemented at the software level, whereas CAS are implemented directly at the hardware level (processor and memory) and can be considered as “hardware locks”

The Java language specification states that in the Java language, reads and writes to any type of variable other than 64-bit are atomic operations. The Java VIRTUAL machine is not required to guarantee atomicity for reading and writing 64-bit data types such as long and double. The Java VIRTUAL machine can choose whether to implement this. So if multiple threads concurrently read and write the same long/double shared variable, one thread might read the “intermediate result” of another thread updating that variable. The reason for the intermediate result is that the virtual machine may split the 64-bit write operation into two steps, such as writing the lower 32 bits first and then writing the higher 32 bits, causing an external thread to read an intermediate result value. But this is not an issue to be particularly concerned about, since almost all commercial Java virtual machines today choose to implement atomic operations for reading and writing 64-bit data. In addition, the Java language specification specifies atomicity for reading and writing volatile long/double variables.

Note that the volatile keyword guarantees atomicity only for variable reads and writes, but not for other operations such as read-modify-write and check-then-act

visibility

In a multithreaded environment, an update made by one thread to a shared variable may not be immediately or ever read by subsequent threads, which illustrates one of the safety issues of multithreading: visibility. Visibility is the question of whether the results of one thread’s updates to a shared variable are visible to other threads reading the corresponding shared variable. Problems with visibility in multithreaded programs mean that some threads are reading old data, which often leads to unexpected problems in our programs

There will be visibility issues. On the one hand, the JIT compiler may automatically “optimize” the code to make it more efficient, invalidating shared variable updates. On the one hand, the processor does not directly access the shared variable in the main memory, but each retains a copy of the shared variable in its own cache. What the processor directly accesses is the copy data, and the modification of the copy data can be visible to other processors only after synchronization back to the main memory. So updates made by one processor to a shared variable may not be immediately synchronized to other processors, leading to visibility problems

A shared variable whose value is updated by one thread is called a relatively new value if the updated value can be read by another thread. If the thread reading the shared variable cannot update the value of the variable while reading and using it, the value read by the thread is called the latest value of the variable. The guarantee of visibility only means that a thread can read the relative new value of a shared variable, not that it can read the latest value of the corresponding variable

Visibility problems are caused by the use of multiple threads, regardless of whether the processor is currently single-core or multi-core. Under a single core processor, multi-thread concurrent is through the distribution of time slice, although multiple threads are running at this time in the same processor, but because in a context switch, one thread to modify a Shared variables will be as the context information stored, this will lead to another thread could not immediately read into this thread to modify a Shared variables

order

Before WE talk about ordering, let’s talk about reordering

Sequential structure is a basic structure in structured programming. It indicates that we want one operation to be executed before another, but in a multi-core processor environment, this order of operation execution may not be guaranteed. The compiler and processor can reorder the instructions of the source code without affecting the results of single-threaded execution. The processor may not execute the instructions in exactly the order specified by the program’s object code. In addition, multiple operations performed on one processor may not be in the order specified by the object code from the perspective of other processors. This phenomenon is called reordering. Reordering is an optimization for memory access-related operations (read and write) that can improve the performance of a single-threaded program without affecting its correctness. However, it can have an impact on the correctness of multithreaded programs, that is, it can cause thread-safety problems

Reordering is classified into the following types:

  • Compiler optimized reordering. The compiler can rearrange the execution order of statements without changing the semantics of a single-threaded program
  • Instruction – level parallel reordering. Modern processors use instruction-level parallelism to superimpose multiple instructions. If there is no data dependency, the processor can change the execution order of the machine instructions corresponding to the statement
  • Memory system reordering. Because the processor uses caching and read/write buffers, this makes the load and store operations appear to be out of order

Orderliness refers to the conditions under which threads running on one processor perform memory accesses in an order that appears to be out of order to other threads running on another processor. Out-of-order is when the order of memory access operations appears to change

Unsafe thread-safe classes

As mentioned earlier how to define whether a class is thread-safe, Java also provides many classes that are called thread-safe, such as java.util.Vector. Many of the add(), removeAt(), and size() methods inside the Vector class use Synchronize to ensure security in a multi-threaded environment, but this synchronization does not prevent developers from writing logically unsafe code

For example, for the following code. Even though the add() and removeAt() methods ensure that the two methods are executed sequentially due to the internal synchronization of the Vector class, the caller may read an outdated vector.size value due to the lack of additional synchronization. Eventually lead to index crossed throw ArrayIndexOutOfBoundsException

So a thread-safe class might just be thread-safe for its own single-operation behavior, so that we don’t need additional synchronization guarantees when we call it. However, for some consecutive calls in a particular order by the user, additional synchronization means need to be implemented externally to ensure the correctness of the call, otherwise it is possible to write unsafe code with thread-safe classes

/ * * * the author: leavesC * time: 2020/8/26 22:52 * description: * GitHub:https://github.com/leavesC * /
private val vector = Vector<Int> ()private val threadNum = 5

fun main(a) {
    val addThreadList = mutableListOf<Thread>()
    for (i in 1..threadNum) {
        val thread = object : Thread() {
            override fun run(a) {
                for (item in 1.10.) {
                    vector.add(item)
                }
            }
        }
        addThreadList.add(thread)
    }
    val printThreadList = mutableListOf<Thread>()
    for (i in 1..threadNum) {
        val thread = object : Thread() {
            override fun run(a) {
                for (index in 1..vector.size) {
                    vector.removeAt(i)
                }
            }
        }
        printThreadList.add(thread)
    }
    addThreadList.forEach {
        it.start()
    }
    printThreadList.forEach {
        it.start()
    }
    addThreadList.forEach {
        it.join()
    }
    printThreadList.forEach {
        it.join()
    }
}
Copy the code

4. Context switch

Concurrency is implemented regardless of whether there are multiple processors, even if there is only one processor, concurrency can be achieved through processor time slice allocation technology. The operating system assigns each thread a short period of time to run, then quickly switches to the next thread when each thread’s running time is up. Multiple threads perform concurrent and separate tasks in this intermittent fashion. A process in which a thread is deprived of the processor and suspended is called a cut, and a process selected by the thread scheduler to occupy the processor and run is called a cut

Operating system will be divided into a time slice, each thread each run will be allocated to several time slices, time slice determines a thread can continuously occupy the processor running time length, generally only dozens of milliseconds, multithreading on a single processor is through this time slice allocation way to achieve concurrency. When a thread in a process runs out of time or is forced or actively suspended due to its own reasons, another thread (in the current process or in another process) can be selected by the thread scheduler to occupy the processor and start running. This process in which one thread is deprived of the processor and suspended while another thread is given the processor and started is called thread context switching

Thread context switching is an inevitable outcome of real-world scenarios where the number of processors is much smaller than the number of concurrent threads the system needs to support. This also means that the operating system needs to store and recover the progress information of the corresponding thread at the time of cutting and cutting, i.e. the progress of the instructions executed by the corresponding thread at the moment of cutting and cutting. This progress information is called context

The process of switching a thread’s life cycle state from RUNNABLE to non-runnable is a context switch. When the suspended thread is selected by the operating system to resume running, the operating system restores the context previously saved for the thread so that it can continue to complete its tasks on this basis

According to the factors leading to context switching, context switching can be divided into voluntary context switching and non-voluntary context switching

  • Spontaneous context switching. This is a thread that cuts out because of its own cause. From the Perspective of the Java platform, a thread that performs any of the following operations during its execution causes a spontaneous context switch
    • Thread.sleep()
    • Object.wait()
    • Thread.yieid()
    • Thread.join()
    • LockSupport.park()
    • I/O operations
    • Wait for the lock held by another thread
  • An involuntary context switch. A thread that is being pushed out due to the thread scheduler. This is usually the case when the time slice of the cut thread runs out, or when a thread with a higher priority than the cut thread needs to run. In addition, the Garbage collection action of the Java virtual machine can cause an involuntary context switch because the garbage collector may need to suspend all application threads during GC to complete

The more context switches a system has over a period of time, the more processor resources it consumes, and the less processor resources it can actually use to execute the object code during that period, so we need to consider minimizing the number of context switches, which will be covered in a subsequent article

Five, thread scheduling

Thread scheduling is the process by which the operating system assigns processor rights to threads. There are two main scheduling methods:

  • Collaborative thread scheduling. In this strategy, the execution timing of a thread is determined by the thread itself, and the thread cedes control of the processor by actively notifying the system to switch to another thread. The advantage of this strategy is that it is simple to implement and can avoid thread safety problems by precisely controlling the execution order of threads. The disadvantage is that the process can be blocked because of a code defect in a single thread that prevents it from switching to the next thread
  • Preemptive thread scheduling. This is also the thread scheduling strategy used by the Java platform. In this strategy, the operating system decides which thread will use the current processor time slice, and the thread cannot decide when and in which order to run. Although we can passThread.yieid()The Thread class also provides a way to set Thread priorities, but the order in which threads are executed depends on the system in which they are running. The advantage of this strategy is that the whole process will not be blocked due to the problem of one thread and the concurrency is improved. The disadvantage is that the implementation is more complex, and will bring multi-thread security issues

Resource contention and resource scheduling

1. Resource contention

Resources that can only be occupied by one thread at a time are called exclusive resources. Common exclusive resources include locks, processors, files, and so on. Because of resource scarcity or the nature of the resource itself, we often need to share the same exclusive resource across multiple threads. When a thread occupies an exclusive resource without releasing its ownership, the phenomenon of other threads trying to access the resource is called resource contention, or contention for short. Obviously, contention is a phenomenon that occurs in a concurrent environment, and the greater the number of threads simultaneously attempting to access an exclusive resource that is already occupied by other threads, the higher the level of contention, and vice versa. The corresponding contention is called high contention and low contention

The higher the number of threads running in the same period of time, the higher the degree of concurrency, referred to as high concurrency. Although high concurrency increases the likelihood of contention, high concurrency does not necessarily mean high contention, because threads do not always apply for resources together at some point, resource requests may be staggered across multiple threads, or each thread may hold exclusive resources for a short time. The ideal of multithreaded programming is high concurrency and low contention

2. Resource scheduling

When multiple threads at the same time to apply for the same exclusive resources, application resources failed thread will tend to be placed into a waiting queue, when subsequent resource to be released in its thread, if just have an active threads to application resources, choose a thread at this time the exclusive rights to access to resources is a process of resource scheduling, Fairness is an important attribute of resource scheduling policy. Fairness refers to whether the applicants of resources are granted exclusive rights in strict accordance with the order of application. If any first applicant of a resource can always obtain the exclusive right of the resource before any second applicant, then the policy is called fair scheduling policy. If the later applicant of a resource is likely to obtain the exclusive right of the resource before the earlier applicant, then the policy is called unfair scheduling policy. Note that the unfair scheduling policy does not guarantee the fairness of resource scheduling, that is, it only allows unfair resource scheduling, rather than deliberately causing unfair resource scheduling

A fair resource scheduling strategy does not allow queue-jumping and resource applicants always obtain the exclusive right of resources in the order of first come first come first. If the current wait queue is empty, the thread applying for the resource can directly obtain exclusive rights to the resource. If the wait queue is not empty, each new incoming thread is inserted at the end of the wait queue. The advantage of a fair resource scheduling strategy is that the deviation of the time required by each resource applicant from the beginning of applying for resources to obtaining the exclusive right of corresponding resources is relatively small, that is, the time required by each applicant to successfully apply for resources is basically the same, and the phenomenon of thread hunger can be avoided. The disadvantage is that the throughput rate is low and the possibility of thread context switching is increased to ensure FIFO

Unfair resource scheduling policies allow queue-jumping. The incoming thread directly attempts to apply for the resource and is inserted into the end of the queue only if the application fails. Consider a scenario where two threads compete for the same exclusive resource:

  1. When a resource is released, if there is an active thread applying for the resource, that thread can preempt the resource without having to wake up the thread in the waiting queue. In this scenario, the relatively fair scheduling strategy eliminates the two operations of suspending incoming threads and waking up threads waiting for the head of the queue, and resources are used as well
  2. Even if a thread in the waiting queue have awakened to try to preempt resources monopoly, if the arrival of the new active thread take up resources of the time is not long, so it is possible at the start of the thread is awakened before apply for resources, the arrival of the new active thread has released the exclusive rights to resources, so as to not impede awakened thread application resources. This scenario also avoids the need to suspend new incoming threads

Therefore, the advantages of the unfair scheduling strategy are mainly as follows:

  1. Throughput is generally higher than fair scheduling, which means it can allocate resources to more applicants per unit of time
  2. Reduces the probability of context switching

The disadvantages of the unfair scheduling strategy are as follows:

  1. Because queue jumping is allowed, in extreme cases, threads in the waiting queue may never be able to obtain the resources they need, that is, the active fault phenomenon of thread starvation occurs
  2. The deviation of the time required by each resource applicant from the beginning of applying for resources to obtaining the exclusive right of corresponding resources may be large, that is, some threads may apply for resources quickly, while others need to pause and wake up several times before successfully applying for resources

In conclusion, the fair scheduling strategy is applicable to the situation where resources are held for a long time or the average time interval between threads applying for resources is long, or the time deviation required to apply for resources is small. In general, the cost of using a fair scheduling policy is higher than that of using an unfair scheduling policy. Therefore, the unfair scheduling policy should be used by default if there is no special requirement

7. Reference books

Java Multithreaded Programming Practical Guide (Core)

In-depth Understanding of the Java Virtual Machine

The Art of Concurrent Programming in Java