JUC common concurrency tools tutorial (including ReentrantLock, CountDownLatch, ReadWriteLock, semaphore Semaphore, common queue, etc.)

1. Introduction

JUC (java.util.concurrent): Java concurrency toolkit for processing threads, which provides a variety of tool classes for controlling synchronization and thread communication, supported by JDK 1.5 or above.
The package structure is as follows:
insert image description here
insert image description here
insert image description here

2. Commonly used tools

1.ReentrantLock

1.1 Introduction
ReentrantLock is a mutex and a reentrant lock. The ReentrantLock lock can only be held by one thread lock at the same time point, but it can be acquired multiple times by the same thread. The state of AQS is incremented by 1 every time it is acquired, and decremented by 1 every time the state is released. The main problem to be solved is to avoid thread deadlock. If a lock is not reentrant, a thread that has acquired synchronization lock X, when competing for lock X before releasing lock X, is equivalent to waiting for itself to release the lock.

1.2 Comparison between ReentrantLock and Synchronized
ReentrantLock is implemented through AQS, and Synchronized through monitor mode;
ReentrantLock is more flexible by using lock and unlock to lock and unlock, and supports methods such as tryLock and lockInterruptibly;
ReentrantLock has fair mode and unfair mode, and Synchronized is unfair Lock;
ReentrantLock can be associated with multiple conditional queues, and Synchronized can only be associated with one conditional queue.

1.3 Easy to use

