Summary of Java multithreading Basics

Keywords: Java Multithreading

< ------------------------------------------------------ Liao Xuefeng learns Java ---------------------------------------- >

1. Multithreading Basics

  • The computer calls a task a process, the browser is a process, and the video player is another process. Similarly, both the music player and Word are processes
  • Subtasks in a process are called threads
  • Relationship between process and thread: a process can contain one or more threads, but there will be at least one thread
  • The minimum task unit scheduled by the operating system is thread. Commonly used operating systems such as Windows and Linux use preemptive multitasking. How to schedule threads is completely determined by the operating system. The program itself cannot decide when to execute and how long to execute
  • The same application can have multiple processes or threads. There are several methods to realize multitasking:
1. Multi process mode (only one thread per process)
2. Multithreading mode (multiple threads in a process)
3. Multi process+Multithreading (multiple processes and each process can have one or more threads)
  • Compared with multithreading, the disadvantages of multithreading are:
1. Creating a process is more expensive than creating a thread, especially in Windows On the system;
2. Inter process communication is slower than inter thread communication, because inter thread communication is to read and write the same variable, which is very fast.
  • Compared with multithreading, the advantages of multithreading are:
The stability of multi process is higher than that of multi thread, because in the case of multi process, the collapse of one process will not affect other processes, while in the case of multi thread, the collapse of any thread will directly lead to the collapse of the whole process
  • The Java language has built-in multithreading support: a java program is actually a JVM process. The JVM process uses a main thread to execute the main() method. Within the main() method, we can start multiple threads. In addition, the JVM has other worker threads responsible for garbage collection

2. Create multithreading

  • To create a new thread, you just need to instantiate a Thread and call its **start() * method.
  • How to assign tasks to threads:

Method 1: customize the Thread subclass and override the run method (the method body contains the code to be executed)

public class Main {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start(); // Start a new thread
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("start new thread!");
    }
}

Method 2: when creating a Thread instance, pass in a Runnable instance

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start(); // Start a new thread
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("start new thread!");
    }
}

//Or use the lambda syntax introduced in Java 8 to further simplify it as
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("start new thread!");
        });
        t.start(); // Start a new thread
    }
}
  • Call Thread.sleep() to force the current thread to pause for a period of time; The parameter passed in by sleep() is milliseconds
  • Calling the run() method of the Thread instance directly is invalid: it is equivalent to executing the run method of the Thread object in the main Thread, and no new Thread will be created
  • A private native void start0() method is called inside the start() method. The native modifier indicates that this method is implemented by C code inside the JVM virtual machine, not Java code
  • Set priority for threads. The method of setting priority is: Thread.setPriority(int n) // 1~10, the default value is 5

3. Thread status

  • Java threads have the following states:
1. New: The newly created thread has not been executed;
2. Runnable: Running thread, executing run()Methodical Java code;
3. Blocked: A running thread is suspended because some operations are blocked;
4. Waiting: A running thread because some operations are waiting;
5. Timed Waiting: Running thread because of execution sleep()Method is timing and waiting;
6. Terminated: Thread terminated because run()Method execution completed.

  • The reasons for thread termination are:
1. Normal thread termination: run()Method execution to return Statement return;
2. Thread terminated unexpectedly: run()Method causes the thread to terminate because of an uncapped exception;
3. To a thread Thread Instance call stop()Method force termination (strongly deprecated).
  • A thread can wait for another thread (the current thread suspends execution) until the thread ends running: Thread.join() method
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello");
        });
        System.out.println("start");
        t.start();
        t.join();
        System.out.println("end");
    }
}
  • Get the current Thread: obtained through the static method of Thread class
Thread t = Thread.currentThread();

4. Interrupt thread

  • Interrupting a thread means that other threads send a signal to the thread. After receiving the signal, the thread ends executing the run() method, so that its own thread can immediately end running
  • In other threads, the interrupt() method is called on the target thread. The target thread needs to repeatedly detect whether the state is interrupted (calling the isInterrupted() method in the notification thread), if it is, it will terminate the operation immediately.
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1); // Pause for 1 ms
        t.interrupt(); // Interrupt t thread
        t.join(); // Wait for the t thread to end
        System.out.println("end");
    }
}

class MyThread extends Thread {
    public void run() {
        int n = 0;
        while (! isInterrupted()) {
            n ++;
            System.out.println(n + " hello!");
        }
    }
}

Note: if the calling isInterrupted method is not displayed in the child thread, the thread will not stop even if another thread sends an interrupt notification

  • If a thread is in the waiting state, for example, t.join() will make the main thread enter the waiting state. At this time, if interrupt() is called on the main thread, the join() method will immediately throw an InterruptedException. Therefore, as long as the target thread catches the InterruptedException thrown by the join() method, it indicates that other threads have called the interrupt() method, Normally, the thread should end immediately
public class MulThread_03 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1000);
        t.interrupt(); // Interrupt t thread
        t.join(); // Wait for the t thread to end
        System.out.println("end from main Thread");
    }
}

class MyThread extends Thread {
    public void run() {
        Thread hello = new HelloThread();
        hello.start(); // Start the hello thread
        try {
            hello.join(); // Wait for the hello thread to end
        } catch (InterruptedException e) {
            System.out.println("interrupted! from MyThread");
        }
        hello.interrupt();
    }
}

class HelloThread extends Thread {
    public void run() {
        int n = 0;
        while (!isInterrupted()) {
            n++;
            System.out.println(n + " hello!");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                System.out.println("!!!!");
                break;
            }
        }
    }
}

