Java并发编程学习总结(二)


1. 并发编程基础

1.1 线程的基本概念

线程是操作系统能够进行运算调度的最小单位。在Java中,线程可以看作是方法执行的控制线程,每个线程都独立于其他线程运行,并拥有自己的堆栈、局部变量和指令计数器。Java虚拟机(JVM)允许一个应用程序拥有多个线程同时运行,以提高程序的执行效率和响应速度。

  • 线程创建:Java中可以通过继承Thread类或实现Runnable接口来创建线程。继承Thread类需要重写run方法,而实现Runnable接口则需要将Runnable实例传递给Thread对象。
  • 线程调度:线程的调度是由操作系统负责的,Java程序通过调用Thread类的start方法来启动一个线程,一旦线程开始运行,它就处于可运行(Runnable)状态,等待被操作系统调度到CPU上执行。

1.2 线程的状态及生命周期

线程在其生命周期中会经历不同的状态,包括新建、可运行、阻塞、死亡等。

  • 新建状态:线程对象被创建,但还没有调用start方法。
  • 可运行状态:调用start方法后,线程变为可运行状态,等待JVM的线程调度器分配CPU时间片。
  • 阻塞状态:线程在运行过程中可能因为等待监视器锁(synchronized block)或其他原因而被阻塞,阻塞状态的线程不会占用CPU资源。
  • 死亡状态:线程的run方法执行完毕后,线程进入死亡状态,此时线程不可再被启动。

1.3 线程同步与协作

线程同步是确保多个线程在访问共享资源时保持一致性的一种机制。Java提供了多种同步机制,包括synchronized关键字、Lock接口、volatile关键字等。

  • synchronized关键字:可以用于方法或代码块,确保同一时间只有一个线程可以执行该段代码。
  • Lock接口:提供了比synchronized更灵活的锁定机制,允许尝试锁定、定时锁定和可中断的锁定。
  • volatile关键字:保证了变量的可见性,即当一个线程修改了volatile变量的值时,新值对其他线程来说是立即可见的。
  • 协作机制:包括wait/notify、join、CountDownLatch等,这些机制允许线程之间进行有效的通信和协作。例如,CountDownLatch允许一个或多个线程等待其他线程完成工作后再继续执行。

2. Java内存模型

2.1 内存可见性

内存可见性是指当多个线程访问同一个变量时,一个线程修改了该变量的值,其他线程能够立即看到这一变化。在Java中,内存可见性问题通常与缓存和线程的本地内存相关。为了解决可见性问题,Java提供了volatile关键字,确保变量的读写操作直接作用于主内存,而非线程的本地内存。据统计,约有80%的并发问题可以通过正确使用volatile关键字来解决。

  • volatile变量:当一个变量被声明为volatile,它确保每次读取都是从主内存中获取,每次写入都是同步回主内存,从而保证所有线程看到的都是最新值。
  • 内存屏障:在某些情况下,volatile变量可能不足以保证复杂的内存可见性需求,此时需要使用内存屏障(memory barrier)来确保在屏障之前的读写操作在屏障之后的操作之前完成。

2.2 指令重排与原子性

指令重排是指编译器或处理器为了优化性能,可能会改变代码的执行顺序。然而,这可能会导致线程安全问题,因为其他线程可能依赖于特定的执行顺序。原子性问题是指一个操作或者多个操作成为一个不可分割的单元,要么全部执行,要么全部不执行。

  • 原子操作:Java中的原子操作包括基本类型的赋值操作、CAS(Compare-And-Swap)等。java.util.concurrent.atomic包提供了一系列的原子类,如AtomicInteger,来保证操作的原子性。
  • synchronizedLock:为了防止指令重排,可以使用synchronized关键字或Lock接口来保证代码块的原子性。synchronized通过监视器锁机制来确保同一时刻只有一个线程执行代码块。

2.3 happens-before原则

happens-before原则是Java内存模型中的核心概念,它定义了一种规则,确保在多线程环境中,一个操作的结果对其他线程可见。这个原则是理解内存可见性和有序性的基石。

  • 程序顺序规则:在一个线程内,按照程序代码顺序,前面的操作happens-before后面的操作。
  • 监视器锁规则:对一个锁的解锁操作happens-before于后续对这个锁的加锁操作。
  • volatile变量规则:对volatile变量的写操作happens-before于后续对这个变量的读操作。
  • 线程启动和终止规则:线程的start()方法happens-before于线程的所有操作,线程的所有操作happens-before于线程的join()方法。
  • 传递性:如果操作A happens-before操作B,操作B happens-before操作C,那么操作A happens-before操作C。

通过深入理解这些原则和机制,开发者可以更好地编写出线程安全的并发程序。

3. 并发关键字

