Java并发编程

1、线程的状态

      在Java中,线程(Thread)可以处于以下六种状态之一,这些状态在java.lang.Thread.State枚举中定义:

  1. NEW (新建): 线程对象已经被创建,但是还没有调用start()方法。此时的线程还没有真正开始执行。

  2. RUNNABLE (可运行): 线程已经调用了start()方法,可能正在运行,也可能正在等待CPU分配时间片。在这个状态下,线程有资格运行,但是还没有被操作系统选中运行。即将RUNNINGREADY两个状态统一抽象为RUNNABLE

  3. BLOCKED (阻塞): 线程正在等待监视器锁(monitor lock),以进入同步区域/方法。一旦其他线程释放了锁,并且该线程获得了锁,其状态可以变回RUNNABLE。

  4. WAITING (等待): 线程进入等待状态,因为它在等待其他线程显式地通知唤醒,通常用在调用了如下方法:

    • Object.wait() (没有超时时间的)
    • Thread.join() (没有超时时间的)
    • LockSupport.park()
  5. TIMED_WAITING (定时等待): 线程处于定时等待状态,这意味着它在等待另一个线程的通知,或者在指定的睡眠时间内等待,通常出现在调用了一些带有时间参数的方法,例如:

    • Thread.sleep(long millis)
    • Object.wait(long timeout)
    • Thread.join(long millis)
    • LockSupport.parkNanos(Object blocker, long nanos)
    • LockSupport.parkUntil(long deadline)
  6. TERMINATED (终止): 线程已经执行完了它的任务或者因为异常退出了run方法,线程的运行结束。

        以上是Java线程可能出现的状态,Java程序可以通过Thread类的getState()方法检查线程的状态。这个状态模型有助于理解和调试多线程Java程序,因为它描述了线程在其生命周期中的各种状态。

        对于自动唤醒来说,内部机制通常涉及操作系统的调度器和时间管理系统,确保线程能在适当的时间恢复执行。

2、内存模型

2.1 指令重排

        指令重排(Instruction Reordering)是一种性能优化技术,它允许编译器和处理器改变指令的执行顺序,而不会改变单线程程序的执行结果。这种重新排序的目的是为了提高执行效率、更好地利用 CPU 缓存、减少指令延迟和提高并行度。

在 Java 内存模型(Java Memory Model,JMM)中,有两种形式的指令重排:

  1. 编译器重排:Java 编译器在生成字节码时,可以重新排序指令,以提高性能。
  2. 运行时重排:JVM 在运行时(特别是即时编译器 JIT)和硬件在执行机器指令时,都可能进行重排。

2.2 顺序一致性

        顺序一致性(Sequential Consistency)是一个内存模型概念,它描述了程序执行时操作的顺序。在顺序一致的内存模型中,操作的结果看起来就像是按照程序代码的顺序,一次一个地,按序执行的,不会出现重排序(reordering)现象。这个模型非常简单直观,因为它与我们编写程序时考虑的执行顺序一致。

顺序一致性的主要特点如下

  1. 操作顺序:操作结果看起来就像它们是按照程序代码中指定的顺序执行的。
  2. 即时可见性:单个处理器上的写操作立即对所有其他处理器可见。

2.3 Happens-before

就是一堆规则,代码里的指令命中这些规则,就不会发生指令重排。

 2.4 as-if-serial

与顺序一致性同一个概念。

3、volatile

Java的volatile关键字是Java虚拟机提供的轻量级的同步机制。volatile变量主要用于确保将变量的更新操作通知到其他线程。当一个字段被声明为volatile后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作进行重排序。volatile保证了变量的可见性和部分有序性,但不保证原子性。

3.1 可见性

在没有使用volatile关键字的情况下,线程可以把变量保留在本地内存中,而不是直接在主内存中进行读写。这就可能会导致一个线程在修改了一个变量后,其他线程看不到更新后的值。

当一个变量被声明为volatile之后,所有对该变量的读写都会直接在主内存中进行。这意味着当一个线程更新了一个volatile变量后,这个新值对其他线程来说是立即可见的。同时,当从一个volatile变量读取值的时候,会从内存中读取,而不是从线程的本地缓存。

3.2 禁止指令重排序

Java内存模型允许编译器和处理器对指令进行重排序,以提高执行性能。但是,当变量被声明为volatile后,就会禁止与这个变量相关的重排序操作。这就保证了volatile变量的操作是有序的。

3.3 实现细节

在底层,volatile的实现依赖于特定硬件的内存屏障指令。对于主流的x86架构,volatile读操作大致相当于一个无操作指令(no-op),而写操作则会插入一条store barrier指令,以防止写操作与之前的读写操作进行重排序。而对于读操作,大多数平台在读取volatile变量后会插入load barrier指令,以确保不会有指令跑到内存屏障之前进行。

