Without further ado, picture above.

1. Basic concepts

To speak of threads, you must speak of processes.

  • Process: a process is a running activity of code on the data set. It is the basic unit of system resource allocation and scheduling.
  • Thread: A thread is the execution path of a process. There is at least one thread in a process. Multiple threads in a process share the resources of the process.

The operating system allocates resources to processes, but CPU resources are allocated to threads, because threads are the basic unit of CPU allocation.

In Java, when we start main, we actually start a JVM process, and the main thread is one of the threads in the process, also known as the main thread.

The schematic diagram is as follows:

There are multiple threads in a process that share the heap and method area resources of the process, but each thread has its own program counter and stack.

2. Thread creation and execution

There are three ways to create threads in Java, namely, inheriting Thread class, implementing Runnable interface, and implementing Callable interface.

  • Inheriting the Thread class, overriding the run() method and calling the start() method to start the Thread
public class ThreadTest {

    /** * inherits Thread class */
    public static class MyThread extends Thread {
        @Override
        public void run(a) {
            System.out.println("This is child thread"); }}public static void main(String[] args) {
        MyThread thread = newMyThread(); thread.start(); }}Copy the code
  • Implement the Runnable interface run() method
public class RunnableTask implements Runnable {
    public void run(a) {
        System.out.println("Runnable!");
    }

    public static void main(String[] args) {
        RunnableTask task = new RunnableTask();
        newThread(task).start(); }}Copy the code

Neither of the above returns a value.

  • Implement the Callable interface call() method, which retrieves the return value of a task execution from FutureTask
public class CallerTask implements Callable<String> {
    public String call(a) throws Exception {
        return "Hello,i am running!";
    }

    public static void main(String[] args) {
        // Create an asynchronous task
        FutureTask<String> task=new FutureTask<String>(new CallerTask());
        // Start the thread
        new Thread(task).start();
        try {
            // Wait for execution to complete and get the return result
            String result=task.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch(ExecutionException e) { e.printStackTrace(); }}}Copy the code

3. Common methods

3.1. Thread waiting and notification

The Object class has several functions for waiting and notifying threads.

  • Wait () : When a thread calls a wait() method on a shared variable, the calling thread blocks and does not return until one of the following events occurs: (1) the thread calls notify() or notifyAll() on the shared object; (2) Another thread calls the interrupt() method of the thread, which returns InterruptedException.

  • Wait (long timeout) : This method uses a timeout parameter compared to wait(). The difference is that if a thread calling the shared object suspends the method, it is not awakened by another thread calling the notify() or notifyAll() method within the specified timeout ms. The function will still return due to timeout.

  • Wait (long timeout, int nanos), which internally calls wait(long timout).

Above is the thread waiting method, and wake up the thread is mainly the following two methods:

  • Notify () : A thread calling notify() on a shared object wakes up a thread that has been suspended after calling the wait series of methods on that shared variable. Multiple threads may be waiting on a shared variable, and it is random which waiting thread is awakened.

  • NotifyAll () : Unlike calling notify() on a shared variable, which wakes up one thread blocked on that shared variable, notifyAll() wakes up all threads suspended on that shared variable due to calls to the WAIT series.

If you have a scenario where you need to wait for several things to complete before proceeding, for example, if multiple threads load resources, you need to wait for all the threads to complete the load before summarizing and processing. The Thread class has a join method to implement.

3.2. Thread sleep

The Thread class has a static sleep method. When a Thread calls a Thread’s sleep method, the calling Thread temporarily cedes execution rights for a specified period of time. That is, it does not participate in CPU scheduling during this period, but the monitor resources owned by the Thread, such as locks, are still held. When the specified sleep time is up, the function returns normally, the thread is ready, and then participates in CPU scheduling. After obtaining CPU resources, the thread can resume running.

3.3. Give priority

Thread has a static yield method. When a Thread calls the yield method, it is actually signaling to the Thread scheduler that the current Thread is requesting its CPU usage, but the Thread scheduler can ignore the hint unconditionally.

When a thread calls yield, the current thread will yield CPU usage and then be in the ready state, and the scheduler will fetch the thread with the highest priority from the ready queue, or it may schedule to the thread that just yielded CPU to obtain CPU allocation.

3.4. Thread interruption

Thread interrupt in Java is a cooperative mode between threads. The execution of the thread can not be terminated directly by setting the interrupt flag of the thread, but the interrupted thread will handle it according to the interrupt state.

