Java并发

线程池

通过内置的java.util.concurrent.Executors类可以创建不同类型的线程池实现,它们的都是通过ThreadPoolExecutor实现的,具体如下:

  • newCachedThreadPool() - 根据需要创建新线程的线程池。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。
  • newFixedThreadPool() - 可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
  • newScheduledThreadPool() - 可安排在给定延迟后运行命令或者定期地执行。
  • newSingleThreadExecutor() - 只有一个线程的线程池。这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。
  • newVirtualThreadPerTaskExecutor() - 创建一个使用Virtual Thread的无界线程池。

线程状态

  1. NEW: 线程的初始状态。它已创建但尚未开始执行。线程对象存在,但其run方法尚未调用。
  2. RUNNABLE: 线程正在积极运行代码或已准备好运行代码但正在等待线程调度.
  3. BLOCKED: 当线程等待当前不可用的资源时,线程会进入阻塞状态。这个线程在等待一个monitor锁以进入synchronized块或方法。
  4. WAITING: 指示线程正在等待另一个线程执行特定操作。当一个线程在另一个线程对象上调用wait()join()等方法时,通常会发生这种情况。
  5. TIMED_WAITING: 与WAITING类似,但有所不同。在这里,线程正在等待另一个线程的操作,但有指定的时间限制。如果该操作未在该时间内发生,线程将转换到不同的状态。
  6. 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形式的代码意味着从头到尾的结构性的大改动。