Java multithreading (super detailed)

Java multithreading

Multitasking

There are two types of multitasking:

  • process based

    A process is a “self-contained” running program. It is directly managed and run by the operating system. It has its own address space. Each process will consume memory when it is started.

    The process-based feature allows the computer to run multiple programs at the same time

  • thread based

    In a program, a program fragment that can run independently is called a “thread”, and a thread is a single sequential flow of control within a process

    A process has multiple threads. Multiple threads share the memory space of a process

    In a thread-based multitasking environment, a thread is the smallest processing unit

Advantages of multithreading

In multitasking, each process needs to allocate their own independent address space

Multiple threads can share the same address space and share the same process

Calling between threads involves more overhead than thread communication

The cost of switching between threads is lower than that of switching between processes

Main thread

When a program starts, a process is created by the operating system, and a thread runs immediately at the same time. This thread is usually called the main thread of the program.

Once the main method is executed, the main thread is started

Each process has a main thread

Features of the main thread:

  1. first start
  2. end at last
  3. spawn other child threads
  4. After the child thread ends, clean up the memory resources occupied by the child thread

Stages of threads

  • new state

    After using the new keyword and the Thread class or its subclasses to create a thread object, the thread object is in the newly created state. It remains in this state until the program start() the thread.

  • ready state

    When the thread object calls the start() method, the thread enters the ready state. The thread in the ready state is in the ready queue, waiting for the scheduling of the thread scheduler in the JVM.

  • Operating status

    If the thread in the ready state obtains CPU resources, it can execute run(), and the thread is in the running state at this time. A thread in the running state is the most complex, and it can become blocked, ready, and dead.

  • blocked state

    If a thread executes sleep (sleep), suspend (suspend) and other methods, after losing the occupied resources, the thread enters the blocked state from the running state. The ready state can be re-entered after sleep time has elapsed or device resources have been obtained. Can be divided into three types:

    • Waiting for blocking: the thread in the running state executes the wait() method to make the thread enter the waiting blocking state
    • Suspend blocking: Call the yield() method, and the thread explicitly gives up CPU control
    • Synchronous blocking: the thread fails to acquire the synchronized synchronization lock (because the synchronization lock is occupied by other threads)
    • Other blocking: When an I/O request is issued by calling the thread’s sleep() or join()-forced preemption, the thread enters the blocked state. When the sleep() state times out, join() waits for the thread to terminate or time out, or the I/O processing is completed, and the thread is transferred to the ready state again
  • Destroyed (dead) state

    A thread in the running state switches to the terminated state when it completes its task or when another termination condition occurs.

Create thread

There are three basic methods of creating threads in Java:

  • By inheriting from the Thread class itself;

    The second way to create a thread is to create a new class that extends the Thread class, and then create an instance of that class.

    Inheriting classes must override the run() method, which is the entry point for new threads. It must also call the start() method to execute.

    Although this method is listed as a multi-threaded implementation, it is essentially an instance that implements the Runnable interface.

    package com.lovo.main;
    
    public class MyThread extends Thread {<!-- -->
        private String name;
    
        public MyThread(String name) {<!-- -->
            super(name);
            this.name = name;
        }
    
        @Override
        public void run() {<!-- -->
            for (int i = 0; i < 50; i ++ ) {<!-- -->
                System.out.println("MyThread" + i);
            }
        }
    }
    

    Methods called by the Thread object

    Method Description
    public void start() causes the thread to start executing; Java The virtual machine calls the thread’s run method.
    public void run() If the thread is constructed using a separate Runnable run object, then Calls the Runnable object’s run method; otherwise, the method does nothing and returns.
    public final void setName(String name) Change the thread name to be the same as the parameter name.
    public final void setPriority(int priority) Change the priority of the thread.
    public final void setDaemon(boolean on) Mark the thread as a daemon thread or a user thread.
    public final void join(long millisec) The maximum time to wait for this thread to terminate is millis milliseconds .
    public void interrupt() Interrupt the thread.
    public final boolean isAlive() Tests whether the thread is alive.

    Static methods of the Thread class

    Method Description
    public static void yield() Suspend the currently executing thread object and execute other threads.
    public static void sleep(long millisec) Let the current execution within the specified number of milliseconds A thread sleeps (pauses execution), which is subject to the precision and accuracy of system timers and schedulers.
    public static boolean holdsLock(Object x) if and only if the current thread is on the specified object Only returns true when the monitor lock is held.
    public static Thread currentThread() Returns a reference to the currently executing thread object.
    public static void dumpStack() Print the stack trace of the current thread to the standard error stream.
  • By implementing the Runnable interface;

    Override Runnable’s run method

    package com.lovo.main;
    
    public class MyRunnable implements Runnable {<!-- -->
    
        @Override
        public void run() {<!-- -->
            for (int i = 0; i < 20; i ++ ) {<!-- -->
                System.out.println("MyRunnable" + i);
            }
        }
    }
    
  • Create threads through Callable and Future.

    package com.lovo.main;
    
    import java.util.concurrent.Callable;
    
    public class MyCallable implements Callable<String> {<!-- -->
    
        @Override
        public String call() throws Exception {<!-- -->
            for (int i = 0; i < 20; i ++ ) {<!-- -->
                System.out.println("MyCallable" + i);
            }
            return "ok";
        }
    }
    
    1. Create an implementation class of the Callable interface and implement the call() method. The call() method will be used as the thread execution body and has a return value.
    2. Create an instance of the Callable implementation class, use the FutureTask class to wrap the Callable object, and the FutureTask object encapsulates the return value of the call() method of the Callable object.
    3. Creates and starts a new thread using a FutureTask object as the target of a Thread object.
    4. Call the get() method of the FutureTask object to obtain the return value after the execution of the child thread is completed.

