Summary: eight ways to realize thread synchronization

Time:2020-11-24

preface:

In multithreading, the execution order of threads depends on which thread gets the execution right of cup first. Although it can be set by thread priority, it is only the high probability point of obtaining the execution right of cup, but it does not necessarily have to be executed first. In this case, how to ensure that threads are executed in a certain order, I’d like to make a summary today and introduce several ways.
1、 Through wait and notify of object
2、 Through awiat and signal of condition
3、 Through a blocking queue
4、 Through two blocking queues
5、 Through synchronous queue
6、 Callback through thread pool
7、 By synchronizing the auxiliary class countdownlatch
8、 By synchronizing the auxiliary class cyclicbarrier

1、 Through wait and notify of object

Write a test, add the main method, and write an internal class man to test. The main method is as follows: it creates two threads and passes it to the runnable object.

public static boolean flag = false;

public static int num = 0;

public static void main(String[] args) {
    Man man = new Man();

    new Thread(() -> {
        man.getRunnable1();
    }).start();
    new Thread(() -> {
        man.getRunnable2();
    }).start();
}

Getrunnable1 and getrunnable2 represent two tasks to be executed in two threads. Method 1 is used for data production and method 2 is for data acquisition. The initial value of data is num= To ensure the balance between production and acquisition, we need to use wait and notify methods. The use of these two methods must be locked. Therefore, synchronized is used for locking. To demonstrate this effect, we add a sleep method to simulate the processing time, as follows:

public static class Man {
    
    public synchronized void getRunnable1() {
        for (int i = 0; i < 20; i++) {
            while (flag) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println ("produced: + (+ + + Num) +" pieces ");
            flag = true;
            notify();
        }
    }
    
    public synchronized void getRunnable2() {
        for (int i = 0; i < 20; i++) {
            while (!flag) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //Simulation load time
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println (take out: + (Num --));
            System.out.println("------------------");

            flag = false;
            notify();
        }
    }
}

Analyze its loading process and analyze it from method 1. Since the initial condition of flag is false, method 1 does not enter into waiting, and directly carries out production. After the production is completed, update the flag value to true, and notify the wait method of the next method 2 to make it wake-up. At this time, because method 1 is locked, other parts of method 1 cannot be executed. When method 1 is executed, method 1 can be executed. However, the flag of method 1 is true, and it is blocked after entering wait. Therefore, method 2 can only be executed at this time. Because method 2 is awakened, the blocking is relieved, and then the data is obtained. When the acquisition is finished, the flag is changed to false again. The notify method 1 removes the blocking and executes method 1 again. This is a continuous loop to ensure the orderly execution of different threads until the program is terminated.

The operation effect is as follows:

2、 Through awiat and signal of condition

The implementation of the first one above is blocking and waiting to ensure the orderly execution of threads, but communication between two threads is not possible. The condition described in the following section has such a function. To obtain the condition object, you must first obtain the lock object. It is a lock mechanism added after JDK1.5, which has better performance than synchronized. Similar to the above, copy the code and see the main method

public static boolean flag = false;

public static int num = 0;

public static void main(String[] args) {
    Man man = new Man();

    new Thread(() -> {
        man.getRunnable1();
    }).start();
    new Thread(() -> {
        man.getRunnable2();
    }).start();
}

The situation is consistent with the analysis of the first implementation method, which is not repeated here. We mainly look at method 1 and method 2 in the internal class man. First create a lock object, change synchronized to lock with lock, then create condition object through lock, replace wait method of object class with await method of condition, and finally replace notify method with signal method. The execution principle is consistent with the above analysis, and the code is as follows:

public static class Man {
    public static ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();

    public void getRunnable1() {
        lock.lock();
        try {
            for (int i = 0; i < 20; i++) {
                while (flag) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println ("produced: + (+ + + Num) +" pieces ");
                flag = true;
                condition.signal();
            }
        } finally {
            lock.lock();
        }
    }

    public void getRunnable2() {
        lock.lock();
        try {
            for (int i = 0; i < 20; i++) {
                while (!flag) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println (take out: + (Num --));
                System.out.println("------------------");
                flag = false;
                condition.signal();
            }
        } finally {
            lock.unlock();
        }
    }
}

The results are as follows:

This is my IOS development exchange group:519832104No matter you are Xiaobai or Daniel, welcome to settle in. You can share experience, discuss technology, learn and grow together!
Also attached is a copy of the interview questions collected by friends. You need IOS development learning materials and real interview questions. You can download them when you enter the group!

Click here to communicate with IOS Daniel immediately

3、 Through a blocking queue

The code of the above two methods is relatively cumbersome. If it is implemented by blocking the queue, it will be more concise. Here we use the commonly used arrayblocking queue with a capacity of 64. The main method is as follows:

public static void main(String[] args) {
    Man man = new Man();

    new Thread(() -> {
        man.getRunnable1();
    }).start();
    new Thread(() -> {
        man.getRunnable2();
    }).start();
}

We mainly look at method 1 and method 2 in man. In method 1, production data is stored in the queue. At the same time, method 2 fetches data. If method 1 is full or method 2 is finished, it will be blocked. Wait until method 1 is finished or method 2 is taken out, and then proceed. The code is as follows:

public static class Man {

    ArrayBlockingQueue queue = new ArrayBlockingQueue(64);

