多线程之六:并发工具&异步编程

目录

一、CountDownLatch

1.1 javadoc文档内容

1.2 CountDownLatch计数器的使用

1.3 使用总结

1.4 源码分析

1.4.1 有参构造

1.4.2 await方法

1.4.3 countDown方法

1.5 CountDownLatch在GitCode的介绍★★★

1.6 具体使用可参考★★★

二、CyclicBarrier

2.1 CyclicBarrier介绍

2.2 CyclicBarrier与CountDownLatch区别

2.3 循环屏障的使用

2.3.1 基础的场景

2.3.2 复杂场景

2.4 CyclicBarrier的使用场景&详细示例★★★

2.5 GitCode的介绍

2.6 CyclicBarrier源码分析

三、Semaphone

3.1 Semaphore介绍

3.2 如何使用

3.3 Semaphore在GitCode的介绍★★★

四、总结以上三个辅助类★★★

====五、以下为异步编程=======

5.1 CompletableFuture

1. 在GitCode的介绍

2. CompletableFuture出现的意义

3. CompletableFuture使用场景

4. `CompletableFuture`的`thenApply`, `thenAccept`和`thenRun`有什么区别?


一、CountDownLatch

1.1 javadoc文档内容

A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.

一种同步辅助工具,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。


A CountDownLatch is initialized with a given count. The await methods block until the current count reaches zero due to invocations of the countDown method, after which all waiting threads are released and any subsequent invocations of await return immediately. This is a one-shot phenomenon -- the count cannot be reset. If you need a version that resets the count, consider using a CyclicBarrier.

CountDownLatch 使用给定的计数进行初始化。由于调用 countDown 方法,await 方法会阻塞,直到当前计数达到零之后将释放所有等待线程,并且 await 的任何后续调用都会立即返回。这是一个一次性现象 -- 计数无法重置如果需要重置计数的版本,请考虑使用 CyclicBarrier。

A CountDownLatch is a versatile synchronization tool and can be used for a number of purposes. A CountDownLatch initialized with a count of one serves as a simple on/off latch, or gate: all threads invoking await wait at the gate until it is opened by a thread invoking countDown. A CountDownLatch initialized to N can be used to make one thread wait until N threads have completed some action, or some action has been completed N times.

CountDownLatch 是一种多功能同步工具,可用于多种用途。以计数 1 初始化的 CountDownLatch 用作简单的开/关锁存器或门:所有调用等待的线程都在门上等待,直到调用 countDown 的线程打开它。初始化为 N 的 CountDownLatch 可用于使一个线程等待,直到 N 个线程完成某个操作,或者某个操作已完成 N 次。

A useful property of a CountDownLatch is that it doesn't require that threads calling countDown wait for the count to reach zero before proceeding, it simply prevents any thread from proceeding past an await until all threads could pass.

CountDownLatch 的一个有用属性是,它不需要调用 countDown 的线程在继续之前等待计数达到零,它只是阻止任何线程继续通过 await,直到所有线程都可以通过。

1.2 CountDownLatch计数器的使用

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 模拟发送短信。
 * 创建线程数为3的线程池对象,去执行发送20条短信的任务(这期间主线程处于阻塞状态),
 * 每发送一条则计数器count减1,直至count为0时,唤醒主线程,主线程被唤醒后执行其他的操作
 */
public class TestCountDownLatch {
    static volatile int TOTAL_MSG = 20;//短信条数
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch count = new CountDownLatch(20);
        ReentrantLock lock = new ReentrantLock();
        //创建线程池对象(线程数为3)
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        //让线程池对象创建线程去执行“发送短信”的任务
        for (int i = 0; i < 20; i++) {

            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        TimeUnit.MILLISECONDS.sleep(500);
                        lock.lock();
                        try {
                            System.out.println(Thread.currentThread().getName() + "发送了第" + TOTAL_MSG + "条短信,未发送条数变为:" + --TOTAL_MSG);
                            count.countDown();
                        } catch (Exception e) {
                            e.printStackTrace();
                        } finally {
                            lock.unlock();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        //关闭线程池资源
        threadPool.shutdown();

        //发送完20条短信后告诉(即唤醒)main线程,执行其他任务
        count.await();
        System.out.println("短信发送完毕!大家可以回家了~");
    }
}
/**
 * 模拟一件事情做20次后,主线程才能干别的活
 */
public class TestCountDownLatch {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch count = new CountDownLatch(20);