runnable uses execute

callable uses submit, he will return the future object, obtained through the get method

Note: The Start() method is called when the thread starts. After the thread is started, the CPU will decide which thread to schedule and execute the run()/call() method to complete the processing of the thread business

Note: If the run()/call() method is called directly, it is only called as a normal method

Thread pool management thread

The application creates a large number of threads with short execution time through the new thread() method, which consumes a lot of system resources and slows down the system response

Solution: use the thread pool to manage a large number of threads that have been executed in a certain period of time. By reusing existing threads, reduce the consumption caused by thread creation and destruction, and improve the system response speed

How the thread pool works

1. In the thread pool programming mode, the task is submitted to the entire thread pool instead of directly to a certain thread. After the thread pool gets the task, it will look for an idle thread internally. If there is, hand off the task to an idle thread
2. A thread can only execute one task at the same time, but can submit multiple tasks to a thread pool at the same time

Thread pool

7 ways to create thread pools in Java

  • Manually create thread pools via ThreadPoolExecutor.
  • Automatically create thread pools through Executors.
  1. Executors.newFixedThreadPool: Create a thread pool with a fixed size, which can control the number of concurrent threads, and the excess threads will wait in the queue.
  2. Executors.newCachedThreadPool: Create a cacheable thread pool. If the number of threads exceeds the processing requirements, the cache will be recycled after a period of time. If the number of threads is not enough, new threads will be created.
  3. Executors.newSingleThreadExecutor: Create a thread pool with a single number of threads, which can guarantee the execution order of first-in-first-out.
  4. Executors.newScheduledThreadPool: Create a thread pool that can execute delayed tasks.
  5. Executors.newSingleThreadScheduledExecutor: Create a single-threaded thread pool that can execute delayed tasks.
  6. Executors.newWorkStealingPool: Create a thread pool for preemptive execution (task execution order is uncertain) [JDK 1.8 added].
  7. ThreadPoolExecutor: The way to manually create a thread pool, it can set up to 7 parameters when it is created (ThreadPoolExecutor is the most primitive and recommended way to manually create a thread pool, it provides up to 7 parameters when creating parameters are available for setting.)

7 core parameters of thread pool

1. corePoolSize

Refers to the core thread size. The thread pool maintains a minimum number of threads. Even if these threads are idle, they will always exist in the pool (unless core thread timeout is set. >).

2. maximumPoolSize

Refers to the maximum number of threads allowed in the thread pool. When the core threads in the thread pool are all processing the execution state, there are newly requested tasks:

1. The work queue is not full: newly requested tasks are added to the work queue

2. The work queue is full: the thread pool will create a new thread to execute this task. Of course, creating new threads is not unlimited, because it will be limited by the maximum number of threads in maximumPoolSize.

3. keepAliveTime

It refers to idle thread survival time. Specifically, when the number of threads is greater than the number of core threads, the idle thread is waiting for the maximum time for a new task to arrive. If there is no task request beyond this time, the idle thread will be destroyed.

4. unit