    public void getRunnable1() {
        for (int i = 0; i < 8; i++) {
            System.out.println ("produced: + I +" PCs.);
            try {
                queue.put(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println ("--- finished production --------");
    }

    public void getRunnable2() {
        for (int i = 0; i < 8; i++) {
            try {
                int num = (int) queue.take();
                System.out.println (take out): + Num);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Obviously, the blocking queue code has been refined a lot. It can also be found that this blocking queue has the function of caching. Many Android network access frameworks use this for caching, such as volley, okhttp, and so on.

The operation effect is as follows:

4、 Through two blocking queues

Using one blocking queue can achieve thread synchronization function, and two blocking queues can also achieve thread synchronization. The principle is that arrayblocking queue has capacity. If its capacity is positioned as 1, it means that it can only put in one element, and the second party will be blocked. According to this principle, two block queues with capacity of 1 are defined, one for storing data and the other for controlling order. The main method is the same as above. Let’s take a look at the two methods in the man class

static class Man {
    //Data storage
    ArrayBlockingQueue queue1 = new ArrayBlockingQueue(1);
    //Used to control program execution
    ArrayBlockingQueue queue2 = new ArrayBlockingQueue(1);

    {
        try {
            //Queue2 puts in an element and getrunnable2 blocks
            queue2.put(22222);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void getRunnable1() {
        new Thread(() -> {
            for (int j = 0; j < 20; j++) {
                try {
                    //Queue1 puts in an element and getrunnable1 blocks

                    queue1.put(j);
                    System.out.println (storage thread Name:+ Thread.currentThread (). Getname() + "- data is -" + J);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                try {
                    //Queue2 takes out the element and getrunnable2 enters
                    queue2.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    public void getRunnable2() {
        new Thread(() -> {
            for (int j = 0; j < 20; j++) {
                try {
                    //Queue2 puts in an element and getrunnable2 blocks
                    queue2.put(22222);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                try {
                    //Queue1 puts an element and getrunnable1 enters

                    int i = (int) queue1.take();
                    System.out.println (get thread Name:+ Thread.currentThread (). Getname() + "- data is -" + I);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

Once again, it reminds Queue2 that it is used to control the execution order of the program, which has no practical meaning. Finally, we can see the operation effect. It is very clear to save one or take one, as follows:

5、 Through synchronous queue

Synchronous queue is different from general data and other threads, but threads wait for data. It is a BlockingQueue without data buffer. The insertion operation put of producer thread must wait for the consumer’s removal operation take, and vice versa. Through this feature, a solution to the problem of multi thread synchronization is implemented. The code is as follows:

/**
 *Use blocking queue synchronousqueue
 *Offer to insert data into the end of the queue
 *Take takes out the data, if not, blocks it until there is data being obtained
 */
public static void test() {
    SynchronousQueue queue = new SynchronousQueue();
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
                queue.offer(9);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    try {
        int take = (int) queue.take();
        System.out.println(take);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

If the sub thread is not completed and the sub thread is not completed, the main thread will be blocked and unable to perform the next step.

6、 Callback through thread pool

In the process of creating a thread, there is a creation method that can return the thread result, that is, callback. It can return the execution result of the thread, and then operate in the main thread through the result returned by the sub thread. It is also a synchronization method. This synchronization is particularly applicable in Android, such as the task creation part of asynctask source code in Android. The code is as follows:

private static void test() {
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    Future submit = executorService.submit(new Callable() {
        @Override
        public Boolean call() throws Exception {
            return false;
        }
    });
    try {
        if (submit.get()) {
            System.out.println(true);
        } else {
            System.out.println(false);
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

7、 By synchronizing the auxiliary class countdownlatch

Countdownlatch is a synchronized helper class that allows one or more threads to wait for another set of threads to complete the operation before continuing. Other classes are actually controlled by means of counters. When creating, an int value is passed in. Whenever we call the countdown() method, the value of this variable is subtracted by 1. For the await() method, we judge whether the value of this int variable is 0. If yes, all operations have been completed. Otherwise, we will continue to wait. Can be understood as a countdown lock.

public class Test7 {
    public static void main(String[] args) {
        //Start the two threads and execute the main thread after the execution
        CountDownLatch countDownLatch = new CountDownLatch(2);
 
        //Thread 1 execution
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println ( Thread.currentThread (). Getname() + "thread execution completed");
            countDownLatch.countDown();
        });
        //Thread 2 execution
        Thread thread2 = new Thread(() -> {
            System.out.println ( Thread.currentThread (). Getname() + "thread execution completed");
            countDownLatch.countDown();
        });
 
 
        thread1.start();
        thread2.start();
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        //Execution main thread
        System.out.println (the main thread has finished executing);
    }
}

The results were as follows:

8、 By synchronizing the auxiliary class cyclicbarrier

Cyclicbarrier is a synchronous auxiliary class, similar to the countdownlatch above. The difference is that it allows a group of threads to wait for each other to reach a common point before continuing to execute. It can be seen as an obstacle. All threads must arrive at the same time before they can pass the obstacle together.

public class Test8 {
    public static void main(String[] args) {
        //Start the two threads and execute the main thread after the execution
        CyclicBarrier barrier  = new CyclicBarrier(2, () -> {
            //Execution main thread
            System.out.println (the main thread has finished executing);
 
        });
 
        //Thread 1 execution
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
 
            System.out.println ( Thread.currentThread (). Getname() + "thread execution completed");
 
            try {
                barrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        });
 
        //Thread 2 execution
        Thread thread2 = new Thread(() -> {
            System.out.println ( Thread.currentThread (). Getname() + "thread execution completed");
            try {
                barrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        });
 
 
        thread1.start();
        thread2.start();
    }
}

Operation results:

See more:IOS interview questions