Concurrent programming: sharing and collaboration between threads

In the last article, we introduced the basis of threading. I believe everyone has a general understanding. This article is about thread sharing and collaboration. Let's have a look!

1, Sharing between threads

Synchronized

synchronized is a keyword in Java. It is a kind of synchronous lock. It modifies the following objects:

   1.  Modify a code block. The modified code block is called a synchronization statement block. Its scope of action is the code enclosed in braces {}, and the object of action is the object calling the code block; 
   2.  Modify a method. The modified method is called a synchronous method. Its scope of action is the whole method, and the object of action is the object calling the method; 
   3.  Modify a static method, its scope of action is the whole static method, and the object of action is all objects of this class; 
   4.  Modify a class. Its scope of action is the part enclosed in parentheses after synchronized. The main object is all objects of this class.

volatile

Volatile is arguably the lightest synchronization mechanism provided by the Java virtual machine. But it is not easy to understand correctly, and many programmers will use synchronized when they encounter thread safety problems in concurrent programming. The JAVA memory pattern tells us that each thread will copy the shared variables from the main memory to the working memory, and then the execution engine will operate and process based on the data in the working memory. When will a thread write to main memory after operating in working memory? There is no provision for ordinary variables at this time, and special conventions are given to the Java virtual machine for volatile modified variables. The modification of volatile variables by threads will be immediately perceived by other threads, that is, there will be no dirty reading of data, so as to ensure the "visibility" of data.

Now we have a general impression that the variable modified by volatile can ensure that each thread can obtain the latest value of the variable, so as to avoid dirty data reading.

ThreadLocal

threadlocal is easy to use

static final ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
sThreadLocal.set()
sThreadLocal.get()

threadlocal is an internal thread storage class that can store data in the specified thread. After data storage, only the specified thread can get the stored data. The official explanation is as follows.

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 */

ThreadLocal provides the ability for threads to store variables in memory. The difference between these variables is that the variables read by each thread are corresponding and independent of each other. The corresponding value of the current thread can be obtained through the get and set methods.

As an inappropriate metaphor, ThreadLocal is equivalent to maintaining a map. The key is the current thread and the value is the object to be stored.

The metaphor here is inappropriate. In fact, the static internal class ThreadLocalMap of ThreadLocal maintains an array table for each Thread. ThreadLocal determines an array subscript, which is the corresponding location of value storage..

As a data storage class, the key point is in the get and set methods.

2, Collaboration between threads

1. Implementation of waiting and notification mechanism

wait() method

wait() is a method of the Object class. Its function is to make the thread currently executing the wait method wait. This method puts the current thread into the "pre execution queue" and stops execution at the code line where wait() is located. Execution cannot continue until it is notified or interrupted. The thread must get the Object lock of the Object, that is, the wait() method can only be invoked in the synchronization method or the synchronous method block. After executing the wait() method, the current thread releases the Object lock which is owned. If wait() does not hold the Object lock, it will throw the IllegalMonitorStateException exception.

notify() method

notify() is a method of Object class. The function is to make the stopped thread continue to run, and also to call it in synchronous method or synchronous block. This method is used to notify other threads that may be waiting for the object lock of the object. If there are multiple threads waiting, the thread planner randomly selects a thread in the wait state and sends a notification notify to it. However, the notified thread will not immediately execute the code behind the wait, because the thread using notify will not immediately release the lock, Therefore, the notified thread will not get the lock immediately. If notify is called without holding an object lock, an IllegalMonitorStateException is thrown

class MyThread1 extends Thread {
​
    private Object lock;
​
    public MyThread1(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName()
                    + " start wait time = " + System.currentTimeMillis());
            try {
                //wait stops thread thread1
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + " end wait time = " + System.currentTimeMillis());
        }
    }
}
​
class MyThread2 extends Thread {
​
    private Object lock;
​
    public MyThread2(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName()
                    + " start notify time = " + System.currentTimeMillis());
            //Thread thread2 causes stopped thread1 to continue running
            lock.notify();
            System.out.println(Thread.currentThread().getName()
                    + " end notify time = " + System.currentTimeMillis());
        }
    }
    
}
​
public class Test extends Thread {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        MyThread1 thread1 = new MyThread1(lock);
        thread1.start();
        Thread.sleep(2000);
        MyThread2 thread2 = new MyThread2(lock);
        thread2.start();
    }
    
}
​

The result is:

AAA start wait time = 1540528981658
BBB start notify time = 1540528983660
BBB end notify time = 1540528983661
AAA end wait time = 1540528983661

