当前位置: 首页 > article >正文

Java并发(知识整理)

并发入门

一、java 多线程入门

  • 进程,是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发。
  • 线程,是进程的子任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。

创建线程的三种方法(前两个的缺点:在执行完任务之后无法获取执行结果。)

  • 继承 Thread 类,重写 run 方法;
  • 实现 Runnable 接口,重写 run 方法;
  • 实现 Callable 接口,重写 call 方法,该方法通过 FutureTask 获取任务执行的返回值;

默认的 run()方法不做任何事情,为了让线程执行实际的任务,故且需要重写;

  • run():封装线程执行的代码,直接调用相当于调用普通方法。
  • start():启动线程,然后由 JVM 调用此线程的 run() 方法。

实现 Runnable 接口好的原因;

  • 避免了 Java 单继承的局限性,Java 不支持多重继承,因此如果我们的类已经继承了另一个类,就不能再继承 Thread 类了
  • 适合多个相同的程序代码去处理同一资源的情况,把线程、代码和数据有效的分离,更符合面向对象的设计思想。Callable 接口与 Runnable 非常相似,但可以返回一个结果

控制线程的方法:

  • sleep():使当前正在执行的线程暂停指定的毫秒数,也就是进入休眠的状态。(使用 sleep 的时候要对异常进行处理
  • join():等待这个线程执行完才会轮到后续线程得到 cpu 的执行权,使用这个也要捕获异常。(即使用这个方法的线程获得优先处理的权限
  • setDaemon():将此线程标记为守护线程,准确来说,就是服务其他的线程,像 Java 中的垃圾回收线程,就是典型的守护线程。(即使用该方法能够在后台一直运行,优先级低,当然主线程执行完毕,jvm 退出,该线程会停止的
  • yield():是一个静态方法,用于暗示当前线程愿意放弃其当前的时间片,允许其他线程执行。然而,它只是向线程调度器提出建议,调度器可能会忽略这个建议。具体行为取决于操作系统和 JVM 的线程调度策略。(暗示建议线程放弃,但偶尔会失效

并发编程中的线程之间存在以下两个问题:

  • 线程间如何通信?即:线程之间以何种机制来交换信息
    • 共享变量:多个线程可以访问共享的变量,通过对共享变量的读写实现信息的传递。
    • 管道 (Pipes):利用输入流和输出流的方式进行线程间的通信。
    • 条件变量 (Condition Variables):使用 wait() 和 notify() 方法,线程可以在条件不满足时等待,条件满足时通知其他线程。
    • BlockingQueue:Java 提供的 java.util.concurrent 包中的 BlockingQueue 接口,允许线程安全地在多个线程之间传递数据。

.

  • 线程间如何同步?即:线程以何种机制来控制不同线程间发生的相对顺序
    • synchronized 关键字:Java 提供的内置锁,通过在方法或代码块上加上 synchronized 修饰符,确保同一时刻只有一个线程可以访问被修饰的代码。
    • Lock 接口:java.util.concurrent.locks 包中的 Lock 接口提供了比 synchronized 更灵活的锁机制,支持可重入锁、公平锁等。
    • Semaphore:信号量是一种计数信号机制,用于控制访问共享资源的线程数量。
    • CountDownLatch 和 CyclicBarrier:这些是用于线程之间同步的工具,允许线程在特定条件下等待,或者在特定数量的线程到达时继续执行
      .

      由于上面存在的问题,诞生了一些模型来应对,这里只举例三个,如:

      • 共享内存模型:多个线程通过共享内存区进行通信和数据交换。
      • 消息传递模型:线程间通过消息传递进行通信,而不共享内存。
      • 事件驱动模型:通过事件机制进行通信,线程响应特定事件的发生。
        值得注意的是:Java 使用的是共享内存并发模型

二、Callable、 ExecutorService、Future 和 FutureTaskd 的理解

  1. Callable
  • 定义:Callable 是一个函数式接口,类似于 Runnable,但它可以返回结果并且可以抛出异常。
  • 用途:用于创建可以在多线程中执行的任务,并能够返回结果。
Callable<Integer> task = () -> {
    // 执行一些计算
    return 123;
};
  1. ExecutorService
  • 定义:ExecutorService 是 Java 中用于管理和控制线程池的接口。它提供了一个高效的方式来管理多个线程的执行,避免了手动创建和管理线程的复杂性。
  • 主要功能:
    • 线程池管理:可以创建固定大小的线程池、缓存线程池等,方便地管理线程。
    • 任务提交:可以提交 Runnable 或 Callable 任务进行异步执行。
    • 任务调度:提供了调度任务在未来某个时间执行的功能。
    • 优雅关闭:可以优雅地关闭线程池,确保已经提交的任务完成后再关闭。
  • 常用方法:
    • submit(): 提交一个任务并返回一个 Future,用于获取结果。
    • invokeAll(): 提交一组任务并等待所有任务完成,返回结果列表。
    • shutdown(): 启动关闭线程池,不再接受新任务,但会完成已提交的任务。
    • shutdownNow(): 尝试立即关闭线程池,返回尚未执行的任务列表。
ExecutorService executorService = Executors.newFixedThreadPool(3); // 创建一个包含3个线程的池

Callable<Integer> task = () -> {
    // 执行一些计算
    return 123;
};

Future<Integer> future = executorService.submit(task); // 提交任务并获取 Future

executorService.shutdown(); // 关闭线程池
  1. Future
  • 定义:Future 是一个接口,代表异步计算的结果。它提供了一些方法来检查计算是否完成、等待计算完成、获取计算结果等。
  • 主要方法:
    • get(): 获取计算结果,可能会阻塞直到结果可用。
    • isDone(): 检查计算是否完成。
    • cancel(): 取消任务。
Future<Integer> future = executorService.submit(task);
if (!future.isDone()) {
    // 任务还在运行
}
Integer result = future.get(); // 获取结果
  1. FutureTask
  • 定义:FutureTask 是 Runnable 和 Future 的结合体。它是一个可以被执行的任务,并且能获取执行结果。
  • 用途:可以用作 Callable 的实现,支持取消和查询状态。
FutureTask<Integer> futureTask = new FutureTask<>(task);
new Thread(futureTask).start();
Integer result = futureTask.get(); // 获取结果

三、线程的 6 种状态

OS 线程状态三个:

  • 就绪状态(ready):线程正在等待使用 CPU,经调度程序调用之后进入 running 状态。
  • 执行状态(running):线程正在使用 CPU。
  • 等待状态(waiting): 线程经过等待事件的调用或者正在等待其他资源(如 I/O)。

Java 线程状态六个:

// Thread.State 源码
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

  1. 新建状态 (New)
  • 描述:线程被创建但尚未开始运行。
  • 作用:在这个状态下,线程对象已经被实例化,但 start() 方法未被调用。
  • 功能:可以调用 start() 方法使线程进入就绪状态。
  1. 就绪状态 (Runnable)
  • 描述:线程在就绪状态下,等待 CPU 分配时间片以便运行。
  • 作用:线程已经准备好执行,但可能因为系统调度原因而未立即运行。
  • 功能:当 start() 被调用后,线程进入此状态; 系统会根据线程优先级调度执行。
  1. 运行状态 (Running)
  • 描述:线程正在执行其任务。
  • 作用:这是线程生命周期中的主要活动状态,线程正在运行其代码。
  • 功能:执行线程中的 run() 方法的代码。
  1. 阻塞状态 (Blocked)
  • 描述:线程因等待获取锁而被阻塞。
  • 作用:当一个线程试图访问一个被其他线程占用的同步块或方法时,它会进入此状态。
  • 功能:等待获得所需资源(如锁),一旦获得锁后,它将重新进入就绪状态。
  1. 等待状态 (Waiting)
  • 描述:线程等待其他线程的特定操作(如通知、加入等)而处于等待状态。
  • 作用:线程在此状态下不会消耗 CPU 资源,直到其他线程发出通知或中断。
  • 功能:通过调用 Object.wait()、Thread.join() 或 LockSupport.park() 等方法进入此状态。
  1. 超时等待状态 (Timed Waiting)
  • 描述:线程在指定的时间内等待其他线程的操作。
  • 作用:线程在此状态下会在超时后自动返回就绪状态。
  • 功能:通过调用 Thread.sleep(milliseconds)、Object.wait(milliseconds)、Thread.join(milliseconds) 等方法进入此状态。
  1. 终止状态 (Terminated)
  • 描述:线程已完成执行或由于异常终止。
  • 作用:线程执行完毕或由于未处理的异常而停止。
  • 功能:此状态无法返回,线程对象仍然存在,但其生命周期结束。

四、线程组和线程优先级

线程组 (Thread Group)

  • 定义:线程组是一个包含多个线程的集合。它用于管理和控制一组线程的生命周期和行为。
  • 作用:
    • 组织管理:可以将相关的线程归为一组,方便管理
    • 统一控制:可以对整个线程组进行操作,例如启动、停止、挂起或恢复所有线程
  • 方法:
    • ThreadGroup(String name):创建一个新的线程组。
    • start():启动线程组中的所有线程。
    • interrupt():中断线程组中的所有线程。
    • activeCount():返回当前线程组中活动线程的数量。
    • list():列出线程组中的所有线程及其属性。
ThreadGroup group = new ThreadGroup("MyThreadGroup");

Thread thread1 = new Thread(group, () -> {
    // 线程任务
}, "Thread1");

Thread thread2 = new Thread(group, () -> {
    // 线程任务
}, "Thread2");

thread1.start();
thread2.start();

System.out.println("Active threads: " + group.activeCount());
group.list(); // 列出所有线程

线程优先级 (Thread Priority)

  • 定义:线程优先级是一个整数值,用于指示线程相对于其他线程的优先级。优先级范围为 1 到 10,默认值为 5。
  • 作用:
    • 调度影响:优先级用于影响线程调度,允许系统决定哪个线程应该被优先执行。
    • 资源分配:高优先级的线程可能在资源有限的情况下获得更多的 CPU 时间
  • 方法:
    • setPriority(int priority):设置线程的优先级。
    • getPriority():获取线程的当前优先级。
    • Thread.MIN_PRIORITY:优先级最低(1)。
    • Thread.NORM_PRIORITY:默认优先级(5)。
    • Thread.MAX_PRIORITY:优先级最高(10)。
Thread thread1 = new Thread(() -> {
    // 线程任务
});
thread1.setPriority(Thread.MAX_PRIORITY); // 设置为最高优先级
thread1.start();

线程组和线程优先级之间的关系

  • 如果某个线程的优先级大于线程所在线程组的最大优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级;
  • Java 只是给操作系统一个优先级的参考值,线程最终在操作系统中的优先级还是由操作系统决定;
  • Java 默认的线程优先级为 5,线程的执行顺序由调度程序来决定,线程的优先级会在线程被调用之前设定;
  • 通常情况下,高优先级的线程将会比低优先级的线程有更高的概率得到执行;
  • 线程的调度策略采用抢占式的方式,优先级高的线程会比优先级低的线程有更大的几率优先执行。
  • 在优先级相同的情况下,会按照“先到先得”的原则执行。
  • 每个 Java 程序都有一个默认的主线程,就是通过 JVM 启动的第一个线程——main 线程。
  • 如果某线程是守护线程,那如果所有的非守护线程都结束了,这个守护线程也会自动结束。
  • 当所有的非守护线程结束时,守护线程会自动关闭,这就免去了还要继续关闭子线程的麻烦。
  • 线程默认是非守护线程,可以通过 Thread 类的 setDaemon 方法来设置为守护线程

五、进程与线程的区别

  • 进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即 CPU 分配时间的单位
  • 进程拥有独立的内存空间,线程共享所属进程的内存空间。
  • 进程的创建和销毁需要资源的分配和回收,开销较大;线程的创建和销毁只需要保存寄存器和栈信息,开销较小。
  • 进程间的通信比较复杂,而线程间的通信比较简单。
  • 进程间是相互独立的,一个进程崩溃不会影响其他进程;线程间是相互依赖的,一个线程崩溃可能影响整个程序的稳定性。

六、线程的安全问题

线程安全问题

  • 原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
    • 原子操作:即不会被线程调度机制打断的操作,没有上下文切换。
  • 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

活跃性是指某件正确的事情最终会发生,但当某个操作无法继续下去的时候,就会发生活跃性问题。

  • 死锁:死锁是指多个线程因为环形等待锁的关系而永远地阻塞下去。
  • 活锁:当多个线程都在运行并且都在修改各自的状态,而其他线程又依赖这个状态,就导致任何一个线程都无法继续执行,只能重复着自身的动作,于是就发生了活锁。
  • 饥饿:
    • 高优先级的线程一直在运行消耗 CPU,所有的低优先级线程一直处于等待;
    • 一些线程被永久堵塞在一个等待进入同步块的状态,而其他线程总是能在它之前持续地对该同步块进行访问;

性能

  • 创建线程:是直接向系统申请资源的,对操作系统来说,创建一个线程的代价是十分昂贵的,需要给它分配内存、列入调度等。
  • 上下文切换:当 CPU 从执行一个线程切换到执行另一个线程时,CPU 需要保存当前线程的本地数据,程序指针等状态,并加载下一个要执行线程的本地数据,程序指针等
    • 解决方法:
      • 无锁并发编程:可以参照 ConcurrentHashMap 锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
      • CAS 算法,利用 Atomic + CAS 算法来更新数据,采用乐观锁的方式,可以有效减少一部分不必要的锁竞争带来的上下文切换。
      • 使用最少线程:避免创建不必要的线程,如果任务很少,但创建了很多的线程,这样就会造成大量的线程都处于等待状态。
      • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

七、JMM 内存模型


JMM:一个抽象的概念,定义了 Java 程序中变量的访问规则,确保多线程环境下的可见性、原子性和有序性。

  • 可见性:确保一个线程对共享变量的修改能被其他线程看到。
  • 原子性:确保操作在执行时不可被中断。
  • 有序性:确保操作的执行顺序符合程序的逻辑顺序。

Java 运行时内存区域:运行时的内存划分是具体的,是 JVM 运行 Java 程序时必要的;

  • 方法区 (Method Area) :存储类的结构信息,如字段、方法、常量池等,JDK 8 后称为 Metaspace。
  • 堆 (Heap) :存放对象实例和数组,是所有线程共享的内存区域。垃圾回收主要在此进行。
    • 虽然堆是共享的,但堆中还是会有内存不可见问题
      • 现代的计算机,往往会在高速缓存区中缓存共享变量,因为 CPU 访问缓存区比访问内存要快得多;
      • 线程之间的共享变量存在于主存中,每个线程都有一个私有的本地内存,存储了该线程的读、写共享变量的副本。本地内存是 Java 内存模型的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器等;

        所以线程 B 并不是直接去主存中读取共享变量的值,而是先在本地内存 B 中找到这个共享变量,发现这个共享变量已经被更新了,然后本地内存 B 去主存中读取这个共享变量的新值,并拷贝到本地内存 B 中,最后线程 B 再读取本地内存 B 中的新值。

  • 栈 (Stack) :每个线程私有,存储局部变量、方法调用和返回地址,栈帧在方法调用时创建。
  • 程序计数器 (PC Register): 每个线程独立,记录当前执行的字节码指令地址,用于线程调度。
  • 本地方法栈 (Native Method Stack): 支持 Java 调用本地方法的栈,功能类似于 Java 栈。

指令重排序:通过重新安排指令的执行顺序,使得 CPU 可以更有效地利用资源,减少空闲时间,可能导致可见性和一致性问题

  • 编译器优化重排,编译器在不改变单线程程序语义的前提下,重新安排语句的执行顺序。

  • 指令并行重排,现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。

  • 内存系统重排,由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差


happens-before

  1. 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么 JMM 也允许这样的重排序

在 Java 中,有以下天然的 happens-before 关系:

  • 程序顺序规则:一个线程中的每一个操作,happens-before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
  • volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
  • 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
  • start 规则:如果线程 A 执行操作 ThreadB.start()启动线程 B,那么 A 线程的 ThreadB.start()操作 happens-before 于线程 B 中的任意操作。
  • join 规则:如果线程 A 执行操作 ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功返回。

八、volatitle

volatile 可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在 JVM 底层 volatile 是采用“内存屏障”来实现的。

内存屏障:一个处理器指令,可以对 CPU 或编译器重排序做出约束

内存屏障的功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  • 它会强制将对缓存的修改操作立即写入主存;
  • 如果是写操作,它会导致其他 CPU 中对应的缓存行无效。

九、synchronized

相关背景知识

  • synchronized 提供了三种应用方式:实例方法、静态方法和同步代码块。
    • 实例方法、静态方法:即当使用该方法,该方法内的所有地方都同步,可能会有点影响性能;
    • 同步代码块: - 使用 this 作为锁对象,意味着同一个实例的所有线程在访问; - 使用类对象 SynchronizedClassExample.class 作为锁,这意味着所有线程在访问该类的静态方法时会被同步; - 定义了一个自定义的锁对象 lock,在 synchronizedBlock 方法中使用这个对象进行同步。这样可以实现更灵活的锁控制;
      • 多个对象的实例锁是各自独立的,只有在使用静态同步方法时,才会涉及到所有实例共享同一把锁
      • 临界区:某一块代码区域,它同一时刻只能由一个线程执行,即 synchronized 关键字添加的方法,代码块的区域。

.

  • 使用 synchronized 可以防止指令重排带来的可见性问题,确保共享变量的正确性。
    • 当一个线程获取锁时,JMM 确保:
      • 该线程的所有后续操作在锁释放之前不会被重排序。
      • 线程在释放锁时,所有对共享变量的修改都会被刷新到主内存中。
      • 线程在获取锁时,会从主内存中读取共享变量的最新值。
  • synchronized 是可重入的,允许同一线程多次获得同一个锁,防止死锁的发生。这使得在复杂的多线程环境中更容易编写安全的代码。

十、锁的四种状态及锁降级

在 JDK 1.6 以前,所有的锁都是“重量级”锁,因为使用的是操作系统的互斥锁,当当多个线程针对同一个资源时,会涉及到线程的上下文切换和内核的切换,效率低。

在 JDK1.6 后,加入了四种锁状态,级别由低到高为:

优点缺点适用场景
无锁状态线程可以自由访问共享资源,没有任何同步机制,开销较小。多线程同时访问可能导致数据不一致。适用于无竞争的情况下,或只读场景。
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。如果线程间存在锁竞争,会带来额外的锁撤销的消耗。适用于只有一个线程访问同步块场景。
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度。如果始终得不到锁竞争的线程使用自旋会消耗 CPU。追求响应时间。同步块执行速度非常快。
重量级锁线程竞争不使用自旋,不会消耗 CPU。线程阻塞,响应时间缓慢。追求吞吐量。同步块执行时间较长。

Java 多线程的锁都是基于对象的,Java 中的每一个对象都可以作为一个锁,类锁其实也是对象锁

对象头结构

  1. Mark Word:存储对象的元数据,包括:
  • 哈希码(Hash Code)
  • 锁状态(无锁、偏向锁、轻量级锁、重量级锁)
  • 偏向的线程 ID(如果是偏向锁)
  • 指向其他锁信息的指针(在轻量级锁和重量级锁状态下)
  1. 类型指针(Class Pointer):
  • 指向对象的类元数据,包含对象的类型信息。

锁的状态

  • 无锁:Mark Word 中的状态指示没有任何锁。
  • 偏向锁:Mark Word 中存储偏向的线程 ID。
  • 轻量级锁:Mark Word 中指示为轻量级锁,并包含锁记录的指针。
  • 重量级锁:Mark Word 中标记为重量级锁,指向操作系统的监视器(Monitor)
  • 偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,偏向锁在资源无竞争情况下消除了同步语句,连 CAS 操作都不做了,提高了程序的运行性能。
  • 轻量级锁是通过 CAS 操作和自旋来实现的,如果自旋失败,则会升级为重量级锁。
  • 重量级锁依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU。

锁的升级流程

每一个线程在准备获取共享资源时:
第一步,检查 MarkWord 里面是不是放的自己的 ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。

第二步,如果 MarkWord 不是自己的 ThreadId,锁升级,这时候,用 CAS 来执行切换,新的线程根据 MarkWord 里面现有的 ThreadId,通知之前线程暂停,之前线程将 Markword 的内容置为空。

第三步,两个线程都把锁对象的 HashCode 复制到自己新建的用于存储锁的记录空间,接着开始通过 CAS 操作, 把锁对象的 MarKword 的内容修改为自己新建的记录空间的地址的方式竞争 MarkWord。

第四步,第三步中成功执行 CAS 的获得资源,失败的则进入自旋 。

第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败 。

第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己


十一、Unsafe

Unsafe 是 Java 中一个非常特殊的类,它为 Java 提供了一种底层、"不安全"的机制来直接访问和操作内存、线程和对象。正如其名字所暗示的,Unsafe 提供了许多不安全的操作,因此它的使用应该非常小心,并限于那些确实需要使用这些底层操作的场景

功能:

  • 内存管理
    • 直接内存分配:
      • allocateMemory(long size):分配指定大小的内存(不受 Java 垃圾回收的影响)。
      • freeMemory(long address):释放之前分配的内存。
    • 内存访问:
      • getInt(long address):从指定内存地址读取一个 int 值。
      • putInt(long address, int x):将一个 int 值写入指定内存地址。
  • 原子操作
    • 原子变量:提供对变量的原子操作,确保在多线程环境中的安全性。
      • compareAndSwapInt(Object obj, long offset, int expected, int x):比较并交换整数值。
      • getAndAddInt(Object obj, long offset, int delta):获取当前值并加上一个增量。
  • 内存屏障
    • 内存屏障:确保特定操作的执行顺序。
      • storeFence():确保在此方法之前的所有写操作在此方法之后完成。
      • loadFence():确保在此方法之前的所有读操作在此方法之后完成。
  • 对象操作
    • 对象字段操作:
      • getObject(Object obj, long offset):获取对象中指定偏移量的字段值。
      • putObject(Object obj, long offset, Object x):将对象写入指定偏移量的字段。

使用场景

  • 性能优化:在性能关键的应用程序中,Unsafe 可以用来减少内存管理的开销。
  • 底层系统交互:需要直接访问硬件或进行系统级编程时,Unsafe 提供了相应的能力。
  • 实现数据结构:一些高性能的数据结构(如非阻塞队列)可能依赖 Unsafe 提供的原子操作。

十二、Java 并发编程通信工具类 Semaphore、Exchanger、CountDownLatch、CyclicBarrier、Phaser

Semaphore 是一个计数信号量,用于控制对共享资源的访问。它可以限制同时访问某个特定资源的线程数量。

  • 主要功能
    • 获取和释放许可:线程可以获取许可以访问资源,并在使用完毕后释放许可。
    • 并发控制:可以设置可以同时访问资源的最大线程数。
  • 使用场景
    • 限制数据库连接池的最大连接数。
    • 控制并发访问文件、网络等共享资源。

Exchanger 是一个同步点,两个线程可以在此交换数据。每个线程在到达交换点时,会阻塞直到对方也到达。
当一个线程调用 exchange 方法后,会处于阻塞状态,只有当另一个线程也调用了 exchange 方法,它才会继续执行
exchange 是可以重复使用的。也就是说。两个线程可以使用 Exchanger 在内存中不断地再交换数据
只有前两个线程会交换数据,第三个线程会进入阻塞状态

  • 主要功能
    • 数据交换:允许两个线程互相交换对象。
    • 双向通信:线程之间可以实现双向的数据传递。
  • 使用场景
    • 线程间的直接数据交换,如生产者-消费者模式。

CountDownLatch 是一个同步辅助类,允许一个或多个线程等待直到一组操作完成。它的计数器可以被减到零时,所有等待的线程会被释放。

  • 主要功能
    • 计数器:可以设置初始计数,线程在调用 await() 方法时会阻塞,直到计数减为零。
    • 一次性使用:计数器只能被减到零一次,不能重用。
  • 使用场景
    • 在所有参与的线程完成某个任务后,主线程再继续执行。
    • 比如游戏加载等情况。

CyclicBarrier 是一个同步辅助类,允许一组线程在达到某个条件时相互等待,直到所有线程都到达屏障点。

  • 主要功能
    • 重用:可以在所有线程到达后重置,允许重复使用。
    • 等待所有线程:所有参与的线程必须在屏障点调用 await() 方法,才能继续执行。
  • 使用场景
    • 多线程计算任务的分阶段执行,例如,分阶段的任务处理流程。

Phaser 是一个更灵活的同步辅助类,类似于 CyclicBarrier 和 CountDownLatch 的组合。它允许多个线程在不同阶段之间相互等待。

  • 主要功能
    • 动态参与者:可以在运行时添加或减少参与者。
    • 阶段性步骤:支持多阶段的任务执行。
  • 使用场景
    • 复杂的并发任务管理,例如多阶段的处理流程。

十三、Fork/Join 的分治框架

该框架主要运用的思想是分治法的思想,将任务分成多个相同的小项目,并且将后续的结果合并,主要充分利用多核进行同时处理。

工作窃取:Fork/Join 框架使用工作窃取算法,允许空闲线程从忙碌线程的任务队列中窃取任务,以提高资源利用率。

核心类:

  • ForkJoinPool:这是 Fork/Join 框架的核心执行器,负责管理工作线程和执行任务。它是一个特殊类型的线程池,使用工作窃取算法来调度任务。
  • RecursiveTask:一个可以返回结果的任务,通常用于有返回值的计算。
  • RecursiveAction:一个不返回结果的任务,适用于只执行某些操作而不需要返回值的情况。

使用 Fork/Join 框架的步骤:

  • 创建一个 ForkJoinPool 实例。
  • 定义一个任务类,继承 RecursiveTask 或 RecursiveAction。
  • 在任务类中实现 compute() 方法,在该方法中定义如何分叉和合并任务。
  • 提交任务到 ForkJoinPool 并获取结果。

十四、java 中的原子类

Java 的原子操作类提供了一种高效且简洁的方式来处理多线程环境中的共享变量。通过使用这些原子类,开发者可以避免使用显式的锁,从而提高并发程序的性能

使用场景

  • 计数器:在多线程环境中安全地递增或递减计数器。
  • 状态标志:使用 AtomicBoolean 来表示某种状态,例如任务是否完成。
  • 缓存:使用 AtomicReference 来实现无锁的缓存机制。

以下是一些常用的原子类:

  • AtomicInteger:提供对 int 类型的原子操作。
  • AtomicLong:提供对 long 类型的原子操作。
  • AtomicBoolean:提供对 boolean 类型的原子操作。
  • AtomicReference:提供对引用类型的原子操作,可以存储任何对象的引用。
  • AtomicIntegerArray:提供对 int 类型数组的原子操作。
  • AtomicLongArray:提供对 long 类型数组的原子操作。
  • AtomicReferenceArray:提供对对象数组的原子操作。

这些原子类通常提供以下操作方法:

  • get():获取当前值。
  • set():设置新值。
  • compareAndSet(expectedValue, newValue):如果当前值等于预期值,则更新为新值。这是实现 CAS(Compare-And-Swap)算法的 基础。
  • incrementAndGet():原子地将当前值增加 1,并返回新的值。
  • decrementAndGet():原子地将当前值减少 1,并返回新的值。
  • addAndGet(delta):原子地将当前值增加指定的增量,并返回新的值。
  • getAndSet(newValue):获取当前值并设置为新值。

十五、ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor 是 Java 中用于执行定时任务和周期性任务的一个强大工具。它是 ThreadPoolExecutor 的子类,提供了额外的功能来支持延迟执行和定期执行任务

  • 定时任务:在指定的延迟后执行的任务。
  • 周期性任务:按照指定的时间间隔重复执行的任务。
  • 线程池:用于管理和复用线程,避免频繁创建和销毁线程带来的开销。

主要方法:

  • schedule(Runnable command, long delay, TimeUnit unit):在指定的延迟后执行给定的任务。
  • schedule(Callable callable, long delay, TimeUnit unit):在指定的延迟后执行并返回结果。
  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):在初始延迟后,以固定的时间间隔周期性执行任务。
  • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):在初始延迟后,每次任务完成后再延迟指定时间执行下一个任务。

实现原理:

  • 任务调度:ScheduledThreadPoolExecutor 内部使用一个延迟队列(DelayQueue)来管理定时任务。每个任务都有一个执行时间,调度器会检查队列中的任务并在适当的时间执行。
  • 线程管理:它根据核心线程池的大小管理线程,复用已存在的线程以提高性能。
  • 任务执行:当任务到达其执行时间时,执行器会从队列中取出该任务并使用线程池中的线程来执行。

十六、偏向锁深入了解

偏向撤销

  • 定义:当有其他线程尝试获取一个已偏向某个线程的锁时,偏向锁需要被撤销,转变为轻量级锁。
  • 过程:撤销过程涉及检查当前持有锁的线程,如果发现其他线程争用该锁,JVM 会将偏向锁状态撤销,并允许新的线程获取锁,当然这个阶段是在安全点的时候进行撤销。
  • 开销:偏向撤销会引入一定的性能开销,这在高竞争情况下可能影响程序性能。

安全点,线程处于不同状态:

  • 线程不存活,或者活着的线程退出了同步块,很简单,直接撤销偏向就好了
  • 活着的线程但仍在同步块之内,那就升级成轻量级锁

撤销出现的场景:

  • 一个线程创建了大量对象并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作。这种 case 下,会导致大量的偏向锁撤销操作
  • 明知有多线程竞争(生产者/消费者队列),还要使用偏向锁,也会导致各种撤销

批量重偏向(Bulk Rebias)

  • 定义:批量重偏向是指在偏向锁撤销后,JVM 将允许多个线程在同一时间重新偏向某个线程,而不是逐个重偏向。
  • 过程:当一个线程获取到偏向锁并完成重偏向后,其他线程可以在同一时刻进行重偏向,减少了锁的竞争和开销。
  • 优点:通过批量重偏向,系统可以更高效地管理锁,从而减少性能损耗。

工作原理

  • JVM 为每个类维护一个偏向锁撤销计数器。
  • 当一个类的偏向锁被撤销的次数达到设定阈值(默认 20 次),JVM 会认为该类的偏向锁存在问题,并触发批量重偏向。
  • 在每一个类中的对象头中有一个 markwork,里面的 epoch 默认都是 0,代表此时的对象属于无竞争状态为偏向锁,然后不同线程的获取通过 CAS 进行获取,当多个线程竞争且达到阀值时(失败时 JVM 进行记录),对应的类的 epoch 和实例的 epoch 都进行变更为 1,后续的当线程访问时,发现其 epoch 变换为 1,此时将偏向锁进行升级为轻量级或重量级的锁。

批量撤销(Bulk Revoke)

  • 定义:当偏向锁被撤销时,JVM 可以一次性撤销多个偏向锁,而不是每个锁单独处理。
  • 过程:在高竞争场景下,多个线程争用偏向锁时,JVM 会批量撤销这些锁,减少了单次撤销的开销。
  • 优点:批量撤销能够提高系统的整体效率,在锁的竞争激烈时优化性能。
  1. 重偏向阈值:
  • 当一个类的撤销计数达到重偏向阈值(BiasedLockingBulkRevokeThreshold = 40),JVM 会认为该类的使用场景存在多线程竞争,并将该类标记为不可偏向。
  1. 直接转为轻量级锁:
  • 标记为不可偏向后,后续对该类的锁请求将直接使用轻量级锁的逻辑,跳过偏向锁的机制。
  1. 机会与计时器:
  • 在达到批量撤销阈值之前,JVM 会提供一次改过自新的机会。这通过一个计时器来实现,参数为 BiasedLockingDecayTime = 25000(25 秒)。
  • 如果在距离上次批量撤销发生的 25 秒内,累计撤销计数达到 40,则会执行批量撤销,彻底禁用偏向锁。
  • 如果超过 25 秒,会重置在 20 到 40 次之间的撤销计数,给予一次机会。

偏向锁与 HashCode

  • 关系:偏向锁会影响对象的哈希码计算。在对象的偏向锁状态下,哈希码的计算会使用偏向线程的 ID。
  • 开销:因为偏向锁的存在,哈希码的计算可能会受到影响,尤其是在对象的锁状态频繁变化时,可能导致哈希计算的开销增大。

重量级锁和 Object.wait

  • 重量级锁:当偏向锁和轻量级锁无法满足锁的需求时,锁会升级为重量级锁。重量级锁使用操作系统的互斥量实现,性能开销较大。
  • Object.wait:在持有重量级锁的线程中,调用 Object.wait() 方法会释放锁,使其他线程能够获取该锁。等待的线程会被放入等待队列,直到被其他线程唤醒。
  • 关系:重量级锁的使用通常伴随着更高的上下文切换开销,而 Object.wait() 机制则是实现线程间协作的重要手段。

十七、乐观锁

  • 假设无竞争:乐观锁的基本思想是,认为在大多数情况下,多个线程对同一资源的操作不会发生冲突。
  • 乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能
  • 操作流程:
    1. 读取数据:线程在操作数据之前,会读取数据的当前状态(或版本号)。
    2. 处理数据:线程对数据进行处理(例如,计算、修改等)。
    3. 验证:在更新数据时,线程会检查数据的状态(或版本号)是否与开始读取时相同。
    4. 提交更新:如果状态未变,线程可以安全地提交更新;如果状态已变,则需要重试。

乐观锁的实现通常依赖于版本号或时间戳,通常使用一种称为 CAS 的技术来保证线程执行的安全性.

CAS 是一种原子操作,其中 CAS 中有三个值,V:要更新的变量(var)、E:预期值(expected)、N:新值(new);
过程:判断 V 是否等于 E,如果等于,将 V 的值设置为 N;如果不等,说明已经有其它线程更新了 V,于是当前线程放弃更新,什么都不做。

CAS 常出现的三个问题:

  • ABA 问题;就是一个值原来是 A,变成了 B,又变回了 A。这个时候使用 CAS 是检查不出变化的,但实际上却被更新了两次
    • 解决:在变量前面追加上版本号或者时间戳。
  • 长时间自旋;CAS 多与自旋结合。如果自旋 CAS 长时间不成功,会占用大量的 CPU 资源。
    • 解决:让 JVM 支持处理器提供的 pause 指令,pause 指令能让自旋失败时 cpu 睡眠一小段时间再继续自旋,从而使得读操作的频率降低很多,为解决内存顺序冲突而导致的 CPU 流水线重排的代价也会小很多
  • 多个共享变量的原子操作:当对一个共享变量执行操作时,CAS 能够保证该变量的原子性。但是对于多个共享变量,CAS 就无法保证操作的原子性,这时通常有两种做法;
    • 解决:使用 AtomicReference 类保证对象之间的原子性,把多个变量放到一个对象里面进行 CAS 操作;使用锁。锁内的临界区代码可以保证只有当前线程能操作

十八、AQS

AQS(AbstractQueuedSynchronizer)是 Java 并发包中的一个重要组件,它为构建锁和同步器提供了一个框架。AQS 通过维护一个 FIFO(先进先出)队列来管理线程的请求,支持各种同步器的实现,如互斥锁、读写锁和信号量等。以下是对 AQS 的深入浅出解释

FIFO:

Node 节点的使用是隐式的,由 AQS 管理

  • 状态设置:通过 setWaitStatus(Node node, int status) 方法设置节点的状态。
  • 状态获取:通过 getWaitStatus(Node node) 方法获取节点的状态。
  • 状态检查:例如,检查节点是否处于 CANCELLED 状态,以决定是否需要唤醒下一个等待的线程。

工作原理:

  1. 获取锁:

    • 当线程尝试获取锁时,首先检查当前状态。
    • 如果状态表示锁可用,线程成功获取锁并更新状态。
    • 如果锁被占用,线程会被加入到等待队列中,并阻塞。
  2. 释放锁:

    • 当持有锁的线程释放锁时,更新状态,并唤醒在队列中等待的线程。
  3. 队列管理:

    • AQS 使用一个双向链表来维护等待的线程。每个线程在失败获取锁时,会被封装成一个节点,并加入到队列中。
    • 唤醒时,会根据队列的顺序唤醒线程。

需要重写常用的方法

  1. tryAcquire(int arg)
    • 尝试获取锁,返回成功与否。
    • 参数:arg 是尝试获取锁的参数,通常用于表示锁的模式(如独占或共享)。
    • 返回值:如果获取锁成功,返回 true;否则返回 false。
    • 实现:需要在子类中重写该方法,定义获取锁的具体逻辑。
  2. tryRelease(int arg)
    • 尝试释放锁,返回成功与否。
    • 参数:arg 是释放锁的参数,通常用于表示锁的模式。
    • 返回值:如果释放锁成功,返回 true;否则返回 false。
    • 实现:需要在子类中重写该方法,定义释放锁的具体逻辑。
  3. isHeldExclusively()
    • 判断当前线程是否独占锁。
    • 返回值:如果当前线程持有锁,返回 true;否则返回 false。
    • 用途:可以用于调试或实现某些特定的同步逻辑,确保锁的独占性。
  4. acquire(int arg)
    • 获取锁的方法,可能会阻塞。
    • 参数:arg 是获取锁的参数,通常表示尝试获取锁的模式。
    • 功能:调用该方法时,如果锁可用,线程将成功获取锁;如果不可用,线程将被加入到等待队列并阻塞,直到锁可用。
    • 实现:该方法会调用 tryAcquire,如果获取失败则会进入阻塞状态,直到成功获取锁。
  5. release(int arg)
    • 释放锁的方法。
    • 参数:arg 是释放锁的参数,通常表示释放的模式。
    • 功能:调用该方法将释放锁,并可能唤醒等待队列中的其他线程。
    • 实现:该方法会调用 tryRelease,并在成功释放锁后,唤醒等待的线程。

不需要重写的常用方法

  1. acquireShared(int arg)
    • 获取共享锁的方法,可能会阻塞。
    • 参数:arg 表示获取共享锁的模式。
    • 功能:类似于 acquire,但允许多个线程同时获取共享锁。
  2. releaseShared(int arg)
    • 释放共享锁的方法。
    • 参数:arg 表示释放共享锁的模式。
    • 功能:释放共享锁,并可能唤醒等待的线程。
  3. getState()
    • 获取当前锁的状态。
    • 返回值:当前锁的状态值,通常是一个整数。
    • 用途:可以用于调试和监控锁的状态。
  4. setState(int newState)
    • 设置锁的状态。
    • 参数:newState 是新状态的值。
    • 用途:允许在实现中直接设置状态值,通常在释放锁时使用。
  5. compareAndSetState(a,b) ,位于 unsafe 中的一个原子类方法用于比较

十九、ReentrantLock

ReentrantLock 是 Java 中提供的一种可重入的、基于互斥的锁,属于 java.util.concurrent.locks 包

  • 可重入性:同一个线程可以多次获得同一把锁而不会导致死锁。例如,如果一个线程已经持有了 ReentrantLock,它可以再次获取该锁而不被阻塞。适用于递归和复杂的线程调用场景
  • 互斥性:只有持有锁的线程可以访问被保护的共享资源,其他线程在尝试获取锁时会被阻塞。

常见方法:

  • 构造函数:
    • ReentrantLock():创建一个非公平锁。
    • ReentrantLock(boolean fair):创建一个锁,可以选择公平或非公平。
  • 锁定与解锁:
    • lock():获取锁,如果锁已经被其他线程持有,则阻塞当前线程。
    • unlock():释放锁,必须在持有锁的线程中调用,否则会抛出异常。
  • 尝试获取锁:
    • tryLock():尝试获取锁,如果锁可用则获取并返回 true,否则返回 false。
    • tryLock(long timeout, TimeUnit unit):尝试在指定的时间内获取锁,如果成功则返回 true,否则返回 false。
  • 条件变量:
    • newCondition():创建一个 Condition 对象,用于在某些条件下进行线程的等待和通知。

ReentrankLock 与 synchronized 的区别:

  • ReentrantLock 是一个类,而 synchronized 是 Java 中的关键字;
  • ReentrantLock 可以实现多路选择通知(可以绑定多个 Condition),而 synchronized 只能通过 wait 和 notify/notifyAll 方法唤醒一个线程或者唤醒全部线程(单路通知);
  • ReentrantLock 必须手动释放锁。通常需要在 finally 块中调用 unlock 方法以确保锁被正确释放。
  • synchronized 会自动释放锁,当同步块执行完毕时,由 JVM 自动释放,不需要手动操作。
  • ReentrantLock: 通常提供更好的性能,特别是在高竞争环境下。
  • synchronized: 在某些情况下,性能可能稍差一些,但随着 JDK 版本的升级,性能差距已经不大了。

二十、ReentrantReadWriteLock

ReentrantReadWriteLock 是 Java 的一种读写锁,它允许多个读线程同时访问,但只允许一个写线程访问(会阻塞所有的读写线程)。这种锁的设计可以提高性能,特别是在读操作的数量远远超过写操作的情况下

锁降级的过程

  • 获取读锁:首先尝试获取读锁来检查某个缓存是否有效。
  • 检查缓存:如果缓存无效,则需要释放读锁,因为在获取写锁之前必须释放读锁。
  • 获取写锁:获取写锁以便更新缓存。此时,可能还需要重新检查缓存状态,因为在释放读锁和获取写锁之间可能有其他线程修改了状态。
  • 更新缓存:如果确认缓存无效,更新缓存并将其标记为有效。
  • 写锁降级为读锁:在释放写锁之前,获取读锁,从而实现写锁到读锁的降级。这样,在释放写锁后,其他线程可以并发读取,但不能写入。
  • 使用数据:现在可以安全地使用缓存数据了。
  • 释放读锁:完成操作后释放读锁。

方法概述:

  • unlock():释放实例 (用法: rwLock.readLock().unlock();)
  • lock():获取实例 (用法: rwLock.readLock().lock();)
  • readLock():返回读锁的实例。
  • writeLock():返回写锁的实例。
  • getReadLockCount():获取当前持有的读锁数量。
  • isWriteLocked():检查写锁是否被某个线程持有。

二十一、Condition

Condition 是 Java 中用于实现线程间协调的重要工具,属于 java.util.concurrent.locks 包。它提供了一种机制,使得线程可以在特定条件下等待和通知其他线程,从而更灵活地控制线程的执行顺序。

提供的类的方法:

  • lock.newCondition():condition 对象的创建
  • await():线程等待直到被通知或者中断。类似于 Object.wait()。
  • awaitUninterruptibly():线程等待直到被通知,即使在等待时被中断也不会返回。没有与之对应的 Object 方法。
  • await(long time, TimeUnit unit):线程等待指定的时间,或被通知,或被中断。类似于 Object.wait(long timeout),但提供了更灵活的时间单位。
  • awaitNanos(long nanosTimeout):线程等待指定的纳秒时间,或被通知,或被中断。没有与之对应的 Object 方法。
  • awaitUntil(Date deadline):线程等待直到指定的截止日期,或被通知,或被中断。没有与之对应的 Object 方法。
  • signal():唤醒一个等待的线程。类似于 Object.notify()。
  • signalAll():唤醒所有等待的线程。类似于 Object.notifyAll()

注意事项:

  • 锁的获取:在调用 await() 或 signal() 方法之前,必须先获取相应的锁。未持有锁时调用这些方法会抛出 IllegalMonitorStateException。
  • 重复检查条件:在调用 await() 之前,应该在一个循环中检查条件(如 while 循环),以防止虚假唤醒(spurious wakeup)。即使 await() 被唤醒,也可能不是因为条件满足,而是由于其他原因(例如,超时或中断)。

二十一、LockSupport

LockSupport 允许线程在没有锁的情况下挂起(阻塞)和恢复运行。它主要通过两个方法实现:

  • park():使当前线程挂起。
  • unpark(Thread thread):唤醒指定的线程。

阻塞线程

  • void park():阻塞当前线程,如果调用 unpark 方法或线程被中断,则该线程将变得可运行。请注意,park 不会抛出 InterruptedException,因此线程必须单独检查其中断状态。
  • void park(Object blocker):功能同方法 1,入参增加一个 Object 对象,用来记录导致线程阻塞的对象,方便问题排查。
  • void parkNanos(long nanos):阻塞当前线程一定的纳秒时间,或直到被 unpark 调用,或线程被中断。
  • void parkNanos(Object blocker, long nanos):功能同方法 3,入参增加一个 Object 对象,用来记录导致线程阻塞的对象,方便问题排查。
  • void parkUntil(long deadline):阻塞当前线程直到某个指定的截止时间(以毫秒为单位),或直到被 unpark 调用,或线程被中断。
  • void parkUntil(Object blocker, long deadline):功能同方法 5,入参增加一个 Object 对象,用来记录导致线程阻塞的对象,方便问题排查。

唤醒线程

  • void unpark(Thread thread):唤醒一个由 park 方法阻塞的线程。如果该线程未被阻塞,那么下一次调用 park 时将立即返回。这允许“先发制人”式的唤醒机制。

二十二、并发集合容器 ConcurrentHashMap、阻塞队列和 CopyOnWrite 容器

二十一、ConcurrentHashMap

ConcurrentHashMap 是 Java 中一个线程安全的哈希表实现,设计用于在多线程环境中高效地存储和访问键值对

特点:

  • 分段锁(Segment Locking):
    • ConcurrentHashMap 将整个哈希表分成多个段(segment),每个段都有自己的锁,而每一个段有对应的 HashEntry,HashEntry 里面存放的是对应的键和值。这样,多个线程可以并发地访问不同段的数据,减少了锁的争用。
    • 在 Java 8 及以后版本中,使用了更细粒度的锁(如使用 synchronized 和 CAS),进一步提高了性能。
  • 高效的读操作:
    • 读操作(如 get)不需要加锁,这使得并发读取的性能非常高。
    • 只有在写操作(如 put 和 remove)时使用 CAS 操作,确保在并发写入时的安全性。如果发生冲突,会再次尝试
  • 动态扩容:
    • 当哈希表的负载因子达到一定阈值时,ConcurrentHashMap 会进行扩容。扩容是分段进行的,因此不会影响其他段的访问。

主要方法:

  • put(K key, V value):将键值对放入哈希表中。如果键已存在,更新其值。
  • get(Object key):根据键获取对应的值。如果键不存在,返回 null。
  • remove(Object key):移除指定键及其对应的值。
  • containsKey(Object key):检查哈希表中是否包含指定的键。
  • size():返回哈希表中键值对的数量。

jdk1.7 与 jdk1.8 时的区别:

在 JDK 1.7 中,ConcurrentHashMap 使用了分段锁的策略:

  • 分段(Segment):整个哈希表被分为多个段,每个段是一个独立的哈希表,并使用独立的锁来管理。
  • 操作并发:多个线程可以同时访问不同的段,增加了并发性能。
  • 锁粒度:锁的粒度是段级别的,减少了锁的竞争。
ConcurrentHashMap
  ├── Segment[0]
  │     ├── Node
  │     └── Node
  ├── Segment[1]
  │     ├── Node
  │     └── Node
  └── Segment[n]
        ├── Node
        └── Node

在 JDK 1.8 中,ConcurrentHashMap 进行了重构,主要改进体现在以下几个方面:

  • 无段的设计:不再使用分段锁,整个数据结构由一个数组和链表(或红黑树)构成。
  • CAS 操作:使用 Compare-And-Swap(CAS)操作来实现无锁的更新,减少了锁的使用,提高了性能。
  • 链表与红黑树:当某个桶中的元素超过阈值(默认为 8),链表会转换为红黑树,以提高查找效率。
ConcurrentHashMap
  ├── Bucket[0]
  │     ├── Node (链表)
  │     ├── Node (链表)
  │     └── Red-Black Tree (如果超过阈值)
  ├── Bucket[1]
  │     ├── Node (链表)
  │     └── Node (链表)
  └── Bucket[n]
        ├── Node (链表)
        └── Node (链表)

二十一、ConcurrentLinkedQueue

特点:

  • FIFO 队列:ConcurrentLinkedQueue 遵循先进先出原则,意味着第一个加入的元素会第一个被移除。
  • 无界队列:该队列不设定大小限制,可以容纳任意数量的元素。
  • 不允许 null 元素:ConcurrentLinkedQueue 不支持存储 null 值的元素。

构造:

  • 基于链接节点:ConcurrentLinkedQueue 由节点组成,每个节点包含元素和指向下一个节点的引用。
  • HOPS 设计:
    • HOPS(Head-Optimized for Performance and Scalability)允许对队列的头(head)和尾(tail)进行延迟更新。
    • 通过延迟更新,多个线程可以同时进行 CAS 操作而不会相互阻塞。

方法:

  • offer(E e):
    • 将指定元素插入队列的尾部。
    • 成功添加元素返回 true,失败(如元素为 null)返回 false。
  • poll():
    • 检索并删除队列的头部元素。
    • 如果队列为空,返回 null。
  • peek():
    • 检索但不删除队列的头部元素。
    • 如果队列为空,返回 null。
  • isEmpty():
    • 检查队列是否为空。
    • 返回 true 如果队列没有元素,返回 false 否则。
  • size():
    • 返回队列当前元素的数量。
    • 由于该方法不是严格的,返回的大小可能不是完全准确,尤其是在高并发情况下。

二十二、BlockingQueue

阻塞队列是一种线程安全的队列,支持在多线程环境下进行安全的插入和删除操作。它通过阻塞机制来管理线程之间的交互。当队列为空时,消费者线程会被阻塞,直到有数据可供消费;当队列已满时,生产者线程会被阻塞,直到有空间可用.

共同方法:

  • void put(E e):将指定元素插入队列,如果队列已满,则阻塞直到有空间可用。
  • E take():检索并删除队列头部的元素,如果队列为空,则阻塞直到有数据可用。
  • boolean offer(E e):尝试将元素插入队列,如果队列已满,返回 false。
  • E poll():检索并删除队列头部的元素,如果队列为空,返回 null。
  • E peek():检索但不删除队列头部的元素,如果队列为空,返回 null。
  • int remainingCapacity():返回队列中可用的剩余空间(适用于有界队列)。
  • boolean isEmpty():检查队列是否为空。

常用队列:

  • ArrayBlockingQueue
    • 固定大小,适合内存使用可控的场景。
    • 由于是基于数组的实现,访问速度较快。
    • 在多线程环境中,能够有效减少竞争,因为生产者和消费者使用不同的锁。
  • LinkedBlockingQueue
    • 可选容量(可以是有界或无界),灵活性高。
    • 基于链表,适合高并发读写操作,减少了锁的竞争。
    • 在多线程环境下,生产者和消费者使用不同的锁,进一步提高了性能。
  • PriorityBlockingQueue
    • 支持优先级排序,适合需要处理不同优先级任务的场景。
    • 无界队列,能够容纳任意数量的元素。
    • 在多线程环境中,使用比较器来决定元素的顺序,提高了灵活性。
  • SynchronousQueue
    • 不存储元素,生产者和消费者必须同时存在,适合需要直接传递数据的场景。
    • 可以实现高效的线程间协作,降低了内存使用。
    • 在高并发情况下,不会产生任何队列的开销。
  • LinkedTransferQueue
    • 无界队列,支持 transfer 方法,将元素转移到等待的消费者,如果不存在等待的消费者,则元素会入队并阻塞直到该元素被消费。
    • 对于需要快速传递数据的场景非常有效,具有较低的延迟。
    • 支持多种插入和移除操作,灵活性高。
  • transfer(E e),将元素转移到等待的消费者,如果不存在等待的消费者,则元素会入队并阻塞直到该元素被消费。
  • tryTransfer(E e),尝试立即转移元素,如果有消费者正在等待,则传输成功;否则,返回 false。
  • LinkedBlockingDeque
    • 双端队列,允许在两端进行插入和删除。
    • 适合需要从两端进行操作的场景,灵活性强。
    • 在多线程环境中,能够有效减少锁的竞争。
  • addFirst(E e), addLast(E e): 在队列的开头/结尾添加元素。
  • takeFirst(), takeLast(): 从队列的开头/结尾移除和返回元素,如果队列为空,则等待。
  • putFirst(E e), putLast(E e): 在队列的开头/结尾插入元素,如果队列已满,则等待。
  • pollFirst(long timeout, TimeUnit unit), pollLast(long timeout, TimeUnit unit): 在队列的开头/结尾移除和返回元素,如果队列为空,则等待指定的超时时间。
  • DelayQueue
    • 支持延迟获取元素,适合定时任务处理。
    • 通过实现 Delayed 接口,允许元素在过期前不可被消费。
    • 在多线程环境中,适合管理需要等待的任务。

二十一、CopyOnWriteArrayList

CopyOnWriteArrayList 是线程安全的,可以在多线程环境下使用。CopyOnWriteArrayList 遵循写时复制的原则,每当对列表进行修改(例如添加、删除或更改元素)时,都会创建列表的一个新副本,这个新副本会替换旧的列表,而对旧列表的所有读取操作仍然可以继续

  • 写时复制:

    • 在 CopyOnWriteArrayList 中,所有的写操作(如 add、remove 和 set)都会先创建底层数组的一个副本,然后在新副本上进行修改,最后将新副本替换旧数组。这与 ArrayList 的直接修改不同
  • 读取操作性能:

    • CopyOnWriteArrayList:读取操作(如 get()、size())直接访问底层数组,因此在读操作频繁的场景中性能较高,不会加锁。
    • ArrayList:在多线程环境下,读取操作需要额外的同步机制来确保线程安全。
  • 内存开销:

    • CopyOnWriteArrayList:由于每次写操作都会创建新数组,内存使用可能会更高,特别是在频繁写操作的场景中。
    • ArrayList:直接在原数组上操作,内存开销较低,但在多线程环境中需要额外的同步。
  • CopyOnWriteArrayList 适合以下场景:

    • 读多写少的场景:当读取操作远多于写入操作时,如缓存、观察者模式等场景。
    • 需要遍历的场景:在遍历时不需要担心并发修改,适合需要频繁遍历列表的应用。

二十二、ThreadLocal

ThreadLocal 是 Java 中的一种用于创建线程局部变量的机制,属于 java.lang 包。它允许每个线程都有自己的独立变量副本,从而避免了多线程环境中共享变量带来的数据一致性问题

特点:

  • 线程局部变量:每个线程都可以通过 ThreadLocal 创建一个变量,该变量在不同线程中是独立的。即使多个线程使用相同的 ThreadLocal 实例,它们访问的都是各自的副本。
  • 隔离性:ThreadLocal 变量在不同线程之间是隔离的,线程 A 修改的值不会影响线程 B 的值。

工作原理:

  • ThreadLocal 的工作原理基于每个线程都有一个 ThreadLocalMap,用于存储线程局部变量:
  • 存储:当一个线程第一次访问 ThreadLocal 变量时,ThreadLocal 会在该线程的 ThreadLocalMap 中创建一个新的条目,存储该线程的局部值。
  • 获取:当线程需要获取这个变量时,ThreadLocal 会从 ThreadLocalMap 中查找该线程的副本。
  • 清理:当线程结束时,ThreadLocal 中的变量会被垃圾回收,但如果不手动清理,可能会导致内存泄漏

方法:

  • T get():获取当前线程所对应的线程局部变量的值。
  • void set(T value):设置当前线程所对应的线程局部变量的值。
  • void remove():删除当前线程所对应的线程局部变量的值。

ThreadLocal 适合以下场景:

  • 用户会话信息:在 Web 应用中,可以使用 ThreadLocal 存储用户会话信息。
  • 数据库连接:可以在每个线程中存储数据库连接,避免在不同线程之间共享连接。
  • 事务管理:在处理事务时,可以使用 ThreadLocal 存储事务状态信息。

二十三、ThreadPool

线程池是 Java 中用于管理和复用线程的一种技术,属于 java.util.concurrent 包。它通过预先创建和管理一组线程,来提高性能并减少线程创建和销毁的开销

工作原理:

  • 初始化:创建一个线程池,指定核心线程数、最大线程数、空闲线程存活时间、任务队列等参数。
  • 提交任务:当一个任务被提交到线程池时,线程池会首先尝试使用一个空闲线程执行任务。
  • 线程复用:如果有空闲线程,线程池会复用这些线程来执行新任务,而不是创建新的线程。
  • 任务排队:如果所有核心线程都在忙碌,新的任务会被放入任务队列中,等待执行。
  • 线程超出:如果任务队列已满且当前线程数小于最大线程数,线程池会创建新的线程以处理任务。
  • 线程销毁:如果线程在一定时间内没有执行任务,它们将被销毁以节省资源。

线程池类型:

  • FixedThreadPool:
    • 创建一个固定数量的线程池,适合于处理较为稳定的任务数量。
    • 示例:ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
  • CachedThreadPool:
    • 创建一个可缓存的线程池,适合于执行大量短期异步任务。线程池会根据需要创建新线程,并在空闲时回收线程。
    • 示例:ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
  • SingleThreadExecutor:
    • 创建一个只有一个线程的线程池,适合于需要保证任务顺序执行的场景。
    • 示例:ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
  • ScheduledThreadPool:
    • 创建一个可以调度任务的线程池,适合于需要定期执行任务的场景。
    • 示例:ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

调用的方法:

  • submit(Callable task):提交一个可以返回结果的任务,并返回一个 Future,用于获取任务的执行结果。
  • submit(Runnable task):提交一个没有返回值的任务,并返回一个 Future<?>,用于获取任务的执行状态。
  • invokeAll(Collection<? extends Callable> tasks):提交一组任务并等待所有任务完成,返回每个任务的 Future 列表。
  • invokeAny(Collection<? extends Callable> tasks):提交一组任务并返回第一个完成的任务的结果,如果没有任务完成则抛出异常。
  • shutdown():停止接收新任务,并在所有已提交的任务完成后关闭线程池。
  • shutdownNow():尝试停止所有正在执行的任务,并返回尚未执行的任务列表。
  • awaitTermination(long timeout, TimeUnit unit):在指定时间内等待线程池中的所有任务完成,如果所有任务在指定时间内完成则返回 true。
  • schedule(Runnable command, long delay, TimeUnit unit):计划在指定的延迟后执行一个 Runnable 任务。
  • schedule(Callable callable, long delay, TimeUnit unit):计划在指定的延迟后执行一个 Callable 任务,并返回一个 ScheduledFuture,用于获取结果。
  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):计划以固定的速率周期性执行一个 Runnable 任务,第一次执行的延迟为 initialDelay,后续执行的间隔时间为 period。
  • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):计划以固定的延迟周期性执行一个 Runnable 任务,第一次执行的延迟为