package com.example.springb_web.juc;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {
    
    


    //默认是非公平锁,这里可以指定为公平锁
    //公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,
    // 永远都是队列的第一位才能得到锁
    Lock lock_fair = new ReentrantLock(true);

    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(()->{
    
    
            Lock lock = new ReentrantLock();
            //加锁,相当于synchronized(this),这里必须要紧跟在try代码
            lock.lock();
            try {
    
    
                System.out.println("t1执行开始");
                //每隔0.5秒执行一个任务
                for(int i=0;i<5;i++){
    
    
                    Thread.sleep(500);
                    System.out.println("t1执行任务"+i);
                }
                System.out.println("t1执行结束");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally{
    
    
                //lock必须手动释放锁,且必须放在finally第一行
                lock.unlock();
            }
        });
        t1.start();

        Thread t2 = new Thread(()->{
    
    
            Lock lock = new ReentrantLock();
            boolean locked = false;
            try {
    
    
                Instant start = Instant.now();
                //tryLock:在指定时间内获取锁,如果获取不到,线程可以继续执行
                locked = lock.tryLock(5, TimeUnit.SECONDS);
                Instant end = Instant.now();
                long timeElapsed = Duration.between(start, end).toMillis();
                if(locked){
    
    
                    System.out.println("t2获取锁,耗时"+timeElapsed);
                }else{
    
    
                    System.out.println("t2没获取锁,耗时"+timeElapsed);
                }

            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally{
    
    
                //lock必须手动释放锁
                if(locked){
    
    
                    lock.unlock();
                }
            }
        });
        t2.start();

        Thread t3 = new Thread(()->{
    
    
            Lock lock = new ReentrantLock();
            try {
    
    
                //在一个线程等待锁的过程中,可以被打断
                lock.lockInterruptibly();
                System.out.println("t3执行开始");
                //每隔0.5秒执行一个任务
                for(int i=0;i<5;i++){
    
    
                    Thread.sleep(500);
                    System.out.println("t3执行任务"+i);
                }
                System.out.println("t3执行结束");
            } catch (Exception e) {
    
    
               System.out.println("t3被打断了");
            } finally{
    
    
                //lock必须手动释放锁
                lock.unlock();
            }
        });
        t3.start();
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        //这里让线程3秒后被打断,只有t3能成功打断,t1打断会报错
        //t1.interrupt();
        t3.interrupt();

    }




}

After the code runs correctly, the result is as follows:
insert image description here

2.CountDownLatch

2.1 Introducing
CountDownLatch: a synchronization helper class that allows one or more threads to wait until a set of operations being performed in other threads is completed. The most important methods of CountDownLatch are countDown() and await(). The function of the former is to decrement the counter data by one, and the await method will block until the current count reaches zero.

Application scenario: There is a task that wants to be executed, but it must wait until other tasks are executed before continuing to execute.

2.2 Implementation principle
①CountDownLatch is a synchronous counter, which allows one or more threads to wait until another group of threads is executed. Based on the AQS sharing mode,
② is implemented through a counter. The initial value of the counter is the number of threads. . Whenever a thread is executed, the value of the counter is -1. When the value of the counter is 0, it means that all threads have been executed, and then the thread waiting on the lock can resume work.

2.3 CountDownLatch and join
CountDownLatch can be manually controlled to call the countDown() method n times in n threads to make the counter decrement by one, or it can be called n times in one thread to perform the decrement operation.
The implementation principle of join() is to constantly check whether the join thread is alive, and if the join thread is alive, the current thread will wait forever. Therefore, CountDownLatch is relatively more flexible to use between the two.

2.4 CountDownLatch and CyclicBarrier
(1) CountdownLatch cannot be reused, CyclicBarrier can;
(2) CountdownLatch is the main thread waiting for multiple worker threads to end, and CyclicBarrier is multiple threads waiting for each other until all threads reach a barrier point (Barrier point) ;
(3) The responsibilities of the threads involved in CountDownLatch are different, some are counting down, and some are waiting for the countdown to end. The thread responsibilities of CyclicBarrier are the same as that of CyclicBarrier. One of the threads of CyclicBarrier permanently leaves the barrier point due to interruption, error, or timeout, and other threads will also throw exceptions (the former await method is written outside the thread, and the latter is written inside the thread)

2.5 Easy to use

package com.example.springb_web.juc;

import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest {
    
    

    public static volatile int job_count;

    public static void main(String[] args) {
    
    
        new CountDownLatchTest().test();
    }

    //假设有100个任务,有五个线程同时运行,每个线程最多可执行30次任务,并且在任务执行完成后需要第一时间
    //得到通知,以便执行后续的任务,此时可以使用countDownLatch
    public void test(){
    
    
        Thread[] threads = new Thread[5];
        job_count = 100;
        CountDownLatch latch = new CountDownLatch(job_count);
        for(int i = 0;i<threads.length;i++){
    
    
            int order = i;
            threads[i] = new Thread(()->{
    
    
                for(int j=0;j<30;j++){
    
    
                    synchronized (this) {
    
    
                        if (job_count > 0) {
    
    
                            System.out.println("线程" + order + "执行了第" + j + "个任务");
                        }
                        job_count --;
                        latch.countDown();
                    }
                }

            });
        }
        for(int i = 0;i<threads.length;i++){
    
    
            threads[i].start();
        }
        try {
    
    
            //计数器归0前会一直阻塞,后面的程序不会走
            latch.await();
            System.out.println("任务执行完毕");
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }

    }
}

3.CyclicBarrier

3.1 Introduction to
CyclicBarrier: Cyclic barriers can add logical levels to discrete tasks.
Each thread will wait for each other until all threads are completed, and then break through the barrier together. For example, during class, the teacher will not start the class until all the students have arrived.

3.2 Easy to use

package com.example.springb_web.juc;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierTest {
    
    

    public static void main(String[] args) {
    
    
        new CyclicBarrierTest().test();
    }

    //假设有2个班级,每个班级学生数量为5个,每个班级为一个单位,一个班级交卷了才会执行给出提示。
    // 如果使用countDownLatch的话,需要写两遍代码,CyclicBarrier就可以执行两次
    public void test(){
    
    
        Thread[] threads = new Thread[5];
        //第一个参数代表执行的任务数,第二个参数
        CyclicBarrier cyclicBarrier = new CyclicBarrier(threads.length,()->System.out.println("这个班级已经全部交卷"));
        //将屏障重置为初始状态。
        cyclicBarrier.reset();
        for(int i = 0;i<threads.length*2;i++){
    
    
            int order = i;
            new Thread(()->{
    
    
                //这里必须加锁
                synchronized (this) {
    
    
                    System.out.println("学生" + order + "已经交卷");
                    //查询这个障碍是否处于破碎状态。
                    boolean broken = cyclicBarrier.isBroken();
                    if (!broken) {
    
    
                        //返回目前正在等待障碍的各方的数量。
                        int done = cyclicBarrier.getNumberWaiting();
                        //返回突破障碍所需的数量
                        int wait = cyclicBarrier.getParties();
                        System.out.println( "共有"+ wait + "个学生,已交卷" + done + "份");
                    }
                }
                try {
    
    
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
    
    
                    e.printStackTrace();
                }
            }).start();

        }



    }
}


4.Semaphore

(1) Introduction: Semaphore is usually called a semaphore, which can be used to control the number of threads accessing a specific resource at the same time, and coordinate each thread to ensure a reasonable use of resources.

(2) Usage scenarios: It is mainly used in scenarios where resources have a clear limit on the number of accesses, and is often used for current limiting.
For example: in the database connection pool, there is a limit on the number of threads that can be connected at the same time. When the number of connections reaches the limit, the subsequent threads can only be queued up to wait for the previous thread to release the database connection to obtain the connection.

(3) Implementation principle:
①The core of the semaphore class is the Sync internal class, which inherits the AQS class, rewrites some methods appropriately, and other methods call the methods in this Sync, including two subclasses of the Sync class: FairSync( Fair lock) and NonFairSync (unfair lock). Unfair locks are used by default.
②The semaphore mechanism of the semaphore uses the state attribute of the AQS class, and uses the CAS spin to ensure that the operation on the state attribute is atomic. The default is 1 every time the semaphore is acquired or released, unless you specify the semaphore to be used or the number of semaphores released.

(4) Example of use
①Common API:
public void acquire(): Indicates that a thread obtains 1 license, then the number of thread licenses is reduced by one correspondingly
public void release(): Indicates that 1 license is released, then the number of thread licenses will increase accordingly
void acquire(int permits): Indicates that a thread acquires n licenses, this number Determined by the parameter permits
void release(int permits): Indicates that a thread releases n permits, the number is determined by the parameter permits
int availablePermits(): Returns the current number of semaphore thread permits
int getQueueLength(): Returns the estimated number of threads waiting to obtain the permit

② Example:

package com.example.user.utils;

import java.util.concurrent.Semaphore;

public class SemaphoreTest {
    
    

    //模拟线程池连接数量控制
    public void dataBaseConnectionControl() {
    
    

        int maxConnectionNum = 8;
        int threadNum = 30;
        Semaphore semaphore = new Semaphore(maxConnectionNum);

        for(int i=0;i<threadNum;i++) {
    
    
            new Thread(() -> {
    
    
                try {
    
    
                    semaphore.acquire();
                    System.out.println("成功获取到数据库连接,当前可用的连接数共"+semaphore.availablePermits()
                            +"个,当前等待的线程共"+semaphore.getQueueLength()+"个。");
                    Thread.sleep(1000);
                    semaphore.release();
                    System.out.println("已有一个连接使用完毕!");
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }).start();
        }
    }

    public static void main(String[] args) {
    
    
        SemaphoreTest demo = new SemaphoreTest();
        demo.dataBaseConnectionControl();
    }

}

5.Exchanger

(1) Introduction: The function of the Exchanger class is to exchange data between two threads. The exchange() method in the Exchanger class has a blocking function, that is, this method waits for other threads to obtain data after being called, and if no other thread obtains data, it keeps blocking and waiting.
It is more convenient than the wait/notify used by the producer and consumer mode.

(2) Usage scenarios: scenarios such as genetic algorithm, data proofreading, listing and trading.
Such as: genetic algorithm (high school biology), many traits have gene sequences, and the possibility of various traits of the child can be calculated according to different gene sequences (such as AA/Aa combined with aa is Aa or aa)).

(3) Implementation principle:
Two threads exchange data through the exchange method. If the first thread executes the exchange method first, it will wait for the second thread to also execute the exchange. When both threads reach the synchronization point, the two Threads can exchange data and pass the data produced by this thread to the other party.

(4) Examples of usage
①Common APIs:
String exchange(V x): for exchange, start the exchange and wait for another thread to call exchange;
String exchange(V x,long timeout,TimeUnit unit): for exchange, start the exchange and wait for another thread to call exchange, and set the maximum waiting time, when the waiting time exceeds timeout stop waiting

② Example:

package com.example.user.utils;

import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExchangerTest {
    
    

    private static void lplTrade(String data1, Exchanger exchanger) {
    
    
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "在交易截止之前把 " + data1 + " 交易出去");
            Thread.sleep((long) (Math.random() * 1000));

            String data2 = (String) exchanger.exchange(data1);
            System.out.println(Thread.currentThread().getName() + "交易得到" + data2);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
    
    

        ExecutorService executor = Executors.newCachedThreadPool();

        final Exchanger exchanger = new Exchanger();

        //注意这里的交易成员的数量是双数,如果是单数,最后会有一个线程得不到交换,一直在阻塞。
        executor.execute(new Runnable() {
    
    
            String data1 = "the shy";

            @Override
            public void run() {
    
    
                lplTrade(data1, exchanger);
            }
        });


        executor.execute(new Runnable() {
    
    
            String data2 = "369";

            @Override
            public void run() {
    
    
                lplTrade(data2, exchanger);
            }
        });

        executor.execute(new Runnable() {
    
    
            String data3 = "bin";

            @Override
            public void run() {
    
    
                lplTrade(data3, exchanger);
            }
        });

        executor.execute(new Runnable() {
    
    
            String data4 = "ale";

            @Override
            public void run() {
    
    
                lplTrade(data4, exchanger);
            }
        });

        executor.execute(new Runnable() {
    
    
            String data5 = "yskm";

            @Override
            public void run() {
    
    
                lplTrade(data5, exchanger);
            }
        });

        executor.execute(new Runnable() {
    
    
            String data5 = "zeus";

            @Override
            public void run() {
    
    
                lplTrade(data5, exchanger);
            }
        });

        executor.shutdown();
    }

}