  • Void interrupt() : Interrupts A thread. For example, while thread A is running, thread B can call the money interrupt() method to set the thread’s interrupt flag to true and return immediately. Setting flags is just setting flags, thread A is not actually interrupted and will continue. If thread A blocks and hangs because of calling the Wait (), join, or sleep methods, thread B calls interrupt() and thread A returns with InterruptedException where it called these methods.

  • Boolean isInterrupted() method: checks whether the current thread has been interrupted.

  • Boolean interrupted() : Checks whether the current thread has been interrupted. Unlike isInterrupted, this method clears the interrupt flag if it detects that the current thread has been interrupted.

4. Thread status

The thread creation method and some common methods are organized above, which can be connected by the thread life cycle.

In Java, threads have six states:

state instructions
NEW Initial state: The thread is created, but the start() method has not been called
RUNNABLE Running state: Java threads refer to the ready and running states of the operating system as “running” in a catchy way
BLOCKED Blocked: Indicates that the thread is blocked on the lock
WAITING Wait state: a thread enters a wait state, which indicates that the current thread is waiting for other threads to do some specific action (notification or interrupt)
TIME_WAITING Timeout wait state: Unlike WAITIND, this state can return by itself at a specified time
TERMINATED Terminated: Indicates that the current thread has finished executing

Threads are not in a fixed state during their life cycle, but switch between different states as the code executes. Java thread states change as shown in the figure below:

5. Thread context switch

The purpose of using multithreading is to make the most of the CPU, but realize that each CPU can only be used by one thread at a time.

In order to make the user feel that multiple threads are executing at the same time, CPU resources are allocated by time slice rotation, that is, each thread is allocated a time slice, and the thread occupies THE CPU to perform tasks in the time slice. When a thread runs out of time slices, it becomes ready and frees up the CPU for another thread. This is a context switch.

6. Thread deadlocks

Deadlock refers to the phenomenon that two or more threads are waiting for each other during execution due to competing for resources. Without external force, these threads will wait for each other and cannot continue to run.

So why do deadlocks occur? The following four conditions must be met for a deadlock to occur:

  • Mutually exclusive condition: a thread can use a resource that has been acquired by another thread. That is, the resource is occupied by only one thread at a time. If another thread requests the resource at this point, the requester can only wait until the thread holding the resource releases it.
  • Request and hold condition: a thread has already held at least one resource, but it makes a new resource request, and the new resource has been occupied by another thread, so the current thread will be blocked, but the blocking does not release its acquired resources.
  • Inalienable condition: A thread cannot preempt a resource until it uses it up. It can release the resource only after it uses it up.
  • Loop waiting condition: when deadlock occurs, there must be a thread — resource loop chain, namely thread set {T0, T1, T2… Tn} T0 is waiting for a resource used by T1, Tl1 is waiting for a resource used by T2…… Tn is waiting for a resource already occupied by T0.

How do you avoid deadlocks? The answer is to break at least one condition for deadlocks to occur.

There’s no way to break the mutual exclusion condition, because locking is mutual exclusion. But there is a way to destroy the other three conditions, how do you do it?

  • For the “request and hold” condition, you can request all resources at once.

  • For the condition of “inalienable”, the thread that occupies part of the resource can actively release the resource it occupies when applying for other resources, so that the condition of “unpreemption” is broken.

  • The “loop waiting” condition can be prevented by applying resources sequentially. Sequential application means that resources are in linear order. When applying, you can apply for resources with small sequence number first and then apply for resources with large sequence number. In this way, there is no loop after linearization.

7. Thread classification

There are two types of threads in Java: daemon threads and user threads.

The main function is called when the JVM starts, and the money in main is a user thread. Many daemon threads, such as garbage collection threads, are also started inside the JVM.

So what’s the difference between a daemon thread and a user thread? One of the differences is that the JVM exits normally when the last non-daemon thread bundle is left, regardless of whether or not a daemon thread currently exists, meaning that the JVM exits regardless of whether or not the daemon thread terminates. In other words, the JVM normally does not exit as long as a user thread is not finished.

8 ThreadLocal.

ThreadLocal is a JDK package that provides thread-local variables, which means that if you create a ThreadLocal, each thread accessing that variable will have a local copy of that variable. When multiple threads manipulate the variable, they’re actually manipulating the variable in their own local memory. This avoids thread safety issues. Once the ThreadLocal variable is created, each thread is copied to its local memory.

A value can be set using the set(T) method and retrieved from the current thread using the get() method.

Let’s look at an example of ThreadLocal:

public class ThreadLocalTest {
    Create a ThreadLocal variable
    static ThreadLocal<String> localVar = new ThreadLocal<String>();