Briefly analyze the whole process. Thread AAA first executes the run() method, obtains the lock object lock, outputs a line, and then executes lock The wait () method means that the thread AAA releases the object lock, and then the thread AAA holding the object lock enters the waiting state, but the thread AAA is still in the synchronized synchronization block; Because the thread AAA stopped, the thread BBB started executing the run() method to get the lock object lock and output a row, then called lock.. Notify() wakes up the thread AAA waiting for the object lock to enter the ready state, but the thread BBB does not release the object lock immediately, but continues to execute the remaining methods in its own synchronization methods. Only after the thread BBB executes the synchronized synchronization block can the object lock be released. At this time, the awakened thread AAA can regain the object lock, and then execute the code after the wait() method

The whole process is shown in the figure. In short, thread AAA is waiting, and then thread BBB wakes up thread AAA with notify, but does not release the lock immediately. The lock is not released until thread BBB executes the synchronization block. At this time, thread AAA obtains the lock and starts to execute the method after wait

It should be noted that the wait() method makes the thread that owns the Object lock wait temporarily, which has nothing to do with who owns the Object lock. For example, in this example, thread AAA owns the Object lock of the Object object Object. At the same time, call the wait() method with the Object lock in the run() method, and then the Object lock is released. Note, The Object lock held by thread AAA is released because thread AAA is in the synchronization block. As a result, thread AAA enters the waiting state. This is very important!!!

At the same time, in the synchronous code block, the wait, nofity or notifyAll method of the lock object must be called. In the waiting thread, the object calling the wait() method must be the object in the bracket (synchronized), and in the notification thread, the object calling the notify() method must also be the object in the bracket, if not, An IllegalMonitorStateException is thrown

notify() can wake up only one thread

If multiple threads are waiting, the notify() method can wake up only one thread randomly, and other threads that are not awake are still waiting. However, the notity() method can be called multiple times to wake up multiple threads randomly

//Service2 method
class Service2 {
​
    public void testMethod(Object lock) {
​
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName()
                    + " beg1in wait " + System.currentTimeMillis());
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + " end wait " + System.currentTimeMillis());
        }
​
    }
​
}

Create three threads ThreadA6, ThreadB6, and ThreadC6

class ThreadA6 extends Thread {
​
    private Object lock;
​
    public ThreadA6(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        Service2 service2 = new Service2();
        service2.testMethod(lock);
    }
​
}
​
class ThreadB6 extends Thread {
​
    private Object lock;
​
    public ThreadB6(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        Service2 service2 = new Service2();
        service2.testMethod(lock);
    }
​
}
​
class ThreadC6 extends Thread {
​
    private Object lock;
​
    public ThreadC6(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        Service2 service2 = new Service2();
        service2.testMethod(lock);
    }
}

Method of randomly waking up a waiting thread

class NotifyOne extends Thread {
​
    private Object lock;
​
    public NotifyOne(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("NotifyOne");
            lock.notify();
        }
    }
}

Multiple calls can randomly wake up multiple waiting threads

class NotifyMulti extends Thread {
​
    private Object lock;
​
    public NotifyMulti(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("NotifyMulti");
            lock.notify();
            lock.notify();
            lock.notify();
            lock.notify();
            lock.notify();
        }
    }
}

test method

public class Test2 {
​
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        ThreadA6 threadA6 = new ThreadA6(lock);
        threadA6.start();
        ThreadB6 threadB6 = new ThreadB6(lock);
        threadB6.start();
        ThreadC6 threadC6 = new ThreadC6(lock);
        threadC6.start();
​
        Thread.sleep(2000);
​
        NotifyOne notifyOne = new NotifyOne(lock);
        notifyOne.start();
        /*NotifyMulti notifyMulti = new NotifyMulti(lock);
        notifyMulti.start();*/
    }
​
}

The result is:

Thread-0 beg1in wait 1540536524678
Thread-1 beg1in wait 1540536524679
Thread-2 beg1in wait 1540536524679
notifyOne Awakened a thread 1540536526679
Thread-0 end wait 1540536526679

Since only one thread can be awakened, the other two threads that are still in the waiting state are in the waiting state forever because they are not awakened. If the annotated statement is called, multiple threads can be awakened

Thread-0 beg1in wait 1540536666626
Thread-2 beg1in wait 1540536666626
Thread-1 beg1in wait 1540536666626
NotifyMulti
Thread-0 end wait 1540536668627
Thread-1 end wait 1540536668627
Thread-2 end wait 1540536668627

notifyAll() can wake up multiple threads

Because we don't know how many threads are waiting, we can't keep calling the notify() method, which will be very troublesome. Therefore, we can use the notifyAll() method to wake up all the waiting methods

When interrupt method encounters wait method

When the thread is in the wait() state, an InterruptedException exception will occur when calling the interrupt() method of the thread object