        new Thread(new Runnable() {
            @Override
            public void run() {
                int TOTAL_NUM = 0;
                while (true) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                        System.out.println("已完成次数为:" + ++TOTAL_NUM);
                        count.countDown();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (TOTAL_NUM == 20) {
                        break;
                    }
                }
            }
        }, "t1线程").start();

        //t1线程执行任务20次后,计数器减为0,唤醒主线程
        count.await();
        System.out.println("good job...");
    }
}

1.3 使用总结

CountDownLatch,计数器,常用来阻塞主线程、等待被子线程唤醒,或者在子线程中阻塞等待被主线程唤醒。

 参考:CountDownLatch详解-CSDN博客

1.4 源码分析

CountDownLatch的内部类Sync继承了AQS,CountDownLatch就是基于AQS实现的计数器。

AQS就是一个state属性,以及AQS双向链表

猜测计数器的数值实现就是基于state去玩的。

主线程阻塞的方式,也是阻塞在了AQS双向链表中。

1.4.1 有参构造

就是构建内部类Sync,并且给AQS中的state赋值

// CountDownLatch的有参构造
public CountDownLatch(int count) {
    // 健壮性校验
    if (count < 0) throw new IllegalArgumentException("count < 0");
    // 构建内部类,Sync传入count
    this.sync = new Sync(count);
}

// AQS子类,Sync的有参构造
Sync(int count) {
    // 就是给AQS中的state赋值
    setState(count);
}

1.4.2 await方法

await方法就时判断当前CountDownLatch中的state是否为0,如果为0,直接正常执行后续任务

如果不为0,以共享锁的方式,插入到AQS的双向链表,并且挂起线程。

1.4.3 countDown方法

countDown方法本质就是对state - 1,如果state - 1后变为0,需要去AQS的链表中唤醒挂起的节点

1.5 CountDownLatch在GitCode的介绍★★★

CountDownLatch是一个同步工具类,用于协调多线程之间的协作。它基于一个计数器,当计数器归零时,等待的所有线程将被释放。

基本使用和功能:

1)初始化: 创建CountDownLatch时传入一个整数值,这个值代表计数器的初始值。

CountDownLatch latch = new CountDownLatch(initialCount);

2)倒计数: 在需要减少计数的地方调用countDown()方法,每次调用会使计数减一。

latch.countDown();

3)阻塞等待: 使用await()方法可以让线程等待直到计数器变为0。

try {
    latch.await(); // 等待计数器归零
} catch (InterruptedException e) {
    // handle interruption
}

4)一次性使用: 一旦计数器归零,await()方法将立即返回,且计数器无法再次设置,因此CountDownLatch通常是一次性的。

5)检查状态: 通过getCount()方法可以获取当前的计数器值。

1.6 具体使用可参考★★★

二、CyclicBarrier

2.1 CyclicBarrier介绍

CyclicBarrier通常称为循环屏障,也被称为栅栏。CyclicBarrier功能:让多个线程互相等待,直到到达同一个同步点,再一起执行。

Barrier屏障:让一个或多个线程达到一个屏障点,会被阻塞。屏障点会有一个数值,当达到一个线程阻塞在屏障点时,就会对屏障点的数值进行-1操作,当屏障点数值减为0时,屏障就会打开,唤醒所有阻塞在屏障点的线程。在释放屏障点之后,可以先执行一个任务,再让所有阻塞被唤醒的线程继续执行后续任务。

Cyclic循环:所有线程被释放后,屏障点的数值可以再次被重置。

CyclicBarrier是一种同步机制,允许一组线程互相等待。线程达到屏障点其实是基于await方法在屏障点阻塞。

CyclicBarrier是基于ReentrantLock锁的机制去实现了对屏障点减减(--),以及线程挂起的操作;而CountDownLatch本身是基于AQS,对state进行release操作后,可以减1。

CyclicBarrier每来一个线程执行await,都会对屏障数值进行-1操作,每次-1后,立即查看数值是否为0,如果为0,直接唤醒所有的互相等待的线程。

2.2 CyclicBarrier与CountDownLatch区别

  • 底层实现不同。CyclicBarrier基于ReentrantLock做的。CountDownLatch直接基于AQS做的。

  • 应用场景不同。CountDownLatch的计数器只能使用一次。而CyclicBarrier在计数器达到0之后,可以重置计数器。CyclicBarrier可以实现相比CountDownLatch更复杂的业务,执行业务时出现了错误,可以重置CyclicBarrier计数器,再次执行一次。

  • CyclicBarrier还提供了很多其他的功能:

    • 可以获取到阻塞的线程有多少;

    • 在线程互相等待时,如果有等待的线程中断,可以抛出异常,避免无限等待的问题。

  • CountDownLatch一般是让主线程等待,让子线程对计数器--。CyclicBarrier更多的让子线程也一起计数和等待,等待的线程达到数值后,再统一唤醒。