    // Print the function
    static void print(String str) {
        // Prints the value of the localVar variable in the current thread's local memory
        System.out.println(str + ":" + localVar.get());
        // Clears the localVar variable in the previous thread's local memory
        //localVar.remove();
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            public void run(a) {
                // Set the value of the local variable localVal in thread 1
                localVar.set("Value of thread 1");
                // Call the print function
                print(Thread 1 "");
                // Prints the value of the local variable
                System.out.println("After thread 1 prints the local variable:"+ localVar.get()); }}); Thread thread2 =new Thread(new Runnable() {
            public void run(a) {
                // Set the value of the local variable localVal in thread 2
                localVar.set("Value of thread 2");
                // Call the print function
                print(Thread 2 "");
                // Prints the value of the local variable
                System.out.println("After thread 2 prints local variables:"+ localVar.get()); }}); thread1.start(); thread2.start(); }}Copy the code

9. Java Memory model

In Java, all instance fields, static fields, and array elements are stored in heap memory, which is shared between threads within the heap.

Communication between Java threads is controlled by the Java Memory model, which determines when a write by one thread to a shared variable is visible to another thread.

From an abstract point of view, the Java Memory model defines an abstract relationship between threads and Main Memory: shared variables between threads are stored in Main Memory, and each thread has a private Local Memory where it stores copies of shared variables to read/write. Local memory is an abstraction of the Java memory model and does not really exist. It covers caching, write buffers, registers, and other hardware and compiler optimizations.

An abstract representation of the Java memory model is shown below:

In the actual implementation, the working memory of the thread is shown as follows:

10 and synchronized

A synchronized block is an atomic built-in lock provided by Java that can be used by every object in Java as a synchronization lock. These built-in Locks that cannot be seen by Java users are called internal locks, also known as monitor locks.

The thread’s executing code automatically acquires an internal lock before entering a synchronized block, which is blocked and suspended by other threads accessing the synchronized block. The thread that holds the internal lock releases the internal lock after either exiting the synchronized block normally or throwing an exception, or when the synchronized block calls the resource WAIT methods. A built-in lock is an exclusive lock. When a thread acquires the lock, other threads must wait for the thread to release the lock before acquiring the lock.

Synchronized memory semantics: The semantics of synchronized blocks remove variables used in synchronized blocks from the thread’s working memory. This way, when used within a synchronized block, the variable is not fetched from the thread’s working memory, but directly from main memory. The memory semantics of exiting a synchronized block are to flush shared changes made in a synchronized block to main memory.

11, volatile

As described above, locking can be used to solve shared memory visibility problems, but locking is cumbersome because of the overhead of thread context switching. Java also provides a weak form of volatile synchronization to solve memory visibility problems, using the volatile keyword. This keyword ensures that updates to a variable are immediately visible to other threads.

When a variable is declared volatile, the thread does not cache the value in a register or elsewhere when writing to the variable. Instead, it flusher the value back to main memory. When other threads read the shared variable, they retrieve the latest value from main memory instead of using the value in the current thread’s working memory.

Volatile, while providing visibility guarantees, does not guarantee atomicity of operations.

Atomic operations in Java

Atomic operations refer to a series of operations that are either all or none performed. There is no case where only part of the operations are performed.

For example, when designing counters, it is common to read the current value first, then +1, and then update. This process is read – change – write process, if the process is not guaranteed to be atomic, then there will be thread safety problems.

So how do you guarantee atomicity for multiple operations? The simplest way to do this is to use the synchronized keyword. You can also operate with CAS. Since Java 1.5, the JDK has also provided classes to support atomic operations.

Synchronized is an exclusive lock, and threads that do not acquire the internal lock are blocked, greatly degrading concurrency.

13. CAS operations in Java

In Java, locks have a place in concurrent processing, but the downside of using locks is that threads that do not acquire locks are blocked and suspended, resulting in thread context switching and rescheduling overhead.

Java provides the non-blocking volatile keyword to solve the visibility problem of shared variables. This partially offsets the cost of locking, but volatile only protects visibility of shared variables, not atomic problems such as read-modi-write.

