Java并发¶
线程池¶
通过内置的java.util.concurrent.Executors
类可以创建不同类型的线程池实现,它们的都是通过ThreadPoolExecutor
实现的,具体如下:
- newCachedThreadPool() - 根据需要创建新线程的线程池。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。
- newFixedThreadPool() - 可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
- newScheduledThreadPool() - 可安排在给定延迟后运行命令或者定期地执行。
- newSingleThreadExecutor() - 只有一个线程的线程池。这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。
- newVirtualThreadPerTaskExecutor() - 创建一个使用Virtual Thread的无界线程池。
线程状态¶
- NEW: 线程的初始状态。它已创建但尚未开始执行。线程对象存在,但其
run
方法尚未调用。 - RUNNABLE: 线程正在积极运行代码或已准备好运行代码但正在等待线程调度.
- BLOCKED: 当线程等待当前不可用的资源时,线程会进入阻塞状态。这个线程在等待一个monitor锁以进入synchronized块或方法。
- WAITING: 指示线程正在等待另一个线程执行特定操作。当一个线程在另一个线程对象上调用
wait()
或join()
等方法时,通常会发生这种情况。 - TIMED_WAITING: 与WAITING类似,但有所不同。在这里,线程正在等待另一个线程的操作,但有指定的时间限制。如果该操作未在该时间内发生,线程将转换到不同的状态。
- TERMINATED: 线程的最终状态。它已完成其代码的执行,并且其所有资源已被释放。线程一旦终止就无法重新启动。
API¶
- 锁 Lock
- ReadWriteLock, ReentrantLock
- Synchronized
- 类实例方法 - 在某个时刻每个类实例只有一个线程可以执行这些方法。
- 静态方法 - 在某个时刻只有一个线程可以执行该静态方法,这是类级别的。
- 代码块 - 提供比方法更小粒度的锁,只同步某一小段代码,而不想同步整个方法,如只锁定某个类字段以实现同步访问。
- Collection
- BlockingQueue - ArrayBlockingQueue, LinkedBlocingQueue, DelayQueue, PriorityBlockingQueue, BlockingDeque
- ConcurrentMap, ConcurrentHashMap.newKeySet() - 使用分段锁以提供线程安全和原子性操作的Map和Set
- 线程间协作
- CountDownLatch - 可让当前线程等待有限的其他线程执行完毕再继续执行
- CyclicBarrier - 可让有限的多个线程等待到都达到同一状态再全部继续执行
- Semaphore - 控制同时访问某个资源的线程个数
- Atomic
- AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference
注意,Java API只针对单个JVM有效。分布式环境下的并发控制须使用分布式方案,推荐使用Redis。
Synchronized vs ReentrantLock¶
- Synchronized依赖操作系统底层的线程调度, 是重量级锁,是非公平锁。ReentrantLock是基于AQS(Abstract Queued Synchronizer)实现的在用户态中管理的轻量级锁,可作为公平锁或非公平锁使用。
- JDK1.6对Synchronized进行了一系列的优化,如锁消除和锁粗化等,在某些情况下性能和ReentrantLock相当。但总体来说,ReentrantLock是在用户态下实现,不会阻塞操作系统线程,并支持终端等待,锁定多个对象等,所以在性能上比Synchronized更好,在复杂的场景中应首选使用ReentrantLock。
- JDK21正式引入虚拟线程(Virtual Thread)技术,虚拟线程是mount在平台线程上运行,虚拟线程调度在用户态中完成。当使用虚拟线程时,Synchronized会将线程固定在平台线程上,而ReentrantLock则不会,所以在包并发状态下Synchronized会影响虚拟线程的调度效率。
Abstract Queued Synchronizer¶
AQS使用了一种叫做CAS(Compare-And-Swap)的无锁操作来实现原子性的锁操作。CAS操作可以原子性地比较和交换一个变量的值。在AQS中,使用了一个名为state的volatile变量来表示锁的状态。AQS使用了等待队列来管理等待锁的线程,等待队列是一种轻量级的同步机制。
- ReentrantLock:state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程在tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证state是能回到零态的。
- CountDownLatch:任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从
await()
函数返回,继续后余动作。
Volatile¶
Volatile 是并发编程中非常重要的一个特性,它提供了一种轻量级的同步机制。它的作用是确保变量的可见性和禁止指令重排。
- 可见性:确保一个线程修改了共享变量的值后,其他线程能够立即得知这个修改。在没有volatile修饰的多线程环境中,线程可能因为读取本地内存(线程的私有堆栈)中的变量副本而不知道其他线程对变量的修改。
- 禁止指令重排:在编译执行过程中,为了优化程序性能,编译器和处理器可能会对指令做重排。使用volatile可以部分禁止指令重排,确保程序执行的顺序与代码的顺序一致。
优点:
- 相比于synchronized,volatile是一种更轻量级的同步策略,不会造成线程的阻塞。
- 适用于一写多读的场景,能够保证读取到的数据是最新的。
缺点:
- volatile变量的操作不是原子性的(例如自增操作)。
- 只能保证可见性和部分有序性,不能保证操作的原子性。
例子:
public class Demo {
// 如果不使用volatile修饰flag,worker线程可能永远不会看到flag变为true
private volatile boolean flag = false;
public void runExample() throws InterruptedException {
Thread worker = new Thread(() -> {
while (!flag) {
// 循环等待flag变为true
}
System.out.println("Worker thread sees the change to true.");
});
worker.start();
Thread.sleep(000); // 延迟确保线程启动
flag = true; // 更改flag
System.out.println("Flag has been set to true.");
}
public static void main(String[] args) throws InterruptedException {
new Demo().runExample();
}
}
Spring Framework¶
Spring对如何在框架内进行并发编程有良好的支持,参阅Spring条目。
Virtual Thread¶
Virtual Thread是一种基于用户态的轻量级线程,JVM会将线程固定在Platform Thread上,并在用户态中完成线程调度。当Virtual Thread被阻塞,JVM会将它从Platform Thread中卸载,等待其可以继续往下执行时再重新固定到Platform Thread上继续。Virtual Thread在内存中比Platform Thread占用更少的空间,JVM可以轻松管理多达100万个Virtual Thread。Virtual Thread应该是我们在需要时创建的一次性实体,不鼓励将它们池化和在不同的任务中重用。
Virtual Thread的引入对Reactive编程产生很大的挑战。使用Virtual Thread和使用传统BIO的代码十分相似,转成使用Virtual Thread只需要改动极少量代码。而将项目BIO转成使用Reactive形式的代码意味着从头到尾的结构性的大改动。