6.phaser

(1) Introduction: Phaser is a new synchronization auxiliary class "phaser" added by JDK 7, which is used to solve the scenario problem of controlling multiple threads to complete tasks in stages. Its role is more flexible than CountDownLatch and CyclicBarrier.

(2) Usage scenario: Same as CoutDownLatch and CyclicBarrier, and it supports dynamic adjustment of tasks and supports hierarchical structure to achieve higher throughput.
For example: 5 students take the exam together, and there are three questions in total. Only when all students are present can the exam start. Only when all students finish the first question can students continue to do the second question. Only when all students finish the second question can they Do the third question, the third question that all students have finished, the exam is over.
Analysis of this topic: This is a multi-threaded (5 students) staged problem (examination exam, first question completed, second question completed, third question completed), so it is very suitable to use Phaser to solve this problem. .

(3) Implementation principle:
reference link

(4) Example of use
① API:
int register(): Add a number, return the current stage number
int arriveAndAwaitAdvance(): wait for other tasks to arrive after arrival, return the arrival stage number
protected boolean onAdvance(int Phase , int registeredParties): similar to the trigger command of CyclicBarrier, increase the stage arrival action by rewriting this method
boolean isTerMinated(): judge whether it is over

② Example:

package com.example.user.utils;

import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;

public class PhaserTest extends Phaser {
    
    

