好的,各位观众老爷,欢迎来到“Java并发工具箱:玩转线程,告别阻塞”讲堂!我是你们的老朋友,人称“并发小能手”的码农老王。今天,咱们不讲高深的理论,就聊聊实战,手把手教你玩转Java并发工具类,让你的程序跑得飞起,告别“假并发”,拥抱真高效!😎
开场白:并发编程,痛并快乐着?
说起并发编程,相信很多小伙伴都是又爱又恨。爱的是它可以让程序充分利用多核CPU,提升性能,让你的服务器不再“卡成PPT”。恨的是,并发编程就像潘多拉的魔盒,一不小心就会冒出各种诡异的问题,比如死锁、活锁、数据竞争等等,让你debug到怀疑人生。😩
但是,别怕!Java为我们准备了一整套强大的并发工具类,就像武侠小说里的各种神兵利器,只要掌握了它们,就能轻松驾驭并发,化险为夷。今天,我们就来逐一剖析这些“神兵利器”,让你也能成为并发编程的高手!
第一章:ExecutorService线程池:让线程“劳有所得”
首先,我们来聊聊ExecutorService线程池。想象一下,你要开一家餐厅,如果每次来一个客人就临时招一个服务员,那效率得多低?线程池就像一个预先准备好的服务员队伍,来一个任务就分配一个服务员,任务完成服务员也不会立刻走人,而是等待下一个任务。这样就避免了频繁创建和销毁线程的开销,大大提升了效率。
1.1 线程池的优势:
- 降低资源消耗: 重用已创建的线程,避免频繁创建和销毁线程的开销。
- 提高响应速度: 任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性: 可以统一管理和监控线程,例如设置最大线程数、空闲线程存活时间等。
1.2 常见的线程池类型:
Java提供了多种线程池类型,每种类型都有不同的特点和适用场景。
线程池类型 | 特点 | 适用场景 |
---|---|---|
FixedThreadPool |
固定大小的线程池,核心线程数和最大线程数相等。 | 适合任务量比较稳定,需要保证响应速度的场景。 |
CachedThreadPool |
线程数可以动态增长的线程池,没有核心线程数,最大线程数为Integer.MAX_VALUE。 | 适合任务量波动较大,需要快速响应的场景。 |
SingleThreadExecutor |
只有一个线程的线程池,所有任务按照提交顺序依次执行。 | 适合需要保证任务顺序执行的场景。 |
ScheduledThreadPool |
可以执行定时任务和周期性任务的线程池。 | 适合需要执行定时任务和周期性任务的场景。 |
1.3 如何使用ExecutorService:
使用ExecutorService非常简单,只需要以下几个步骤:
- 创建线程池: 使用
Executors
工厂类创建不同类型的线程池。 - 提交任务: 使用
submit()
或execute()
方法提交任务。 - 关闭线程池: 使用
shutdown()
或shutdownNow()
方法关闭线程池。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorServiceDemo {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交10个任务
for (int i = 0; i < 10; i++) {
final int taskNum = i;
executor.submit(() -> {
System.out.println("Thread " + Thread.currentThread().getName() + " is running task " + taskNum);
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
}
}
这段代码创建了一个包含5个线程的线程池,并提交了10个任务。每个任务会打印出当前线程的名称和任务编号,并模拟执行1秒钟。最后,我们关闭了线程池,不再接受新的任务。
第二章:Future:让线程“未雨绸缪”
有时候,我们需要知道异步任务的执行结果,或者判断任务是否完成。Future接口就是用来解决这个问题的。它可以让你在任务执行过程中获取任务的状态和结果,就像一个“未来预言家”,提前告诉你任务的命运。🔮
2.1 Future的常用方法:
方法 | 作用 |
---|---|
get() |
获取任务的执行结果,如果任务尚未完成,则会阻塞等待。 |
get(timeout, unit) |
在指定时间内获取任务的执行结果,如果超时则抛出TimeoutException 。 |
isDone() |
判断任务是否完成。 |
isCancelled() |
判断任务是否被取消。 |
cancel(mayInterruptIfRunning) |
取消任务的执行,如果mayInterruptIfRunning 为true,则会尝试中断正在执行的任务。 |
2.2 如何使用Future:
import java.util.concurrent.*;
public class FutureDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
ExecutorService executor = Executors.newFixedThreadPool(1);
// 提交一个Callable任务,返回一个Future对象
Future<String> future = executor.submit(() -> {
System.out.println("Task is running...");
Thread.sleep(2000); // 模拟任务执行时间
return "Task completed!";
});
// 检查任务是否完成
System.out.println("Task is done: " + future.isDone());
// 获取任务的执行结果,最多等待1秒
try {
String result = future.get(1, TimeUnit.SECONDS);
System.out.println("Task result: " + result);
} catch (TimeoutException e) {
System.out.println("Task timeout!");
}
// 关闭线程池
executor.shutdown();
}
}
这段代码提交了一个Callable任务,并使用future.get(1, TimeUnit.SECONDS)
方法获取任务的执行结果。由于任务需要执行2秒钟,而我们只等待1秒钟,所以会抛出TimeoutException
。
第三章:CountDownLatch:让线程“守株待兔”
CountDownLatch是一个计数器,它可以让一个或多个线程等待其他线程完成操作后再执行。就像赛跑比赛前的倒计时,只有所有运动员都准备好了,才能鸣枪起跑。🏃♀️🏃♂️
3.1 CountDownLatch的原理:
CountDownLatch维护一个计数器,初始值为指定的值。每当一个线程完成操作后,就调用countDown()
方法将计数器减1。当计数器变为0时,所有等待的线程都会被唤醒。
3.2 CountDownLatch的常用方法:
方法 | 作用 |
---|---|
countDown() |
将计数器减1。 |
await() |
阻塞当前线程,直到计数器变为0。 |
await(timeout, unit) |
阻塞当前线程,直到计数器变为0,或者超时。 |
3.3 如何使用CountDownLatch:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 创建一个CountDownLatch,初始值为3
CountDownLatch latch = new CountDownLatch(3);
// 创建3个线程,每个线程完成操作后调用countDown()
for (int i = 0; i < 3; i++) {
final int threadNum = i;
new Thread(() -> {
System.out.println("Thread " + Thread.currentThread().getName() + " is running...");
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread " + Thread.currentThread().getName() + " finished.");
latch.countDown(); // 计数器减1
}).start();
}
// 主线程等待,直到计数器变为0
latch.await();
System.out.println("All threads finished!");
}
}
这段代码创建了一个CountDownLatch,初始值为3。然后创建3个线程,每个线程完成操作后调用latch.countDown()
方法将计数器减1。主线程调用latch.await()
方法等待,直到计数器变为0,才继续执行。
第四章:CyclicBarrier:让线程“集体行动”
CyclicBarrier也叫做循环栅栏,它可以让一组线程互相等待,直到所有线程都到达某个屏障点,然后才能继续执行。就像团队旅游,大家都要在集合地点等待,人齐了才能一起出发。 ✈️
4.1 CyclicBarrier的原理:
CyclicBarrier维护一个计数器,初始值为指定的值。每当一个线程到达屏障点时,就调用await()
方法,计数器减1。当计数器变为0时,所有等待的线程都会被唤醒,并且计数器会被重置为初始值,可以再次使用。
4.2 CyclicBarrier的常用方法:
方法 | 作用 |
---|---|
await() |
阻塞当前线程,直到所有线程都到达屏障点。 |
await(timeout, unit) |
阻塞当前线程,直到所有线程都到达屏障点,或者超时。 |
reset() |
重置计数器,将所有等待的线程抛出BrokenBarrierException 。 |
4.3 如何使用CyclicBarrier:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) throws InterruptedException {
// 创建一个CyclicBarrier,初始值为3
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All threads arrived at the barrier!");
});
// 创建3个线程,每个线程到达屏障点后调用await()
for (int i = 0; i < 3; i++) {
final int threadNum = i;
new Thread(() -> {
System.out.println("Thread " + Thread.currentThread().getName() + " is running...");
try {
Thread.sleep(1000); // 模拟任务执行时间
System.out.println("Thread " + Thread.currentThread().getName() + " is waiting at the barrier...");
barrier.await(); // 等待其他线程到达屏障点
System.out.println("Thread " + Thread.currentThread().getName() + " continues to run.");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
这段代码创建了一个CyclicBarrier,初始值为3。当所有线程都到达屏障点时,会执行一个Runnable任务,打印出“All threads arrived at the barrier!”。然后,每个线程继续执行后面的代码。
第五章:Semaphore:让线程“排队入场”
Semaphore信号量,它可以控制同时访问某个资源的线程数量。就像停车场,只有一定数量的停车位,当车位满了,其他车辆就需要在外面等待。 🚗
5.1 Semaphore的原理:
Semaphore维护一个计数器,初始值为指定的值,表示可用的资源数量。每当一个线程需要访问资源时,就调用acquire()
方法获取一个许可,计数器减1。当线程释放资源时,就调用release()
方法释放一个许可,计数器加1。如果计数器为0,表示资源已用完,其他线程需要等待。
5.2 Semaphore的常用方法:
方法 | 作用 |
---|---|
acquire() |
获取一个许可,如果许可不可用,则阻塞当前线程。 |
acquire(permits) |
获取指定数量的许可,如果许可不可用,则阻塞当前线程。 |
tryAcquire() |
尝试获取一个许可,如果许可可用则返回true,否则返回false,不会阻塞当前线程。 |
tryAcquire(timeout, unit) |
尝试在指定时间内获取一个许可,如果超时则返回false,不会阻塞当前线程。 |
release() |
释放一个许可。 |
release(permits) |
释放指定数量的许可。 |
5.3 如何使用Semaphore:
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String[] args) {
// 创建一个Semaphore,初始值为3,表示有3个许可可用
Semaphore semaphore = new Semaphore(3);
// 创建5个线程,每个线程尝试获取一个许可
for (int i = 0; i < 5; i++) {
final int threadNum = i;
new Thread(() -> {
try {
semaphore.acquire(); // 获取一个许可
System.out.println("Thread " + Thread.currentThread().getName() + " acquired a permit.");
Thread.sleep(1000); // 模拟任务执行时间
System.out.println("Thread " + Thread.currentThread().getName() + " released a permit.");
semaphore.release(); // 释放一个许可
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
这段代码创建了一个Semaphore,初始值为3,表示有3个许可可用。然后创建5个线程,每个线程尝试获取一个许可。由于只有3个许可可用,所以只有3个线程可以立即获取到许可并执行,其他线程需要等待。
第六章:总结与展望:并发编程,永无止境
好了,各位小伙伴,今天我们一起学习了Java并发工具箱中的几个重要成员:ExecutorService线程池、Future异步结果获取、CountDownLatch、CyclicBarrier和Semaphore。掌握了它们,你就可以更加轻松地编写高效、稳定的并发程序,让你的程序跑得更快,更稳!🚀
但是,并发编程的世界是广阔而深邃的,还有很多值得我们探索的地方。比如,更加高级的并发容器、原子类、锁等等。希望大家在学习并发编程的道路上不断进步,成为真正的并发编程高手!💪
最后,送给大家一句并发编程的真理:“并发虽好,可不要贪杯哦!” 😉
感谢大家的收听,我们下期再见!👋