3.1 volatile关键字

volatile关键字在Java并发编程中扮演着重要角色,它是一个轻量级的同步机制,主要用于保证变量的可见性。

  • 可见性保证:当一个变量被声明为volatile时,它确保了对该变量的写操作对所有线程都是立即可见的,即当一个线程修改了这个变量的值,新值对其他线程来说是立即可见的。
  • 禁止指令重排:volatile还防止了编译器和处理器对读写操作的重排序优化,这有助于避免在多线程环境下出现不可预期的结果。
  • 典型应用场景:volatile常用于状态标记、状态标志等需要快速响应的场景。例如,一个线程需要等待另一个线程完成某些操作,可以使用volatile修饰的布尔标志来控制流程。
  • 性能考量:虽然volatile提供了可见性的保证,但它并不提供原子性保证,也就是说,对于复合操作(如递增操作),volatile是不够的,需要使用synchronized或其他并发工具来保证操作的原子性。

3.2 final关键字

final关键字在Java中用于修饰变量、方法和类,它提供了不变性保证,是并发编程中实现线程安全的一种手段。

  • 变量不变性:当变量被声明为final后,它就只能被赋值一次,这确保了变量的值在初始化后不会被改变,为其他线程提供了一个稳定的状态。
  • 方法不变性:将方法声明为final可以防止该方法被重写,这在构建不可变类时非常有用,因为不可变类是线程安全的。
  • 类不变性:final类不能被继承,这避免了子类改变父类的行为,确保了类的线程安全性。
  • 应用场景:final关键字常用于创建不可变对象,这些对象在创建后状态不会改变,因此在并发环境中使用时不需要额外的同步措施。

3.3 synchronized关键字

synchronized关键字是Java中最基本的同步机制之一,它用于控制对共享资源的并发访问。

  • 同步块:synchronized可以用来修饰一个代码块,只有持有锁的线程才能执行这个代码块,其他线程必须等待。
  • 同步方法:synchronized也可以修饰整个方法,确保在同一时刻只有一个线程能执行该方法。
  • 锁的获取与释放:当线程执行到synchronized修饰的代码时,它必须先获取锁,执行完代码后再释放锁,其他线程才能获取锁并执行。
  • 性能影响:虽然synchronized提供了必要的线程安全保证,但它也可能成为性能瓶颈,特别是在高并发环境下。因此,合理使用synchronized,避免过度同步是并发编程中的一个重要原则。
  • 其他锁机制:除了synchronized,Java并发包java.util.concurrent.locks提供了更灵活的锁机制,如ReentrantLock,它提供了与synchronized类似的同步功能,但增加了尝试非阻塞获取锁、可中断获取锁等特性。

4. JUC并发包

4.1 锁框架

JUC并发包中的锁框架主要提供了Lock接口和一系列实现该接口的类,如ReentrantLock,提供了比synchronized关键字更灵活的锁定机制。

  • ReentrantLock:一个可重入的互斥锁,支持尝试非阻塞地获取锁、可中断地获取锁以及超时获取锁等操作。它还提供了公平锁和非公平锁两种模式,公平锁能够按照线程请求锁的顺序来分配锁,而非公平锁则可能让新请求的线程优先获得锁,从而可能提高系统的吞吐量。
  • ReadWriteLock:允许多个读操作同时进行,但在写操作执行时,它会阻塞所有的读操作和写操作。ReentrantReadWriteLock是这一接口的常见实现。
  • StampedLock:是Java 8中引入的一个新的读写锁,它通过使用乐观读锁和悲观写锁来提高并发性能,同时提供了一个版本号来检测在读操作期间是否有写操作发生。
  • LockSupport:一个用于创建锁和其他同步类的基本线程阻塞原语。它提供了parkunpark方法来阻塞和唤醒线程,这些方法比Thread.suspendThread.resume更安全,因为它们不会引发死锁。

4.2 并发集合

JUC并发包提供了多种线程安全的并发集合类,这些集合类在多线程环境下能够保证线程安全,同时提供高效的并发性能。

  • ConcurrentHashMap:一个线程安全的HashMap实现,它通过分段锁的概念来提高并发访问的性能。在JDK 8中,它的实现进一步优化,使用了CAS操作和synchronized来保证线程安全,减少了锁的粒度。
  • ConcurrentLinkedQueue:一个基于链接节点的无界线程安全队列,它按照FIFO原则对元素进行排序,适用于高并发场景下的线程间数据交换。
  • BlockingQueue:一个支持阻塞操作的队列,包括ArrayBlockingQueueLinkedBlockingQueueDelayQueue等实现。当队列为空时,从队列中获取元素的线程会被阻塞,直到队列中有可用的元素;当队列满时,向队列中添加元素的线程会被阻塞,直到队列中有可用的空间。
  • CopyOnWriteArrayListCopyOnWriteArraySet:这两个类提供了线程安全的变体,适用于读多写少的场景。它们在每次修改时都会复制整个底层数组,从而保证了迭代器的一致性和线程安全。