    /**
     * 题目:20个学生参加考试,一共有两道题,要求所有学生到齐才能开始考试,全部做完第一题,才能继续做第二题。
     *
     * Phaser有phase和party两个重要状态,
     * phase表示阶段,party表示每个阶段的线程个数,
     * 只有每个线程都执行了phaser.arriveAndAwaitAdvance();
     * 才会进入下一个阶段,否则阻塞等待。
     * 例如题目中20个学生(线程)都调用phaser.arriveAndAwaitAdvance();就进入下一步
     */
    public static void main(String[] args) {
    
    
        MyPhaser phaser = new MyPhaser();
        StudentTask[] studentTask = new StudentTask[20];
        //注册
        for (int i = 0; i < studentTask.length; i++) {
    
    
            studentTask[i] = new StudentTask(phaser);
            phaser.register();    //注册一次表示phaser维护的线程个数
        }

        Thread[] threads = new Thread[studentTask.length];
        for (int i = 0; i < studentTask.length; i++) {
    
    
            threads[i] = new Thread(studentTask[i], "Student "+i);
            threads[i].start();
        }

        //等待所有线程执行结束
        for (int i = 0; i < studentTask.length; i++) {
    
    
            try {
    
    
                threads[i].join();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }

        System.out.println("Phaser has finished:"+phaser.isTerminated());

    }

}
class MyPhaser extends Phaser {
    
    

    @Override
    protected boolean onAdvance(int phase, int registeredParties) {
    
        //在每个阶段执行完成后回调的方法

        switch (phase) {
    
    
            case 0:
                return studentArrived();
            case 1:
                return finishFirstExercise();
            case 2:
                return finishExam();
            default:
                return true;
        }

    }

    private boolean studentArrived(){
    
    
        System.out.println("学生准备好了,学生人数:"+getRegisteredParties());
        return false;
    }

    private boolean finishFirstExercise(){
    
    
        System.out.println("第一题所有学生做完");
        return false;
    }


    private boolean finishExam(){
    
    
        System.out.println("第二题所有学生做完,结束考试");
        return true;
    }

}


class StudentTask implements Runnable {
    
    

    private Phaser phaser;

    public StudentTask(Phaser phaser) {
    
    
        this.phaser = phaser;
    }

    @Override
    public void run() {
    
    
        System.out.println(Thread.currentThread().getName()+"到达考试");
        phaser.arriveAndAwaitAdvance();

        System.out.println(Thread.currentThread().getName()+"做第1题时间...");
        doExercise1();
        System.out.println(Thread.currentThread().getName()+"做第1题完成...");
        phaser.arriveAndAwaitAdvance();

        System.out.println(Thread.currentThread().getName()+"做第2题时间...");
        doExercise2();
        System.out.println(Thread.currentThread().getName()+"做第2题完成...");
        phaser.arriveAndAwaitAdvance();
    }

