preface

In the previous article, thread interrupts (3), we covered thread interrupts. Now let’s take a look at communication between threads. Communication between threads is essential in our real projects, and in most cases we need to create multiple threads to work together on a task. Reasonable and correct use of communication between threads, as a good programmer must master the skills. Let’s take a look at how communication between threads is handled in Java. You may need to know the following points to read the article:

  • Java Memory Model for Concurrent Programming (PART 1)
  • Volatile for Java Concurrent Programming (Part 2)
  • Synchronized (3)
  • Java Concurrent Programming Lock Interface (7)
  • Concurrent Java programming of locking mechanism of AQS (AbstractQueuedSynchronizer) (8)
  • LockSupport for Concurrent Programming in Java
  • Condition Interface for Java Concurrent Programming Locking Mechanism (10)
  • Reentrant locking for Concurrent programming in Java
  • Read/write Locking for Concurrent Programming in Java (11)

Thread state

Before understanding the communication knowledge of threads, we need to understand the state of threads, familiar with the state of threads, not only helps us better investigate the deadlock, thread safety and other problems in multi-threaded projects, but also better let us analyze and understand the communication between threads. Let’s take a look at the thread states.

Threads have the following five states in Java, as shown below:

Thread state meaning
NEW The thread has been created, but the start() method has not been executed
RUNNABLE Runnable state, where threads can run in the JVM but wait for resources to be allocated by the CPU
BLOCKED A state that is blocked when synchronized has not acquired the corresponding lock
WAITING The wait state, which is entered when the wait()/join/ locksupport.park method is used in a thread
TIMED_WAITING The timed wait state, which is entered when thread.sleep () or Object.wait(xx) or thread.join (xx) or locksupport.parknanos or locksupport.partuntil is called
TERMINATED Thread interrupt state. A thread enters this state when it is interrupted or finished running

In the table above, the five states of threads correspond to different Java methods, as shown below:

Note that the two states highlighted in red are thread states in the operating system, and Java merges these two states into RUNNABLE states. In the operating system, the READY state indicates that the thread is READY and waiting for the CPU to allocate the time slice. ** RUNNING state ** indicates that when a thread is in a time slice, the thread begins to execute.

The use of volatile

In the Java memory model, we mentioned that Java divides memory into working memory (thread exclusive, not shared with other threads) and main memory to give an indication of how fast a program can run. When multiple threads access the same object or variable at the same time, each thread needs to copy the object or variable into its working memory. Because the working memory of a thread is private and not shared with other threads. So when one thread changes the value of a variable, it will not be visible to other threads. The Java memory model is shown below:

To ensure visibility of the data. Java provides the volatile keyword. The volatile keyword decorates a variable by telling the thread that access to that variable must be retrieved from main memory. Changes to it must be synchronized to main memory. This ensures that thread access to the variable is visible. For the use of volatile, see the following example:

class VolatileDemo {

    int a = 1;
    int b = 2;

    public void change(a) {
        a = 3;
        b = 4;
    }

    public void print(String threadName) {
        System.out.println(threadName + "- >" + "a = " + a + "; b = " + b);
    }

    public static void main(String[] args) {
        final VolatileDemo volatileDemo = new VolatileDemo();
        new Thread(new Runnable() {
            @Override
            public void run(a) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                volatileDemo.change();
            }
        }).start();

        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run(a) {
                    try {
                        Thread.sleep(10);
                    } catch(InterruptedException e) { e.printStackTrace(); } volatileDemo.print(Thread.currentThread().getName()); } }).start(); }}}Copy the code

Program output result:

Thread-1--->a = 1; b =2 / / error
Thread-3--->a = 1; b =2 / / error
Thread-2--->a = 1; b =2 / / error
Thread-5--->a = 3; b =4
Thread-4--->a = 3; b =4
Thread-6--->a = 3; b =4
Thread-7--->a = 3; b =4
Thread-8--->a = 3; b =4. Omit the otherCopy the code

In the code above, if we do not use the volatile keyword to modify a and B, then other threads are still fetching the values of a and B in their own working memory. To keep access to public variables visible to other threads, we need to make the variables volatile. Modify our code:

volatile  int a = 1;
volatile  int b = 2;
Copy the code

Using the volatile modifier, the output is as follows

Thread-2--->a = 3; b =4
Thread-1--->a = 3; b =4
Thread-6--->a = 3; b =4
Thread-3--->a = 3; b =4
Thread-4--->a = 3; b =4
Thread-9--->a = 3; b =4
Thread-10--->a = 3; b =4. Omit the otherCopy the code

It is important to note that volatile operations are visible to other threads only on a single variable, and that such operations as a++, which read the value of a, perform the operation, and then reassign the value of a variable, still cause thread-safety problems. For more information on volatile, see Volatile in Concurrent Programming in Java (ii).