Note: under the combination of join and interrupt, the parent process will force the join method to report InterruptedException. This error is perceived by the child process, and the child process can configure different response methods

  • Another common way to interrupt a thread is to set the flag bit. We usually use a running flag bit to identify whether the thread should continue to run. In the external thread, we can end the thread by setting HelloThread.running to false:
public class Main {
    public static void main(String[] args)  throws InterruptedException {
        HelloThread t = new HelloThread();
        t.start();
        Thread.sleep(1);
        t.running = false; // Flag position is false
    }
}

class HelloThread extends Thread {
    public volatile boolean running = true;
    public void run() {
        int n = 0;
        while (running) {
            n ++;
            System.out.println(n + " hello!");
        }
        System.out.println("end!");
    }
}
  • Note that the flag boolean running is a variable shared between threads. Variables shared between threads need to be marked with volatile keyword to ensure that each thread can read the updated variable value
  • Why declare variables shared between threads with the keyword volatile?
    In the Java virtual machine, the value of the variable is saved in main memory, but when the thread accesses the variable, it will first obtain a copy and save it in its own working memory. If the thread modifies the value of the variable, the virtual opportunity will write the modified value back to the main memory at a certain time, but the time is uncertain! This will cause that if one thread updates a variable, the value read by another thread may still be the value before the update
    volatile keyword solves the * * visibility problem * *: when a thread modifies the value of a shared variable, other threads can immediately see the modified value

5. Daemon thread

  • Java program entry is that the JVM starts the main thread, and the main thread can start other threads. When all threads have finished running, the JVM exits and the process ends
  • If a thread does not exit, the JVM process will not exit, so it is necessary to ensure that all threads can end execution
  • There is a thread whose purpose is to loop indefinitely. For example, a thread that triggers a task regularly:
class TimerThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println(LocalTime.now());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}
  • Who is responsible for ending this thread?
    The answer is to use daemon threads. A Daemon Thread is a thread that serves other threads. In the JVM, after all non daemon threads are executed, the virtual machine will automatically exit whether there is a Daemon Thread or not.
  • How to create a daemon thread?
    Before calling the start() method, call setDaemon(true) to mark the thread as a daemon thread.
    In the daemon thread, you should pay attention to when writing code: the daemon thread cannot hold any resources that need to be closed, such as opening files, because when the virtual machine exits, the daemon thread has no chance to close files, which will lead to data loss

6. Thread synchronization

  • When multiple threads execute at the same time, there is a problem that does not exist under the single thread model: if multiple threads read and write shared variables at the same time, there will be data inconsistency
    Data consistency problem!!
  • Take the following pseudo code as an example:
main{
	static int count = 0
	addThread.start  // This thread is responsible for adding shared variables 100 times
	subThread.start  // The thread performs subtraction operation on the contribution variable for 100 times
	addThread.join
	subThread.join
	sout(count) // ? ? ?
}

If there is no data consistency problem, the answer is obvious. The final output count is 0, but in fact, the output result always changes. Why?
First, supplement the basic knowledge. A simple add instruction (add = add + 1), in our opinion, is a single instruction, but for the bottom layer, the instruction will be decomposed into multiple instructions (load: load variables; add: execute add operations; store: store variable values); It should be noted that a thread may be replaced by the operating system after executing any instruction (when there are no atomic operations or other lock operations). In this case, the following situation may occur:

That is, an add instruction is replaced during execution, and its add operation has not been completed at this time; The subtraction thread has been executed for replacement. When it reads the shared variable value, because the last operation of adding thread has not been completed and the variable value has not been updated, it reads the last value, which leads to the problem of data inconsistency.

  • To ensure correct logic, when reading and writing shared variables, you must ensure that a group of instructions are executed atomically: that is, when a thread executes, other threads must wait:

    By locking and unlocking, it can be ensured that three instructions are always executed by one thread, and no other thread will enter this instruction interval. Even if a thread is interrupted by the operating system during execution, other threads cannot enter this instruction interval because they cannot obtain a lock. Only after the execution thread releases the lock can other threads have the opportunity to obtain the lock and execute. This code block between locking and unlocking is called Critical Section. At most one thread in the Critical Section can execute at any time

  • Java programs use the synchronized keyword to lock an object: synchronized (object) {...}; Note: when entering the synchronized code block, obtain the lock of the specified object. When exiting the synchronized code block, the lock of the object will be released:

public class Main {
    public static void main(String[] args) throws Exception {
        var add = new AddThread();
        var dec = new DecThread();
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println(Counter.count);
    }
}

class Counter {
    public static final Object lock = new Object();  // Create a lock object
    public static int count = 0;
}

class AddThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.count += 1;
            }
        }
    }
}

class DecThread extends Thread {
    public void run() {
        for (int i=0; i<10000; i++) {
            synchronized(Counter.lock) {
                Counter.count -= 1;
            }
        }
    }
}
  • Because it can only be run serially when passing through the synchronized statement block, synchronized will reduce the execution efficiency of the program
  • Actions that do not require synchronized:
JVM The specification defines several atomic operations:
	Basic type( long and double (except) assignment, for example: int n = m;
	Reference type assignment, for example: List<String> list = anotherList. 
	//Long and double are 64 bit data. The JVM does not specify whether the 64 bit assignment operation is an atomic operation. However, the JVM on x64 platform implements the assignment of long and double as atomic operations

Note: synchronized is not required for single line assignment statements to ensure data synchronization. However, in case of multi line assignment statements, it is still necessary to artificially ensure data consistency, but the assignment statements can be converted through certain methods to make them atomic:

class Pair {  // Before transformation
    int first;
    int last;
    public void set(int first, int last) {
        synchronized(this) {
            this.first = first;
            this.last = last;
        }
    }
}

class Pair {  // After transformation
    int[] pair;
    public void set(int first, int last) {
        int[] ps = new int[] { first, last };
        this.pair = ps;
    }
}

7. Synchronization method

  • In the above example, we give the permission to obtain the lock to the thread (that is, the thread decides who gets the lock). This operation is complex and easy to cause logical confusion
  • A better way is to encapsulate the synchronized logic (put the lock logic in the execution method of the operation object, and the thread does not need to actively obtain the lock permission):
public class Counter {
    private int count = 0;

    public void add(int n) {
        synchronized(this) {
            count += n;
        }
    }

    public void dec(int n) {
        synchronized(this) {
            count -= n;
        }
    }

    public int get() {
        return count;
    }
}
  • If a class is designed to allow multiple threads to access it correctly, we say that this class is * * thread safe**
  • Some invariant classes, such as String, Integer and LocalDate, all their member variables are final. When multiple threads access at the same time, they can only read but not write. These invariant classes are also thread safe
  • Classes like Math that only provide static methods without member variables are thread safe
  • Unless otherwise specified, a class is non thread safe by default.
  • When we lock this instance, we can actually modify this method with synchronized. The following two expressions are equivalent:
// Writing method I
public void add(int n) {  
    synchronized(this) { // Lock this
        count += n;
    } // Unlock
}

// Writing method 2
public synchronized void add(int n) { // Lock this
    count += n;
} // Unlock
  • The method modified with synchronized is the synchronous method, which means that the whole method must be locked with this instance
  • Any Class has a Class instance automatically created by the JVM. Therefore, add synchronized to the static method to lock the Class instance of the Class, for example:
public synchronized static void test(int n) {
    ...
}
// Equivalent to ============================== >
public class Counter {
    public static void test(int n) {
        synchronized(Counter.class) {
            ...
        }
    }
}

8. Deadlock

  • Java's thread lock is a reentrant lock: the JVM allows the same thread to acquire the same lock repeatedly. This lock that can be acquired repeatedly by the same thread is called a reentrant lock
  • Because the thread lock of Java is a reentrant lock, when acquiring a lock, you should not only judge whether it is the first acquisition, but also record how many times it is acquired. Every time the lock is acquired, record + 1. Every time the synchronized block is exited, record - 1. When it is reduced to 0, the lock will be released
  • When acquiring multiple locks, different threads acquire locks of multiple different objects, which may lead to deadlock. For example, in this case:
public void add(int m) {
    synchronized(lockA) { // Obtain lock of lockA
        this.value += m;
        synchronized(lockB) { // Obtain lock of lockB
            this.another += m;
        } // Release the lock of lockB
    } // Release the lock of lockA
}

public void dec(int m) {
    synchronized(lockB) { // Obtain lock of lockB
        this.another -= m;
        synchronized(lockA) { // Obtain lock of lockA
            this.value -= m;
        } // Release the lock of lockA
    } // Release the lock of lockB
}
  • After a deadlock occurs, there is no mechanism to release the deadlock, and the JVM process can only be forcibly terminated

9. Use wait and notify

  • The principle of multi thread coordinated operation is: when the conditions are not met, the thread enters the waiting state; When the conditions are met, the thread is awakened and continues to execute the task
  • wait() method:
1. wait()Method can only be used in synchronized(lock)Used in statement blocks
2. wait()Methods can only be called by lock objects, as described above lock.wait(),Another example this.wait()(If the locked object is this)
3. If it can be implemented wait(),It means that the current thread has obtained the specified lock object, and all other code blocks requiring the lock object cannot be executed
4. wait()The effect of the method is that the current thread abandons the lock object and enters the waiting state
5. wait()Method has no return value. Only when other threads release the lock of the object and "wake up" the thread can the thread get the lock object again and continue to execute
6. wait()The underlying implementation of the method is native Method( c Method)
  • notify() method:
7. notify()Method can only be used in synchronized(lock)Used in statement blocks
8. notify()Methods can only be called by lock objects, such as lock.notify(),Another example this.notify()
9. notify()Method will wake up the thread in the waiting state due to the lock object and return the lock object to the waiting thread
10.If there is no thread waiting because of the lock object, there is no execution effect 
  • Example:
public class Main {
    public static void main(String[] args) throws InterruptedException {
        var q = new TaskQueue();
        var ts = new ArrayList<Thread>();
        for (int i=0; i<5; i++) {
            var t = new Thread() {
                public void run() {
                    // Execute task:
                    while (true) {
                        try {
                            String s = q.getTask();
                            System.out.println("execute task: " + s);
                        } catch (InterruptedException e) {
                            return;
                        }
                    }
                }
            };
            t.start();
            ts.add(t);
        }
        var add = new Thread(() -> {
            for (int i=0; i<10; i++) {
                // Put task:
                String s = "t-" + Math.random();
                System.out.println("add task: " + s);
                q.addTask(s);
                try { Thread.sleep(100); } catch(InterruptedException e) {}
            }
        });
        add.start();
        add.join();
        Thread.sleep(100);
        for (var t : ts) {
            t.interrupt();
        }
    }
}

class TaskQueue {
    Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String s) {
        this.queue.add(s);
        this.notifyAll();
    }

    public synchronized String getTask() throws InterruptedException {
        while (queue.isEmpty()) {
            this.wait();
        }
        return queue.remove();
    }
}

