Deeply understand the communication between threads of concurrent programming

Deeply understand the communication between threads of concurrent programming

1, What is thread communication

Communication between threads is to enable threads to send signals to each other. When multiple threads are processing the same resource and the tasks are different (normally, multi-threaded execution is processed at the same time, but we expect different times), thread communication is needed to help solve the use or operation of the same variable between threads. As long as threads do not operate on shared variables at the same time, thread safety problems will not occur.

2, Communication between threads

1. wait() and notify()

  • wait(): the current thread releases the marked and enters the waiting state. Generally, the interview is often compared with yield() and sleep(). Yield() also releases the marked, but the current thread will still rob the marked. wait() will not rob after releasing the lock mark. Sleep() does not release the marked.
  • Notify(): wakes up the current thread. When waiting() above, the thread enters the waiting state. If we want the thread to execute, we need to wake up the thread through notify().
  • notifyAll(): wakes up all threads that have entered the waiting state.
    Look at the following code:
public class Test001 {

    //Write thread
   static class InThread extends Thread{
        private Res res;

        public InThread(Res res){
            this.res = res;
        }

        @Override
        public void run() {
            boolean flag = true;
            while (true){
                if(flag){
                    res.setName("Xiao Ming");
                    res.setSex("male");
                }else{
                    res.setName("Xiao Hong");
                    res.setSex("female");
                }
                flag = !flag;
            }
        }
    }
	//Read thread, two threads
    static class OutThread extends  Thread{
        private Res res;

        public OutThread(Res res){
            this.res = res;
        }
        @Override
        public void run() {
            while (true){
                System.out.println(res.toString());
            }
        }
    }
	//Run read and write threads at the same time
    public static void main(String[] args) {
        Res res = new Res();
        new InThread(res).start();
        new OutThread(res).start();
    }
}

Two threads have a write operation and a read operation. What we expect is to read the normally written data "Xiaoming man, Xiaohong woman". At this time, because two threads operate the same shared variable at the same time, there is a thread safety problem. At this time, we can control through the communication between threads to achieve the desired effect.

Of course, we can also use locking at this time. Let's lock it first to see the effect:

public class Test001 {

    //Write thread
   static class InThread extends Thread{
        private Res res;

        public InThread(Res res){
            this.res = res;
        }

        @Override
        public void run() {
            boolean flag = true;
            while (true){
                synchronized (res){
                    if(flag){
                        res.setName("Xiao Ming");
                        res.setSex("male");
                    }else{
                        res.setName("Xiao Hong");
                        res.setSex("female");
                    }
                    flag = !flag;
                }
            }
        }
    }

    static class OutThread extends  Thread{
        private Res res;

        public OutThread(Res res){
            this.res = res;
        }
        @Override
        public void run() {
            while (true){
                synchronized (res) {
                    System.out.println(res.toString());
                }
            }
        }
    }

    public static void main(String[] args) {
        Res res = new Res();
        new InThread(res).start();
        new OutThread(res).start();
    }
}

Take a look at the code execution effect. This is a piece of Xiaoming and then a piece of Xiaohong (this is because the lock mark holding event is long. It may have been written for 100 times and read for 100 times. At this time, only the last data of the operation is read). Although the data is not disordered, it is not what we want. What we want is to read after each modification, The following introduces the communication between threads.

wait() and notify/notifyAll() are a pair of methods, just like lock/unlock appear together every time.
We use wait() and notify() to modify the above example. After each read / write operation, we release the lock mark to make the current thread enter the waiting state, and then wake up the other thread. When waiting () and notify() are used, the outside must be locked, because we operate on the lock mark.

public class Test001 {

    //Write thread
   static class InThread extends Thread{
        private Res res;

        public InThread(Res res){
            this.res = res;
        }