4.3 线程池与调度器

JUC并发包中的线程池和调度器提供了一种有效管理线程和任务执行的方式,它们能够减少创建和销毁线程的开销,提高资源利用率。

  • ExecutorExecutorsExecutor是一个接口,用于执行提交的任务,而Executors是一个工厂类,提供了一些静态方法来创建不同类型的线程池。
  • ThreadPoolExecutor:是JUC并发包中线程池的核心实现类,它提供了丰富的构造参数来自定义线程池的行为,如核心线程数、最大线程数、工作队列、线程工厂、拒绝策略等。
  • ScheduledThreadPoolExecutor:继承自ThreadPoolExecutor,它支持定时任务和周期性任务的执行。通过它可以安排一个任务在一定的延迟后运行,或者定期执行。
  • ForkJoinPool:是Java 7中引入的专门用于分治任务的线程池,它基于工作窃取算法,将大任务分割成小任务并发执行,然后合并结果,特别适合于可以并行计算的任务。

5. 并发工具类

5.1 CountDownLatch

CountDownLatch是一个同步辅助类,它允许一个或多个线程等待一组操作完成。在Java并发编程中,CountDownLatch通常用于实现一个线程需要等待其他线程完成某些操作的场景。例如,一个主线程需要等待多个工作线程处理完数据后才能进行汇总操作。

  • 原理:CountDownLatch通过一个计数器来实现,计数器的初始值等于需要等待的事件数量。每次一个给定事件完成时,计数器的值就会减少,当计数器的值达到零时,等待在CountDownLatch上的线程被释放。

  • 使用场景:CountDownLatch适用于一次性的计数任务,例如,初始化操作、一次性的资源释放等。

  • 实现:在Java中,可以通过CountDownLatch(int count)构造方法创建一个CountDownLatch对象,并通过countDown()方法递减计数器,通过await()方法阻塞直到计数器的值为零。

    5.2 CyclicBarrier

CyclicBarrier是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。与CountDownLatch不同的是,CyclicBarrier可以重复使用,适用于需要多次等待的场景。

  • 原理:CyclicBarrier通过一个计数器和锁机制来实现,计数器的初始值同样等于需要等待的线程数量。当一个线程到达屏障点时,计数器递减,如果计数器的值到达零,则释放所有等待的线程。之后,CyclicBarrier可以被重置,再次使用。

  • 使用场景:CyclicBarrier适用于固定数量的线程需要多次协作的场景,例如,多线程分阶段处理任务。

  • 实现:在Java中,可以通过CyclicBarrier(int parties, Runnable barrierAction)构造方法创建一个CyclicBarrier对象,parties参数表示需要等待的线程数量,barrierAction是所有线程到达屏障点后执行的操作。通过await()方法阻塞当前线程直到所有线程都到达屏障点。

    5.3 Semaphore

Semaphore是一个计数信号量,它通过维护一个许可集合来控制对有限资源的访问。Semaphore可以用来限制对某个特定资源的访问数量,或者控制执行某个任务的线程数量。

  • 原理:Semaphore内部维护了一个许可集合,每个许可代表一个资源的使用权。线程可以通过acquire()方法获取一个许可,如果许可不足,则阻塞等待。线程使用完资源后,通过release()方法释放许可。

  • 使用场景:Semaphore适用于需要控制同时访问资源的线程数量的场景,例如,限流、线程池大小控制等。

  • 实现:在Java中,可以通过Semaphore(int permits)构造方法创建一个Semaphore对象,permits参数表示许可的数量。通过acquire()release()方法来控制对资源的访问。

    5.4 Exchanger

Exchanger是一个用于线程间协作的工具类,它允许两个线程在某个点交换数据。Exchanger主要用于两个线程需要交换数据并继续执行的场景。

  • 原理:Exchanger内部维护了一个交换队列,当一个线程到达交换点并调用exchange()方法时,它会将自己的数据放入队列,并等待另一个线程到达。当另一个线程到达并调用exchange()方法时,两个线程可以交换数据并继续执行。

  • 使用场景:Exchanger适用于两个线程需要进行数据交换的场景,例如,两个线程分别从不同的数据源读取数据,然后交换数据进行下一步处理。

  • 实现:在Java中,可以通过Exchanger(V item)构造方法创建一个Exchanger对象,item参数表示线程在交换点等待交换的数据。通过exchange(V x)方法进行数据交换。

6. 原子类

6.1 基本类型原子类