这些内存屏障指令确保了在它们之前的所有内存操作完成后,才执行屏障后面的操作。这正是volatile如何实现其可见性和有序性保证的基础。

3.4 注意事项

虽然volatile可以保证变量修改的可见性和有序性,但它并不能保证复合操作的原子性。比如,即使是volatile变量的自增操作(如volatileVar++)也不是原子的,因为这实际上是由多条指令完成的(读取-修改-写入)。因此,在这种情况下,还是需要使用synchronizedjava.util.concurrent里面的原子类。

4、DCL

 4.1  单例模式

   DCL(DCL 是 "Double-Checked Locking"(双重检验锁)的缩写

class Singleton {
    private static volatile Singleton instance;

    private Singleton() {} // 私有构造函数

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检测
            synchronized (Singleton.class) { // 同步
                if (instance == null) { // 第二次检测
                    instance = new Singleton(); // 问题的根源在这里
                }
            }
        }
        return instance;
    }
}

这个模式看起来非常完美,因为它既保证了只在单例未被创建时进行同步,又避免了每次调用 getInstance() 方法时的同步开销。但在早期 Java 版本中,这个模式是有问题的。

问题在于 instance = new Singleton(); 这行代码并不是原子操作,它涉及到下面的步骤:

  1. 为 Singleton 实例分配内存。
  2. 调用 Singleton 构造函数来初始化成员字段。
  3. 将 instance 对象指向分配的内存空间(执行完这步 instance 就不再是 null 了)。

由于 Java 的编译器允许所谓的 "指令重排"(instruction reordering),以上的执行顺序有可能变成 1-3-2。在多线程环境中,如果线程 A 执行了 1 和 3,还没执行 2,此时线程 B 进行第一次 instance 检查时发现 instance 不是 null,就返回了一个尚未初始化的对象。

为了解决这个问题,instance 字段必须被声明为 volatile。在 Java 5 及之后的版本,volatile 的语义加强了,保证了避免指令重排,并且确保读取 volatile 变量时前面的操作都已经完成,写入 volatile 变量时已经将变更对所有线程可见。这确保了 DCL 的正确实施。

另外,现代 JVM 的 synchronized 实现已经非常轻量化,性能已经大幅提高,并且从 Java 6 开始,还可以利用 java.util.concurrent 包下的 AtomicReference 或相关类更简洁高效地实现懒加载单例模式,所以现在使用 DCL 的场景越来越少。

5、synchronized

synchronized 关键字是 Java 中的一个原子特性,它用于实现线程间的同步,以确保在同一时刻只有一个线程可以访问特定资源。了解 synchronized 的工作原理,有助于更有效地编写多线程代码。这里简要介绍 synchronized 的内部工作机制。

5.1 锁的等级

synchronized 可以用于方法(实例方法和静态方法)和代码块上。根据加锁的目标不同,锁可以分为对象锁(监视器锁)和类锁。

  • 对象锁:当 synchronized 修饰实例方法或同步代码块时,锁定的是对象实例。
  • 类锁:当 synchronized 修饰静态方法或同步代码块并以 .class 对象作为锁时,锁定的是整个类。

5.2 锁的获取与释放

  • 当线程进入 synchronized 修饰的方法或代码块时,它会自动获取锁。
  • 当线程正常退出或抛出异常从而离开这段同步代码时,锁会自动被释放。

这意味着其他线程必须等待直到锁被释放后,才能获得锁进入临界区。

5.3 监视器锁(Monitor)

在 JVM 层面,synchronized 依赖于内部的一个监视器锁(Monitor),这是实现同步的基础。对象在 Java 中都有一个监视器与之关联,当多个线程尝试进入被 synchronized 修饰的代码段时,这些线程会被放置到 Entry Set 或者 Wait Set 中。

  1. Entry Set:所有尝试获取锁的线程将被放置在 Entry Set,当线程获取锁时,它会从 Entry Set 中移出。
  2. Wait Set:若线程已经获得了锁,但调用了该对象的 wait() 方法,它将会被放置在 Wait Set 中。当其他线程调用同一个对象上的 notify() 或 notifyAll() 方法时,等待的线程(可能)会被移出 Wait Set 并尝试重新获取锁。

5.4 锁的优化

随着 JVM 的发展,synchronized 的性能得到了显著提升。一些重要的优化技术包括锁偏向、轻量级锁(自旋锁)和重量级锁。

  • 偏向锁:大多数情况下,锁不仅是不必要的,而且是被同一线程多次获得。偏向锁优化是通过消除锁释放和再次获取的成本,来提高性能的。
  • 轻量级锁:当偏向锁不再适用时(即,有多个线程竞争同一个锁),轻量级锁通过自旋来等待锁释放,以减少线程挂起的开销。
  • 重量级锁:当有大量线程竞争同一个锁,且自旋等待未能获得锁时,轻量级锁会升级为重量级锁,导致其他线程阻塞,直到锁被释放。

