JUC包常用类原理
放眼望去,java.util.concurrent
包下类大致包括:atomic 原子类、锁、并发集合、线程池、工具类。我们挑重要的了解一下。
Atomic 原子类
Java针对并发编程已经有了各种锁,为什么还需要原子类?原子类一定有些特别的应用场景?
在很多时候,我们需要的仅仅是一个简单的、高效的、线程安全的递增或者递减方案,这个方案一般需要满足以下要求:
1、 简单:操作简单,底层实现简单
2、 高效:占用资源少,操作速度快
3、 安全:在高并发和多线程环境下要保证数据的正确性
对于是需要简单的递增或者递减的需求场景,使用 synchronized 关键字和 lock 固然可以实现,但代码写的会略显冗余,且性能会有影响,此时用原子类更加方便。
Atomic 类的原理
通过 CAS 实现线程安全访问。CAS 可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。但并发量很大的话,CPU 会花费大量的时间在试错上面。如果并发量小的情况,这些消耗可以忽略不计。因此 Atomic 类会因为并发度太高而性能变差。
AtomicInteger
在Java中,++i
和i++
操作并不是线程安全的,因为他们不是原子操作。在并发场景下,数值加减操作有线程不安全,而 synchronized 这种锁又大大降低了并发效率。这时候就可以考虑使用AtomicInteger
。它是一个提供原子操作的Integer
的类。
我们先来看看AtomicInteger
给我们提供了什么接口:
int getAndIncrement(); // 获取当前值,然后自加,相当于i++
int getAndDecrement(); // 获取当前值,然后自减,相当于i--
int incrementAndGet(); // 自加1后并返回,相当于++i
int decrementAndGet(); // 自减1后并返回,相当于--i
int getAndAdd(int delta); // 获取当前值,并加上预期值
int getAndSet(int newValue); // 获取当前值,并设置新值
AtomicLong
AtomicLong
也是在高并发下对单一变量进行 CAS 操作,从而保证其原子性。
LongAdder
LongAdder
类继承了Striped64
类,LongAdder
是一种以空间换时间的解决方案。其内部维护了一个long
类型的base
变量,和一个cell
数组,当线程写base
有冲突时,将其写入数组的一个cell
中。将base
和所有cell
中的值求和就得到最终LongAdder
的值了。
- 在高并发的场景,
LongAdder
比AtomicLong
更高效。代价是更高的空间消耗。 - 在并发度不高的情况下,
LongAdder
和AtomicLong
性能差不多。
LongAdder
可以作为数据库主键生成器。
Atomic 类总结
并发度不高用 AtomicInteger
、AtomicLong
,并发度高用LongAdder
。
锁
AQS
AbstractQueuedSynchronizer(AQS)
提供了一套可用于实现锁同步机制的框架。AQS
通过一个FIFO
队列维护线程同步状态,实现类只需要继承该类,并重写指定方法即可实现一套线程同步机制。AQS
根据资源互斥级别提供了独占和共享两种资源访问模式;同时其定义Condition
结构提供了wait/signal
等待唤醒机制。在JUC
中,诸如ReentrantLock
、CountDownLatch
等都基于AQS
实现。
ReentrantLock
ReentrantLock
和synchronized
的作用是相同的,它们的比较:
- 它们都是可重入锁。
synchronized
是Java语言层面提供的语法,不需要考虑异常,且会自动释放锁,而ReentrantLock
是Java代码实现的锁,必须先获取锁,然后在finally
中正确释放锁。 ReentrantLock
可以尝试获取锁,超时还获取不到锁就可以处理别的事情,如tryLock(long timeout, TimeUnit unit)
;而synchronized
不能,只能一直等待。所以,使用ReentrantLock
比直接使用synchronized
更安全,线程在tryLock()
失败的时候不会导致死锁。- 可中断性:
synchronized
锁是不可中断的,无法响应中断请求。ReentrantLock
支持中断,可以响应中断请求。 - 锁的公平性:
synchronized
关键字是非公平锁,即不保证等待线程获取锁的先后顺序。ReentrantLock
可以实现公平锁和非公平锁,默认是非公平锁,但可以通过构造方法来实现公平锁。公平锁:公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁。
ReentrantLock 使用 Condition
synchronized
可以配合wait
和notify
实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock
我们怎么编写wait
和notify
的功能呢?
class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
public void addTask(String s) {
lock.lock();
try {
queue.add(s);
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() {
lock.lock();
try {
while (queue.isEmpty()) {
condition.await();
}
return queue.remove();
} finally {
lock.unlock();
}
}
}
可见,使用Condition
时,引用的Condition
对象必须从Lock
实例的newCondition()
返回,这样才能获得一个绑定了Lock
实例的Condition
实例。
Condition
提供的await()
、signal()
、signalAll()
原理和synchronized
锁对象的wait()
、notify()
、notifyAll()
是一致的,并且其行为也是一样的:
await()
会释放当前锁,进入等待状态;signal()
会唤醒某个等待线程;signalAll()
会唤醒所有等待线程;- 唤醒线程从
await()
返回后需要重新获得锁。
ReadWriteLock
因为synchronized
和ReentrantLock
这两种锁都是独占锁,每次只允许一个线程执行临界区代码,所以它们是比较重量级的锁。有些读多写少的场景,只用保证写的时候只有一个线程,而读的时候可以多个线程同时读。此时读写锁能派上用场了。
ReadWriteLock
的特点:
- 只允许一个线程写入(其他线程既不能写入也不能读取);
- 没有写入时,多个线程允许同时读(提高性能)
ReadWriteLock
大大提高了并发读的执行效率。
注意:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。
使用举例:
public class Counter {
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock rlock = rwlock.readLock();
private final Lock wlock = rwlock.writeLock();
private int[] counts = new int[10];
public void inc(int index) {
wlock.lock(); // 加写锁
try {
counts[index] += 1;
} finally {
wlock.unlock(); // 释放写锁
}
}
public int[] get() {
rlock.lock(); // 加读锁
try {
return Arrays.copyOf(counts, counts.length);
} finally {
rlock.unlock(); // 释放读锁
}
}
}
并发集合
👉 集合知识点梳理
线程池
ThreadPoolExecutor
👉 线程池原理
ForkJoin
Fork/Join是一种基于“分治”的算法:把大任务分解成小任务,并行执行,最后合并结果得到最终结果。
如何使用
使用ForkJoinPool
来进行并行计算,主要分为两步:
- 定义一个
ForkJoinTask
,一般是继承它的子类:RecursiveTask
或RecursiveAction
,然后重写compute()
方法,定义拆分逻辑和计算逻辑。RecursiveTask
有返回值,RecursiveAction
没有返回值。 - 初始化线程池及计算任务,丢入线程池处理,取得处理结果。
ForkJoinTask
ForkJoinTask
实现了Future
,跟FutureTask
类似。它有两个常用方法:
fork()
:把任务推入当前工作线程的工作队列里。join()
:等待处理任务的线程处理完毕,获得返回值。
并不是所有的任务都适合 Fork/Join 框架,比如上面的例子任务划分过于细小反而体现不出效率。因为Fork/Join是使用多个线程协作来计算的,所以会有线程通信和线程切换的开销。
例子1:计算斐波那契数列前n个数的和,如果设f(n)为该数列的第n项(n∈N*),那么有:f(n) = f(n-1) + f(n-2)。
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
System.out.println("CPU核数:" + Runtime.getRuntime().availableProcessors());
long start = System.currentTimeMillis();
Fibonacci fibonacci = new Fibonacci(40);
Future<Integer> future = forkJoinPool.submit(fibonacci);
System.out.println(future.get());
long end = System.currentTimeMillis();
System.out.printf("耗时:%d ms%n", end - start);
}
}
class Fibonacci extends RecursiveTask<Integer> {
int n;
public Fibonacci(int n) {
this.n = n;
}
@Override
protected Integer compute() {
// 这里先假设 n >= 0
if (n <= 1) {
return n;
} else {
Fibonacci f1 = new Fibonacci(n - 1); // f(n-1)
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2); // f(n-2)
f2.fork();
return f1.join() + f2.join(); // f(n) = f(n-1) + f(n-2)
}
}
}
例子2:计算一个长度为2000的随机数组的元素的和。
public class Main {
static Random random = new Random(0);
static long random() {
return random.nextInt(10000);
}
public static void main(String[] args) throws Exception {
// 创建2000个随机数组成的数组:
long[] array = new long[2000];
long expectedSum = 0;
for (int i = 0; i < array.length; i++) {
array[i] = random();
expectedSum += array[i];
}
System.out.println("Expected sum: " + expectedSum);
// fork/join:
ForkJoinTask<Long> task = new SumTask(array, 0, array.length);
long startTime = System.currentTimeMillis();
Long result = ForkJoinPool.commonPool().invoke(task);
long endTime = System.currentTimeMillis();
System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
}
}
class SumTask extends RecursiveTask<Long> {
static final int THRESHOLD = 500;
long[] array;
int start;
int end;
SumTask(long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
// 如果任务足够小,直接计算:
long sum = 0;
for (int i = start; i < end; i++) {
sum += this.array[i];
// 故意放慢计算速度:
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
}
return sum;
}
// 任务太大,一分为二:
int middle = (end + start) / 2;
System.out.println("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end);
SumTask subtask1 = new SumTask(this.array, start, middle);
SumTask subtask2 = new SumTask(this.array, middle, end);
ForkJoinTask.invokeAll(subtask1, subtask2);
Long subresult1 = subtask1.join();
Long subresult2 = subtask2.join();
Long result = subresult1 + subresult2;
System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result);
return result;
}
}
工具类
Semaphore
Semaphore
本质上就是一个信号计数器,用于限制同一时间的最大访问数量。例如,最多允许3个线程同时访问:
public class AccessLimitControl {
// 任意时刻仅允许最多3个线程获取许可:
final Semaphore semaphore = new Semaphore(3);
public String access() throws Exception {
// 如果超过了许可数量,其他线程将在此等待:
semaphore.acquire();
try {
// TODO:
return UUID.randomUUID().toString();
} finally {
semaphore.release();
}
}
}
使用Semaphore
先调用acquire()
获取,然后通过try ... finally
保证在finally
中释放。
使用场景:如果要对某一受限资源进行限流访问,可以使用Semaphore
,保证同一时间最多N个线程访问受限资源。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现