class Service5 {
​
    public void testMethod(Object lock) {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " be1gin wait "
                + System.currentTimeMillis());
            try {
                lock.wait();
                System.out.println(Thread.currentThread().getName() + " end wait "
                        + System.currentTimeMillis());
            } catch (InterruptedException e) {
                System.out.println("exception occurred...");
                e.printStackTrace();
            }
        }
    }
​
    public void testMethod2(Object lock) {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " begin 2");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " end 2");
        }
    }
​
}
​
class ThreadB5 extends Thread {
​
    private Object lock;
​
    public ThreadB5(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        Service5 service5 = new Service5();
        service5.testMethod2(lock);
    }
}
​
public class ThreadA5 extends Thread {
​
    private Object lock;
​
    public ThreadA5(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        Service5 service5 = new Service5();
        service5.testMethod(lock);
    }
​
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        ThreadA5 threadA5 = new ThreadA5(lock);
        threadA5.start();
        threadA5.interrupt();
        Thread.sleep(2000);
​
        ThreadB5 threadB5 = new ThreadB5(lock);
        threadB5.start();
    }
}

The result is:

Thread-0 be1gin wait 1540534325308
 exception occurred...
java.lang.InterruptedException
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:502)
    at edu.just.Service5.testMethod(ThreadA5.java:10)
    at edu.just.ThreadA5.run(ThreadA5.java:60)
Thread-1 begin 2
Thread-1 end 2
​

It can be seen that when an error is reported, the code after the wait() method is also executed because an exception is encountered during the execution of the synchronous code block, resulting in the termination of the thread and the release of the lock. At this time, if another thread holds the object lock, the code behind the wait will not be executed, but will directly report an error

Notification time

wait(long) method

If there are parameters in the wait() method, it means that if no other thread wakes up the waiting thread within a period of time, the waiting thread will wake up automatically after this time

  1. If you wake up within the specified time, the code of other threads will be executed first, and then the code after wait will be executed

  2. If you wake up outside the specified direct, you will execute the code after wait first and execute the code of other threads

public class MyRunnable {
​
    private static Object lock = new Object();
​
    private static Runnable runnable = new Runnable() {
        @Override
        public void run() {
            synchronized (lock) {
​
                System.out.println(Thread.currentThread().getName()
                        + " wait begin time " + System.currentTimeMillis());
                try {
                    //The thread is automatically awakened after 1 s
                    lock.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + " wait end time " + System.currentTimeMillis());
            }
        }
    };
​
    private static Runnable runnable1 = new Runnable() {
        @Override
        public void run() {
            synchronized (lock) {
​
                System.out.println(Thread.currentThread().getName()
                        + " wait begin time2 " + System.currentTimeMillis());
                try {
                    lock.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + " wait end time " + System.currentTimeMillis());
            }
        }
    };
​
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(runnable);
        thread.setName("AAA");
        thread.start();
        //Thread AAA is awakened within the specified time statement 1
        Thread.sleep(1100);
        //Thread AAA is awakened outside the specified time statement 2
//        Thread.sleep(900);
        Thread thread2 = new Thread(runnable1);
        thread2.setName("BBB");
        thread2.start();
    }
​
}

Comment out statement 2 first, and the result is:

AAA wait begin time 1540538435802
AAA wait end time 1540538436802
BBB wait begin time2 1540538436902
BBB wait end time 1540538436903

You can see that thread AAA wakes up automatically before it is manually awakened by thread BBB, so you directly execute the method behind wait

When executing statement 2, comment statement 1. The result is:

AAA wait begin time 1540538528885
BBB wait begin time2 1540538529784
BBB wait end time 1540538529784
AAA wait end time 1540538529885

At this time, thread BBB wakes up thread AAA before thread AAA is automatically awakened. At this time, the code of thread BBB is executed first, and the code behind thread AAA wait() method is executed

The difference between wait() and sleep() methods

The wait() method is very similar to the sleep() method. A comparison can be made below:

  1. wait is a member variable of the Object class, and sleep is a static method of the Thread class

  2. You need to obtain the object lock before calling the wait method, but you don't need to obtain the object lock before calling the sleep method

  3. The thread calling the wait method needs to wake up with notify, and the sleep method must set the timeout value

  4. After calling the wait method, the thread will release the lock first, while the sleep method will not release the lock

2. join method

The following is the schematic diagram of join

Function demonstration

public class JoinDemo implements Runnable{
    public void run() {
        System.err.println("join thread demo ");
    }
​
    public static void main(String[] args) throws Exception {
        System.err.println("main thread start... ");
        Runnable r = new JoinDemo();
        Thread t = new Thread(r);
        t.setName("ibli joinTest ...");
        t.start();
//        t.join();
        System.err.println("main thread end... ");
    }
}