        @Override
        public void run() {
            boolean flag = true;
            while (true){
                synchronized (res){
                    try {
                        //After each write, release the mark and wait
                        res.wait();
                        if(flag){
                            res.setName("Xiao Ming");
                            res.setSex("male");
                        }else{
                            res.setName("Xiao Hong");
                            res.setSex("female");
                        }
                        flag = !flag;
                        //To wake up other waiting threads
                        res.notify();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    static class OutThread extends  Thread{
        private Res res;

        public OutThread(Res res){
            this.res = res;
        }
        @Override
        public void run() {
            while (true){
                synchronized (res) {
                    try {
                         //Wait for the operation of the write thread
                        res.wait();
                        System.out.println(res.toString());
                        //After a read operation, release the lock mark and wake up the write thread
                        res.notify();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        Res res = new Res();
        new InThread(res).start();
        new OutThread(res).start();
    }
}

Take a look at the execution results, which meet our expectations. Read the data after writing each time.

2.volatile keyword

volatile keyword is to realize the real sharing between thread variables, which is our ideal sharing state. Multiple threads monitor the shared variables at the same time. When the variables change, other threads change immediately. The specific implementation is related to JMM memory model, which will be discussed tomorrow.
First look at the following code, which is used for thread safety mentioned earlier:

public class Test001 {


    static class Thread001 extends Thread {

        private  boolean flag = false;

        @Override
        public void run() {
           while (true){
               if(flag){
                   System.out.println("flag = " + flag);
               }
           }
        }

        public void setFlag(boolean flag) {
            this.flag = flag;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread001 thread= new Thread001();
        thread.start();
        Thread.sleep(1000);
        for(int i=1;i<1000;i++){
            Thread.sleep(10);
            thread.setFlag(true);
        }
    }

}

We changed the flag to true in the main thread and expected to output the print statement. Even if we uniformly modified the variable 1000 times in the main thread, the print statement did not appear because the main thread did not receive the modified flag sub thread. When we add volatile modifier to flag:

public class Test001 {
    static class Thread001 extends Thread {

        private volatile boolean flag = false;

        @Override
        public void run() {
           while (true){
               if(flag){
                   System.out.println("flag = " + flag);
               }
           }
        }

        public void setFlag(boolean flag) {
            this.flag = flag;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread001 thread= new Thread001();
        thread.start();
        Thread.sleep(1000);
        for(int i=1;i<1000;i++){
            Thread.sleep(10);
            thread.setFlag(true);
        }
    }

}

We achieved the desired results:

This is because volatile ensures the visibility of variables, which means that when you have threads to modify variables, the variables of other threads will also be updated. The main thread modified the value of flag, and the child thread received it, so it printed.

join()

The function of the join() method is to make thread A join thread B for execution. Thread B enters the blocking state. Thread B will continue to execute only after thread A finishes running. Similar to the concept of jumping in line at dinner, this jumping in line is A bit overbearing. You will jump in line during the shopping process. Even if you pay for the plate, I have to wait. You can't continue until I finish eating.
Look at the following code:

public class Test001 {
    
    static class Thread001 extends Thread {

        @Override
        public void run() {
           for(int i=0;i<100;i++){
               try {
                   Thread.sleep(10);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println("Child thread of execution");
           }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread001 thread= new Thread001();
        thread.start();
        System.out.println("Main thread of execution");
    }
}

Since the sub thread and the main thread are two threads that execute at the same time, the print statement of the main thread is not the last one of the output statements.

What if we want the sub thread to execute first and the main thread to execute later? We can use thread Join() queue jumping, let the child thread execute the main thread first and then execute.

public class Test001 {

    static class Thread001 extends Thread {

        @Override
        public void run() {
           for(int i=0;i<100;i++){
               try {
                   Thread.sleep(10);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println("Child thread of execution");
           }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread001 thread= new Thread001();
        thread.start();
        thread.join();
        System.out.println("Main thread of execution");
    }

}

Seeing the result is what we expect

CountDownLatch

CountDownLatch is a counter that is shared by multiple threads. When CountDownLatch is created, the initial value of a counter will be passed. Each time CountDownLatch is executed When countdown(), the counter will be decremented by one, and CountDownLatch. Is executed When await(), the current thread will stop waiting until the counter is cleared.
Take a look at the example of thread safety used yesterday:

public class Thread007 {


    static AtomicInteger count = new AtomicInteger(1000);

    public static void main(String[] args) {
        //This is a counter used to wait for 10 threads to finish executing
        final CountDownLatch latch = new CountDownLatch(10);
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 100; j++) {
                    count.decrementAndGet();
                }
                //The execution end counter of the current thread is decremented by one
                latch.countDown();
            }).start();
        }
     /*   try {
            //Here, we will wait for all threads to finish executing, and the latch will not be released until it is 0
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
        System.out.println(count);
    }
}

The expected result is that all threads execute and then print, but the main thread itself is a thread, so they execute at the same time. When printing, they are not sure where to execute.

When we open the commented code, the main thread code will always be stuck in the latch await(); Until the counter is cleared, that is, all sub threads are finished, and we get the desired result.

3, Summary

The above just briefly talks about several ways of thread communication. In fact, there are many ways of thread communication. Because the switch of CUP is random and the operation of multiple threads does not interfere with each other under normal circumstances, the process of thread operation is out of our control. If we want multiple threads to run regularly, we need thread communication. The communication between threads can enable multiple threads to execute according to our expected operation process.

Keywords: Java Back-end Multithreading

Added by coily on Wed, 26 Jan 2022 03:42:53 +0200