10. Use ReentrantLock

  • Starting with Java 5, an advanced java.util.concurrent package is introduced to deal with concurrency. It provides a large number of more advanced concurrency functions, which can greatly simplify the writing of multithreaded programs
  • The Java language directly provides the synchronized keyword for locking, but this kind of lock is very heavy. Second, you must wait all the time when obtaining it, and there is no additional attempt mechanism
  • ReentrantLock provided by the java.util.concurrent.locks package is used to replace synchronized locking
  • Code transformation: (realize the same function of synchronized based on ReentrantLock):
public class Counter {
    private final Lock lock = new ReentrantLock();
    private int count;

    public void add(int n) {
        lock.lock();
        try {
            count += n;
        } finally {
            lock.unlock();
        }
    }
}
  • synchronized is the syntax provided at the Java language level, so we don't need to consider exceptions. ReentrantLock is the lock implemented by java code. We must first obtain the lock and then release the lock correctly in finally
  • ReentrantLock is a reentrant lock. Like synchronized, a thread can acquire the same lock multiple times
  • ReentrantLock can attempt to acquire a lock:
if (lock.tryLock(1, TimeUnit.SECONDS)) { 
	//Parameter 1 indicates the maximum number of waiting time units, and parameter 2 indicates the length of time units
    try {
        ...
    } finally {
        lock.unlock();
    }
}

When the above code tries to obtain the lock, it can wait for 1 second at most. If the lock is not obtained after 1 second, tryLock() returns false, and the program can do some additional processing instead of waiting indefinitely

  • Using ReentrantLock is safer than using synchronized directly. Threads will not cause deadlock when tryLock() fails
  • A short test code is attached here. Those interested can view the corresponding results by modifying the waiting time:
public class MulThread_05 {
    public static ReentrantLock lock = new ReentrantLock(); // Create lock object
    public static void main(String[] args) throws InterruptedException {

        lock.lock(); // The main thread acquires the lock object
        System.out.println("main thread: I get the lock !!");
        new MyThread_1().start();
        Thread.sleep(3000);
        lock.unlock();
        System.out.println("main thread: ok let it go !!");
        System.out.println("main thread end !!");
    }
}

