5 线程池
5.1 线程池概述
- 什么是线程池?
Java 线程池是一种管理和复用线程的机制,它能够提供一种高效的方式来处理并发任务。使用线程池可以避免频繁地创建和销毁线程,减少了线程创建和上下文切换的开销,提高了系统的性能和资源利用率。
线程池的优势包括:线程复用、线程管理、减少线程创建和销毁的开销、控制并发线程数量、提供任务队列来保存等待执行的任务等。
- 为什么要使用线程池? 使用线程池可以有效地管理和调度大量的任务,提高系统的性能和资源利用率,同时也可以控制并发线程的数量,避免资源耗尽的问题。
5.2 JDK自带线程池构建方式
JDK中基于Executors提供了很多种线程池
5.2.1 newFixedThreadPool
newFixedThreadPool 是 Executors 类中提供的一个静态方法,用于创建一个固定大小的线程池。该线程池会维护指定数量的线程,并且线程池中的线程数是固定的(线程懒加载),不会随着任务的提交和完成而动态调整。
线程是懒加载的,在线程池创建之初线程并没有被构建出来,而是随着任务的提交才会将线程在线程池中构建出来。如果线程没有构建,线程会等待任务执行而被创建和执行。如果线程已经构建好了,此时新增的任务会被放到LinkedBlockingQueue阻塞队列中去,等待线程从LinkedBlockingQueue中take出任务。
以下是一个示例,展示如何使用 newFixedThreadPool 创建一个固定大小的线程池并提交任务:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
// 创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务给线程池执行
for (int i = 0; i < 100; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
});
}
// 关闭线程池
executor.shutdown();
}
}
在上述示例中,我们使用 Executors.newFixedThreadPool(5) 创建了一个固定大小为 5 的线程池。然后,我们通过循环提交了 10 个任务给线程池执行。每个任务输出了自己的任务编号和执行线程的名称。最后,我们调用 executor.shutdown() 方法来关闭线程池,表示不再接受新的任务,等待已提交的任务完成执行。
注意:当任务提交给线程池时,线程池会自动分配任务给可用的线程来执行。如果所有线程都正在执行任务,新提交的任务将在队列中等待,直到有可用的线程执行任务。
- 构造方法
//其中的阻塞队列是为了放置可执行的任务
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
5.2.2 newSingleThreadExecutor
newSingleThreadExecutor 是 Executors 类中提供的一个静态方法,用于创建一个只有单个线程的线程池。该线程池中只有一个工作线程,可以顺序地执行任务,并保证任务按照提交的顺序执行。
方法签名如下:
public static ExecutorService newSingleThreadExecutor()
以下是一个示例,展示如何使用 newSingleThreadExecutor 创建一个单线程的线程池并提交任务:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadExecutorExample {
public static void main(String[] args) {
// 创建单线程的线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// 提交任务给线程池执行
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
});
}
// 关闭线程池
executor.shutdown();
}
}
在上述示例中,我们使用 Executors.newSingleThreadExecutor() 创建了一个只有单个线程的线程池。然后,我们通过循环提交了 10 个任务给线程池执行。每个任务输出了自己的任务编号和执行线程的名称。最后,我们调用 executor.shutdown() 方法来关闭线程池,表示不再接受新的任务,等待已提交的任务完成执行。
由于线程池只有单个线程,所以任务会按照提交的顺序一个接一个地执行。即使有多个任务被提交,也只会有一个任务在执行,其他任务会在队列中等待。这种线程池适用于需要保证任务按序执行的场景,例如顺序处理日志记录、事件处理等。
5.2.3 newCachedThreadPool
newCachedThreadPool 是 Executors 类中提供的一个静态方法,用于创建一个具有自动线程回收机制的线程池。该线程池可以根据需要动态地创建和回收线程,并且线程池中的线程数是根据任务的数量和需求进行动态调整的。最大的一个特点就是任务只要提交到当前的newCachedThreadPool中,就必然有工作线程可以处理。
方法签名如下:
public static ExecutorService newCachedThreadPool()
以下是一个示例,展示如何使用 newCachedThreadPool 创建一个具有自动线程回收机制的线程池并提交任务:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolExample {
public static void main(String[] args) {
// 创建具有自动线程回收机制的线程池
ExecutorService executor = Executors.newCachedThreadPool();
// 提交任务给线程池执行
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
});
}
// 关闭线程池
executor.shutdown();
}
}
在上述示例中,我们使用 Executors.newCachedThreadPool() 创建了一个具有自动线程回收机制的线程池。
然后,我们通过循环提交了 10 个任务给线程池执行。每个任务输出了自己的任务编号和执行线程的名称。
由于线程池具有自动线程回收机制,它会根据任务的数量和需求动态创建和回收线程。如果有新的任务需要执行,线程池会创建新的线程;如果没有任务需要执行并且线程闲置一段时间,线程池会回收线程。
最后,我们调用 executor.shutdown() 方法来关闭线程池,表示不再接受新的任务,等待已提交的任务完成执行。
newCachedThreadPool 适用于需要执行大量短期任务的场景,其中任务的数量和执行时间不确定。它可以根据任务的数量和需求动态调整线程池的大小,以适应不同的工作负载。由于线程数是根据需求动态调整的,因此在任务数量较少时,它可以节省系统资源;在任务数量增加时,它可以提供更多的线程来处理任务,提高并发性能。
- 构造方法
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
5.2.4 newScheduleThreadPool
newScheduledThreadPool 是 Executors 类中提供的一个静态方法,用于创建一个具有定时任务执行功能的线程池。该线程池可以按照指定的时间间隔执行任务,支持延迟执行和周期性执行。
方法签名如下:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
参数 corePoolSize 表示线程池的核心线程数,即同时执行任务的最大线程数。当任务数量超过核心线程数时,线程池会创建额外的线程来处理任务。
以下是一个示例,展示如何使用 newScheduledThreadPool 创建一个具有定时任务执行功能的线程池:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPoolExample {
public static void main(String[] args) {
// 创建具有定时任务执行功能的线程池
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
// 延迟执行任务
executor.schedule(() -> {
System.out.println("Task 1 executed after 2 seconds");
}, 2, TimeUnit.SECONDS);
// 延迟执行周期性任务
executor.scheduleAtFixedRate(() -> {
System.out.println("Task 2 executed every 3 seconds");
}, 1, 3, TimeUnit.SECONDS);
// 关闭线程池
executor.shutdown();
}
}
在上述示例中,我们使用 Executors.newScheduledThreadPool(2) 创建了一个具有定时任务执行功能的线程池。指定的核心线程数为 2。然后,我们使用 schedule() 方法在延迟 2 秒后执行一个任务,并使用 scheduleAtFixedRate() 方法延迟 1 秒后开始执行一个周期性任务,每隔 3 秒执行一次。最后,我们调用 executor.shutdown() 方法来关闭线程池,表示不再接受新的任务,等待已提交的任务完成执行。
newScheduledThreadPool 适用于需要在指定的时间间隔执行任务的场景,例如定时任务调度、周期性数据更新等。它提供了灵活的任务调度机制,可以按照延迟时间或固定的时间间隔执行任务。通过合理地设置核心线程数和时间间隔,可以实现各种定时任务的需求。
5.2.5 newWorkStealingPool
newWorkStealingPool 是 Executors 类中提供的一个静态方法,用于创建一个工作窃取线程池。工作窃取线程池是 Java 7 引入的一种线程池实现,它使用了工作窃取算法来提高并行任务的执行效率。
在ThreadPoolExecutor中只有一个阻塞队列存放当前线程池的任务:
ForkJoinPool的特点是当有一个特别大的任务的时候,如果是ThreadPoolExecutor则只会有一个线程去执行。
- ForkJoinPool的第一个特点是可以将大任务拆分成数个小任务,放到当前线程的阻塞队列中,其它的空闲线程也可以从阻塞队列中获取任务并执行,这样就不会有线程被闲置,CPU利用率高。
方法签名如下:
public static ExecutorService newWorkStealingPool()
以下是一个示例,展示如何使用 newWorkStealingPool 创建一个工作窃取线程池并提交任务:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class WorkStealingThreadPoolExample {
public static void main(String[] args) {
// 创建工作窃取线程池
ExecutorService executor = Executors.newWorkStealingPool();
// 提交任务给线程池执行
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
});
}
// 关闭线程池
executor.shutdown();
}
}
在上述示例中,我们使用 Executors.newWorkStealingPool() 创建了一个工作窃取线程池。然后,我们通过循环提交了 10 个任务给线程池执行。每个任务输出了自己的任务编号和执行线程的名称。工作窃取线程池的特点是可以自动根据任务的执行情况来调整线程的负载。当某个线程执行完自己的任务后,它会尝试从其他线程的任务队列中窃取任务来执行,以提高线程的利用率。
需要注意的是,工作窃取线程池适用于执行计算密集型的任务,例如遍历、搜索、排序等。它能够自动将任务分配给可用的线程,并充分利用多核处理器的并行性能。在一些并行计算的场景中,工作窃取线程池可以提供更好的性能和吞吐量。
5.3 ThreadPoolExecutor
5.3.1 为什么要自定义线程池?
当线程处于阻塞状态时,它会暂时停止执行并释放 CPU 资源,因此不会消耗 CPU 资源。阻塞状态意味着线程无法继续执行,通常是因为等待某个条件的满足或等待某个操作的完成。
在阻塞状态下,线程将进入等待状态,并让出 CPU 的执行权,让其他可执行的线程有机会运行。这种机制称为线程调度。一旦线程的阻塞条件得到满足,或者被其他线程唤醒,线程将从阻塞状态转换为就绪状态,然后等待调度程序将其重新分配给可用的 CPU 资源。
在阻塞状态下,线程并不会占用 CPU 时间片,因此不会消耗 CPU 资源。相反,它允许其他线程在 CPU 上执行任务,提高了系统的并发性和资源利用率。
需要注意的是,尽管阻塞线程不会直接消耗 CPU 资源,但在一些特定情况下,阻塞操作可能会对系统的整体性能产生影响。例如,如果大量线程被阻塞并频繁地进行上下文切换,可能会导致额外的开销。因此,在设计和实现多线程应用程序时,需要谨慎考虑线程的阻塞和同步机制,以避免性能瓶颈和资源竞争的问题。
- ThreadPoolExecutor提供的七个核心参数
public ThreadPoolExecutor(int corePoolSize, //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //线程存活时间
TimeUnit unit, //时间单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //构建线程的线程工厂
RejectedExecutionHandler handler); //拒绝策略,当线程池无法处理投递过来的任务时,执行拒绝策略
5.3.2 ThreadPoolExecutor应用
ThreadPoolExecutor 是 Java 提供的一个强大而灵活的线程池实现,它是 ExecutorService 接口的一个具体实现类。可以使用 ThreadPoolExecutor 来创建自定义的线程池,以满足特定的需求。
当使用ThreadPoolExecutor来创建自定义线程池时,您可以指定拒绝策略来处理无法提交给线程池执行的任务。以下是一个示例,演示了如何使用ThreadPoolExecutor自定义线程池和拒绝策略:
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor.AbortPolicy;
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 创建一个自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
500, // 线程空闲时间
TimeUnit.MILLISECONDS, // 空闲时间单位
new LinkedBlockingQueue<>(10), // 任务队列
new CustomRejectedExecutionHandler()); // 拒绝策略
// 提交20个任务给线程池
for (int i = 0; i < 20; i++) {
Runnable worker = new WorkerThread("Task " + (i + 1));
executor.execute(worker);
}
// 关闭线程池
executor.shutdown();
try {
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("所有任务已完成");
}
}
class WorkerThread implements Runnable {
private String taskName;
public WorkerThread(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 开始执行任务: " + taskName);
// 执行任务的代码
try {
// 模拟任务执行时间
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 完成任务: " + taskName);
}
}
class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("任务被拒绝: " + ((WorkerThread) r).taskName);
// 自定义拒绝策略的处理逻辑
}
}
在上述示例中,我们创建了一个自定义线程池ThreadPoolExecutor,并设置了以下参数:
- 核心线程数为5
- 最大线程数为10
- 线程空闲时间为500毫秒
- 任务队列使用
LinkedBlockingQueue,容量为10 - 拒绝策略使用
CustomRejectedExecutionHandler类
然后,我们提交了20个任务给线程池,每个任务由WorkerThread类表示,实现了Runnable接口的run方法。每个任务在执行时会打印出线程名称和任务名称,并在模拟的任务执行时间后完成。
如果线程池无法接受新的任务,由于任务队列已满或达到最大线程数,就会触发拒绝策略。在本例中,我们自定义了一个CustomRejectedExecutionHandler类,实现了RejectedExecutionHandler接口的rejectedExecution方法。在该方法中,我们打印出被拒绝的任务的名称,并可以根据需要添加自定义的处理逻辑。
最后,我们调用executor.shutdown()方法关闭线程池,并使用executor.awaitTermination()方法等待所有任务完成。
通过使用ThreadPoolExecutor和自定义拒绝策略,您可以更灵活地处理无法提交给线程池执行的任务。根据具体需求,您可以选择适合的拒绝策略或编写自定义的拒绝策略来处理这些情况。
- Java中线程的4种拒绝策略:
Java的线程池提供了四种内置的拒绝策略,用于处理无法提交给线程池执行的任务。这些拒绝策略在RejectedExecutionHandler接口中定义。以下是这四种拒绝策略:
-
AbortPolicy(默认): 当线程池无法接受新任务时,会抛出RejectedExecutionException异常,阻止任务提交。这是线程池默认的拒绝策略。 -
CallerRunsPolicy: 当线程池无法接受新任务时,会使用提交任务的线程来执行被拒绝的任务。这意味着提交任务的线程将被阻塞,直到线程池有空闲线程可用。 -
DiscardPolicy: 当线程池无法接受新任务时,会默默地丢弃被拒绝的任务,不会抛出任何异常。任务提交者将不会得到任何通知,也无法知道任务是否被执行。 -
DiscardOldestPolicy: 当线程池无法接受新任务时,会丢弃队列中最早提交的任务,然后尝试重新提交被拒绝的任务。 -
自定义拒绝策略:根据业务可以自定义拒绝策略,例如可以将被拒绝的任务丢到数据库。
5.3.3 ThreadPoolExecutor源码剖析
5.3.3.1 ThreadPoolExecutor核心参数
核心属性就是ctl,基于ctl拿到线程池的状态以及工作线程的个数
//表示着线程池中2个核心状态:
//线程池状态:ctl高三位,表示线程池状态。
//工作线程的数量:ctl的低29位,表示工作线程的个数。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//Interger.SIZE:获取Integer的bit位数
private static final int COUNT_BITS = Integer.SIZE - 3;
//00000000 00000000 00000000 00000001 | 1
//00100000 00000000 00000000 00000000 | 1 << COUNT_BITS
//00011111 11111111 11111111 11111111 | (1 << COUNT_BITS) - 1
//当前工作线程能记录的最大值
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
//线程池状态标识
//当前5个状态中,只有RUNNING状态代表线程池没问题,可以正常接收处理任务
//111:代表RUNNING状态,RUNNING可以处理任务,并且处理阻塞队列中的任务
private static final int RUNNING = -1 << COUNT_BITS;
//000:代表shutdown状态,不会接收新的任务,正在处理的任务正常进行,阻塞队列的任务也会做完
private static final int SHUTDOWN = 0 << COUNT_BITS;
//001:代表stop状态,不会接收新任务,正在处理的任务线程会被终端,阻塞队列的任务一个不管
private static final int STOP = 1 << COUNT_BITS;
//010:代表tidying状态,这个状态是从shutdown或者stop状态转换过来的,代表当前线程池马上关闭,就是过度状态。
private static final int TIDYING = 2 << COUNT_BITS;
//011:代表terminated状态,这个状态是从tidying状态转换过来的,转换过来需要一个terminate方法。
private static final int TERMINATED = 3 << COUNT_BITS;
//在使用下面几个方法的时候需要传递ctl
//取得高三位的值
private static int runStateOf(int c) { return c & ~CAPACITY; }
//取低29位的值
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
5.3.3.2 ThreadPoolExecutor的有参构造
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//健壮性校验
//核心线程个数是允许为0的
//最大线程数目必须大于0,最大线程数必须大于等于核心线程数
//非核心线程最大空闲时间可以为0
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException(); //不满足则抛出参数异常
//阻塞队列,线程工厂,拒绝策略都不允许为null,为null就报空指针异常
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
//系统资源访问决策,和线程池核心业务关系不大
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
5.3.3.3 ThreadPoolExecutor的execute方法
execute方法是将任务提交到线程池的核心方法,下面是execute的完整执行流程。
public void execute(Runnable command) {
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) { //工作线程数少于核心线程数
//创建核心线程
//addWorker返回true,代表添加工作线程成功
//addWorker返回false,代表添加工作线程失败
//addWorker会基于线程池状态,以及工作线程个数做判断,查看能否添加工作线程
if (addWorker(command, true))
//工作线程构建完成,任务也已经交给command去处理了
return;
//说明线程池状态或者工作线程个数发生了变化,导致添加失败,需要重新获取一次ctl
c = ctl.get();
}
//添加核心线程失败,判断线程池状态是否为RUNNING
if (isRunning(c) && workQueue.offer(command)) { //线程池状态正常,则向阻塞队列中添加任务
//任务添加到阻塞队列成功
//任务丢到线程池,线程状态改变了
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command)) //如果线程池状态出现异常,则移除任务
reject(command); //拒绝任务
else if (workerCountOf(recheck) == 0) //这里说明阻塞队列有我刚刚放入的任务,查看工作线程是否为0个
addWorker(null, false); //添加一个非核心工作线程
}else if (!addWorker(command, false)) //任务添加到阻塞队列失败
reject(command); //添加失败,执行拒绝策略
}
5.3.3.4 ThreadPoolExecutor的addWorker方法
addWorker中主要分成两大块去看
-
第一块:校验线程池状态以及校验工作线程个数
-
第二块:添加工作线程并启动工作线程
private boolean addWorker(Runnable firstTask, boolean core) {
//外层for巡检跳出标记
retry:
//外层for巡检校验线程池状态
//内层for循环校验工作线程个数
for (;;) {
//获取ctl
int c = ctl.get();
//拿到ctl高三位值
int rs = runStateOf(c);
//如果线程池状态是shutdown,并且此时阻塞队列有任务,工作线程个数为0,添加一个工作线程去处理任务
//判断线程池的状态死否大于等于shutdown,如果满足说明线程池不是RUNNING状态
if (rs >= SHUTDOWN &&
//如果这三个条件都满足,就代表是要添加非核心工作线程去处理阻塞队列任务
//如果三个条件有一个没满足,返回false就代表不需要添加
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
//不需要添加工作线程
return false;
for (;;) {
//拿到低29位的值,代表当前工作线程个数
int wc = workerCountOf(c);
//如果工作线程个数大于最大值,不可以添加,返回false
if (wc >= CAPACITY ||
//基于core来判断添加的是否是核心工作线程
//如果是核心:基于corePoolSize去判断
//如果非核心:基于maximumPoolSize去判断
wc >= (core ? corePoolSize : maximumPoolSize))
//代表不能添加,工作线程个数不满足要求
return false;
//以CAS的方式,将ctl加1
if (compareAndIncrementWorkerCount(c))
//CAS成功后,直接退出外层循环,代表可以执行添加工作线程的操作了
break retry;
//重新获取一个ctl的值
c = ctl.get();
//判断重新获取到的ctl中,表示的线程池状态跟之前的是否有区别
//如果状态不一样,说明有变化,重新去判断线程池状态
if (runStateOf(c) != rs)
//跳出一次外层for循环
continue retry;
}
}
//添加工作线程以及启动工作线程
boolean workerStarted = false; //工作线程是否启动
boolean workerAdded = false; //工作线程是否添加
Worker w = null;
try {
//构建工作线程,并且将任务传递进去
w = new Worker(firstTask);
//获取了Worker中的Thread对象
final Thread t = w.thread;
//判断thread是否为null,在new Worker时,内部会通过指定的ThreadFactory去构建Thread交给Worker
//一般如果为null,代表ThreaedFactory有问题
if (t != null) {
//加锁,保证使用workers成员变量以及对largestPoolSize赋值时保证线程安全
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//再次获取线程池状态
int rs = runStateOf(ctl.get());
//再次判断
//如果 rs < shutdown 说明线程池是RUNNING状态,状态正常
//如果线程池状态为shutdown,并且firstTask为null,添加非核心工作线程处理阻塞队列任务
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
//到这,可以添加工作线程
//校验ThreadFactory构建线程后,不能自己启动线程,如果启动了,抛出异常
if (t.isAlive())
throw new IllegalThreadStateException();
//将new好的Woker添加到hashSet中
workers.add(w);
//获取hashSet的size属性,拿到工作线程的个数
int s = workers.size();
//记录最大线程个数的记录
//如果当前工作线程个数大于最大线程个数记录则复制
if (s > largestPoolSize)
largestPoolSize = s;
//添加工作线程的个数
workerAdded = true;
}
} finally {
mainLock.unlock();
}
//如果工作线程添加成功
if (workerAdded) {
//直接启动worker中的线程
t.start();
workerStarted = true;
}
}
} finally {
//做补救的操作,如果工作线程启动失败,将这个添加失败的工作线程处理掉
if (! workerStarted)
//
addWorkerFailed(w);
}
//返回工作线程是否启动成功
return workerStarted;
}
//工作线程启动失败,需要补偿的操作
private void addWorkerFailed(Worker w) {
//因为操作了workers,需要加锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//如果w不为null,说明之前的worker已经new出来了
if (w != null)
//从hashset中移除该worker
workers.remove(w);
//同时对ctl进行-1,代表去掉一个工作线程个数
decrementWorkerCount();
//因为工作线程启动失败,判断一下状态问题是不是可以走TIDYING 最后到TERMINATE状态
tryTerminate();
} finally {
mainLock.unlock();
}
}
5.3.3.5 ThreadPoolExecutor的Woker工作线程
Worker对象主要包含了2个内容
-
工作线程要执行任务
-
工作线程可能会被中断,控制中断
//继承AQS的目的就是控制工作线程的中断
//Worker还实现了Runnable,内部的Thread对象在执行start时,必然要执行Worker中的一些操作
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
//线程工厂构建的线程
final Thread thread;
//当前worker要执行的任务
Runnable firstTask;
//记录当前工作线程处理了多少个任务
volatile long completedTasks;
//有参构造
Worker(Runnable firstTask) {
//跟中断有关,将state设置为-1,代表当前不允许中断线程
setState(-1);
//任务赋值
this.firstTask = firstTask;
//基于线程工厂构建Thread,并且传的Runnable是worker
this.thread = getThreadFactory().newThread(this);
}
//当thread执行start方法时,调用的是Worker的run方法
public void run() {
//任务执行时,执行的是runWorker方法
runWorker(this);
}
protected boolean isHeldExclusively() {
return getState() != 0;
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//设置state为0
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
//当前方法是中断工作线程时执行的方法
void interruptIfStarted() {
Thread t;
//只有worker中的state大于等于0的时候可以中断工作线程
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
//如果状态正常,并且线程未中断,这边就中断线程
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
5.3.3.6 ThreadPoolExecutor中的runWoker方法
工作线程启动后执行的任务
final void runWorker(Worker w) {
//先拿到当前线程
Thread wt = Thread.currentThread();
//从worker对象中拿到任务
Runnable task = w.firstTask;
//将worker中的firstTask置空
w.firstTask = null;
//将worker中的state置为0,代表当前线程是可以中断的
w.unlock();
boolean completedAbruptly = true;
try {
//获取任务
//直接拿到第一个任务去执行
//如果第一个任务为null,就去阻塞队列中获取任务
while (task != null || (task = getTask()) != null) {
//执行worker的lock方法,当前在被lock时,shutdown操作不能中断当前线程,因为当前线程正在处理任务
w.lock();
//比较ctl是否>=STOP,如果满足这个状态,说明线程池已经到了STOP状态
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
5.3.4 线程池的核心参数设计规则
线程池的使用难度不大,难度在于线程池的参数不好配置。主要难点在于任务类型无法控制,比如任务有CPU密集型号和IO密集型,甚至还有混合型。因为IO我们无法控制,所以按照书上提供的一些方法去配置是无法解决问题的。
想要获得一个符合当前任务情况的核心参数最好的办法就是测试,需要将项目部署到测试环境或是沙箱环境中,经过各种压测去得到一个相对符合的参数,如果每次修改项目都需要重新部署则成本过高。
因此我们可以实现一个动态监控以及修改线程池的方案,线程池的核心参数无非就是:
-
corePoolSize:核心线程数
-
maximumPoolSize:最大线程数
-
workQueue:工作队列
线程池中提供了获取核心信息的get方法,同时也提供了修改核心属性的set方法。
5.3.5 线程池整体执行流程图
5.4定时线程池
占坑
5.4.1 ScheduleThreadPoolExecutor介绍
5.4.2 ScheduleThreadPoolExecutor应用
5.4.3 ScheduleThreadPoolExecutor源码剖析
6. 并发集合
6.1 ConcurrrentHashMap
6.1.1 ConcurrrentHashMap简介
ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它是对Hashtable的改进和扩展。它在多线程环境下提供高效的并发访问,并且保证线程安全。
ConcurrentHashMap的主要特点如下:
-
线程安全性:ConcurrentHashMap通过使用细粒度的锁和一些特殊的算法来实现线程安全。它可以同时支持多个线程的并发读操作,而写操作仍然是互斥的。
-
分段锁:ConcurrentHashMap内部将数据分为多个段(Segment),每个段都维护着一个小的哈希表。在进行写操作时,只需要锁住对应的段,而不是整个哈希表,从而提高并发性能。
-
并发度:ConcurrentHashMap的并发度是通过调整段的数量来实现的。默认情况下,并发级别为16,可以在创建时指定并发级别,或者使用默认值。
-
高效迭代:ConcurrentHashMap提供了一些特殊的迭代器(Iterator)来支持并发环境下的高效遍历操作,如ConcurrentHashMap.KeySetView、ConcurrentHashMap.Values等。
-
动态扩容:ConcurrentHashMap可以根据需要自动进行扩容,它会在保持线程安全的同时进行并发扩容操作,不会阻塞正在进行的读操作。
ConcurrentHashMap与Hashtable相比,具有更好的并发性能和可伸缩性,因此在多线程环境下被广泛使用。它适用于需要高效并发访问的场景,如缓存管理、并发计算、多线程任务处理等。需要注意的是,虽然ConcurrentHashMap提供了线程安全的操作,但仍然需要注意对于复合操作的原子性保证。在某些特定的应用场景下,可能需要额外的同步机制来保证一致性和原子性操作。
6.1.2 ConcurrrentHashMap存储结构
ConcurrrentHashMap在JDK1.8中是以CAS+syhchronzied保证线程安全的:
-
CAS:在没有hash冲突时(Node要放在数组上时)
-
synchronized:出现hash冲突时(Node存放得到位置已经有数据了)
存储结构:数组+链表+红黑树
6.1.3 ConcurrrentHashMap核心属性
transient volatile Node<K,V>[] table; //链表,根据hash算法计算存储位置
private transient volatile int sizeCtl; //table初始化和更改时的控制参数,如果是-1说明是在初始化,否则值为-(1+resizing的线程数)
private static final int DEFAULT_CAPACITY = 16; //初始化桶的容量为16
private static final int ASHIFT;
private static final long ABASE;
6.1.3 ConcurrrentHashMap存储操作源码解析
- put方法:
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
//初始化桶
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) { //对桶加锁
if (tabAt(tab, i) == f) {
if (fh >= 0) { //如果还是链表,向链表尾添加元素
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) { //如果是树,则插入红黑树
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i); //将链表转成红黑树
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//如果table还没初始化或者是正在resizing
if ((sc = sizeCtl) < 0)
//让出CPU,线程恢复到Runnable状态,等待table初始化
Thread.yield();
//否则以CAS的方式设置SIZECTL的值
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
//以n的容量初始化桶
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//sc = n - n/2^2,很随意的获取一个初始化值
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
6.1.4 ConcurrrentHashMap读取操作源码解析
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//计算键值key的hash值,通过spread方法扩展范围
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
6.2 CopyOnWriteArrayList
6.2.1 CopyOnWriteArrayList简介
CopyOnWriteArrayList是Java中的一个线程安全的List实现类,它在并发环境下提供了高效的并发访问,并且保证读操作的线程安全性。CopyOnWriteArrayList是基于lock锁和数组副本的形式去保证线程安全的。
- 写数据:先获取lock锁,需要赋值一个副本数组,将数据插入到副本数组中,将副本数组赋值给CopyOnWriteArrayList中的array。由于CopyOnWriteArrayList每次写数据都需要构建一个副本,因此如果你的业务写操作比较多,应该避免去使用CopyOnWriteArrayList,这里会构建大量数据副本,比较占用内存资源。
CopyOnWriteArrayList是弱一致性的,写操作先执行,但是副本还没有落到CopyOnWriteArrayList的属性中,因此读操作是无法查询到的。
CopyOnWriteArrayList的主要特点如下:
-
线程安全性:CopyOnWriteArrayList通过使用"写时复制"(Copy-On-Write)策略来实现线程安全。在进行写操作时,它会创建一个底层数组的副本,并在副本上进行修改,而不影响原始数组,从而保证读操作的线程安全。
-
写时复制:当有写操作发生时,CopyOnWriteArrayList会创建一个原数组的副本,并在副本上进行修改操作。这样可以确保读操作不受写操作的影响,读操作可以在原数组上进行,无需加锁,从而提高读操作的并发性能。
-
适用于读多写少:CopyOnWriteArrayList适用于读多写少的场景,因为写操作需要复制底层数组,如果写操作频繁或数据量很大,会导致较高的内存开销和复制时间。
-
高效迭代:CopyOnWriteArrayList的迭代器是基于原数组进行的,所以迭代过程中不会受到写操作的影响,可以保证快照一致性。
-
适用于不经常变动的数据集合:CopyOnWriteArrayList适用于那些不经常变动的数据集合,例如配置信息、观察者列表等。由于写操作需要复制底层数组,频繁的写操作会导致性能下降。
需要注意的是,CopyOnWriteArrayList适用于读多写少的场景,当写操作频繁或数据量较大时,性能会受到影响。此外,由于每次写操作都需要复制底层数组,可能会占用较大的内存空间。CopyOnWriteArrayList在Java并发编程中的应用广泛,特别适用于那些需要高并发读取,对一致性要求较高,写操作较少的场景。
6.2.2 CopyOnWriteArrayList核心属性&方法
CopyOnWriteArrayList的核心属性和方法如下:
核心属性:
1. private transient volatile Object[] array:CopyOnWriteArrayList内部维护的数组,存储元素。
2. private static final ReentrantLock lock:用于在写操作时获取锁。
3. private final transient Lock readLock:读锁,用于在读操作时获取锁。
核心方法:
1. public CopyOnWriteArrayList():构造一个空的CopyOnWriteArrayList。
2. public CopyOnWriteArrayList(Collection<? extends E> c):构造一个包含指定集合c的CopyOnWriteArrayList。
3. public int size():返回列表的元素数量。
4. public boolean isEmpty():判断列表是否为空。
5. public boolean contains(Object o):判断列表是否包含指定元素。
6. public Object[] toArray():将列表转换为数组。
7. public <T> T[] toArray(T[] a):将列表转换为指定类型的数组。
8. public boolean add(E e):将指定元素添加到列表的末尾。
9. public boolean remove(Object o):从列表中移除指定元素。
10. public boolean containsAll(Collection<?> c):判断列表是否包含指定集合的所有元素。
11. public boolean addAll(Collection<? extends E> c):将指定集合的所有元素添加到列表的末尾。
12. public boolean removeAll(Collection<?> c):从列表中移除指定集合中的所有元素。
13. public boolean retainAll(Collection<?> c):仅保留列表中属于指定集合的元素,移除其他元素。
14. public void clear():清空列表。
15. public E get(int index):获取指定索引位置的元素。
16. public E set(int index, E element):将指定索引位置的元素替换为指定元素。
17. public void add(int index, E element):在指定索引位置插入指定元素。
18. public E remove(int index):移除指定索引位置的元素。
6.2.3 CopyOnWriteArrayList读写操作源码解析
占坑
6.2.4 CopyOnWriteArrayList移除数据
占坑
6.2.5 CopyOnWriteArrayList覆盖数据&清空集合
占坑
6.2.6 CopyOnWriteArrayList迭代器
占坑
7. JUC并发工具
7.1 CountDownLatch
7.1.1 CountDownLatch介绍
CountDownLatch是Java中的一个同步工具类,用于实现线程间的等待。它的作用是允许一个或多个线程等待其他线程完成操作后再继续执行。
CountDownLatch的基本原理如下:
- 初始化时,需要指定一个计数器(count),表示需要等待的线程数量。
- 每个线程在完成自己的任务后,可以调用CountDownLatch的
countDown()方法,将计数器减一。 - 主线程或其他等待线程可以通过调用CountDownLatch的
await()方法,阻塞等待,直到计数器变为0。 - 当计数器变为0时,阻塞的线程会被唤醒,继续执行后续操作。
CountDownLatch的主要方法如下:
- public CountDownLatch(int count):创建一个CountDownLatch对象,并指定初始计数器的值。
- public void countDown():将计数器减一。
- public void await() throws InterruptedException:等待计数器变为0,阻塞当前线程。
CountDownLatch常用于以下场景:
- 实现多个线程并行操作:可以让主线程等待所有子线程完成某项任务后再继续执行。
- 等待外部事件的发生:主线程可以等待某个外部事件(如网络请求、文件加载等)完成后再继续执行。
需要注意的是,CountDownLatch的计数器是无法重置的,一旦计数器变为0,就无法再次使用。如果需要重复使用类似的功能,可以考虑使用CyclicBarrier类。此外,需要谨慎使用CountDownLatch,避免出现死锁或其他并发问题。
7.1.2 CountDownLatch应用
以下是一个使用CountDownLatch的简单示例:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int threadCount = 5;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
Thread thread = new WorkerThread(latch, "Thread-" + (i + 1));
thread.start();
}
System.out.println("Main thread is waiting for worker threads to complete.");
// 主线程等待所有工作线程完成任务
latch.await();
System.out.println("All worker threads have completed their tasks. Main thread resumes.");
// 主线程继续执行其他操作
}
static class WorkerThread extends Thread {
private final CountDownLatch latch;
public WorkerThread(CountDownLatch latch, String name) {
super(name);
this.latch = latch;
}
@Override
public void run() {
System.out.println("Thread " + getName() + " is performing a task.");
// 模拟任务执行
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread " + getName() + " has completed its task.");
// 任务完成后调用countDown()方法,将计数器减一
latch.countDown();
}
}
}
在上述示例中,主线程创建了5个WorkerThread工作线程,并将CountDownLatch对象传递给每个工作线程。每个工作线程执行任务后都会调用countDown()方法,将计数器减一。主线程通过await()方法等待计数器变为0,直到所有工作线程完成任务后,才会继续执行后续操作。运行上述示例,可以看到主线程会在所有工作线程完成任务后恢复执行,并输出相应的日志信息。这个例子展示了CountDownLatch在线程间等待和同步的应用。
7.1.3 CountDownLatch源码分析
- 有参构造
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
Sync(int count) {
setState(count);
}
- countDown
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared(); //真正的释放锁
return true;
}
return false;
}
//尝试锁能不能被释放,这里并没有真实的去释放锁和唤醒被挂起的线程
protected boolean tryReleaseShared(int releases) {
for (;;) { //自旋
int c = getState(); //获取state
if (c == 0) //值为0,释放失败
return false;
int nextc = c-1; //否则state-1
if (compareAndSetState(c, nextc)) //以CAS的方式更改c的值
return nextc == 0;
}
}
private void doReleaseShared() {
for (;;) { //自旋
Node h = head; //获取队列头
if (h != null && h != tail) { //队列不为空
int ws = h.waitStatus; //AQS队列状态
if (ws == Node.SIGNAL) { //head节点处于SIGNAL状态
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //将节点状态设置为默认状态
continue;
unparkSuccessor(h);
}else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
//激活当前节点的线程,唤醒等待队列中后继节点线程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus; //获取等待状态
if (ws < 0) //非默认状态,则设置为默认状态
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next; //获取后继节点
if (s == null || s.waitStatus > 0) { //后继节点如果是CANNEL状态
s = null;
//从尾节点开始向前遍历,直到遇到最后一个非CANCEL状态的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); //唤醒后继节点的线程
}
- await
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) //当前线程被中断,抛出异常
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
//锁还未释放完,即任务还未处理完
//尝试挂起当前线程直到任务结束
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
//如果锁释放完了返回1,否则返回-1
return (getState() == 0) ? 1 : -1;
}
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//向AQS队列中添加一个节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) { //自旋
final Node p = node.predecessor(); //获取前驱节点
if (p == head) { //前驱节点为头节点
int r = tryAcquireShared(arg); //获取state状态
if (r >= 0) { //state>0,任务未处理完
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
7.2 CycleBarrier
7.2.1 CycleBarrier介绍
CyclicBarrier是Java中的一个同步工具类,用于实现线程间的协调等待。它的作用类似于CountDownLatch,但有一些不同之处。
CyclicBarrier的基本原理如下:
- 初始化时,需要指定一个参与线程的数量(parties)。
- 当每个线程到达栅栏点时,会调用CyclicBarrier的
await()方法,此线程被阻塞。 - 当指定数量的线程都调用了
await()方法后,栅栏会打开,所有线程继续执行。 - 在栅栏打开后,CyclicBarrier可以被重用。
CyclicBarrier与CountDownLatch的主要区别在于,CyclicBarrier的参与线程会互相等待,直到达到指定数量后才能继续执行,而CountDownLatch是一次性的。
CyclicBarrier的主要方法如下:
- public CyclicBarrier(int parties):创建一个CyclicBarrier对象,并指定参与线程的数量。
- public int await() throws InterruptedException, BrokenBarrierException:线程到达栅栏点后调用该方法,被阻塞直到所有线程都到达栅栏点。
CyclicBarrier的应用场景:
- 分布式系统中的任务并行化:可以将大型任务分割为多个子任务,每个线程执行一个子任务,然后在CyclicBarrier处等待,直到所有子任务都执行完成后,再进行下一阶段的操作。
- 数据流水线的并行化:可以将数据处理过程划分为多个阶段,每个线程负责一个阶段的处理,在每个阶段结束时等待其他线程,保证数据的连续流动。
需要注意的是,CyclicBarrier的参与线程数量应该与定义时一致,并且线程调用await()方法的次数应该与参与线程数量一致,否则会导致线程无法继续执行或等待永远无法结束的情况。此外,CyclicBarrier不支持重置计数器,如果需要重复使用类似的功能,可以考虑使用CountDownLatch或重新创建新的CyclicBarrier实例。
7.2.2 CycleBarrier应用
以下是一个使用CyclicBarrier的简单示例:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int threadCount = 3;
CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
System.out.println("All threads have reached the barrier. Barrier action is triggered.");
});
for (int i = 0; i < threadCount; i++) {
Thread thread = new WorkerThread(barrier, "Thread-" + (i + 1));
thread.start();
}
}
static class WorkerThread extends Thread {
private final CyclicBarrier barrier;
public WorkerThread(CyclicBarrier barrier, String name) {
super(name);
this.barrier = barrier;
}
@Override
public void run() {
System.out.println("Thread " + getName() + " is performing its task.");
// 模拟任务执行
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread " + getName() + " has finished its task and waiting at the barrier.");
try {
// 线程到达栅栏点,等待其他线程到达
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("Thread " + getName() + " resumes execution after the barrier.");
// 执行后续操作
}
}
}
在上述示例中,主线程创建了3个WorkerThread工作线程,并将CyclicBarrier对象传递给每个工作线程。每个工作线程执行任务后,调用await()方法等待其他线程到达栅栏点。当所有线程都到达栅栏点后,栅栏会打开,执行预定义的栅栏动作(在示例中是输出一条消息),然后所有线程继续执行后续操作。
运行上述示例,可以看到每个工作线程执行任务后都会等待在栅栏处,直到所有线程都到达栅栏点后,栅栏打开并触发栅栏动作,所有线程继续执行后续操作。这个例子展示了CyclicBarrier在线程间协调等待的应用。
7.2.3 CycleBarrier源码解析
7.3 Semaphore
7.3.1 Semaphore介绍
Semaphore是Java中的一个同步工具类,用于控制对共享资源的访问数量。它可以限制同时访问某个资源的线程数量,提供了一种计数器的机制。
Semaphore的基本原理如下:
- 初始化时,需要指定允许访问资源的线程数量。
- 当线程需要访问资源时,首先尝试获取许可(acquire),如果许可数量大于0,则线程可以继续访问资源;如果许可数量为0,则线程被阻塞,等待其他线程释放许可。
- 当线程完成对资源的访问后,需要释放许可(release),许可数量加一。
- 其他被阻塞的线程可以获得许可,继续访问资源。
Semaphore的主要方法如下:
- public Semaphore(int permits):创建一个Semaphore对象,并指定允许访问资源的线程数量。
- public void acquire() throws InterruptedException:尝试获取一个许可,如果没有可用的许可,则线程被阻塞。
- public void release():释放一个许可,许可数量加一。
- public int availablePermits():获取当前可用的许可数量。
Semaphore的应用场景:
- 限制资源访问数量:可以使用Semaphore来控制对共享资源的并发访问数量,例如数据库连接池、线程池等。
- 并发任务的控制:可以使用Semaphore来控制并发执行的任务数量,限制系统的负载压力,防止资源过度占用。
需要注意的是,Semaphore只是一个计数器,并不保证线程之间的公平性,获取许可的顺序可能不按照线程的到达顺序进行。此外,需要确保在获取许可后的释放操作,否则会导致许可数量不足而导致其他线程无法获得许可。
7.3.2 Semaphore应用
以下是一个使用Semaphore的简单示例:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
int threadCount = 3;
Semaphore semaphore = new Semaphore(2); // 允许同时访问资源的线程数量为2
for (int i = 0; i < threadCount; i++) {
Thread thread = new WorkerThread(semaphore, "Thread-" + (i + 1));
thread.start();
}
}
static class WorkerThread extends Thread {
private final Semaphore semaphore;
public WorkerThread(Semaphore semaphore, String name) {
super(name);
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire(); // 获取许可
System.out.println("Thread " + getName() + " is accessing the shared resource.");
// 模拟访问资源
Thread.sleep(2000);
System.out.println("Thread " + getName() + " has finished accessing the shared resource.");
semaphore.release(); // 释放许可
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在上述示例中,主线程创建了3个WorkerThread工作线程,并将Semaphore对象传递给每个工作线程。Semaphore的初始许可数量为2,表示同时允许两个线程访问共享资源。每个工作线程在访问资源前先调用acquire()方法获取许可,如果许可数量大于0,则可以继续访问资源;如果许可数量为0,则线程被阻塞,等待其他线程释放许可。访问资源完成后,线程调用release()方法释放许可。
运行上述示例,可以看到同时只有两个工作线程可以访问共享资源,第三个工作线程需要等待其他线程释放许可后才能访问。这个例子展示了Semaphore的应用,用于限制对共享资源的并发访问数量。