The use of synchronized

In addition to using volatile to implement thread communication, we can also use the corresponding methods wait()/notify(), wait()/notifyAll of synchronized and Object to implement thread communication. Let’s start by looking at the role of the synchronized keyword in Java.

The keyword synchronized can be used to modify methods or code blocks. Synchronized can ensure that only one thread can be in a method or synchronized block at a time, which ensures the visibility and exclusivity of thread access to variables. The following code looks like this:

public class SyncCodeBlock {
   public int i;
   public void syncTask(a){
       // Synchronize the code base
       synchronized (this){ i++; }}}Copy the code

We then decompile the bytecode using javap instructions. To continue analyzing the implementation details of the synchronized keyword, as shown below:

/ / = = = = = = = = = = = main see syncTask method = = = = = = = = = = = = = = = = public void syncTask (); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: 4: aload_0 5: DUP 6: getField #2 // Field I :I 9: iconST_1 10: iadd 11 Putfield #2 // Field I :I 14: ALOad_1 15: monitorexit // note here, exit synchronization method, release lock 16: goto 24 19: astore_2 20: ALOad_1 21: Monitorexit // Notice here, exit synchronization method 22: aload_2 23: athrow 24: return Exception table: // omit other bytecode...... .}Copy the code

In the above bytecode information, synchronized blocks are implemented using monitorenter and Monitorexit directives, whose essence is the acquisition of an object’s monitor. This acquisition process is exclusive, that is, only one thread can acquire the monitor with synchronized protected objects at a time. In Java, every object has its own monitor. When an object has a synchronized block or a synchronized method call for that object, the thread executing the method must obtain the monitor before it can access the synchronized block, or synchronized method. Threads that do not get a monitor are BLOCKED at the entry to a synchronized block or method. As shown below:

From the above figure, it can be concluded that when any thread accesses the Object modified by the synchronized keyword, it must first obtain the monitor of the Object. If the acquisition fails, the thread enters the synchronization queue and its state becomes BLOCKED. The monitor of the Object (Monitorexit) is released when the precursor thread accessing the Object (the thread whose moniterEnter succeeded). Wakes up the thread blocking in the synchronization queue to try to get the monitor. The acquisition and release of the monitor are generally referred to as the acquisition lock and release lock. Both processes are described in the following sections as acquiring and releasing locks.

Synchronized synchronized queue

The locking mechanism implemented by synchronized in JVM is based on synchronous queue and wait queue, which is very similar to the locking mechanism implemented by Lock interface under Courrent package. It is important to note that in the synchronized wait (after) the thread will enter a FIFO queue (synchronous queue), notify ()/notifyAll () is an orderly process of the queue.

Synchronized wait/notification mechanism implementation

Above, we mentioned that synchronized is used to realize communication between threads, and we need to combine the corresponding methods wait()/notify() and wait()/notifyAll in Object. Let’s start by looking at the instructions for this set of methods in Obejet:

Method names describe
wait() The thread calling the method entersWAITINGState, which is returned only when it waits for notification from another thread or is interrupted. Note that the thread needs to obtain the monitor of the object before calling wait(). When the wait() method is called, the object’s monitor is released
wait(long) The thread calling the method entersTIMED_WAITINGState, where the parameter time is milliseconds, waits for the corresponding millisecond event, and returns timeout if no notification is received from another thread
wait(long,int) The thread calling the method entersTIMED_WAITINGState, basically the same as wiat(long), the second parameter represents nanosecond, that is, wait time is milliseconds + nanosecond.
notify() Notifies a thread waiting on the object monitor to return from wait() if it has retrieved the object monitor.
notifyAll() Notifies all threads waiting on the monitor, which thread to wake up is up to the CPU

Object wait()/notify() and wait()/notifyall() are commonly used wait/notification mechanisms. When thread A calls O’s wait() method to enter the wait state, thread A calls O’s wait() method to enter the wait state. The other thread B calls notify or notifyAll on object O. Thread A receives the notification and returns from object O’s wait() method to perform subsequent operations. Below, let’s use an example to understand the use of synchronized thread to complete the communication, as shown below:

class SynchronizedDemo {