CAS stands for Compre and Swap. CAS is a non-blocking atomic operation provided by the JDK that guarantees atomicity for comparison-update operations through hardware. The Unsafe class in the JDK provides a series of compareAndSwap * methods. Taking the compareAndSwapLong method as an example, see what a CAS operation is.

  • Boolean compareAndSwapLong(Object obj,long valueOffset,long expect, long Update) : CAS has four operands, which are the memory location of the object, the offset of the variable in the object, the expected value of the variable, and the new value. Ecpect is updated to UPDATE only when the expected value of the variable in obj whose memory offset is valueOffset is expected. This is an atomic instruction supplied by the processor.

CAS has a classic ABA problem. Because CAS needs to check if the value has changed as it operates on it, and if it hasn’t, it updates it, but if A value that was originally A changes to B and then to A, then CAS checks that it hasn’t changed, but actually has. The solution to the ABA problem is to use version numbers. Append the version number to the variable and increment the version number by 1 each time the variable is updated. Then A→B→A becomes 1A→2B→3A.

14. Overview of locks

14.1 optimistic locks and pessimistic locks

Optimistic locking and pessimistic locking are terms introduced in databases, but similar ideas are introduced in parallel packet locking.

Pessimistic locking refers to the conservative attitude towards data being modified by external threads, believing that data can be easily modified by other threads. Therefore, data is locked before being processed, and the data is locked during the whole process. The implementation of pessimistic locking often relies on the locking mechanism provided by the database, that is, locking records exclusively before performing operations on them. If the lock fails to be acquired, the data is being modified by another thread, and the current thread waits or throws an exception. If the lock is acquired successfully, the record is manipulated and the exclusive lock is released after the transaction is committed.

Optimistic locking is relative to pessimistic locking. It believes that data will not cause conflicts in general, so exclusive locking will not be added before accessing records, and data flushing will be formally detected only when data is submitted for update. Specifically, let the user decide what to do based on the number of rows returned by the UPDATE.

14.2. Fair locks and Unfair locks

According to the preemption mechanism of thread acquiring locks, locks can be divided into fair locks and unfair locks. Fair locks indicate that the order of thread acquiring locks is determined by the time when thread requests locks, that is, the thread that requests locks first will get the locks first.

Non-fair locking is break-in at run time, which means first come, not first served.

ReentrantLock provides an implementation of fair and unfair locks:

  • Fair lock: ReentrantLock pairLock =new eentrantLock(true)

  • Unfair lock: ReentrantLock pairLock = New ReentrantLock(false). If the constructor does not pass the number, it defaults to an unfair lock.

For example, if thread A already holds the lock, thread B requests the lock and it will be suspended. When thread A releases the lock, if thread C also needs to acquire the lock, then according to the thread scheduling policy, either thread B or thread C may acquire the lock without any other interference. If thread A uses the fair lock, thread C needs to suspend and let thread B acquire the current lock.

Use unfair locks when there is no fairness requirement, because fair locks incur performance overhead.

14.3. Exclusive and Shared Locks

Locks can be classified as exclusive or shared depending on whether they can only be held by a single thread or by multiple threads.

An exclusive lock guarantees that only one thread can acquire the lock at any time, and ReentrantLock is implemented in an exclusive manner.

Shared locks can be held by multiple threads at the same time, such as the ReadWriteLock read-write lock, which allows a resource to be read by multiple threads at the same time.

An exclusive lock is a pessimistic lock, while a shared lock is an optimistic lock.

14.4. Reentrant lock

When a thread attempts to acquire an exclusive lock held by another thread, that thread is blocked.

Does a thread block when it acquires a lock that it has already acquired? If the lock is not blocked, the lock is said to be reentrant, meaning that as long as the thread acquires the lock, it can access the code locked by the lock an infinite number of times (strictly a finite number of times).

14.5. Spin locks

Because threads in Java correspond to threads in the operating system, when a thread fails to acquire a lock (such as an exclusive lock), it is switched to kernel state and suspended. When the thread acquires the lock, it needs to switch to the kernel state to wake up the thread. However, switching from user state to kernel state costs a lot, which affects concurrency performance to some extent.

A spin lock is one that the current thread attempts to acquire without immediately blocking if it finds that the lock is already occupied by another thread (the default number is 10, which can be set using -xx :PreBlockSpinsh). It is likely that another thread has released the lock during the next few attempts, and the current thread will be blocked and suspended if the lock is not acquired after the specified number of attempts. This shows that spinning is using CPU time for the overhead of thread blocking and scheduling, but there is a good chance that this CPU time is wasted.

Reference:

[1] : Qu Successively, Xue Bintian, The Beauty of Concurrent Programming

[2] : Geek Time Java Concurrent Programming Practice

[3] : Fang Tengfei et al., The Art of Java Concurrent Programming