class MyThread_1 extends Thread {
    @Override
    public void run() {
        try {
            if(MulThread_05.lock.tryLock(4, TimeUnit.SECONDS)) {
                try {
                    System.out.println("this thread: I get the lock !!!");
                }finally {
                    MulThread_05.lock.unlock();
                }
            }else {
                System.out.println("this thread: whatever I give it up !!");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

11. Use Condition

  • Condition object can cooperate with ReentrantLock to realize wait and notify functions
  • When using Condition, the referenced Condition object must be returned from newCondition() of Lock instance to obtain a Condition instance bound to Lock instance. That is, a Condition object is bound to a Lock object. A Lock can create multiple Condition objects, but the Condition object must be attached to the Lock object
  • The * * await(), signal(), signalAll() * * principles provided by Condition are consistent with the wait(), notify() and notifyAll() of synchronized lock objects, and their behaviors are the same:
1. await()It will release the current lock and enter the waiting state;

2. signal()Will wake up a waiting thread;

3. signalAll()Will wake up all waiting threads;

4. Wake up thread from await()After returning, you need to regain the lock.
  • Similar to tryLock(), await() can wake itself up after waiting for a specified time if it has not been awakened by other threads through signal() or signalAll():
if (condition.await(1, TimeUnit.SECOND)) {
    // Wake up by another thread
} else {
    // No other thread wakes up within the specified time
}

12. Use ReadWriteLock

  • In some cases, the protection of ReentrantLock is too radical: (only one thread is allowed to enter the critical area). In fact, the read method that will not modify the object content can not be protected. At this time, RenentrantLock cannot meet the use requirements
  • What you want to achieve is: multiple threads are allowed to read at the same time, but as long as one thread is writing, other threads must wait. Using ReadWriteLock can solve this problem
  • ReadWriteLock guarantees:
1. Only one thread is allowed to write (other threads can neither write nor read);
2. When there is no write, multiple threads are allowed to read at the same time (improve performance).
  • The ReadWriteLock feature is easy to implement. We need to create a ReadWriteLock instance and obtain the read lock and write lock respectively:
public class Counter {
    private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
    private final Lock rlock = rwlock.readLock();
    private final Lock wlock = rwlock.writeLock();
    private int[] counts = new int[10];

    public void inc(int index) {
        wlock.lock(); // Write lock
        try {
            counts[index] += 1;
        } finally {
            wlock.unlock(); // Release write lock
        }
    }

    public int[] get() {
        rlock.lock(); // Read lock
        try {
            return Arrays.copyOf(counts, counts.length);
        } finally {
            rlock.unlock(); // Release read lock
        }
    }
}
  • When using ReadWriteLock, the applicable condition is that the same data is read by a large number of threads, but only a few threads modify it

13. Use StampedLock

  • There is a potential problem with ReadWriteLock: if a thread is reading, the write thread needs to wait for the read thread to release the lock before obtaining the write lock, that is, writing is not allowed during reading. This is a pessimistic read lock
  • Java 8 introduces a new read-write lock: StampedLock
  • Compared with ReadWriteLock, StampedLock is improved in that it also allows writing after obtaining the write lock during reading! In this way, the data we read may be inconsistent. Therefore, we need some additional code to judge whether there are writes in the process of reading. This kind of read lock is an optimistic lock
  • Optimistic lock: it is optimistic to estimate that there will be no write in the process of reading. Pessimistic lock: write is rejected during reading, that is, write must wait. Obviously, optimistic locks have higher concurrency efficiency, but once a small probability of writing leads to inconsistent read data, it needs to be detected and re read
  • Example:
public class Point {
    private final StampedLock stampedLock = new StampedLock();

    private double x;
    private double y;

    public void move(double deltaX, double deltaY) {
        long stamp = stampedLock.writeLock(); // Get write lock
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            stampedLock.unlockWrite(stamp); // Release write lock
        }
    }

    public double distanceFromOrigin() {
        long stamp = stampedLock.tryOptimisticRead(); // Get an optimistic read lock
        // Note that the following two lines of code are not atomic operations
        // Suppose x, y = (100200)
        double currentX = x;
        // x=100 has been read here, but x and Y may be modified to (300400) by the write thread
        double currentY = y;
        // y has been read here. If it is not written, the reading is correct (100200)
        // If there is a write, the read is wrong (100400)
        if (!stampedLock.validate(stamp)) { // Check whether there are other write locks after reading the lock
            stamp = stampedLock.readLock(); // Get a pessimistic read lock
            try {
                currentX = x;
                currentY = y;
            } finally {
                stampedLock.unlockRead(stamp); // Release read lock
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}
  • Compared with ReadWriteLock, the locking of writing is exactly the same, but the difference is reading. Notice that first, we obtain an optimistic read lock through tryOptimisticRead() and return the version number (stamp). Then read. After reading, we verify the version number through validate(). If the version number is not written in the reading process, the version number remains unchanged, and the verification is successful, we can safely continue the follow-up operation. If there is a write during reading, the version number will change and the verification will fail. In case of failure, we read again by obtaining the pessimistic read lock. Because the probability of writing is not high, the program can obtain data through optimistic read lock in most cases, and pessimistic read lock in very few cases
  • The cost of stampedlock: first, the code is more complex; second, stampedlock is a non reentrant lock, which cannot repeatedly obtain the same lock in a thread
  • StampedLock also provides a more complex function of upgrading pessimistic read locks to write locks. It is mainly used in the if then update scenario: read first, return if the read data meets the conditions, and try to write if the read data does not meet the conditions

14. Use Concurrent set

  • The java.util.concurrent package provides a variety of thread safe collections, such as ArrayBlockingQueue, List, Map, Set, Deque, etc
  • The java.util.concurrent package also provides corresponding concurrent collection classes:
  • Using these concurrent collections is exactly the same as using non thread safe collection classes
  • The java.util.Collections tool class also provides an old thread safe collection converter to convert thread unsafe collections into thread safe collections, which can be used as follows:
Map unsafeMap = new HashMap();
Map threadSafeMap = Collections.synchronizedMap(unsafeMap);

In fact, it wraps a non thread safe Map with a wrapper class, and then locks all read and write methods with synchronized. In this way, the performance of the thread safe collection is much lower than that of the java.util.concurrent collection, so it is not recommended

15. Use Atomic

  • The java.util.concurrent package of Java not only provides underlying locks and concurrent collections, but also provides a set of encapsulated classes for atomic operations, which are located in the java.util.concurrent.atomic package
  • Take AtomicInteger as an example. Its main operations include:
1. Add value and return new value: int addAndGet(int delta)
2. Add 1 to return the new value: int incrementAndGet()
3. Get current value: int get()
4. use CAS Mode setting: int compareAndSet(int expect, int update)
  • Atomic classes are thread safe access implemented in a lock free manner. Its main principle is to use CAS: compare and set (system support)
  • Write incrementAndGet() by yourself through CAS, which is about as follows:
public int incrementAndGet(AtomicInteger var) {
    int prev, next;
    do {
        prev = var.get();  // Get current value
        next = prev + 1;  // Get current value + 1
    } while ( ! var.compareAndSet(prev, next)); // If the value of var is equal to prev, set the value of var to next and return true;
    //Otherwise, put back false and get the current value of VaR in the loop again (ensure the correctness of adding one to var)
    return next;
}

CAS means that in this operation, if the current value of AtomicInteger is prev, it will be updated to next and return true. If the current value of AtomicInteger is not prev, it does nothing and returns false. Through CAS operation and do... While loop, even if other threads modify the value of AtomicInteger, the final result is correct

16. Use thread pool

  • Although the Java language has built-in multithreading support, which is very convenient to start a new thread, creating a thread requires operating system resources (thread resources, stack space, etc.), and it takes a lot of time to create and destroy a large number of threads frequently
  • If we can reuse a group of threads, we can let a group of threads execute many small tasks instead of one task corresponding to a new thread. This kind of thread pool can receive a large number of small tasks and distribute them
  • Several threads are maintained in the thread pool. When there are no tasks, these threads are in a waiting state. If there is a new task, a free thread is assigned to execute it. If all threads are busy, the new task is either put into the queue or a new thread is added for processing
  • The Java standard library provides the ExecutorService interface to represent the thread pool. Its typical usage is as follows:
// Create a fixed size thread pool:
ExecutorService executor = Executors.newFixedThreadPool(3);
// Submit task:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);
  • ExecutorService is just an interface. Several common implementation classes provided by the Java standard library are:
1. FixedThreadPool: A thread pool with a fixed number of threads;
2. CachedThreadPool: Thread pool whose number of threads is dynamically adjusted according to the task;
3. SingleThreadExecutor: Thread pool for single thread execution only.

The methods that create these thread pools are encapsulated in the Executors class

  • Take FixedThreadPool as an example to see the execution logic of thread pool:
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        // Create a fixed size thread pool:
        ExecutorService es = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 6; i++) {
            es.submit(new Task("" + i));
        }
        // Close thread pool:
        es.shutdown();
    }
}

class Task implements Runnable {
    private final String name;

    public Task(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println("start task " + name);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        System.out.println("end task " + name);
    }
}

Execution results:

start task 0
start task 1
start task 2
start task 3
end task 1
start task 4
end task 0
start task 5
end task 2
end task 3
end task 5
end task 4
  • The thread pool is closed at the end of the program. When using the shutdown() method to close the thread pool, it will wait for the executing task to complete first and then close it. shutdownNow() will immediately stop the task being executed, and awaitTermination() will wait for the specified time to shut down the thread pool
  • Create a dynamically adjusted thread pool with 4 ~ 10 threads:
// Executors.newCachedThreadPool() method source code
public static ExecutorService newCachedThreadPool() {
	// What is actually returned is a ThreadPoolExecutor object
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
}
// So we can create it this way:
int min = 4;
int max = 10;
ExecutorService es = new ThreadPoolExecutor(min, max,
        60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
  • There is also a task that needs to be performed repeatedly on a regular basis, such as refreshing the securities price per second. This task itself is fixed and needs to be executed repeatedly. You can use ScheduledThreadPool. Tasks placed in the ScheduledThreadPool can be executed repeatedly on a regular basis
  • Creating a ScheduledThreadPool is still through the Executors class:
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
  • Submit a one-time task, which will execute only once after the specified delay:
// Perform a one-time task in 1 second:
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
  • The task is executed at a fixed rate of every 3 seconds:
// Start to execute the scheduled task after 2 seconds, and execute it every 3 seconds:
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
  • The task is executed at a fixed interval of 3 seconds:
// Start to execute the scheduled task after 2 seconds and execute it at an interval of 3 seconds:
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);
  • The difference between FixedRate and FixedDelay: FixedRate means that a task is always triggered at a fixed time interval, no matter how long the task is executed; FixedDelay refers to waiting for a fixed time interval after the last task is executed before executing the next task

  • The Java standard library also provides a java.util.Timer class, which can also execute tasks regularly. However, a Timer corresponds to a Thread. Therefore, a Timer can only execute one task regularly. Multiple timers must be started for multiple scheduled tasks, and a ScheduledThreadPool can schedule multiple scheduled tasks, We can completely replace the old Timer with ScheduledThreadPool
    Note: multi scheduling here means that a ScheduledThreadPool can create multiple execution queues, and the scheduled tasks in a queue are not executed concurrently (see the analysis in the next point for details)

  • Answer two questions:

Question 1: in the FixedRate mode, assuming that a task is triggered every second, if the execution time of a task exceeds 1 second, will subsequent tasks be executed concurrently?
A: it will not be executed concurrently

Question 2: if the task throws an exception, do the subsequent tasks continue to execute?
A: if the current task throws an exception, subsequent tasks will stop executing

The test code is as follows:

public class MulThread_08 {
    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService ses = Executors.newScheduledThreadPool(4); // Create a thread pool of size 4
        // Specify a fixed cycle task for the thread pool, that is, whether the current task is completed or not, start executing the next task after the specified period
        ses.scheduleAtFixedRate(new MyTask(), 1, 1, TimeUnit.SECONDS);
        ses.scheduleAtFixedRate(new MyTask2(), 1, 1, TimeUnit.SECONDS);
        boolean b = ses.awaitTermination(20, TimeUnit.SECONDS);
    }
}

class MyTask implements Runnable {
    @Override
    public void run() {
        try {
            int id = idGetter.get_id();
            System.out.println("Thread " + id + " start !!");
            Thread.sleep(5000);
            System.out.println("Thread " + id +  " end !!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class MyTask2 implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("Task2" + " start !!");
            Thread.sleep(1);
            System.out.println("Task2" + " end !!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class idGetter {
    static AtomicLong id = new AtomicLong(0);
    public static int get_id() {
        return (int) id.incrementAndGet();
    }
}

17. Use Future

  • There is a problem with the Runnable interface. Its method has no return value. If a task needs a return result, it can only be saved to variables, and additional methods are provided to read, which is very inconvenient. Therefore, the Java standard library also provides a Callable interface, which has one more return value than the Runnable interface:
class Task implements Callable<String> {
    public String call() throws Exception {
        return longTimeCalculation(); 
    }
}

The Callable interface is a generic interface that can return results of a specified type

  • How to get the result of asynchronous execution?
    The ExecutorService.submit() method returns a Future type. An instance of Future type represents an object that can get results in the Future
ExecutorService executor = Executors.newFixedThreadPool(4); 
// Define tasks:
Callable<String> task = new Task();
// Submit the task and get Future:
Future<String> future = executor.submit(task);
// Get the results returned by asynchronous execution from Future:
String result = future.get(); // May block
  • When we submit a Callable task, we will obtain a Future object at the same time. Then, we can call the get() method of the Future object at some time in the main thread to obtain the result of asynchronous execution. When we call get(), if the asynchronous task has been completed, we get the result directly. If the asynchronous task has not completed yet, get() will block and will not return the result until the task is completed
  • A Future interface represents a result that may be returned in the Future. It defines the following methods:
get(): Get results (may wait)
get(long timeout, TimeUnit unit): Obtain the result, but only wait for the specified time;
cancel(boolean mayInterruptIfRunning): Cancel the current task;
isDone(): Determine whether the task has been completed.

18. Use completable future

  • When using Future to obtain asynchronous execution results, either call the blocking method get() or poll to see if isDone() is true. Both methods are not very good because the main thread will also be forced to wait
  • Java 8 began to introduce completable Future, which is improved for the Future. You can pass in the callback object. When the asynchronous task is completed or an exception occurs, the callback method of the callback object will be called automatically
  • Take the stock price as an example to see how to use completable future:
public class Main {
    public static void main(String[] args) throws Exception {
        // To create an asynchronous execution task:
        CompletableFuture<Double> cf = CompletableFuture.supplyAsync(Main::fetchPrice);
        // If successful:
        cf.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // If execution is abnormal:
        cf.exceptionally((e) -> {
            e.printStackTrace();
            return null;
        });
        // Do not end the main thread immediately, otherwise the thread pool used by completable future by default will be closed immediately:
        Thread.sleep(200);
    }

    static Double fetchPrice() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        if (Math.random() < 0.3) {
            throw new RuntimeException("fetch price failed!");
        }
        return 5 + Math.random() * 20;
    }
}
  • Creating a completabilefuture is implemented through completabilefuture. Supplyasync(). It requires an object that implements the Supplier interface:
public interface Supplier<T> {
    T get();
}

Here, the lambda syntax is used to simplify it and directly pass in Main::fetchPrice, because the signature of the static method of Main.fetchPrice() conforms to the definition of the Supplier interface (except for the method name)

  • After the completable future is created, it has been submitted to the default thread pool for execution. We need to define the instances that need callback when completable future is completed and when exceptions occur
  • When completed, completabilefuture calls the Consumer object:
public interface Consumer<T> {
    void accept(T t);
}
  • In case of exception, completabilefuture will call the Function object:
public interface Function<T, R> {
    R apply(T t);
}

lambda syntax is used here to simplify the code

  • The advantages of completable future are:
1. When the asynchronous task ends, it will automatically call back the method of an object;
2. When an asynchronous task makes an error, it will automatically call back the method of an object;
3. After setting the callback, the main thread no longer cares about the execution of asynchronous tasks.
  • The more powerful function of completable future is that multiple completable futures can be executed serially. For example, two completable futures are defined. The first completable future queries the securities code according to the securities name, and the second completable future queries the securities price according to the securities code
public class Main {
    public static void main(String[] args) throws Exception {
        // First task:
        CompletableFuture<String> cfQuery = CompletableFuture.supplyAsync(() -> {
            return queryCode("PetroChina");
        });
        // cfQuery succeeds. Continue to the next task:
        CompletableFuture<Double> cfFetch = cfQuery.thenApplyAsync((code) -> {
            return fetchPrice(code);
        });
        // cfFetch successfully prints the result:
        cfFetch.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // Do not end the main thread immediately, otherwise the thread pool used by completable future by default will be closed immediately:
        Thread.sleep(2000);
    }

    static String queryCode(String name) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        return "601857";
    }

    static Double fetchPrice(String code) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        return 5 + Math.random() * 20;
    }
}
  • In addition to serial execution, multiple completable future can also be executed in parallel. For example, we consider a scenario where we query the securities code from Sina and Netease at the same time. As long as any one returns the result, we will query the price in the next step. As long as any one returns the result, we will complete the operation
public class Main {
    public static void main(String[] args) throws Exception {
        // Two completable future execute asynchronous queries:
        CompletableFuture<String> cfQueryFromSina = CompletableFuture.supplyAsync(() -> {
            return queryCode("PetroChina", "https://finance.sina.com.cn/code/");
        });
        CompletableFuture<String> cfQueryFrom163 = CompletableFuture.supplyAsync(() -> {
            return queryCode("PetroChina", "https://money.163.com/code/");
        });

        // Merge anyOf into a new completable future:
        CompletableFuture<Object> cfQuery = CompletableFuture.anyOf(cfQueryFromSina, cfQueryFrom163);

        // Two completable future execute asynchronous queries:
        CompletableFuture<Double> cfFetchFromSina = cfQuery.thenApplyAsync((code) -> {
            return fetchPrice((String) code, "https://finance.sina.com.cn/price/");
        });
        CompletableFuture<Double> cfFetchFrom163 = cfQuery.thenApplyAsync((code) -> {
            return fetchPrice((String) code, "https://money.163.com/price/");
        });

        // Merge anyOf into a new completable future:
        CompletableFuture<Object> cfFetch = CompletableFuture.anyOf(cfFetchFromSina, cfFetchFrom163);

        // Final result:
        cfFetch.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // Do not end the main thread immediately, otherwise the thread pool used by completable future by default will be closed immediately:
        Thread.sleep(200);
    }

    static String queryCode(String name, String url) {
        System.out.println("query code from " + url + "...");
        try {
            Thread.sleep((long) (Math.random() * 100));
        } catch (InterruptedException e) {
        }
        return "601857";
    }

    static Double fetchPrice(String code, String url) {
        System.out.println("query price from " + url + "...");
        try {
            Thread.sleep((long) (Math.random() * 100));
        } catch (InterruptedException e) {
        }
        return 5 + Math.random() * 20;
    }
}

  • anyOf() can realize "any completable future needs only one success", allOf() can realize "all completable future must succeed", and these combined operations can realize very complex asynchronous process control
  • Note the naming rules of completable future:
1. xxx(): Indicates that the method will continue to execute in the existing thread;
2. xxxAsync(): Indicates that asynchronous execution will be performed in the thread pool.

19. Use ForkJoin

  • Java 7 began to introduce a new Fork/Join thread pool, which can perform a special task: breaking a large task into multiple small tasks for parallel execution
  • Principle of Fork/Join task: judge whether a task is small enough. If so, calculate it directly. Otherwise, divide it into several small tasks and calculate them separately. This process can be repeatedly "fission" into a series of small tasks
  • Example:
public class Main {
    public static void main(String[] args) throws Exception {
        // Create an array of 2000 random numbers:
        long[] array = new long[2000];
        long expectedSum = 0;
        for (int i = 0; i < array.length; i++) {
            array[i] = random();
            expectedSum += array[i];
        }
        System.out.println("Expected sum: " + expectedSum);
        // fork/join:
        ForkJoinTask<Long> task = new SumTask(array, 0, array.length);
        long startTime = System.currentTimeMillis();
        Long result = ForkJoinPool.commonPool().invoke(task);
        long endTime = System.currentTimeMillis();
        System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
    }

    static Random random = new Random(0);

    static long random() {
        return random.nextInt(10000);
    }
}

class SumTask extends RecursiveTask<Long> {
    static final int THRESHOLD = 500;
    long[] array;
    int start;
    int end;

    SumTask(long[] array, int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if (end - start <= THRESHOLD) {
            // If the task is small enough, calculate directly:
            long sum = 0;
            for (int i = start; i < end; i++) {
                sum += this.array[i];
                // Deliberately slow down the calculation speed:
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                }
            }
            return sum;
        }
        // The task is too big and divided into two:
        int middle = (end + start) / 2;
        System.out.println(String.format("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end));
        SumTask subtask1 = new SumTask(this.array, start, middle);
        SumTask subtask2 = new SumTask(this.array, middle, end);
        invokeAll(subtask1, subtask2);
        Long subresult1 = subtask1.join();
        Long subresult2 = subtask2.join();
        Long result = subresult1 + subresult2;
        System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result);
        return result;
    }
}
  • Fork/Join thread pool is applied in Java standard library. The java.util.Arrays.parallelSort(array) provided by the Java standard library can perform parallel sorting. Its principle is to perform parallel sorting on large array splitting through Fork/Join internally, which can greatly improve the sorting speed on multi-core CPU s

20. Use ThreadLocal

  • Background: in real business, based on the implementation method of multithreading, we will let a thread execute a main method. It can be imagined that when the business logic is complex, other format methods may be called in this method; One scheme is to directly assign the main method parameters to each word method during method call, but this method will cause a serious problem: the same data is "copied" many times, which is a serious waste
  • In a thread, the objects that need to be passed across several method calls are usually called Context. It is a state, which can be user identity, task information, etc
  • Adding a context parameter to each method is very troublesome, and sometimes, if the call chain has a third-party library that cannot modify the source code, the context cannot be transmitted
  • The Java standard library provides a special ThreadLocal, which can pass the same object in a thread
  • ThreadLocal instances are usually initialized with static fields as follows:
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
  • Typical usage is as follows:
void processUser(user) {
    try {
        threadLocalUser.set(user);
        step1();
        step2();
    } finally {
        threadLocalUser.remove();
    }
}

By setting a User instance to be associated with ThreadLocal, all methods can get the User instance at any time before removal:

void step1() {
    User u = threadLocalUser.get();
    log();
    printUser();
}

void log() {
    User u = threadLocalUser.get();
    println(u.name);
}

void step2() {
    User u = threadLocalUser.get();
    checkUser(u.id);
}

Note that ordinary method calls must be executed by the same thread. Therefore, the User object obtained by threadLocalUser.get() in step 1 (), step 2 () and log() methods is the same instance

  • ThreadLocal can be regarded as a global map < Thread, Object >: when each Thread obtains the ThreadLocal variable, it always uses the Thread itself as the key:
Object threadLocalValue = threadLocalMap.get(Thread.currentThread());

ThreadLocal is equivalent to opening up an independent storage space for each thread, and the instances associated with ThreadLocal of each thread do not interfere with each other (only one ThreadLocal object is required, and the same ThreadLocal object can store context for multiple threads, which is a one to many relationship)

  • Note that ThreadLocal must be cleared in finally
    Because the current thread is likely to be put back into the thread pool after executing relevant codes. If ThreadLocal is not cleared, the thread will bring in the last state when executing other codes.

  • In order to ensure that the ThreadLocal associated instance can be released, we can use the autoclosable interface with the try (resource) {...} structure to let the compiler close automatically for us. For example, a ThreadLocal that stores the current user name can be encapsulated as a UserContext object:

public class UserContext implements AutoCloseable {

    static final ThreadLocal<String> ctx = new ThreadLocal<>();

    public UserContext(String user) {
        ctx.set(user);
    }

    public static String currentUser() {
        return ctx.get();
    }

    @Override
    public void close() {
        ctx.remove();
    }
}

When using, we use the try (resource) {...} structure to write:

try (var ctx = new UserContext("Bob")) {
    // UserContext.currentUser() can be called arbitrarily:
    String currentUser = UserContext.currentUser();
} // Here, the UserContext.close() method is automatically called to release the ThreadLocal associated object

In this way, ThreadLocal is completely encapsulated in UserContext. External code can call UserContext.currentUser() to obtain the user name bound by the current thread at any time inside try (resource) {...}

Posted by amalosoul on Sun, 07 Nov 2021 20:18:43 -0800