http://www.kler.cn/a/592687.html

相关文章:

  • JS做贪吃蛇小游戏(源码)
  • uni-app——计时器和界面交互API
  • 【笔记】深度学习模型训练的 GPU 内存优化之旅:重计算篇
  • 人工智能中神经网络是如何进行预测的
  • 涨薪技术|Kubernetes(k8s)之yaml语法大全
  • AI实干家:HK深度体验-【第3篇-香港、新加坡、深圳、上海、首尔五座城市在金融数据维度的对比分析】
  • 31天Python入门——第5天:循环那些事儿
  • 【Go每日一练】随机密码生成器
  • 大语言模型黑盒越狱攻击之模板补全
  • Android retrofit 接口请求,提示CLEARTEXT communication处理
  • PostgreSQL:语言基础与数据库操作
  • 苹果电脑mac M1 15.0 安装虚拟机以及Debian系统 |Debian优化汉化中文 |Debian换阿里下载源 |Debian新建用户
  • 【简单有效!】Gradio利用html插件实现video视频流循环播放
  • Java面试黄金宝典3
  • 【Linux】手动部署并测试内网穿透
  • 并发编程面试题三
  • 2000-2016年各省地方财政营业税数据
  • 【人工智能】【Python】在Scikit-Learn中使用网格搜索对决策树调参
  • ROS合集(三)RTAB-Map + EuRoC 数据格式概述
  • 上取整,下取整,四舍五入