2.3 循环屏障的使用

2.3.1 基础的场景

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

/**
 * CyclicBarrier基础功能
 */
public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(2);

        //创建t1线程,执行任务
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    barrier.await();
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("线程名字:" + Thread.currentThread().getName());
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }, "t1线程").start();

        //main线程执行任务
        try {
            barrier.await();
            TimeUnit.SECONDS.sleep(1);
            System.out.println("线程名字:" + Thread.currentThread().getName());
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}
  • 因为主线程和子线程的调度是由CPU决定的,两个线程都有可能先执行,所以既有可能先打印main线程名字,也有可能先打印t1线程名字。
  • 如果把new CyclicBarrier(2)修改成new CyclicBarrier(3),则主线程和子线程会永远等待, 因为没有第三个线程执行await方法,即没有第三个线程到达屏障,所以之前到达屏障的两个线程都不会继续执行。

2.3.2 复杂场景

CyclicBarrier还可以设置回调函数,它是一个 Runnable 实例,用于在线程到达屏障时,优先执行 Runnable 实例,可以处理更复杂的业务场景。

2.4 CyclicBarrier的使用场景&详细示例★★★

下面这个链接是2024.5.9找到的,CyclicBarrier的使用场景及示例特别详尽,非常好!

【高并发】JUC中的循环栅栏CyclicBarrier的6种使用场景-CSDN博客

2.5 GitCode的介绍

CyclicBarrier是Java并发编程中的一个同步辅助类,它允许一组线程等待彼此到达某个屏障点之后再一起继续执行。这个名字来源于田径比赛中的障碍赛跑,所有的参赛者需要达到一个点才能继续下一阶段的比赛。

CyclicBarrier的基本使用和功能:

1)初始化: 创建CyclicBarrier时需要提供一个整数参数parties,表示需要一起等待的线程数量。

CyclicBarrier barrier = new CyclicBarrier(parties);

2)阻塞等待: 当线程调用await()方法时,如果所有parties个线程都调用了await(),那么屏障会释放,所有线程可以继续执行。如果在等待过程中,有线程被中断或者超时,那么屏障会被破坏,所有等待的线程都会接收到异常并恢复运行。

try {
    barrier.await();
} catch (InterruptedException e) {
    // handle interruption
} catch (BrokenBarrierException e) {
    // handle broken barrier
}

3)可重用性CyclicBarrier名称中的"Cyclic"意味着一旦所有线程通过了屏障,它可以被重置并重复使用。

4)带回调函数: 你可以指定一个Runnable动作,在所有线程都到达屏障点后执行,这是一个可选的功能。

CyclicBarrier barrier = new CyclicBarrier(parties, () -> {
    System.out.println("All threads have arrived at the barrier.");
});

5)查询状态: 可以使用isBroken()检查屏障是否已破损,使用getParties()获取初始的线程数,使用getCount()获取还需要多少个线程到达才能触发屏障。

2.6 CyclicBarrier源码分析

核心属性如下:(有参构造、await方法略)

public class CyclicBarrier {
   // 这个静态内部类是用来标记是否中断的
    private static class Generation {
        boolean broken = false;
    }

    /** CyclicBarrier是基于ReentrantLock实现的互斥操作,以及计数原子性操作 */
    private final ReentrantLock lock = new ReentrantLock();
    /** 基于当前的Condition实现线程的挂起和唤醒 */
    private final Condition trip = lock.newCondition();
    /** 记录有参构造传入的屏障数值,不会对这个数值做操作 */
    private final int parties;
    /** 当屏障数值达到0之后,优先执行当前任务  */
    private final Runnable barrierCommand;
    /** 初始化默认的Generation,用来标记线程中断情况 */
    private Generation generation = new Generation();
    /** 每来一个线程等待,就对count进行-- */
    private int count;
}

参考:Java并发工具CyclicBarrier使用详解_cyclicbarrier的getnumberwaiting和getpartaes-CSDN博客

【高并发】JUC中的循环栅栏CyclicBarrier的6种使用场景-CSDN博客

三、Semaphone

3.1 Semaphore介绍