通过这些锁的升级和优化,synchronized 能够在不牺牲太多性能的前提下,提供线程安全保障。

synchronized 是 Java 中实现同步的一种基本方式,广泛用于对方法或代码块进行加锁以保障线程安全。随着 JDK 的发展,为了提高 synchronized 的性能,Java 虚拟机(JVM)在内部对其进行了多种优化。这些优化主要围绕锁的升级与降级,主要包括轻量级锁、偏向锁以及锁粗化等技术。

5.5 锁的升级过程

  1. 偏向锁 (Biased Locking) 当一个锁被一个线程访问后,JVM 假定它很可能再次被同一个线程访问。因此,JVM 会将锁对象标记为偏向于该线程,避免在未来的锁获取过程中进行不必要的同步。偏向锁可以通过 JVM 启动参数 -XX:+UseBiasedLocking 开启(在 JDK 6u23 之后,默认启用)。

    如果另一个线程尝试获取这个锁,偏向模式结束,锁会升级为轻量级锁。

  2. 轻量级锁 (Lightweight Locking) 当偏向锁失败时(即,多个线程尝试获取同一个锁),锁会升级为轻量级锁。轻量级锁通过在对象头上存储锁记录(Lock Record)或者锁指针来实现。如果一个线程已经持有了轻量级锁,其他线程会通过自旋等待锁释放。轻量级锁适用于执行时间短且线程争用少的场景。

    轻量级锁如果导致长时间自旋,为了避免 CPU 资源浪费,将会升级为重量级锁。

  3. 重量级锁 (Heavyweight Locking) 如果轻量级锁的自旋已无法获得锁,那么锁会再次升级为重量级锁。这时,JVM 会从操作系统层面请求锁,此种锁会导致其他尝试获取锁的线程被阻塞(挂起状态),直到锁被释放。重量级锁有明显的性能代价,因为涉及到操作系统的线程调度和上下文切换。

6、并发基础

6.1 AQS

AQS(AbstractQueuedSynchronizer)是Java并发包中的一个核心组件,它为实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等)提供了一个可靠的基础框架。AQS在java.util.concurrent.locks包中,是实现锁和同步器的关键抽象类。

6.2 AQS的核心思想

AQS使用一个整数(state)来表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,它还使用了模板方法模式,自身提供了一系列可重写的保护方法来定制状态的获取和释放。

6.3 AQS的主要特性

  1. 状态(State): AQS内部维护一个状态信息,用于控制同步器的状态。具体意义取决于实现,例如,对于ReentrantLock,状态指的是持有锁的数量;对于Semaphore,状态指的是当前可用的许可证数量。

  2. 节点(Node)和等待队列: 等待获取资源的线程会被封装成节点(Node)实例,并加入到AQS维护的一个FIFO队列中。如果一个线程获取同步状态失败,它会成为该队列中的一个节点。

  3. 获取(acquire)和释放(release)方法: AQS提供了一系列acquire方法来获取状态,以及一系列release方法来释放状态。这些方法可以自定义状态的获取和释放逻辑,以适应不同的同步组件。

6.4 AQS的方法分为两类

  1. 独占模式: 这种模式下每次只能有一个线程持有资源,例如ReentrantLock就是基于AQS独占模式实现的。

  2. 共享模式: 这种模式下多个线程可以同时持有资源,例如SemaphoreCountDownLatch等基于AQS的共享模式实现的。

6.5 AQS提供的方法概览

  • int getState(): 获取当前同步状态。
  • void setState(int newState): 设置当前同步状态。
  • boolean compareAndSetState(int expect, int update): 原子地更新同步状态,期望值与实际值一致时,更新为新的值。

6.6 AQS的子类需要重写的方法

  • protected boolean tryAcquire(int arg): 独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • protected boolean tryRelease(int arg): 独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • protected int tryAcquireShared(int arg): 共享方式。尝试获取资源。负数表示失败;0表示成功,但没有可用资源;正数表示成功,且有剩余资源。
  • protected boolean tryReleaseShared(int arg): 共享方式。尝试释放资源,成功则返回true,失败则返回false。

AQS的这些方法提供了同步器的基本行为,具体的同步组件(如ReentrantLock, Semaphore, CountDownLatch, ReadWriteLock等)会根据需求实现这些方法,以提供其独特的同步机制。

总之,AQS是实现同步锁和其他同步器的一个强大的框架,它利用了状态、队列和模板方法模式的概念来简化同步器的实现,并提供了高效、可靠的多线程同步功能。