It refers to the unit of idle thread survival time. Unit of measure for keepAliveTime. Enumeration type TimeUnit class.

5. workQueue
1. ArrayBlockingQueue

Array-based bounded blocking queue, featuring FIFO (first in, first out).

When the maximum number of threads already exists in the thread pool, if a new task is requested, the task will be added to the tail of the work queue. Once there are idle threads, the task will be taken out from the head of the queue to execute. Because it is an array-based bounded blocking queue, system resource exhaustion can be avoided.

So what if the bounded queue is full and all the threads at the maximum number are in the execution state, and new task requests occur at this time?

At this time, the Handler rejection strategy will be used to process the requested task. It will be introduced in detail later.

2. LinkedBlockingQueue

An unbounded blocking queue based on a linked list, with a default maximum capacity of Integer.MAX_VALUE, which can be regarded as an infinite queue, featuring FIFO.

Regarding whether the maximumPoolSize parameter works when the work queue is LinkedBlockingQueue, we need to decide according to the situation!

Case ①: If the size of the work queue is specified, such as core=2, max=3, workQueue=2, and the number of tasks task=5, the limit on the maximum number of threads in this case is valid.

Case ②: If the task force has a default value, then maximumPoolSize does not work, because new requested tasks can always be added to the queue.

3. Priority Blocking Queue

Priority unbounded blocking queue, the first two work queues are characterized by FIFO, and priority blocking queue can sort tasks through the parameter Comparator, and do not execute according to FIFO.

4.SynchronousQueue

A blocking queue that does not cache tasks, which is actually not a real queue because it does not provide space for storing tasks. When a task request comes from the producer, it will be executed directly, that is to say, This kind of queue is more suitable when there are enough consumers. Because this kind of queue has no storage capacity, only when another thread (consumer) is ready to work, the **put (enqueue) and take (exit)** methods will not be blocked.

The combination of the above four work queues with the thread pool is a producer-consumer design pattern. Producers add new tasks to the work queue, and consumers take out tasks from the queue for consumption. BlockingQueue can use any number of producers and consumers, which achieves decoupling and simplifies the design.

6. threadFactory

Thread factory, the factory used when creating a new thread, can be used to set the thread name, whether it is a daemon thread, etc.

7. handler

The task request processing strategy when Java concurrency exceeds the number of threads and work queues uses the strategy design pattern.

  • Policy 1: ThreadPoolExecutor.AbortPolicy (default)

    in the default processing strategy. The process throws RejectedExecutionException on rejection, refusing to execute.

  • Policy 2: ThreadPoolExecutor.CallerRunsPolicy

    The thread calling the execute method itself runs the task. This provides a simple feedback control mechanism that slows down the rate at which new tasks are submitted.

  • Policy 3: ThreadPoolExecutor.DiscardOldestPolicy

    If the executor is not closed, remove the task at the head of the work queue and retry the execution (possibly failing again, resulting in repeated execution).

  • Policy 4: ThreadPoolExecutor.DiscardPolicy

    Tasks that cannot be executed are simply deleted, and the current task will be discarded. It can be seen from the source code that this policy will not perform task operations.

Priority of threads

Each Java thread has a priority, which helps the operating system determine the scheduling order of the threads.

The priority of a Java thread is an integer ranging from 1 (Thread.MIN_PRIORITY ) to 10 (Thread.MAX_PRIORITY ).

By default, each thread is assigned a priority of NORM_PRIORITY (5).

Threads with higher priority are more important to the program and should be allocated processor resources before lower priority threads. However, thread priorities do not guarantee the order in which threads execute and are very platform dependent.

setPriority(int newp); // Modify the current priority of the thread
getPriority() // Get the priority of the current thread

A high thread priority does not necessarily mean that it will be able to preempt CPU control, it only means that the probability of preemption will be high

Thread synchronization

Thread synchronization means that when two or more threads access the same resource at the same time, in order to ensure data security, only one thread is allowed to access the same shared resource at the same point in time. Thread synchronization is also known as thread safety

Consequences: 1. Thread safety 2. Inefficiency

Thread synchronization, use the synchronization keyword synchronized to identify

Three ways to use synchronized:

  • Modified instance method: acting on the current instance to lock
  • Modified static method: acting on the current class object to lock
  • Modified code block: specify the lock object and lock the given object