Semaphore(信号量)是Java中一个并发控制工具,用于控制对共享资源的访问。它基于计数器的原理,可以限制同时访问某个资源的线程数量。

sync,ReentrantLock是互斥锁,保证一个资源同一时间只允许被一个线程访问;而Semaphore(信号量)保证1个或多个资源可以被指定数量的线程同时访问,底层实现是基于AQS去做的。

Semaphore底层也是基于AQS的state属性做一个计数器的维护。state的值就代表当前共享资源的个数。如果一个线程需要获取的1或多个资源,直接查看state的标识的资源个数是否足够,如果足够的,直接对state - 1拿到当前资源。如果资源不够,当前线程就需要挂起等待。直到持有资源的线程释放资源后,会归还给Semaphore中的state属性,挂起的线程就可以被唤醒。

Semaphore也分为公平和非公平的概念。

使用场景:连接池对象就可以基础信号量去实现管理。在一些流量控制上,也可以采用信号量去实现。再比如去迪士尼或者是环球影城,每天接受的人流量是固定的,指定一个具体的人流量,可能接受10000人,每有一个人购票后,就对信号量进行--操作,如果信号量已经达到了0,或者是资源不足,此时就不能买票。

3.2 如何使用

首先创建Semaphore对象后,在需要访问共享资源的代码段前后,使用acquire()和release()方法来获取和释放信号量:(其中,n是允许同时访问共享资源的线程数量)

Semaphore semaphore = new Semaphore(n);


try {
    semaphore.acquire(); // 获取信号量,如果没有可用的许可证,线程将被阻塞
    // 访问共享资源的代码
} catch (InterruptedException e) {
    // 处理中断异常
} finally {
    semaphore.release(); // 释放信号量,增加一个许可证
}

acquire()方法尝试获取一个许可证,如果当前没有可用的许可证,则该线程将被阻塞,直到有可用的许可证为止。release()方法释放一个许可证,使其可供其他线程使用。

通过适当地使用acquire()和release()方法,在超过信号量允许的线程数量时,可以限制并发访问共享资源的线程数量,实现线程间的同步和互斥。

需要注意的是,Semaphore还提供了一些其他方法,如availablePermits()用于获取当前可用的许可证数量,以及tryAcquire()方法在不阻塞线程的情况下尝试获取许可证等。

具体例子:

public static void main(String[] args) {
    // 1. 创建 semaphore 对象
    Semaphore semaphore = new Semaphore(3);
    // 2. 10个线程同时运行
    for (int i = 0; i < 10; i++) {
        int t = i;
        new Thread(() -> {
            // 3. 获取许可
            try {
                semaphore.acquire();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                System.out.println(t + "线程start");
                TimeUnit.SECONDS.sleep(1);
                System.out.println(t + "线程end...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 4. 释放许可
                semaphore.release();
            }
        }).start();
    }
}

参考:

3.3 Semaphore在GitCode的介绍★★★

Semaphore(信号量)是Java并发包(java.util.concurrent)中的另一个同步工具类,它用于控制同时访问特定资源的线程数量。通过协调对共享资源的访问,它可以防止过多的线程同时访问而导致资源耗尽或者冲突。

基本使用与功能:

1)初始化: 创建一个Semaphore实例,可以指定许可的数量。如果指定了非零初始值,则表示已经有相应数量的许可证可用。

Semaphore semaphore = new Semaphore(initialPermits); // initialPermits >= 0

2)获取许可: 线程在开始执行前需要获取一个许可证,acquire()方法用于获取一个许可证。如果没有可用的许可证,线程会被阻塞直到有其他线程释放一个许可证。

try {
    semaphore.acquire(); // Blocks if no permit available
} catch (InterruptedException e) {
    // Handle interrupted exception
}

3)释放许可: 当线程完成其任务或者不再需要访问共享资源时,应释放许可证,以便其他线程可以使用。这通过调用release()方法实现。

semaphore.release(); // Returns a permit to the semaphore

4)公平性选择Semaphore有一个可选的参数fair,决定是否采用公平锁策略。默认情况下是非公平的,这意味着线程获取许可证的顺序并不保证。

Semaphore fairSemaphore = new Semaphore(initialPermits, true); // Fair lock policy

四、总结以上三个辅助类★★★