6.7 线程的阻塞与唤醒

在JDK的并发包中,线程的阻塞和唤醒是由底层操作系统的线程调度器和Java虚拟机(JVM)协同完成的。Java提供了多种机制来控制线程的阻塞与唤醒,这些机制利用锁、条件变量、等待/通知模式等来管理线程状态。下面讨论一些主要概念和方法:

6.7.1 synchronized 和 Object 的 wait/notify 机制

synchronized关键字可以用来提供一个代码块或者方法的同步锁。 当一个线程进入synchronized代码块时,它获取了一个锁(monitor),其他线程必须等待这个锁被释放才能进入代码块。

  • wait(): 一个线程在持有monitor锁的条件下,可以调用对象的wait()方法导致当当前线程挂起并且释放锁。该线程会放到对象的等待队列中。

  • notify()/notifyAll(): 同样在持有monitor锁的条件下,可以调用对象的notify()方法或notifyAll()方法来唤醒在该对象上调用wait()方法而挂起的线程。notify()唤醒单个线程,而notifyAll()唤醒所有线程。

6.7.2 Lock 和 Condition 接口

Lock接口提供了比synchronized关键字更加丰富的锁操作功能。这些功能包括尝试非阻塞地获取锁、能被中断的锁获取操作,以及超时的锁获取操作。

Lock相关联的是Condition接口,它类似于Object类中的wait()notify()notifyAll()方法:

  • await(): 类似于Object类中的wait()方法,可以使线程挂起并释放锁。

  • signal()/signalAll(): 这些方法类似于Object类中的notify()notifyAll(),可以唤醒一个或多个在Condition上等待的线程。

6.7.3 AQS (AbstractQueuedSynchronizer)

AQS在其内部使用了一个FIFO队列来管理那些请求了互斥锁但是没有成功获取锁(即处于阻塞状态)的线程。当阻塞的线程被唤醒(比如持有锁的线程释放了锁),AQS会选择一个或多个线程来尝试获取锁,并从阻塞状态恢复为就绪状态。

AQS使用了几种状态来控制线程的阻塞与唤醒:

  • acquire(int arg): 如果没有成功获取到锁,会调用内部的方法将线程包装成节点并加入等待队列。
  • release(int arg): 当持有锁的线程执行完成并释放锁时,会调用内部的方法唤醒等待队列中的后续节点(即线程)。

6.7.4 操作系统的支持

Java线程的阻塞和唤醒最终是通过操作系统的支持实现的,无论是使用synchronizedLockCondition还是AQS,底层都涉及到操作系统对线程状态的控制。Java虚拟机通过本地方法接口(JNI)调用操作系统提供的线程相关函数来完成线程的挂起、唤醒和调度工作。

以上方法,不同的同步机制提供了多种多样的管理线程阻塞和唤醒的方式,而在底层,这些机制都依赖于操作系统的线程调度器以及JVM的实现细节来保证其准确性和效率。

6.8 CAS

CAS(比较并交换,Compare-And-Swap)是一种用于实现多线程同步的机制,主要通过硬件提供的原子指令来实现。CAS操作包含三个操作数——内存位置(V,表示要更新的变量),预期原值(A),和新值(B)。CAS指令执行时,只有当内存位置的值与预期原值A相等,系统才会自动将该位置值更新为新值B;如果内存位置的值与A不相等,表示有其他线程已经修改了该值,则CAS操作失败。

CAS操作是无锁编程的基础,因为它允许在不使用锁的情况下进行同步。许多现代编程语言都提供了对CAS操作的支持。在Java中,java.util.concurrent.atomic包下的原子类使用了CAS操作来实现原子性操作。

CAS操作有以下步骤:

  1. 读取当前的值。
  2. 计算新值。
  3. 使用CAS指令尝试原子更新,比较当前值与原始读取的值。
  4. 如果期间未被其他线程更新,则更新成功;如果已被其他线程更新,则失败,可能需要重试。

CAS的好处是性能高,因为它不涉及锁定,所以没有锁的开销,也就避免了上下文切换和死锁的风险。但CAS也有其缺点:

  • ABA问题:由于CAS需要在操作时检测当前值是否发生过变化(只检查了“现在”和“之前”),假如一个变量原来是A,变成了B,然后又变回A,使用CAS进行比较时会认为它没有变化。虽然看起来没有问题,但在并发情况下可能会带来问题。解决办法是使用带版本号的CAS,如AtomicStampedReference,它可以通过版本号来解决这个问题。

 7、锁

 7.1 ReentrantLock

ReentrantLock 是 Java 中的同步机制,用于在多线程场景中实现锁定,保证临界区的代码能够互斥访问,从而避免并发问题。ReentrantLock 实现了 Lock 接口,并提供了与 synchronized 关键字类似的同步功能,但比 synchronized 提供更多的灵活性和扩展性。