    static boolean flag = true;
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(new WaitRunnable(), "WaitThread").start();
        TimeUnit.SECONDS.sleep(1);// The Wait line is executed first
        new Thread(new NotifyRunnable(), "NotifyThread").start();
    }

    static class WaitRunnable implements Runnable {
        @Override
        public void run(a) {
            synchronized (lock) {
                while (flag) {// Notice that the condition is determined through the while loop
                    String name = Thread.currentThread().getName();
                    try {
                        System.out.println(name + "--->wait in " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(name + "--->wake up in " + new SimpleDateFormat("HH:mm:ss").format(newDate())); }}}}static class NotifyRunnable implements Runnable {
        @Override
        public void run(a) {
            String name = Thread.currentThread().getName();

            synchronized (lock) {
                System.out.println(name + "--->notify all in " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                lock.notifyAll();
                flag = false;
            }

            /** * This is done again to verify that when notifyAll is called, * if the thread does not perform monitorexit(i.e., release the lock), no other thread will wake up */
            synchronized (lock) {
                try {
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(name + "--->hold lock again in " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}
Copy the code

The following output is displayed:

WaitThread--->wait in 23:10:11
NotifyThread--->notify all in 23:10:12
NotifyThread--->hold lock again in 23:10:14
WaitThread--->wake up in 23:10:14
Copy the code

From the above, we can draw the following conclusions:

  • To use wait(), notify(), and notifyAll, obtain the monitor of the object first (Monitorenter executed successfully)
  • After the wait() method is called, the state of the thread changes from RUNNING to WAITING and the thread is queued.
  • After notify() or notifyAll() is called, the waiting thread still does not return from wait(). The thread that calls notify() or notfifyAll() releases the monitor of the object (monitorexit). Wait threads have a chance to return from wait().
  • The notify() method moves one thread in the wait queue from the wait queue to the synchronous queue, while the notifyAll() method moves all threads in the wait queue to the synchronous queue, and the status of the moved thread changes from WAITING to BLOCKED.
  • The return from wait() is provided with the monitor for the calling object (monitorenter instruction executed successfully).

Lock wait/notification mechanism implementation

In addition to using synchronized to complete thread communication, we can also use the Lock interface under courrent package. Here, take ReentrantLock as an example. Specific examples are as follows:

class LockDemo {

    static boolean flag = true;
    static Lock lock = new ReentrantLock();
    static Condition codition = lock.newCondition();


    public static void main(String[] args) throws InterruptedException {
        new Thread(new WaitRunnable(), "WaitThread").start();
        TimeUnit.SECONDS.sleep(1);// The Wait line is executed first
        new Thread(new NotifyRunnable(), "NotifyThread").start();
    }

    static class WaitRunnable implements Runnable {
        @Override
        public void run(a) {
            lock.lock();
            try {
                while (flag) {
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "--->wait in " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                    codition.await();
                    System.out.println(name + "--->wake up in " + new SimpleDateFormat("HH:mm:ss").format(newDate())); }}catch (InterruptedException e) {
                e.printStackTrace();
            } finally{ lock.unlock(); }}}static class NotifyRunnable implements Runnable {
        @Override
        public void run(a) {
            lock.lock();
            try {
                String name = Thread.currentThread().getName();
                System.out.println(name + "--->notify all in " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                flag = false;
                codition.signalAll();
            } finally{ lock.unlock(); }}}}Copy the code

Output result:

WaitThread--->wait in 23:39:34
NotifyThread--->notify all in 23:39:35
WaitThread--->wake up in 23:39:35
Copy the code

You can check out the following articles about the use of Lock and how it works, but we won’t analyze it here.

  • Java Concurrent Programming Lock Interface (7)
  • Concurrent Java programming of locking mechanism of AQS (AbstractQueuedSynchronizer) (8)
  • LockSupport for Concurrent Programming in Java
  • Condition Interface for Java Concurrent Programming Locking Mechanism (10)
  • Reentrant locking for Concurrent programming in Java
  • Read/write Locking for Concurrent Programming in Java (11)

The classic paradigm of wait/notification

From the above example, we can summarize and derive a very classic wait/notification paradigm for wait methods (consumers) and notifying parties (producers).

Wait for the party

The waiting party shall follow the following principles:

  1. Gets the lock of the object.
  2. If the condition is not met, then the condition is not met, then the object’s wait() method is called. Continue to check conditions after being notified.
  3. If the conditions are met, the corresponding logic is executed.

Corresponding pseudocodes are as follows:

Synchronized:

synchronized(object) {while{object. Wait (); } Corresponding processing logic}Copy the code

Using lock mode:

    lock.lock();
    try{
        while{condition. Wait (); }}finally{
        lock.unlock();
    }
Copy the code

Notifier code

The notifying party follows the following principles:

  1. Get the lock of the object
  2. Change the conditions
  3. Notifies all threads waiting on an object.

Corresponding pseudocodes are as follows:

Synchronized:

synchronized(object){change the condition object. notifyAll()}Copy the code

Using lock mode:

    lock.lock();
    tryCondition.singleall (); }finally{
        lock.unlock();
    }
Copy the code

Thread. The use of the join

In addition to using the classical paradigm we described above, we can also use the thread.join () method. The join method is used as follows:

When thread A calls the join method of thread B (bThread), it means that thread A waits for thread B to terminate before returning from the call of the bThread.join() code in thread A. Threads also provide join(long millis) and void Join (Long millis, int nanos) methods with timeout features. The meaning of these two methods is if thread B does not terminate within a given amount of time. Thread A will return from this method. Let’s look at an example use of the join method, as follows:

Class AThread extends Thread {public AThread() {super("[AThread] "); } @Override public void run() { String threadName = Thread.currentThread().getName(); System. Out.println (threadName + "-- -- > start"); try { for (int i = 0; i < 5; i++) { System.out.println(threadName + "loop at" + i); TimeUnit.SECONDS.sleep(1); } System. Out.println (threadName + "-- - > end"); } catch (InterruptedException e) { e.printStackTrace(); } } } class BThread extends Thread { private AThread mAThread; Public BThread(AThread AThread) {super("[BThread] "); this.mAThread = aThread; } @Override public void run() { String threadName = Thread.currentThread().getName(); System. Out.println (threadName + "-- -- > start"); try { mAThread.join(); System.out.println(threadName + "-- >end "); threadName + "-- >end "); } catch (InterruptedException e) { e.printStackTrace(); } } } class ThreadJoinDemo { public static void main(String[] args) throws InterruptedException { System. Out.println (Thread. CurrentThread (). The getName () + "-- -- > start"); AThread aThread = new AThread(); BThread bThread = new BThread(aThread); try { aThread.start(); TimeUnit.SECONDS.sleep(1); bThread.start(); aThread.join(); The main thread waits until thread A completes} catch (InterruptedException e) {e.printstacktrace (); } System. Out.println (Thread. CurrentThread (). The getName () + "-- - > end"); }}Copy the code

In the above example, we mainly achieve the following two effects:

  • Main threadWaiting for theA threadPerform the operation only after the operation is complete
  • Thread BWaiting for theA threadPerform the operation only after the operation is complete.

Let’s look at the output:

[AThread]loop at0 //A thread starts loop [AThread]loop at1 [BThread]-->start // Thread B starts, because thread B called athread.join (), thread B will wait for thread A to finish executing, [AThread]loop at2 //A thread continues to execute [AThread]loop at3 [AThread]loop at4 [AThread]-- >end [BThread]-- >end // Wake up thread B to continue executing main-- >end // Main thread completes executingCopy the code

The whole program is running according to our previous logic. Now let’s check the implementation principle of join method in thread. The specific code is as follows:

The Join (Final Long Millis) method is called inside the join() method.

// The default lock for a synchronized method is the object on which the method is called, Join () xxThread public final synchronized void join(final Long millis) throws InterruptedException {if (millis > 0) {// If the wait time is greater than 0 if (isAlive()) {final Long startTime = system.nanotime (); long delay = millis; do { wait(delay); } while (isAlive() && (delay = millis - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0); }} else if (millis == 0) {while (isAlive()) {wait(0); // If the current thread is alive, the current thread will wait (the current thread is the thread running xxthread. join, }} else {throw new IllegalArgumentException("timeout value is negative "); }}Copy the code

When thread B calls thread A’s join(), the lock object is thread A. Wait (0) is called inside the join() method, which causes thread B to wait. Only when thread A has finished executing, so thread A has terminated. It wakes up thread B.

When a thread completes execution or terminates, the thread’s own notifyAll() method is called, notifying all threads waiting on the thread object.

ThreadLocal

In the above article, we have been talking about the communication between multiple threads before, so in the same thread, at some point we want to get the variable set in the thread, we can use ThreadLocal. In the previous article, we introduced the use of ThreadLocal for the Android-handler mechanism. Let’s take a look at the use of ThreadLocal with an example. Examples are as follows:

class ThreadLocalTest { private static ThreadLocal<String> mThreadLocal = new ThreadLocal<>(); Public static void main(String[] args) {mthreadLocal.set (" thread main "); new Thread(new A()).start(); new Thread(new B()).start(); System.out.println(mThreadLocal.get()); } static class A implements Runnable {@override public void run() {mthreadLocal.set (" thread A "); System.out.println(mThreadLocal.get()); }} static class B implements Runnable {@override public void run() {mthreadLocal.set (" thread B "); System.out.println(mThreadLocal.get()); }}}Copy the code

Output result:

Main Thread A Thread BCopy the code

If you are interested in ThreadLocal, check out the Android-Handler ThreadLocal article.

The last

Here is an example of a thread printing odd and even numbers alternately to help you consolidate what you have learned. If you’re interested, check out PrintOddEventNumber.

reference

This article refers to the following book, standing on the shoulders of giants. You can see further.

  • The Art of Concurrent Programming in Java