There are two ways to implement thread synchronization

  1. synchronization method

    // synchronized modified synchronization method
    public synchronized void getMoney(Integer m) {<!-- -->
            if (money < m) {<!-- -->
                System.out.println("withdrawal failed");
            } else {<!-- -->
                this. money -= m;
                System.out.println("Withdrawal successful, withdrawal amount: " + m + ", balance: " + this.money);
            }
    }
    // Equivalent to
    public void getMoney(Integer m) {<!-- -->
            synchronized (this) {<!-- -->
                if (money < m) {<!-- -->
                    System.out.println("withdrawal failed");
                } else {<!-- -->
                    this. money -= m;
                    System.out.println("Withdrawal successful, withdrawal amount: " + m + ", balance: " + this.money);
                }
            }
    }
    
  2. sync block

    public void getMoney(Integer m) {<!-- -->
            synchronized (this) {<!-- -->
                if (money < m) {<!-- -->
                    System.out.println("withdrawal failed");
                } else {<!-- -->
                    this. money -= m;
                    System.out.println("Withdrawal successful, withdrawal amount: " + m + ", balance: " + this.money);
                }
            }
    }
    

Deadlock

Deadlock refers to the situation in which multiple threads compete for resources that depend on each other synchronously in the case of multithreading, resulting in the situation that the multithreading cannot continue to execute.

Both are trying to acquire the lock of the other party, causing both parties to wait indefinitely, which is a deadlock

package com.lovo.main;

public class DieLockTest {<!-- -->
    public static void main(String[] args) {<!-- -->
        Obj o1 = new Obj(1);
        Obj o2 = new Obj(2);
        new Thread(new Runnable() {<!-- -->
            @Override
            public void run() {<!-- -->
                synchronized (o1) {<!-- -->
                    try {<!-- -->
                        Thread. sleep(300);
                        o2.test();
                    } catch (InterruptedException e) {<!-- -->
                        throw new RuntimeException(e);
                    }
                }
            }
        }).start();
        new Thread(new Runnable() {<!-- -->
            @Override
            public void run() {<!-- -->
                synchronized (o2) {<!-- -->
                    try {<!-- -->
                        Thread. sleep(300);
                        o1.test();
                    } catch (InterruptedException e) {<!-- -->
                        throw new RuntimeException(e);
                    }
                }
            }
        }).start();
    }
}

class Obj {<!-- -->
    private Integer num;

    public Obj(Integer num) {<!-- -->
        this.num = num;
    }

    public synchronized void test() {<!-- -->
        System.out.println("test method" + num);
    }
}

Communication between threads

The multi-thread mechanism allows us to divide the tasks to be completed into multiple logical units and hand them over to different threads for completion

In the case of thread synchronization, when a thread is accessing a shared resource, other threads can only wait for access to the resource

However, when a thread finishes accessing shared resources, how to notify other threads that they can access the contributed resources?

This is the wait-notify mechanism proposed by Java

These methods are final methods in Objcet and are only valid in synchronized

wait():

The wait() method, its function is to make the thread currently executing the wait() method wait, suspend execution at the line of code where wait() is located, and release the lock until it is notified or interrupted.

notify()

The notify() method is used to notify other threads that may be waiting for the lock. If there are multiple threads waiting, a one-time notification will be issued in the order in which the wait method is executed (only one notification at a time!), so that the waiting is in the first order The thread acquires the lock. It should be noted that after the notify method is executed, the current thread does not release the lock immediately, but waits until the program is executed, that is, after exiting the synchronized synchronization area.

notifyAll()

The method call notifies all threads of wait() that the thread with the highest priority will run first

The difference between sleep and wait

  1. The sleep method is defined in the thread class Thread, and the wait method is defined in Object
  2. The wait method can only be placed in a synchronization method or a synchronization block to indicate that the current thread is waiting for resources, and the sleep method can be placed anywhere to indicate that the current thread is sleeping
  3. The wait method needs to release the object lock, but the sleep method does not release the object lock.
  4. After the wait method is used, the thread needs to be woken up by notify before continuing to execute. And sleep after the end of sleep, the thread automatically continues to execute

Lock

  1. Lock is an interface, and synchronized is implemented in Java
  2. When an exception occurs in synchronized, it will automatically release the occupied lock of the thread, so it will not cause a deadlock; and when an exception occurs in a Lock, if the lock is not released through the unlock() method, it is likely to cause a deadlock phenomenon, so Lock needs Release the lock in the finally block
  3. Lock can make threads waiting for locks respond to interruptions, but synchronized cannot. When using synchronized, waiting threads will wait forever and cannot respond to interruptions
  4. Lock can successfully know whether the lock has been acquired, but synchronized cannot.
  5. Lock can improve the efficiency of multiple threads for reading operations