7.2 ReentrantLock 的底层原理

  1. 状态变量: ReentrantLock 内部维护了一个表示锁状态的变量,用来记录锁的占用情况。这个状态变量在无锁状态时为 0,在锁被某个线程占用时至少为 1,如果锁是可重入的,状态变量表示锁被持有的次数。

  2. CAS操作: ReentrantLock 使用 compareAndSet 操作(CAS)来尝试获取锁。如果状态变量为 0,CAS 将其设置为 1,表明线程获取了锁。如果状态变量不为 0,则表示锁已经被其他线程占用。

  3. AQS框架: ReentrantLock 底层采用了 AbstractQueuedSynchronizer(AQS)这一同步框架。AQS 使用一个双向队列(CLH 队列)来维护等待锁的线程。如果线程尝试获取锁失败,它会被加入到等待队列的尾部,并处于阻塞状态。

  4. 锁的释放: 当锁的持有者释放锁时,状态变量会被更新。如果有线程在等待队列中,AQS 框架将唤醒队列头部的线程来尝试获取锁。

  5. 公平锁与非公平锁: ReentrantLock 提供公平锁和非公平锁。公平锁的意味着在释放锁时,锁会被授予等待时间最长的线程;非公平锁则允许插队(刚好有新的线程请求获取锁,由于这个线程是未阻塞的,比起唤醒阻塞的线程代价小,这样可以减少线程切换的开销,提供吞吐量),可能导致某些线程等待时间过长,甚至饿死。

7.3 如何使用 ReentrantLock

使用 ReentrantLock 时,需要在代码中显式地获取锁和释放锁。下面是一个简单的使用示例:

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock(); // 获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        
        // ...线程A
        new Thread(() -> {
            counter.increment();
        }).start();
        
        // ...线程B
        new Thread(() -> {
            counter.increment();
        }).start();
        
        // 主线程继续其他工作...
    }
}

在上面的例子中,increment 方法中使用了 ReentrantLock 来确保计数器 count 的自增操作是线程安全的。

总的来说,ReentrantLock 提供了一个更加灵活、更为细粒度的线程同步机制,但要注意始终在一个 try 块中获取锁,并在相应的 finally 块中释放锁,这样可以确保即使在发生异常的情况下,锁也会被正确释放。

7.4 ReentrantReadWriteLock

ReentrantReadWriteLock是Java中的一个高级同步锁,提供了一种具有读写锁特性的同步机制。它允许多个线程同时读取一个资源,但只允许一个线程写入资源。这种机制可以提高性能,特别是在读操作远多于写操作的情况下。

7.4.1 特点

  1. 读写分离: 锁分为读锁和写锁,多个线程可以同时持有读锁,但写锁是独占的。
  2. 公平性选项: 可以配置为公平锁或非公平锁。公平锁按照请求锁的顺序来获取锁,非公平锁则不保证这点。默认情况下是非公平模式。
  3. 可重入性: 线程可以重复获取已经持有的锁。这意味着当一个线程获得写锁后,它可以再次申请得到这个写锁,同理读锁也是。
  4. 锁降级: 支持锁降级从写锁到读锁,反之则不行。
  5. 中断支持: 支持在锁的等待过程中响应中断。

与只在写上加ReentrantLock锁,读不加锁相比,能提供严格的一致性。(写时,读阻塞)

7.4.2 Condition

在Java中,Condition接口配合锁(Lock)使用,提供了一种比传统Object监视器方法(waitnotifynotifyAll)更强大和灵活的线程间协调机制。Condition接口提供了一种分离锁对象(Lock)和等待/通知的方法,它允许多个等待集合与单个锁相关联。

使用Condition时,需要与一个实现了Lock接口的锁对象相关联。通常这是ReentrantLock类的一个实例。每个Condition实例提供了类似于Object监视器方法的行为,但它们对各自的Condition实例来说是相互独立的。

一个典型的Condition对象使用模式是这样的:

  1. 获取一个Lock对象,并与之关联一个Condition对象。
  2. 锁定Lock锁,以获取对相关资源的独占访问。
  3. 调用Condition.await()方法使当前线程等待,直到它被另一个线程的Condition.signal()Condition.signalAll()方法唤醒,或被中断。
  4. 当线程被唤醒后,尝试重新获取锁并继续执行。
  5. 在离开临界区之前,释放锁。