t.join(); Note out that one possible result of execution is as follows:

main thread start... 
main thread end... 
join thread demo
​
There may also be this result:
main thread start... 
join thread demo
main thread end... 
​

However, remove the comments and the results are as follows:

main thread start... 
join thread demo 
main thread end... 
​

This is a very simple demo, and the effect is obvious. When the main thread calls t.join(), it will block its current thread. The main thread can continue to execute only when the execution of T thread reaches the completion state.

Let's take a look at how join() sets the timeout:

public class JoinDemo implements Runnable{
    public void run() {
        System.err.println("join thread demo ");
        try {
            // Thread sleep 4s
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        List<String> strings = null;
        System.err.println(strings.get(0));
    }
​
    public static void main(String[] args) throws Exception {
        System.err.println("main thread start... ");
        Runnable r = new JoinDemo();
        Thread t = new Thread(r);
        t.setName("ibli joinTest ...");
        t.start();
        // However, the timeout of the main thread join is 1s
        t.join(1000);
        System.err.println("main thread end... ");
    }
}

Execution effect:

main thread start... 
join thread demo 
main thread end... 
Exception in thread "ibli joinTest ..." java.lang.NullPointerException
    at com.ibli.threadTest.api.JoinDemo.run(JoinDemo.java:14)
    at java.lang.Thread.run(Thread.java:748)
​

join() source code

First, the join(0) method will be called, which is actually an overloaded method of the join;

public final void join() throws InterruptedException {
        join(0);
}

The following is the core implementation of join:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
        
        // First, check whether the parameters are legal
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
​
        // If the join method has no parameters, it is equivalent to calling the wait method directly
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

The following is the source code of the isAlive method

public final native boolean isAlive();

This is a local method to determine whether the current thread is active. What is active state? The active state is that the thread has been started and not terminated. A thread is considered "alive" when it is running or ready to start running.

  • Here's a point to note: why does the join block the main thread rather than the child thread?

  • The reason why I don't understand is that the method of blocking the main thread is used in the instance of previousthread, which makes people mistakenly think that the previousthread thread should be blocked. In fact, the main thread will hold the lock of the previousThread object and then call the wait method to block it, and the caller of this method is in the main thread. Therefore, the main thread is blocked.

  • In fact, the core of the join() method is wait (). Calling t.join() in the main thread is equivalent to adding new JoinDemo () in the main method. wait(); Is the same effect; Here, it's just that the wait method is written in the method of the child thread.

  • Again, the function of the join method is to block the main thread, wake up the main thread by the sub thread after the sub thread is executed, and then continue to execute the logic after the main thread calls the t.join() method.

In fact, you can look at the source code implementation of the JVM, thread In the cpp file, there is a code:

void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
  // Notify waiters on thread object. This has to be done after exit() is called
  // on the thread (if the thread is the last thread in a daemon ThreadGroup the
  // group should have the destroyed bit set before waiters are notified).
  ensure_join(this);
}

Which calls ensure_join method

static void ensure_join(JavaThread* thread) {
  // We do not need to grap the Threads_lock, since we are operating on ourself.
  Handle threadObj(thread, thread->threadObj());
  assert(threadObj.not_null(), "java thread object must exist");
  ObjectLocker lock(threadObj, thread);
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
  // Thread is exiting. So set thread_status field in  java.lang.Thread class to TERMINATED.
  java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
  // Clear the native thread instance - this makes isAlive return false and allows the join()
  // to complete once we've done the notify_all below
  //This is to clear the native thread. This operation will cause the isAlive() method to return false
  java_lang_Thread::set_thread(threadObj(), NULL);
  // Wake up the waiting thread here
  lock.notify_all(thread);
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
}

In the JVM's code, lock is called at the end of thread execution notify_ All (thread) method to wake up all threads in wait

Usage scenario

  • For example, when we use Callable to execute asynchronous tasks, we need to call the join method when the main thread processes the return value of the task;

  • There are also scenarios where you want threads to execute sequentially;

Comparison between join() method and sleep()

Let's talk about the sleep method first:

  • Let the current thread sleep for a specified time.

  • The accuracy of sleep time depends on the system clock and CPU scheduling mechanism.

  • If the sleep method is invoked in the synchronous context, no other thread can enter into the current synchronization block or synchronization method.

  • You can wake up a dormant thread by calling the interrupt() method.

  • sleep is a static method that can be called anywhere

Compared with the sleep method, sleep is a static method, and the thread of sleep is not a lock resource, while the join method is an object method, and the object lock will be released during the waiting process;

These are the above. If you want to know more about the interview class, please pay attention!

Keywords: Java Back-end Interview Concurrent Programming

Added by winkhere on Thu, 13 Jan 2022 21:49:18 +0200