    private void doExercise1() {
    
    
        long duration = (long)(Math.random()*10);
        try {
    
    
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

    private void doExercise2() {
    
    
        long duration = (long)(Math.random()*10);
        try {
    
    
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

}

7.ReentrantReadWriteLock

(1) Introduction: Read-write locks, including two types of locks - read locks (read operations can be performed when held) and write locks (write operations can be performed when held). Among them, the read lock is a shared lock, and the write lock is an exclusive lock. Concurrent reads are allowed, only exclusive writes.

(2) Usage scenarios: concurrent reading and reading, mutual exclusion of reading and writing, mutual exclusion of writing and writing. If the scenario of concurrent reading of an object is greater than that of concurrent writing, ReentrantReadWriteLock can be used to improve concurrency efficiency while ensuring thread safety. Such as: cache update operation.
PS: In the case of high concurrency, the performance of read-write locks is better than that of exclusive locks. If you are reading at the same time, you don’t need to lock resources. Only when reading and writing work at the same time, you need to lock resources. If you use mutex directly, it will lead to waste of resources.

(3) Implementation principle:
implemented by a custom queue synchronizer, the read and write status is the synchronization status of its synchronizer, and there are two implementation methods of fair lock and unfair lock; the
synchronization status (state, an integer variable) is divided into two Part, the upper 16 bits indicate reading, and the lower 16 bits indicate writing. The respective states of reading and writing are determined through bit operations; the
write lock is an exclusive lock that supports re-entry, and the read lock is a shared lock that supports re-entry. CAS operation modification The value of the state;
use ThreadLocal to encapsulate the HoldCounter object to ensure that each thread records its own number of reentrant locks;
use lock degradation to improve efficiency.

Lock downgrade (does not support lock upgrade): It refers to the process of holding the (currently owned) write lock, obtaining the read lock, and then releasing the (previously owned) write lock.

(4) Example of use
①API:
Construction method:
public ReentrantReadWriteLock(): default construction method, unfair lock
public ReentrantReadWriteLock(boolean fair): true is a fair lock
common API:
public ReentrantReadWriteLock.ReadLock readLock(): return read lock
public ReentrantReadWriteLock. WriteLock writeLock(): return write lock
public void lock(): lock
public void unlock(): unlock
public boolean tryLock(): try to acquire the lock

②Usage example:
For redis usage tutorial, please refer to: In the link
JedisServiceImpl.java, I defined the method getDataByReadWriteLock()

public String getDataByReadWriteLock(String namespace,String key) {
    
    
        //获取读锁
        lock.readLock().lock();
        try{
    
    
            //如果缓存有效, 直接使用data
            String data = (String) cacheManager.get(namespace,key);
            if(!StringUtils.isEmpty(data)){
    
    
                return data;
            }
        }finally {
    
    
            //释放读锁
            lock.readLock().unlock();
        }

        //获取写锁
        lock.writeLock().lock();
        try{
    
    
            //如果缓存无效,更新cache;有效时间120秒
            cacheManager.save(namespace,key,"从数据库查的数据",120);
            return "从数据库查的数据";
        }finally {
    
    
            //释放写锁
            lock.writeLock().unlock();
        }
    }

8.LockSupport

(1) Introduction: A tool class for suspending and waking up threads. The park() and unpark() methods can block and wake up threads respectively (similar to the wait/notify method)

(2) Compared with wait/notify and await/signal
①wait/notify must be used in conjunction with the Synchronized keyword, otherwise an exception will be reported
②The await() and signal() methods of ReentrantLock are the same, and the lock() method must be configured for use, otherwise It will report a monitor exception, and it must be blocked before being woken up
. ③LockSupport can be used directly without other keywords or methods; even if the "wake-up" method is executed first, and then the blocking method is executed, the thread can still execute smoothly

(3) Implementation principle
LockSupport introduces the idea of ​​a license. An object can only have one license at most. When the unpark() method is executed, a license will be assigned to the specified object, and the current object is judged when the park() method is executed. Whether it has a license or not, it will be blocked
until it has a license, and if it has a license, it will be released directly. Clear the license owned by the object after release.

PS: When using LockSupport for multi-threaded operations, it usually needs to be used synchronously with the blocking queue in order to store the objects we need to wake up.

9. Use of common queues

Reference: https://blog.csdn.net/tttalk/article/details/121947982?spm=1001.2014.3001.5501

Guess you like

Origin blog.csdn.net/tttalk/article/details/127066308