下面是一个使用ReentrantLockCondition的简单例子:

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ProducerConsumerExample {
    private final int MAX_CAPACITY = 5;
    private final Queue<Integer> queue = new LinkedList<>();
    private final Lock lock = new ReentrantLock();
    private final Condition bufferNotFull = lock.newCondition();
    private final Condition bufferNotEmpty = lock.newCondition();

    // 生产者方法
    public void produce(int value) {
        lock.lock(); // 获取锁
        try {
            // 当队列满时,等待
            while (queue.size() == MAX_CAPACITY) {
                bufferNotFull.await();
            }
            // 在队列不满时生产一个元素
            queue.add(value);
            System.out.println("Produced: " + value);

            // 通知消费者队列不为空了
            bufferNotEmpty.signalAll(); // 唤醒所有等待消费的线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 确保释放锁
        }
    }

    // 消费者方法
    public void consume() {
        lock.lock(); // 获取锁
        try {
            // 当队列为空时,等待
            while (queue.isEmpty()) {
                bufferNotEmpty.await();
            }
            // 在队列不为空时消费一个元素
            int value = queue.poll();
            System.out.println("Consumed: " + value);

            // 通知生产者队列现在不是满的了
            bufferNotFull.signalAll(); // 唤醒所有等待生产的线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 确保释放锁
        }
    }
}

在这个示例中:

  • 我们有一个MAX_CAPACITY常量来表示队列可以拥有的最大元素数量。
  • produce()方法用于将元素放入队列中,并在队列满时等待。
  • consume()方法用于从队列相取元素,并在队列空时等待。
  • produce()方法在新元素被加入后用signalAll()来唤醒所有可能因为队列空而等待的消费者。
  • consume()方法在元素被消费后用signalAll()来唤醒所有可能因为队列满而等待的生产者。
  • bufferNotFullbufferNotEmpty是与lock锁相关联的两个Condition,分别用于生产者等待/通知和消费者等待/通知。

请注意,由于多个生产者或多个消费者可能会同时被唤醒,所以仍然需要在生产消费逻辑中使用循环(while而不是if)来做判断,以防止虚假唤醒导致的问题。

8、并发工具类

8.1 CountDownLatch

CountDownLatch 是 Java 并发编程中的一个同步辅助类,可用于确保一个任务在其他一组任务全部完成之前,一直等待阻塞。与CyclicBarrier不同,CountDownLatch只能使用一次,其计数器无法重置。

// 假设我们需要等待5个任务
final int taskCount = 5;
final CountDownLatch latch = new CountDownLatch(taskCount);

for (int i = 0; i < taskCount; i++) {
    new Thread(() -> {
        try {
            // 执行任务
            System.out.println(Thread.currentThread().getName() + " 执行任务");
            // 模拟延时
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 每完成一个任务,计数器减1
            latch.countDown();
        }
    }).start();
}

try {
    // 主线程在此等待,直到所有任务都执行完毕(计数器为0)
    latch.await();
} catch (InterruptedException e) {
    e.printStackTrace();
}

System.out.println("所有任务执行完毕,主线程继续执行...");

8.2 CyclicBariier

CyclicBarrier 通常用于在并发程序中实现一种同步机制,即多个线程必须等待彼此同时到达一个共同点,才能各自继续执行。它可以重复使用。

final int threadCount = 3;
CyclicBarrier barrier = new CyclicBarrier(threadCount);

