Java 并发编程(五)|四大并发实战:单例、阻塞队列、定时器与线程池
多线程四大经典案例
前言
面试常考的四大并发实战案例一次性讲透!本文覆盖单例模式、阻塞队列、定时器、线程池四大核心模块,从理论原理到从零手写代码,串联线程安全、阻塞通信、任务调度、线程复用四大并发核心思想,零基础也能看懂,面试复习一站式收藏。
单例模式
一个非常经典的设计模式。
单例:单个实例(对象)。
代码中,有很多用于管理数据的对象,就应该是“单例”的。
此时,需要通过编码技巧,让编译器监督,强制要求,确保这个对象不会出现多个。
单例模式代码的核心要点
1 在类的内部,提供一个现成的实例。
2 把构造方法设置为 private,避免其它代码能够创建出实例。
通过上述方式,就强制了在使用这个类时不会创建出多个对象了。
饿汉模式—代码示例
//单例模式
//期望这个类能够有唯一一个实例
class Singleton{
//static 成员 --- 在 Singleton 类被加载时,就会执行到这里的创建实例的操作
private static Singleton instance = new Singleton();
//通过这个方法来获取到刚才的实例
//后续如果想使用这个类的实例,都通过 getInstance 方法来获取
public static Singleton getInstance() {
return instance;
}
//把构造方法设置为私有,此时类外面的其它代码,就无法 new 出这个类的对象了
private Singleton(){
}
}
public class Demo1 {
public static void main(String[] args) {
//Singleton 实例的使用
Singleton s1=Singleton.getInstance();
Singleton s2=Singleton.getInstance();
//无论 getInstance 使用多少次,得到的对象都是同一个
System.out.println(s1==s2);
}
}
运行结果为:
true
注意:这里,Singleton 实例的创建时机,是在类加载的时候。— 饿汉模式(比较急切)。
与饿汉模式相对的是懒汉模式(在第一次使用时再创建实例)。
懒汉模式—代码示例
//懒汉模式
class SingletonLazy{
private static SingletonLazy instance =null;
//在首次调用 getInstance 时才会真正去创建实例,如果不调用,就不创建
public static SingletonLazy getInstance() {
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() { }
}
饿汉模式 VS 懒汉模式
两种模式都是通过一个静态成员,把这个实例获取到,把构造对象设为private。
不同点:饿汉的创建时机比较早,在类加载时就创建出来了;懒汉的创建时机比较晚,在首次使用时才创建出来。
首先打个比方,文本编辑器,现在需要打开一个非常大的文件,有两种方式:
- 先把所有的内容都加载到内存中,然后再显示内容。— 饿汉模式
- 只加载一小部分数据到内存,立即显示内容,随着用户翻页,再动态加载其他内容。— 懒汉模式
思考:刚刚写的饿汉模式和懒汉模式两种单例的写法,这两种写法是否是线程安全的?也就是说如果多个线程同时调用 getInstance,是否会出现问题?
- 饿汉模式线程安全。
getInstance:只是读取,不会修改。
若多个线程,同时读取同一个变量,不会有线程安全问题。- 懒汉模式线程不安全。
getInstance:既会读取,也会修改
若多个线程,同时修改同一个变量,会有线程安全问题。

思考:如何保证懒汉模式是线程安全的呢?
加锁。
思考:锁要加到哪里?
锁,不是加了就线程安全,一定要确保锁加的位置正确,还要确保锁的对象也得是能够起到合理的锁竞争的效果。
bug 是由于if 判断语句和创建实例这两个操作被穿插形成的,因此加锁的关键就在于要确保这两个操作是一个整体。
class SingletonLazy{
private static SingletonLazy instance =null;
public static SingletonLazy getInstance() {
synchronized (SingletonLazy.class) {
if(instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
private SingletonLazy() { }
}
注意:一旦代码这么写,后续每次调用 getInstance,都需要先加锁了(加锁开销很大,并且可能引起阻塞等待),但是实际上,懒汉模式下的线程安全问题只是出现在最开始(对象还没 new)的时候,一旦对象 new 出来了,后续多线程调用 getInstance,就只有读操作,就不会线程不安全了。
一旦某个代码涉及到加锁,基本和“高性能”无缘了。
思考:是否有办法,既可以让代码线程安全,又不会对执行效率产生太多影响呢?
在加锁语句的外层,再引入一个 if 条件,判定一下,看看当前这里的锁,是否要加上。
如果对象已经有了,线程就安全了,此时就可以不加锁了。
如果对象还没有,存在线程不安全的风险,就需要加锁。
class SingletonLazy{
private static SingletonLazy instance =null;
public static SingletonLazy getInstance() {
//这个if语句判断是否要加锁
if(instance == null) {
synchronized (SingletonLazy.class) {
//这个if语句判断是否要new对象
if(instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() { }
}
这两个 if 语句虽然判断条件相同,但是执行时机、执行结果、作用可能会差异很大。
但是指令重排序可能会对上述代码产生影响。
指令重排序
指令重排序,是在保证逻辑不变的前提下,编译器为了执行效率,可能会调整原有代码的执行顺序。
new操作,可能会触发指令重排序。
new操作分三步:
- 申请内存空间
- 在内存空间上构造对象
- 把内存的地址,赋值给 instance 引用
正常顺序:1 -> 2 -> 3
但是,对于编译器来说,步骤2和步骤3之间不存在数据依赖关系,就会发生指令重排序。
就会产生 1 -> 3 -> 2 的顺序。
在单线程下,无论是先执行2还是3,最终的对象都是完整的,结果是一样的,因此允许被重排序。
但是在多线程下,就可能有问题了,假设按照 1 -> 3 -> 2 的顺序来执行,现在有两个线程 t1和 t2 在执行上述代码,当 t1执行完1和3时,此时 instance 非空,指向的是一个未初始化的非法对象,这时 t2 执行,判定 instance == null 条件不成立,直接 return instance,进一步 t2 线程就可能会访问 instance 里面的属性和方法,就会出现bug。
思考:t1 执行到 new 的过程中,其实是先加了锁的,既然已经加了锁,t2 还能执行吗,还穿插进来吗?
t2 线程执行的第一个 if,没有涉及到任何加锁操作,这个 if 是完全可以执行的。锁的阻塞等待,一定是两个线程都加锁的时候才会触发。
由于 t2 执行第一个 if 时,条件不满足,没有进入 if 内部,此时加锁操作没有真正执行,未涉及到锁的阻塞等待。
针对上述问题,解决方法就是 volatile。
让 volatile 修饰 instance,此时就可以保证 instance 在修改的过程中不会出现指令重排序的现象了。
懒汉模式完整代码
class SingletonLazy{
//使用volatile
private static volatile SingletonLazy instance =null;
public static SingletonLazy getInstance() {
//两个if判断
if(instance == null) {
//正确加锁
synchronized (SingletonLazy.class) {
if(instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() { }
}
本节小结
单例核心依靠私有构造限制实例创建,双重检查锁搭配volatile,同时解决懒汉模式线程安全、指令重排序两大问题,是资源管理器标准落地写法。
阻塞队列
阻塞队列是多线程代码中常用的一种数据结构。
一个特殊的队列:
- 线程安全。
- 带有阻塞特性。
(1)如果队列为空,继续出队列,就会发生阻塞,阻塞到其他线程往队列中添加元素为止。
(2)如果队列为满,继续入队列,就会发生阻塞,阻塞到其他线程从队列中取走元素为止。阻塞队列,可以用来实现“生产者消费者模型”。
生产者消费者模型
生产者,把生产的内容,放到阻塞队列中。
消费者,就会从阻塞队列中获取内容。
解耦合
两个模块,联系越紧密,耦合就越高。
尤其是对于分布式系统来说,解耦合是非常有意义的。
削峰填谷
峰:短时间内,请求量比较多。
谷:短时间内,请求量比较少。
不同的服务器,访问量一样,跑的业务不同,单个访问,消耗的硬件资源不一样。
在生产者消费者模型中,就会解决该问题。
当服务器A收到了较大的请求量,就会把对应的请求写入到队列中,B仍然可以按照自己的节奏来处理请求。— 削峰。
上述的峰值情况只会短时间存在,过了峰值,A的请求量恢复正常,B就可以逐渐把积压的数据处理掉。— 填谷。
阻塞队列的实现方式与使用
两种实现:
- 基于数组—new ArrayBlockingQueue<>();
- 基于链表—new LinkedBlockingDeque<>();
使用:
- put:阻塞式入队列
- take:阻塞式出队列
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue=new LinkedBlockingDeque<>();
queue.put("123");
queue.put("234");
queue.put("345");
queue.put("456");
String elem=queue.take();
System.out.println(elem);
elem=queue.take();
System.out.println(elem);
elem=queue.take();
System.out.println(elem);
elem=queue.take();
System.out.println(elem);
elem=queue.take();
//在这里会阻塞,queue为空
System.out.println(elem);
}
}
实现阻塞队列
如何实现阻塞队列?
基于一个普通的队列,加上线程安全(加锁),加上阻塞(使用 wait + notify)。
普通队列可以基于数组,也可以基于链表。
如何基于数组实现一个队列?
基于数组实现的队列一般是环形队列。
- 初始情况下,队列为空,head 和 tail 是重合的。
- 当队列满的时候,head 和 tail 也是重合的。
针对上述情况,有两种解决方案:
- 浪费一个格子,让tail指向head的前一个位置,就算满了。
- 专门搞一个变量size,来表示元素个数,size为0,就是空,size为数组容量,就是满。
完整代码
//存字符串
class MyBlockingQueue{
private String[] data = new String[1000];
//加上 volatile,后续的代码中,有的要进行读(判定),有的要进行写(修改 )--- 要避免内存可见性问题
private volatile int head = 0;//队列的起始位置
private volatile int tail = 0;//队列的结束位置的下一个位置
private volatile int size=0;//队列中有效元素的个数
//入队列和出队列
public void put(String elem) throws InterruptedException {
//加锁,保证线程安全,锁对象一致就行
synchronized (this) {
//入队列之前先看看当前队列是不是满了
while(size==data.length) {
//如果队列满,继续插入元素就会阻塞
this.wait();
//注意:wait 除了 notify 唤醒之外,还可能因为interrupt方法,中断wait的状态
//所以判断队列是否为满的条件应该为while循环---当wait返回时,进一步确认一下队列是否为满
//使用 wait 时,往往都是使用while作为条件判定的方式,目的就是为了让wait被唤醒之后,还能再确认一下条件是否满足
}
//队列没满,添加元素
data[tail++]=elem;
//如果tail自增后到达了数组末尾,就需要让它回到开头(环形队列)
if(tail==data.length) tail=0;
size++;
this.notify();//用来唤醒take中的wait
}
}
//一个队列,put 和 take只有一边能阻塞。
//如果 put 阻塞了,其他线程继续调用 put也都会阻塞,只能靠 take 唤醒
//如果 take 阻塞了,其他线程继续调用 take也都会阻塞,只能靠 put 唤醒
public String take() throws InterruptedException {
//加锁,保证线程安全,锁对象一致就行
synchronized (this) {
//出队列之前先看看是否队列为空
while(size==0) {
//如果队列为空,取元素就会阻塞
this.wait();
}
//队列不为空,把队首元素(head)删除掉,并返回
String ret=data[head++];
if(head==data.length) head=0;
size--;
this.notify();//用来唤醒put中的wait
return ret;
}
}
}
//借助上述阻塞队列,实现一个简单的生产者消费者模型
public class Demo4 {
public static void main(String[] args) {
MyBlockingQueue queue=new MyBlockingQueue();
//生产者、消费者,分别用线程表示
//消费者
Thread t1=new Thread(()->{
while(true) {
try {
String elem=queue.take();
System.out.println("消费元素:"+elem);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
});
//生产者
Thread t2=new Thread(()->{
int num=1;
while(true) {
try {
queue.put(num+"");
System.out.println("生产元素:"+num);
num++;
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
本节小结
基于wait+notify实现阻塞环形队列,生产者消费者模型实现业务解耦、流量削峰两大工程核心价值,是并发通信基础容器。
定时器
定时器,软件开发中的一个重要组件。
约定一个时间,时间到了之后,执行某个代码逻辑。
尤其在网络通信时会用到定时器。比如客户端给服务器发送了一个请求,一直没有收到回应,但是客户端不能无限地等,所以设置一个最大等待时间,此时,就需要用到定时器。
定时器的使用
在标准库中,有现成的定时器的实现。
import java.util.Timer;
import java.util.TimerTask;
//定时器
public class Demo5 {
public static void main(String[] args) {
// TODO 自动生成的方法存根
Timer timer=new Timer();
//给定时器安排了一个任务,指定在xxx时间执行
timer.schedule(new TimerTask() {
//TimerTask实现了Runnable接口
//使用匿名内部类的写法,继承了TimerTask,并且创建了一个实例,目的是重写run方法,来描述任务的详细情况
@Override
public void run() {
// TODO 自动生成的方法存根
System.out.println("3000");
}
}, 3000);//当前安排的任务在当前时间开始经过多长时间去执行
timer.schedule(new TimerTask() {
@Override
public void run() {
// TODO 自动生成的方法存根
System.out.println("2000");
}
}, 2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
// TODO 自动生成的方法存根
System.out.println("1000");
}
}, 1000);
System.out.println("程序启动!");
}
}
1.主线程执行 schedule 方法时,就是把这个任务放到了 timer 对象中了。
2.与此同时,timer 里面包含一个扫描线程,一旦时间到了,扫描线程就会执行安排的任务。
3.Timer 内部的线程阻止了进程结束,当主线程结束时,timer 还没有结束。
4.Timer里可以安排多个任务。
定时器的实现
1.Timer 中需要有个扫描线程,扫描任务是否到时间去执行了。
2.需要有一个数据结构(优先级队列),把所有的任务都保存起来。
3.创建一个类,通过类的对象来描述一个任务(至少要包含任务内容和时间)。
思考:为什么保存任务的数据结构要用优先级队列呢?
- 如果使用数组 ArrayList,此时,扫描线程就需要不停地遍历数组中的每一个任务,判定每个任务是否都到达执行时间。
- 由于给 Timer 中添加的任务,都有一个时间属性,一定是时间小的先执行,此时使用优先级队列(可以使用O(1)的时间来获取时间最小的任务),就无需每次遍历所有任务了。
import java.util.PriorityQueue;
import java.util.Timer;
import java.util.TimerTask;
class MyTimerTask implements Comparable<MyTimerTask>{
//要执行的任务
private Runnable runnable;
//要执行任务的时间,保存绝对时间更方便
private long time;
//这里的delay,相当于 schedule 方法中的相对时间
public MyTimerTask(Runnable runnable,long delay) {
this.runnable=runnable;
this.time=System.currentTimeMillis()+delay;//绝对时间戳
}
@Override
public int compareTo(MyTimerTask o) {
// TODO 自动生成的方法存根
return (int)(this.time-o.time);
}
public long getTime() {
return this.time;
}
public Runnable getRunnable() {
return this.runnable;
}
}
class MyTimer{
//使用优先级队列保存所有任务
//对于优先级队列来说,要求里面的元素务必是可比较的(还有TreeSet/TreeMap)
private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
private Object locker=new Object();
public void schedule(Runnable runnable,long delay) {
synchronized (locker) {
queue.offer(new MyTimerTask(runnable, delay));
locker.notify();
}
}
//实现扫描线程
public MyTimer() {
Thread t=new Thread(()->{
//不停扫描队首元素,看是否到达时间
while(true) {
try {
synchronized (locker) {
while(queue.isEmpty()) {
//这里的wait在添加新任务时唤醒
locker.wait();
}
MyTimerTask task=queue.peek();
//比较当前队首元素是否可以执行了
long curTime=System.currentTimeMillis();
if(curTime>=task.getTime()) {
task.getRunnable().run();
queue.poll();
}else {
//时间没到时,wait阻塞,线程不会在CPU上调度
//这里使用wait比sleep好,等待过程中,主线程可能会添加一个比此时更早的任务,就会使用notify唤醒
locker.wait(task.getTime()-curTime);
}
}
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
});
t.start();
}
}
public class Demo6 {
public static void main(String[] args) {
// TODO 自动生成的方法存根
MyTimer timer=new MyTimer();
//给定时器安排了一个任务,指定在xxx时间执行
timer.schedule(new Runnable() {
//TimerTask实现了Runnable接口
//使用匿名内部类的写法,继承了TimerTask,并且创建了一个实例,目的是重写run方法,来描述任务的详细情况
@Override
public void run() {
// TODO 自动生成的方法存根
System.out.println("3000");
}
}, 3000);//当前安排的任务在当前时间开始经过多长时间去执行
timer.schedule(new Runnable() {
@Override
public void run() {
// TODO 自动生成的方法存根
System.out.println("2000");
}
}, 2000);
timer.schedule(new Runnable() {
@Override
public void run() {
// TODO 自动生成的方法存根
System.out.println("1000");
}
}, 1000);
System.out.println("程序启动!");
}
}
本节小结
利用优先级队列实现定时任务调度,队首最小时间任务优先执行,搭配等待超时机制,兼顾性能与定时精准性。
线程池
线程池,用来优化频繁创建销毁线程的场景。
- 协程(轻量级线程),相比于线程,把系统调度的过程省略了。
- 线程池—也能使线程不是很慢。
线程池:
- 在使用第一个线程时,提前把后面的线程创建好,放在池子里。
- 后续如果想使用新的线程,就不必重新创建了,此时创建线程的开销就被降低了。
从池子取,这个动作,是纯粹用户态的动作;而创建新的线程,则是需要用户态+内核态相互配合完成。
线程池的使用
工厂模式
- 一种设计模式。
- 通常创建对象,使用new,new关键字就会触发类的构造方法,但是构造方法存在一定的局限性。因此,引入了工厂模式。
- 很多时候,构造一个对象,希望有多种构造方式,此时只能通过重载的方式区分,但重载要求参数类型/个数不同,因此存在局限性。
- 使用工厂设计模式就能解决这个问题。使用普通的方法代替构造方法完成初始化工作,普通方法就可以使用方法的名字来区分了,就不再收到重载的规则制约。
- 一般单独写一个类,给这个类写一些静态方法,由静态方法负责构造对象。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo7 {
public static void main(String[] args) {
// TODO 自动生成的方法存根
ExecutorService service=Executors.newFixedThreadPool(4);
//添加任务,把执行逻辑添加到线程池里
service.submit(new Runnable() {
@Override
public void run() {
// TODO 自动生成的方法存根
System.out.println("Hello");
}
});
//线程池对象不是直接new,而是通过一个专门的方法,返回了一个线程池对象
//此时构造出的线程池对象有一个特点:线程数目是能够动态适应的
//cache:缓存,用过之后不着急释放,先留着以备后续使用
//随着往线程池中添加任务,这个线程池中的线程会根据需要自动被创建出来,创建出来之后也不会着急销毁,会在池子里保留一定的时间
//ExecutorService service=Executors.newCachedThreadPool();
/*
* ExecutorService service=Executors.newFixedThreadPool(4);
* fixed:固定的,指定创建几个线程
*
* ExecutorService service=Executors.newSingleThreadExecutor();
* 只有一个线程的线程池
*
* ExecutorService service=Executors.newScheduledThreadPool(corePoolSize);
* 相当于定时器,不是一个扫描线程负责执行任务了,而是多个线程执行时间到的任务
*
* 上述的几个工厂方法生成的线程池,本质上都是对一个类进行的封装,ThreadPoolExecutor
* 1. 构造---参数很多--经典面试题
* 2 添加任务
* */
}
}
构造方法:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
- int corePoolSize(核心线程数), int maximumPoolSize(最大线程数):描述了线程池中线程的数目;这个线程池里的线程数目是可以动态变化的,变化范围就是[corePoolSize,maximumPoolSize]
- long keepAliveTime(规定线程池中非核心线程的最大空闲存活时间), TimeUnit unit(时间单位:ms,s,min…)
- BlockingQueue workQueue:阻塞队列,用来存放线程池中的任务,可以根据需要灵活设置这里的队列(比如需要优先级,PriorityBlockingQueue;不需要优先级,任务数目恒定,ArrayBlockingQueue;不需要优先级,任务数目变动较大,LinkedBlocking)
- ThreadFactory threadFactory:工厂模式的体现,此处使用ThreadFactory 作为工厂类,由这个类负责创建线程,主要是为了在创建过程中对线程的属性进行设置。
- RejectedExecutionHandler handler:线程池的拒绝策略,一个线程池能容纳的任务数量有上限。不同的拒绝策略有不同的效果(比如,直接抛出异常,或者新添加的任务由添加任务的线程负责执行,或者丢弃任务队列中最老的任务,或者丢弃当前新加的任务)
使用线程池,需要设置线程的数目,数目设置多少合适?
在接触到实际的项目代码之前,无法确定。
一个线程执行的代码,主要有两类:
- CPU密集型:代码里的主要逻辑是在进行算术运算/逻辑判断;
- IO密集型:代码里主要进行IO操作。
假设CPU逻辑核心数为N:
- 一个线程所有的代码都是CPU密集型,此时,线程池的数量就不应该超过N。更多的线程反而会增加调度的开销。
- 一个线程的所有代码都是IO密集型,此时不吃CPU,设置的线程数就可以是一个较大的值。
代码不同,线程池的设置数目就不同。
正确做法:使用实验的方式,对程序进行性能测试,测试过程中尝试修改不同的线程池的线程数目。
线程池的模拟实现
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class MyThreadPool{
//任务队列
private BlockingQueue<Runnable> queue=new ArrayBlockingQueue<>(1000);
//通过这个方法把任务加到队列中
public void submit(Runnable runnable) throws InterruptedException {
//此时的拒绝策略是第五种,阻塞等待
queue.put(runnable);
}
public MyThreadPool(int n) {
//创建出n个线程,负责执行上述队列中的任务
for(int i=0;i<n;i++) {
Thread thread=new Thread(()->{
//让这个线程从队列中消费任务并执行
while (!Thread.currentThread().isInterrupted()) {
Runnable runnable;
try {
runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+" 线程被中断,停止工作");
break; // 中断后跳出循环,安全结束线程
}
}
},"池工作线程-"+i);
thread.start();
}
}
}
public class Demo8 {
public static void main(String[] args) throws InterruptedException {
// TODO 自动生成的方法存根
MyThreadPool myThreadPool=new MyThreadPool(4);
for(int i=0;i<1000;i++) {
int id=i;
myThreadPool.submit(new Runnable() {
@Override
public void run() {
//i 在匿名内部类中,捕获了外部类的变量,变量捕获只能捕获事实final,不能直接用i
System.out.println("执行任务"+id);
}
});
}
}
}
本节小结
核心依靠循环常驻线程复用 + 阻塞队列缓冲任务,避免频繁创建销毁线程的内核开销,是高并发性能优化核心方案。
全文总结
本文一站式拆解 Java 并发四大面试核心实战组件,从底层原理到手写完整代码全覆盖:
- 单例模式:饿汉 / 懒汉两种实现,层层递进解决线程安全、指令重排问题,掌握volatile可见性与有序性落地;
- 阻塞队列:手写环形阻塞队列,理解阻塞等待机制,吃透生产者消费者模型在分布式系统解耦、削峰填谷的落地场景;
- Timer 定时器:原生 API 使用 + 自主实现优先级调度定时器,理解扫描线程、任务优先级调度底层逻辑;
- 线程池:原生线程池七大核心参数、拒绝策略、线程数调优思路,搭配极简手写线程池,吃透线程复用核心设计思想。
本文结合实战案例拆解 Java 并发高频核心考点,所有配套代码均可直接复制运行。后续会持续更新 Java 多线程进阶、锁机制、CAS、JUC 组件等并发干货,欢迎点赞收藏,一起交流技术、共同进步!
🔗 系列文章导航
本篇是「Java并发编程系列」的连载内容,点击链接查看完整系列:
🔹 上一篇:Java并发编程(四)|线程安全:如何让你的并发代码不“打架”
🔹 下一篇:Java 并发编程(六)|并发进阶高频:CAS、锁升级
👉 点击直达「Java并发编程」专栏合集
更多推荐




所有评论(0)