(1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

  • CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
  • CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
  • 另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

(2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

====五、以下为异步编程=======

可参考:【java】CompletableFuture原理与实践-外卖商家端API的异步化_future java rpc-CSDN博客

JUC并发编程学习笔记一-CSDN博客

5.1 CompletableFuture

1. 在GitCode的介绍

CompletableFuture是Java 8引入的一个异步编程模型,它允许你在未来的某个时间处理结果,或者在另一个计算完成后进行操作。下面是一些基本的使用步骤和相关的示例:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableFutureDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建一个未完成的Future
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(2000); // 模拟耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Hello, Future!";
        });

        // 在Future完成后执行操作
        future.thenAccept(result -> System.out.println("Result: " + result));

        // 等待结果完成
        future.get(); // 此行会阻塞,直到结果可用

        // 或者可以不阻塞主线程,做其他事情
        // CompletableFuture<Void> ignoreResult = future.thenRun(() -> System.out.println("Continuing with other tasks..."));
    }

}

这将创建一个异步任务,模拟了2秒的延迟,然后打印结果。thenAccept用于提供一个消费者,在未来的结果可用时消费这个结果,而不会阻塞当前线程。

2. CompletableFuture出现的意义

CompletableFuture是Java 8引入的一个重要特性,它的意义在于提供了更加灵活和强大的异步编程模型,主要体现在以下几个方面:

  1. 简化异步编程:它提供了一种基于回调的方式来处理异步结果,使得代码更清晰,避免了深度嵌套的回调地狱(Callback Hell)。

  2. 链式操作:可以方便地通过thenApplythenAccept, 和 thenRun等方法建立任务间的依赖关系,形成一个异步操作链。

  3. 组合异步操作:通过thenCompose可以将多个异步任务串联起来,当一个任务完成时,下一个任务才会开始。

  4. 异常处理:内置了对异常的处理机制,如exceptionally方法可以在遇到异常时提供默认值或者进行其他操作。

  5. 同步和异步混合:允许开发者在一个程序中同时使用同步和异步代码,提高了灵活性。

  6. 提高性能:由于其非阻塞性质,CompletableFuture能更好地利用多核处理器资源,提升系统整体性能。

3. CompletableFuture使用场景

CompletableFuture是Java 8引入的一个重要特性,它的意义在于提供了更加灵活和强大的异步编程模型,主要体现在以下几个方面:

  1. 简化异步编程:它提供了一种基于回调的方式来处理异步结果,使得代码更清晰,避免了深度嵌套的回调地狱(Callback Hell)。

  2. 链式操作:可以方便地通过thenApplythenAccept, 和 thenRun等方法建立任务间的依赖关系,形成一个异步操作链。

  3. 组合异步操作:通过thenCompose可以将多个异步任务串联起来,当一个任务完成时,下一个任务才会开始。

  4. 异常处理:内置了对异常的处理机制,如exceptionally方法可以在遇到异常时提供默认值或者进行其他操作。

  5. 同步和异步混合:允许开发者在一个程序中同时使用同步和异步代码,提高了灵活性。

  6. 提高性能:由于其非阻塞性质,CompletableFuture能更好地利用多核处理器资源,提升系统整体性能。

4. `CompletableFuture`的`thenApply`, `thenAccept`和`thenRun`有什么区别?

CompletableFuture中的thenApplythenAccept, 和 thenRun都是用于链式处理异步结果的方法,它们的区别在于如何处理结果以及是否需要返回一个新的CompletableFuture

thenApply

  • 作用:接受一个函数作为参数,当CompletableFuture完成时,该函数会被应用到结果上,生成一个新的结果。
  • 是否返回新CompletableFuture:是,返回一个新的CompletableFuture,其结果为函数的返回值。
  • 示例代码:
    CompletableFuture<Integer> future = CompletableFuture.completedFuture("123");
    future.thenApply(Integer::parseInt);
    

thenAccept

  • 作用:接受一个Consumer(消费者)作为参数,当CompletableFuture完成时,消费结果但不返回新的结果。
  • 是否返回新CompletableFuture:否,返回的是void,没有新的CompletableFuture
  • 示例代码:
    CompletableFuture<String> future = CompletableFuture.completedFuture("Hello");
    future.thenAccept(System.out::println);
    

thenRun

  • 作用:接受一个Runnable作为参数,当CompletableFuture完成时,仅运行这个Runnable,它不能访问原始结果。
  • 是否返回新CompletableFuture:否,返回的是void,没有新的CompletableFuture
  • 示例代码:
    CompletableFuture<String> future = CompletableFuture.completedFuture("Ignored Result");
    future.thenRun(() -> System.out.println("Task is done, but no result used."));

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值