Java并发包中的原子类提供了一种无锁的机制,用于在多线程环境下对基本数据类型进行原子操作。这些类包括AtomicIntegerAtomicLong等,它们利用CAS(Compare-And-Swap)算法来保证操作的原子性。

  • AtomicInteger:用于整数的原子操作,常用于计数器和累加器等场景。据统计,AtomicInteger在高并发环境下的性能是传统synchronized方法的10倍以上。

    • 性能表现:在微基准测试中,AtomicInteger在处理大量更新操作时,能够保持低延迟和高吞吐量,这得益于其非阻塞算法的高效性。
    • 应用场景:在多线程统计用户访问量时,AtomicInteger能够确保每个线程的访问计数都能被正确记录,而不会出现数据竞争的问题。

    6.2 引用类型原子类

    对于对象引用的原子操作,Java提供了AtomicReference等类。这些类同样基于CAS算法,但适用于对象引用的线程安全操作。

  • AtomicReference:允许线程安全地操作对象引用。在复杂的并发场景中,它可以用来确保对象引用的一致性和线程安全性。

    • 一致性保证:AtomicReference确保在多线程环境下,对象引用的更新和读取都是原子操作,从而避免了引用不一致的问题。
    • 应用场景:在缓存系统中,AtomicReference可以用来原子地更新缓存中的键值对,确保缓存数据的实时性和准确性。

    6.3 FieldUpdater原子类

    AtomicStampedReferenceAtomicMarkableReference是Java并发包中用于更复杂原子操作的类,它们提供了额外的标记或版本号来支持更复杂的同步需求。

  • AtomicStampedReference:通过引入版本号来保证复杂对象的原子更新操作。它适用于需要检测多个字段是否被同时更新的场景。

    • 更新机制:通过比较和设置版本号,AtomicStampedReference能够保证对象的引用和状态在并发环境下的一致性。
    • 应用场景:在分布式系统中,AtomicStampedReference可以用来同步不同节点的状态,确保数据的一致性和系统的稳定性。
  • AtomicMarkableReference:通过一个布尔标记来支持原子的更新操作,适用于需要在更新时记录状态变化的场景。

    • 状态跟踪:AtomicMarkableReference允许用户在更新引用的同时,标记对象的状态,这对于实现乐观锁等机制非常有用。
    • 应用场景:在金融交易系统中,AtomicMarkableReference可以用来确保交易状态的原子更新,同时记录交易是否成功,从而提高系统的可靠性。

7. 并发设计模式

7.1 生产者-消费者模式

生产者-消费者模式是一种常见的并发设计模式,用于协调生产者和消费者之间的并发操作,以确保数据的一致性和线程安全。

  • 模式原理:生产者负责生成数据,消费者负责处理数据。生产者将数据放入缓冲区,消费者从缓冲区取出数据进行处理。为了避免缓冲区的重复读取或写入,通常需要使用同步机制,如锁或信号量。

  • 实现方式:Java中可以通过BlockingQueue实现生产者-消费者模式,它提供了线程安全的队列操作,内部实现了必要的同步机制。

  • 应用场景:多线程数据处理,如并发下载任务分配、实时数据处理等。

  • 性能考量:合理配置生产者和消费者的数量,以及缓冲区的大小,可以有效提升系统的吞吐量和响应速度。

    7.2 读写锁模式

读写锁模式用于解决多个线程同时对同一资源进行读写时的线程安全问题,允许多个读操作并发执行,但写操作是排他的。

  • 模式原理:通过分离读锁和写锁,使得在没有写操作的情况下,多个读操作可以同时进行,从而提高系统的并发性能。

  • 实现方式:Java中的ReentrantReadWriteLock类提供了读写锁的实现,它支持公平性和非公平性两种锁分配策略。

  • 应用场景:数据库访问、文件系统等需要频繁读取但较少写入的应用。

  • 性能考量:读写锁可以显著提升读操作的并发性能,但写操作的频繁会导致读操作等待,需要根据实际场景进行调优。

    7.3 线程池模式

线程池模式用于管理和复用线程,减少线程创建和销毁的开销,提高资源利用率和系统响应速度。

  • 模式原理:线程池预先创建一定数量的线程,或者在需要时动态创建线程,将任务提交给线程池执行,而不是直接创建新线程。
  • 实现方式:Java中的Executors工厂类和ThreadPoolExecutor类提供了线程池的实现,支持不同的线程池类型和配置。
  • 应用场景:需要大量短期异步任务处理的场景,如Web服务器处理请求、批量任务处理等。
  • 性能考量:线程池的大小、任务队列的选择、拒绝策略等配置都会影响系统的性能和稳定性,需要根据任务特性和系统资源进行合理配置。

文章作者: ring2
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ring2 !
  目录