Methods in Lock

lock() acquires a lock

tryLock(long time, timeUnit unit) Try to wait for the lock to be acquired, and return true if it is obtained within the specified time range.

tryLock() tries to acquire the lock, and returns immediately if it is not obtained

lockInterruotibly() is equivalent to tryLock(long time, TimeUnit unit) to set the timeout to infinite. Threads can be interrupted while waiting for a lock

(The lockInterruptibly method can interrupt the thread waiting to acquire the lock. When two threads acquire a lock through lock.lockInterruptibly() at the same time, if thread A acquires the lock at this time, and thread B only has to wait, then threadB is called on thread B. The interrupt() method can interrupt the waiting process of thread B)

onLock is used to release the lock

package com.lovo.main;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {<!-- -->
    public static void main(String[] args) {<!-- -->
        LockObj obj = new LockObj();
        new Thread(obj::getLock, "the first").start();
        new Thread(new Runnable() {<!-- -->
            @Override
            public void run() {<!-- -->
                obj. getLock();
            }
        }, "Second").start();
    }

}

class LockObj {<!-- -->
    private final Lock lock = new ReentrantLock();

    public void getLock() {<!-- -->
        boolean result = this. lock. tryLock();
        System.out.println(result);
        // The tryLock method is applicable to the return value, which means that it is used to try to acquire the lock. If it is not found, it will return false, and if it is found, it will return false
        try {<!-- -->
            for (int i = 0; i < 10; i ++ ) {<!-- -->
                String threadName = Thread. currentThread(). getName();
                System.out.println(i + "--" + threadName);
                Thread. sleep(10);
            }
        } catch (InterruptedException e) {<!-- -->
            throw new RuntimeException(e);
        } finally {<!-- -->
            this. lock. unlock();
        }
    }

    public void removeLock() {<!-- -->

    }
}

Read-write lock

The synchronized keyword to achieve synchronization will cause a problem

If multiple threads are performing read operations, so when one thread performs read operations, other threads can only wait and cannot perform read operations

Resolve

If multiple threads are performing read operations, so when one thread is performing read operations, other threads can simultaneously perform read operations. Threads applying for write operations can only wait

If a thread has already occupied the write lock, if other threads apply for a write lock or a read lock at this time, the applied thread will wait for the release of the write lock

package com.lovo.main;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {<!-- -->
    public static void main(String[] args) {<!-- -->

        RW obj = new RW();
        // new Thread(obj::readLock, "The first read").start();
        // new Thread(obj::readLock, "Second Read").start();
        new Thread(obj::writeLock, "First write").start();
        new Thread(obj::writeLock, "The second write").start();
    }
}

class RW {<!-- -->
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void readLock() {<!-- -->
        try {<!-- -->
            rwLock. readLock(). lock();
            for (int i = 0; i < 10; i ++ ) {<!-- -->
                String threadName = Thread. currentThread(). getName();
                System.out.println(i + "read" + threadName);
                Thread. sleep(20);
            }
        } catch (Exception e) {<!-- -->
            e.printStackTrace();
        } finally {<!-- -->
            rwLock. readLock(). unlock();
        }
    }

    public void writeLock() {<!-- -->
        try {<!-- -->
            rwLock.writeLock().lock();
            for (int i = 0; i < 10; i ++ ) {<!-- -->
                String threadName = Thread. currentThread(). getName();
                System.out.println(i + "write" + threadName);
                Thread. sleep(200);
            }
        } catch (Exception e) {<!-- -->
            e.printStackTrace();
        } finally {<!-- -->
            rwLock.writeLock().unlock();
        }
    }
}
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

Volatile

The Java language provides a slightly weaker synchronization mechanism, volatile variables, to ensure that other threads are notified of variable update operations.

No locking operation is performed when accessing volatile variables, so the execution thread will not be blocked, so volatile variables are a more lightweight synchronization mechanism than the sychronized keyword.

When a variable is defined as volatile, it will have two characteristics:

  1. Ensure the visibility of this variable to all threads. When a thread modifies the value of this variable, volatile ensures that the new value can be synchronized to the main memory immediately
  2. Disable instruction reordering optimizations