for(int i = 0; i < threadCount; i++) {
    Thread thread = new Thread(() -> {
        System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行任务。");
        try {
            barrier.await();  // 等待其他线程
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
        System.out.println("线程 " + Thread.currentThread().getName() + " 继续执行后续操作。");
    });
    thread.start();
}

外面可以再套一层循环,打印多轮次。

8.3 Semphore

Semaphore(信号量)是Java并发库(java.util.concurrent)中提供的一种同步机制,它可以控制对共享资源的访问数量。通过使用信号量,你可以实现资源池,限制访问资源的线程数量,从而实现如限流控制等功能。在信号量上,线程可以申请许可(permits),当许可可用时,线程可以访问受限资源;当完成资源访问后,线程释放许可,以供其他线程使用。

8.3.1 Semaphore的基本用法

创建Semaphore实例时,你需要指定许可的总数量。Semaphore提供了两个主要方法:acquire()用于获取一个许可,如果没有可用许可它会阻塞直到有许可成为可用;release()用于释放许可,增加可用许可的数量。

import java.util.concurrent.Semaphore;

public class SemaphoreExample {

    public static void main(String[] args) {
        // 创建一个Semaphore实例,它只有3个许可
        Semaphore semaphore = new Semaphore(3);

        // 模拟10个线程,每个线程都尝试获取许可
        for (int i = 0; i < 10; i++) {
            final int index = i;
            new Thread(() -> {
                try {
                    semaphore.acquire();  // 获取一个许可
                    System.out.println("线程" + index + "获得许可");

                    // 模拟工作: 线程持有许可一段时间
                    Thread.sleep((long) (Math.random() * 10000));

                    semaphore.release();  // 释放许可
                    System.out.println("线程" + index + "释放许可");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

8.3.2 信号量的公平性

在创建信号量时,可以选择是否启用公平模式。默认情况下,Semaphore是非公平的,这意味着线程获取许可的顺序不一定按照申请许可的顺序进行。如果选择启用公平模式,那么在多个线程等待许可时,等待时间最长的线程将会优先获得许可。

Semaphore fairSemaphore = new Semaphore(permits, true); // 启用公平模式

9、并发集合

9.1 ConcurrentHashMap

在Java 1.7中,ConcurrentHashMap内部使用了分段锁的机制(Segmentation),其核心组件是一个叫做Segment的数组。每个Segment本质上是一个小的哈希表,附带着一个锁。每个Segment独立地锁定,因此多线程可以同时操作不同的Segment,这样即使是在写操作时也允许高度的并发。当需要执行put或者remove操作时,线程首先根据哈希值选择正确的Segment,再对这个Segment加锁。

这里的关键思想是细化锁的粒度:不是每一次写操作都锁定整个数据结构,而是锁定数据被分成了多个部分,这样就多个写操作往往可以同时进行。

Java 1.8中ConcurrentHashMap还引入了一种不同的更新策略 —— CAS(Compare-And-Swap)操作与synchronized同步块。对于普通的Node进行更新时,首先尝试使用基于CAS的无锁机制;如果CAS操作失败,意味着有其他线程在进行更新操作,这时会使用synchronized来进行加锁处理。

9.2 ConcurrentLinkedQueue

10、原子操作

10.1 基本类型

10.1.1 AtomicBoolean

AtomicBooleanAtomicInteger 类似,其原子性是通过 CAS(Compare-And-Swap)操作实现的。CAS 是最底层的一种原子操作,直接由 CPU 提供支持。CAS 操作需要指定三个参数:一个内存地址 V,一个期望值 E 和一个新值 A。如果地址 V 上的值与期望值 E 相等,那么就将这个地址上的值设置为新值 A。整个操作是原子性的,这意味着中间不会被其他线程打断。

在 Java 中,AtomicBoolean内部使用一个 volatile int 来表示布尔值,这个 int 类型的值为 0 时表示 false,非 0(通常为 1)时表示 truevolatile 修饰符保证了该变量的可见性,即一个线程内对这个变量的修改会立即被其他线程所观察到。

10.1.2 AtomicInteger

AtomicInteger 的原子性主要是依靠 CAS(Compare-And-Swap)算法实现的。CAS 操作包含三个操作数 —— 内存位置(V, 表示变量的地址)、预期原值(E)和新值(A)。CAS 操作的执行逻辑是:

  • 如果内存位置的当前值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。
  • 否则,处理器不做任何操作。
  • 不论更新与否,它都会返回该位置的旧值。

CAS 是一种无锁编程的重要概念,其优点是减少了锁的使用,降低了线程阻塞的可能性,从而提高系统性能。然而,它的缺点包括可能的活锁(两个或多个线程不断尝试更新变量却始终失败,循环重试)以及只能保证一个变量的原子性操作。

 10.1.3 AtomicLong

AtomicLong 具体如何使用 CAS 实现原子操作呢?其内部有一个 volatile long 类型的值,它使用 Unsafe 类来直接执行原子操作。由于 volatile 修饰符的存在,确保了这个变量在多个线程之间的可见性,而 CAS 操作确保了原子性。

10.2 数组

10.2.1 AtomicIntegerArray

AtomicIntegerArray 是 Java 并发包 java.util.concurrent.atomic 中提供的一个类,它保证了对整型数组的元素进行线程安全的原子操作。AtomicIntegerArray 是内部持有一个 int 类型数组的一个封装器,它确保在并发访问的环境下,对数组元素的修改是原子的,并且能够立即对所有线程可见(因为数组的每个元素都被声明为 volatile)。

10.2.2 AtomicLongArray

与上面类似

10.2.3 AtomicReferenceArray

参考AtomicReference,他只是个数组。

10.3 引用类型

10.3.1 AtomicReference

        存储一个对象,他能保证对这个对象的引用操作是原子的,不能保证操作这个对象的内部字段是原子的。

10.3.2 AtomicReferenceFieldUpdater

AtomicReferenceFieldUpdater 是 Java 并发包 java.util.concurrent.atomic 中的一个工具类,它提供了一种线程安全的方式来更新对象的引用字段,即便这个字段不是声明为 volatile 类型的。通过这个类,你可以在不暴露对象的私有字段情况下保持这些字段的线程安全,同时也避免了 AtomicReference 对象本身的包装额外开销。

10.3.3 如何使用 AtomicReferenceFieldUpdater

首先,你需要创建 AtomicReferenceFieldUpdater 的一个实例。下面是一个使用 AtomicReferenceFieldUpdater 的基本示例:

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

class Person {
    volatile String name; // 必须是 volatile 字段

    Person(String name) {
        this.name = name;
    }
}

public class AtomicReferenceFieldUpdaterDemo {
    private static final AtomicReferenceFieldUpdater<Person, String> nameUpdater =
        AtomicReferenceFieldUpdater.newUpdater(Person.class, String.class, "name");

    public static void main(String[] args) {
        Person person = new Person("John");

        // 使用 AtomicReferenceFieldUpdater 来更新字段值
        boolean updated = nameUpdater.compareAndSet(person, "John", "Jane");
        System.out.println("Updated: " + updated + ", New name: " + person.name);
    }
}

11、线程池

11.1 Callable和Future

Callable是实现多线程的一中, 其他两种是实现Runnable接口和继承Thread类。Callable会返回一个Future,且可以抛出异常。

Future get方法阻塞的原理,可以见FutureTask的awaitDone方法,主要是用到了自旋(for(;;))和CAS,通过判断state是否已经执行完。

11.2 CompletableFuture

12 、其他

12.1 ThreadLocal

每个线程类都有个内部类ThreadLocalMap,他是以ThreadLocal为key,用户变量为value。因为多线程下,虽然key是一样的,大家的map是隔离的,也就是线程是安全的。

12.2 Fork/Join

12.2.1 使用场景

Fork/Join框架适用于可分解(分而治之)的大型计算密集型任务,比如:

  • 数组或集合的并行处理
  • 并行快速排序或归并排序
  • 多维数组的大规模操作
  • 并行搜索任务

12.2.2 使用频率

尽管Fork/Join在引入时被誉为并行编程的重要工具,但在实际开发中,它并不是总是首选。对于某些任务来说,它可能比传统的线程池(如ExecutorService)更复杂,而后者很容易被理解和使用。此外,性能并不总是预期那样好,特别是在任务不能很好地分解或者合并结果的消耗很高时。

随着 Java 8 引入的Streams API,其提供了比Fork/Join更高层次的抽象,很多情况下开发者更偏向于使用 Streams 进行并行数据处理,因为它提供了更简单直观的并行处理模型,并且可以自动利用Fork/Join框架。

总之,Fork/Join的使用并不像其他并发机制(如同步、锁、普通的线程池)那样普遍,但它仍然是解决特定问题(尤其是需要分解和递归来处理的并行任务)的有力工具。正确选择并发机制需要根据具体的任务需求、易用性和性能特点来决定。

13、Java线程间通信

         1、使用共享变量(静态变量)

         2、使用Object类的wait()/notify()/notifyAll

         3、使用ReentrantLock加锁类的线程的Condition类的await()/signal()/signalAll()

         4、使用管道进行线程间通信:1)字节流;2)字符流

         5、使用JDK并发工具、并发集合

14、Java线程同步方法

         1、同步方法(synchronized方法)

         2、同步代码块(synchronized代码块)

         3、使用特殊域变量(volatile)实现线程同步(比较适合一写多读)

         4、使用重入锁实现线程同步(ReentrantLock lock()/unlock()方法)

         5、使用局部变量实现线程同步 (个人认为相当于绕过了问题)

         6、使用阻塞队列等并发容器

         7、使用原子变量实现线程同步  

个人认为线程同步方法只有一种,那就是加锁。线程同步就是将并发变成串行,上述方法除了volatile和threadlocal,其他的都是锁,或者底层都使用了锁。使用JDK提供的丰富的并发工具能大幅度简化代码,但实质都是使用了锁,像阻塞队列源码,大量使用了synchronized关键字。

JDK 1.6发布之后,人们就发现Synchronized与ReentrantLock的性能基本上是完全持平了。因此,如果读者的程序是使用JDK 1.6或以上部署的话,性能因素就不再是选择ReentrantLock的理由了,虚拟机在未来的性能改进中肯定也会更加偏向于原生的Synchronized,所以还是提倡在Synchronized能实现需求的情况下,优先考虑使用Synchronized来进行同步----《深入理解Java虚拟机》。

所以如果你的编程能力比较强,一把锁走天下也不是不可以。(synchronized+wait/notify/notifyall,因为并发工具JDK1.5之后才提供的,早期的代码也不得不这么写)。当然我也还是推荐使用高级的API。

线程间通信侧重线程之间交流、通信,线程同步是为了保护不能并发的资源,往往是将并发变成串行。

参考

https://www.cnblogs.com/xdyixia/p/9386133.html

https://www.cnblogs.com/XHJT/p/3897440.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

济南大飞哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值