JUC并发编程

进程和线程的区别

进程是系统资源分配的最小单位,线程是CPU调度的最小单位。

  • 浏览器崩溃一个标签页(进程),其他标签页(其他进程)不受影响
  • Word 保存时卡死(保存线程崩溃),整个 Word 无法操作(UI 线程也挂了)

并发和并行的区别

  • 并行:在同一时刻,有多个指令在多个 CPU 上同时执行
  • 并发:在同一时刻,有多个指令在单个 CPU 上交替执行

创建线程方式

  1. 继承Thread类

    java 复制代码
    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("继承Thread线程执行");
        }
    }
    new MyThread().start(); // 启动线程
  2. 实现Runnable接口

    java 复制代码
    Runnable task = () -> System.out.println("Runnable线程执行");
    new Thread(task).start(); // 启动线程
  3. 实现Callable接口

    java 复制代码
    Callable<String> task = () -> "Callable执行结果";
    FutureTask<String> future = new FutureTask<>(task);
    new Thread(future).start(); // 用Thread启动Callable
    
    // 获取结果(阻塞等待)
    try {
        System.out.println("Callable结果: " + future.get());
    } catch (Exception e) {
        e.printStackTrace();
    }
  4. 线程池方式创建

    java 复制代码
    ExecutorService pool = Executors.newFixedThreadPool(3);
    pool.submit(() -> System.out.println("线程池线程执行"));
    pool.shutdown(); // 必须关闭线程池

线程六种状态的转换

线程的生命周期主要有以下六种状态:

  • New(新创建)
  • Runnable(可运行)
  • Blocked(被阻塞)
  • Waiting(等待)
  • Timed Waiting(计时等待)
  • Terminated(被终止)

状态转换:

  • NEW → RUNNABLE:当调用 t.start() 方法时,由 NEW → RUNNABLE

  • RUNNABLE <--> WAITING:

    • 调用 obj.wait() 方法时

      调用 obj.notify()、obj.notifyAll()、t.interrupt():

      • 竞争锁成功,t 线程从 WAITING → RUNNABLE
      • 竞争锁失败,t 线程从 WAITING → BLOCKED
    • 当前线程调用 t.join() 方法,注意是当前线程在 t 线程对象的监视器上等待

    • 当前线程调用 LockSupport.park() 方法

  • RUNNABLE <--> TIMED_WAITING:

    • 调用 obj.wait(long n) 方法、
    • 当前线程调用 t.join(long n) 方法、
    • 当前线程调用 Thread.sleep(long n)、
    • 调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis)
  • RUNNABLE <--> BLOCKED:t 线程用 synchronized(obj) 获取了对象锁时竞争失败


并发编程中的三个问题(原子性、可见性、有序性)

原子性:在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。

i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令

复制代码
     0: getstatic     #2   // Field i:I      ← 1. 从静态字段读取 i 到操作数栈
     3: iconst_1                          	 ← 2. 将常量 1 压入栈
     4: iadd                               	 ← 3. 弹出两个值,相加,结果压栈
     5: putstatic     #2   // Field i:I      ← 4. 将结果写回静态字段 i

i++是由多条语句组成,多线程情况下就可能会出现问题。

比如一个线程在执行iadd时,另一个线程又执行 getstatic。会导致两次i++,实际上只加了1。

可见性:是指一个线程对共享变量进行修改,另一个先立即得到修改后的最新值。

有序性:是指程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。


Java内存模型(JMM)

JMM 是什么?

  • 不是物理内存结构 ,而是 一组规范,定义了线程如何访问共享变量。
  • 屏蔽底层硬件/操作系统差异,保证 Java 程序在不同平台下并发行为一致。

核心思想:主内存 vs 工作内存

  • 主内存:主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。
  • 工作内存 :每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。

八大内存交互操作(原子)

主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。

操作 作用
lock 主内存变量加锁(仅一个线程可操作)
unlock 解锁
read 从主内存读取 → 工作内存
load 将 read 的值放入工作内存变量
use 工作内存变量 → 执行引擎
assign 执行引擎结果 → 工作内存变量
store 工作内存变量 → 主内存(准备写)
write 将 store 的值写入主内存

synchronized保证 原子性、可见性、有序性的原理

  • synchronized保证原子性的原理,通过 强制同一时间只有一个线程执行关键操作 (临界区),避免了多线程并发导致的操作交叉执行(如读脏数据、写覆盖),从而确保操作的不可分割性(原子性)。

  • synchronized 保证可见性的核心原理在于 锁操作的内存语义:

    • 解锁时 (Unlock):释放锁之前,强制 将当前线程修改的所有共享变量刷新回主内存(刷出修改)。
    • 加锁时 (Lock):获取锁之后,强制 清空当前线程工作内存中对共享变量的缓存,重新从主内存加载最新值(加载最新值)。
    • 规则保证 (Happens-Before):前一个线程的解锁操作 一定发生在 后一个线程的加锁操作之前(JMM规则)。这使得后一个线程必然能看到前一个线程解锁前所做的所有修改。
  • synchronized保证有序性的原理,我们加synchronized后,依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码中的代码。保证有序性


synchronized的特性

可重入特性

什么是可重入:一个线程可以多次执行synchronized,重复获取同一把锁。

原理:synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁,在执行完同步代码块时,计数器的数量会-1,知道计数器的数量为0,就释放这个锁。

不可中断特性

不可中断性:当线程在等待获取 synchronized 锁时,若期间被 interrupt() 方法中断,该线程不会提前退出等待状态,仍然会持续等待锁的释放。

核心原因:synchronized 是 Java 内置锁,其底层机制决定了它在等待锁时不会响应中断(Thread.interrupt() 会生效于阻塞在 Object.wait()、Thread.join()、Thread.sleep() 等非锁等待状态,但不会中断 synchronized 的锁等待)。


synchronized与Lock的区别

  • synchronized是关键字,而Lock是一个接口。
  • synchronized会自动释放锁,而Lock必须手动释放锁。
  • synchronized条件变量仅 Object.wait/notify(1个隐式条件),Lock支持多个 Condition(精准唤醒)。
  • synchronized是不可中断的,Lock支持 lockInterruptibly()
  • synchronized是非公平锁,ReentrantLock可以控制是否是公平锁(构造函数指定)。
  • synchronized无法知道锁是否被持有,而Lock可以isLocked()isHeldByCurrentThread() 等。

synchronized原理

javap 反汇编

通过javap反汇编我们看到synchronized使用编程了monitorentormonitorexit两个指令.每个锁对象都会关联一个monitor(监视器,它才是真正的锁对象),它内部有两个重要的成员变量owner会保存获得锁的线程,recursions会保存线程获得锁的次数,当执行到monitorexit时,recursions会-1,当计数器减到0时这个线程就会释放锁

JVM源码

  • ObjectMonitor 是 C++ 类,结构如下

    • _owner:当前锁持有线程
    • _recursions:可重入计数器
    • _cxq:竞争失败线程队列(快速插入,LIFO)
    • _EntryList:正式等待队列(唤醒顺序,FIFO)
    • _WaitSet:调用 wait() 的线程在此等待
  • 加锁流程: monitorenter ObjectMonitor::enter()

    • 先尝试原子设置_owner,成功则直接获取锁
    • 重入处理,检查当前线程是否已持有锁(THREAD == _owner
    • 自适应自旋减少阻塞开销
    • 竞争失败线程包装为节点压入_cxq
    • 调用操作系统原语进行线程阻塞
  • 解锁流程monitorexitObjectMonitor::exit()

    • 递归减少_recursions直到为0才真正释放
    • 根据策略选择唤醒路径(公平/非公平):
      • QMode=2:直接唤醒_cxq线程(新线程优先)
      • 默认策略:_cxq迁移到_EntryList(公平唤醒)
    • 清空_owner标志位

JDK6 synchronized 锁的优化机制

Java对象的布局

在JVM中,对象在内存中的布局分为三块区域:对象头实例数据对齐填充

对象头

  • Mark Word

    • 作用: 存储对象自身的运行时状态信息
    • 内容: 哈希码(HashCode)、GC 分代年龄、锁状态标志(无锁、偏向锁、轻量级锁、重量级锁)、持有锁的线程、偏向线程ID、偏向时间戳等。
  • klass pointer

    指向该对象所对应的类元数据(即该对象是哪个 Class 的实例)。

锁升级

无锁状态

对象刚被创建,尚未被任何线程获取锁时。

标志位:最低两位为"01"

偏向锁

偏向锁适用于同一个线程反复执行同步块的情况。

触发条件:无锁 → 偏向锁:第一个线程首次获取对象锁

过程

当线程A首次执行synchronized代码块时,检查对象头的Mark Word,发现处于无锁状态

JVM通过CAS操作将线程A的ID记录在Mark Word中,并将偏向锁标志位置为"1" (最低两位仍为"01",但偏向锁标志位设为"1")

线程A后续再次获取同一对象锁时,只需检查Mark Word中的线程ID是否匹配,无需额外同步操作

轻量级锁

轻量级锁适用于在多线程交替执行同步块的情况

触发条件:偏向锁 → 轻量级锁:第二个线程尝试获取已被偏向锁锁定的对象

过程

线程B发现对象头的偏向锁标志位为"1",但线程ID不匹配

JVM暂停持有偏向锁的线程A(需等待全局安全点),检查其状态

若线程A已结束:将对象头重置为无锁状态,线程B重新尝试获取偏向锁

若线程A仍在执行:撤销偏向锁,将偏向锁标志位清除,锁标志位设为"00",进入轻量级锁状态

线程B在栈帧中创建锁记录,尝试通过CAS操作将对象头的Mark Word替换为指向该锁记录的指针

若CAS成功:线程B获得锁,轻量级锁状态建立

若CAS失败:线程B开始自旋,反复尝试CAS操作

重量级锁

触发条件:轻量级锁 → 重量级锁

自旋超过阈值(默认10次,JDK 1.6后采用自适应自旋,根据历史自旋成功率动态调整阈值)

多线程竞争加剧

过程

当线程B自旋多次仍未获取锁时,JVM判断自旋不再高效

锁升级为重量级锁,Mark Word中的锁标志位变为"10",存储指向操作系统互斥量的指针

未获取锁的线程(如线程B、C、D)不再自旋,而是通过操作系统API将自己添加到等待队列中

线程进入阻塞状态,由操作系统管理线程调度和唤醒

锁升级过程不可逆


锁粗化

锁粗化是一种针对长时间持有锁的场景的优化策略。如果一个线程在短时间内需要连续多次加锁和解锁,那么可以将这些加锁和解锁操作合并成一个较大的加锁和解锁操作,从而减少了加锁和解锁的次数,提高了效率。

锁消除

锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。


volatile保证 可见性、有序性的原理

  • volatile 保证可见性的核心原理:使用 volatile 修饰共享变量后,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过 CPU 总线嗅探机制 告知其他线程该变量副本已经失效,需要重新从主内存中读取。

    嗅探机制工作原理

    1. 当处理器修改缓存数据时,向总线广播"无效"请求
    2. 其他处理器的缓存控制器"嗅探"总线,检测到请求后将对应缓存行状态置为Invalid
    3. 下次访问该数据时,处理器会从主存或最新数据持有者处重新加载
  • volatile保证有序性的原理:内存屏障(四种屏障类型)


CAS

CAS (Compare And Swap 比较相同再交换)是并发编程中的原子操作,用于实现无锁同步机制。其核心原理是通过比较内存值与预期值决定是否更新数据:

精简定义:

CAS(V,A,B)={更新内存地址V值为B,返回trueif V==A放弃操作,返回falseif V≠ACAS(V, A, B) = \begin{cases} \text{更新内存地址V值为B,返回true} & \text{if } V == A \\ \text{放弃操作,返回false} & \text{if } V \neq A \end{cases}CAS(V,A,B)={更新内存地址V值为B,返回true放弃操作,返回falseif V==Aif V=A

其中:

  • VVV:内存地址的当前值
  • AAA:读内存时获得的预期值(Expected Value)
  • BBB:需要写入的新值

CPU通过硬件指令 (如x86的CMPXCHG)确保比较与更新步骤不被中断


Atomic

常见原子类:AtomicInteger、AtomicBoolean、AtomicLong

AtomicInteger 原理:自旋锁 + CAS 算法

CAS 算法:有 3 个操作数(内存值 V, 旧的预期值 A,要修改的值 B)

  • 当旧的预期值 A == 内存值 V 此时可以修改,将 V 改为 B
  • 当旧的预期值 A != 内存值 V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋

分析 getAndSet 方法:

AtomicInteger:

java 复制代码
public final int getAndSet(int newValue) {
    /**
    * this: 		当前对象
    * valueOffset:	内存偏移量,内存地址
    */
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}

unsafe 类:

var5:从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到本地内存),然后执行 compareAndSwapInt() 再和主内存的值进行比较,假设方法返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样,修改数据

java 复制代码
// var1: 目标对象(包含待修改字段的对象),
// var2: 目标字段的内存偏移量,
// var4: 要设置的新值
public final int getAndSetInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // var5: 旧的预CDEFRSTVX23456 56U 估值(通过var1, var2得到)
        var5 = this.getIntVolatile(var1, var2);
        
 		// 内存最新值(通过var1, var2得到)与旧的预估值(var5)比较,
        // 若相等则替换为要设置的新值(var4),否则 自旋
    } while(!this.compareAndSwapInt(var1, var2, var5, var4));

    return var5;
}

变量 value 用 volatile 修饰,保证了多线程之间的内存可见性,避免线程从工作缓存中获取失效的变量

java 复制代码
private volatile int value

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果


LongAdder

AtomicInteger的缺陷

并发非常高 的时候,使用AtomicInteger、AtomicLong底层的CAS操作竞争非常激烈,会导致大量线程CAS失败而不断自旋,耗费CPU的性能。

AtomicInteger底层调用了unsafe类的方法

java 复制代码
public final int getAndSetInt(Object var1, long var2, int var4) {
    int var5;
    // CAS的失败的线程,会不断在do... while循环里重试,知道成功为止
    do {
        var5 = this.getIntVolatile(var1, var2);
        
        // while条件判断这里的compareAndSwapInt每次只有一个线程成功
    } while(!this.compareAndSwapInt(var1, var2, var5, var4));

    return var5;
}
  • 我们看一下上述 do...while 循环假设有10000个线程并发的操作 ,由于CAS操作能保证原子性一个时刻只会有一个线程执行compareAndSwapInt方法成功
  • 失败的另外9999个线程进入下一次循环然后成功一个剩下9998个进入下一次循环 ...,这种方式,竞争太过激烈导致大量线程在循环里面自旋重试,此时是不会释放CPU资源的,大大降低CPU的利用率,降低了并发的性能。

竞争非常激烈的时候大量线程在do...while循环里面自旋重试 ,导致非常消耗CPU资源,降低了并发的性能。

LongAdder采用分段锁的思想 ,去减少并发竞争 的;我打个比方 还是上面10000个线程并发操作 ,但是LongAdder内部可能有10个锁不同的线程可能去竞争不同的锁,平均下来可能是1000个线程竞争1个锁这样并发性能这样比起 AtomicInteger可能就提升了10倍

LongAdder原理:分段锁

  • 内部维护一个 base 变量 + Cell[] 数组(每个 Cell 存储部分值)
  • 无竞争时 :直接 CAS 更新 base
  • 有竞争时 :线程通过哈希映射到不同 Cell,各自独立累加,避免 CAS 冲突
  • 读取时sum()):将 base 与所有 Cell 的值相加

ThreadLocal

ThreadLocal翻译成中文比较准确的叫法应该是:线程局部变量

应用场景:当很多线程需要多次使用同一个对象,并且需要该对象具有相同初始化值的时候最适合使用ThreadLocal。

ThreadLocal的常用场景包括:

  • 上下文信息传递:在Web应用中传递用户信息、请求ID等上下文数据。避免方法间层层传递参数。
  • 用户会话管理:Web应用中存储当前登录用户信息。在拦截器中设置,业务方法中获取。
  • 数据库连接管理:ORM框架(如MyBatis)中确保同一线程使用同一连接。

原理

(1) 每个Thread线程内部都有一个Map (ThreadLocalMap)

(2) Map里面存储ThreadLocal对象(key)和线程的变量副本(value)

(3)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。

(4)对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key是 ThreadLocal 实例本身,value 是真正需要存储的 Object。也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。

与synchronized关键字对比

synchronized ThreadLocal
原理 同步机制采用'以时间换空间'的方式, 只提供了一份变量,让不同的线程排队访问 ThreadLocal采用'以空间换时间'的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
侧重点 多个线程之间访问资源的同步 多线程中让每个线程之间的数据相互隔离

ThreadLocal 为什么会导致内存泄漏

ThreadLocalMap 中的 value强引用 ,而 keyWeakReference<ThreadLocal>(弱引用)。

当ThreadLocal变量被手动设置为null,即 ThreadLocal 对象失去强引用后,GC 会回收 ThreadLocal 对象,导致 ThreadLocalMap中出现key 变为 null,但 value 仍被强引用,无法被回收。如果当前线程再迟迟不结束的话(比如线程池的核心线程) ,value 永远无法回收,造成内存泄漏。

内存泄漏的本质:线程长期存活 + ThreadLocalMap 中存在 key 为 null 的 Entry(value 仍被强引用)

实际上,我们的内存泄漏的根本原因是,不再被使用的Entry,没有从线程的ThreadLocalMap中删除。一般删除不再使用的Entry有这两种方式:

一种就是,使用完ThreadLocal,手动调用remove(),把Entry从ThreadLocalMap中删除

另外一种方式就是:ThreadLocalMap的自动清除机制去清除过期Entry.(ThreadLocalMap的get(),set()时都会触发对过期Entry的清除)


线程池

java 复制代码
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

参数介绍:

  • corePoolSize:核心线程数,定义了最小可以同时运行的线程数量

  • maximumPoolSize:最大线程数,当队列中存放的任务达到队列容量时,当前可以同时运行的数量变为最大线程数,创建线程并立即执行最新的任务,与核心线程数之间的差值又叫救急线程数

  • keepAliveTime:救急线程最大存活时间,当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等到 keepAliveTime 时间超过销毁

  • unit:keepAliveTime 参数的时间单位

  • workQueue:阻塞队列,存放被提交但尚未被执行的任务

  • threadFactory:线程工厂,创建新线程时用到,可以为线程创建时起名字

  • handler:拒绝策略,线程到达最大线程数仍有新任务时会执行拒绝策略

    RejectedExecutionHandler 下有 4 个实现类:

    • AbortPolicy:(默认):抛出 RejectedExecutionException
    • CallerRunsPolicy:由调用线程执行任务
    • DiscardPolicy:直接丢弃任务,不抛出异常
    • DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试重新提交

工作原理:

  • 创建线程池,这时没有创建线程(懒惰),等待提交过来的任务请求,调用 execute 方法才会创建线程
  • 当调用 execute() 方法添加一个请求任务时,线程池会做如下判断:
    • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务
    • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列
    • 如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize,那么会创建非核心线程立刻运行这个任务,对于阻塞队列中的任务不公平。这是因为创建每个 Worker(线程)对象会绑定一个初始任务,启动 Worker 时会优先执行
    • 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
  • 当一个线程完成任务时,会从队列中取下一个任务来执行
  • 当一个线程空闲超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小

创建多大容量的线程池合适?

  • CPU密集型: 任务需要大量计算, 很少阻塞, CPU一直处于忙碌状 态. CPU核数 + 1
  • IO密集型: 任务需要频繁的IO操作(与磁盘, 网络交互), CPU经常等 待IO完成. CPU核数 * 2

AQS

AQS(AbstractQueuedSynchronizer) 是Java并发包中java.util.concurrent.locks核心同步框架,是实现锁和同步器(如ReentrantLock、Semaphore、CountDownLatch等)的基础。

原理

  1. 初始状态
    state = 0(锁空闲),exclusiveOwnerThread = null(无持有线程)
  2. 线程1加锁
    • CAS尝试将state从0→1 → 成功
    • 设置exclusiveOwnerThread = 线程1
    • 可重入实现 :若线程1已持有锁,直接递增state(不修改持有线程)
  3. 线程2加锁
    • CAS尝试将state从0→1 → 失败state=1
    • 检查exclusiveOwnerThread是否为自身?→ (是线程1)
    • 入FIFO等待队列,被挂起(park)
  4. 线程1释放锁
    • 递减state(如2→1
    • 仅当state=0 :清空exclusiveOwnerThread = null(完全释放)
  5. 线程2重试
    • 线程1释放后唤醒队头线程(线程2)
    • 线程2重试CAS:state=0→1成功
    • 设置exclusiveOwnerThread = 线程2,从队列出队

ReentrantLock

java 复制代码
// 获取锁
reentrantLock.lock();
try {
    // 临界区
} finally {
	// 释放锁
	reentrantLock.unlock();
}
  1. 可重入

  2. 公平锁:ReentrantLock lock = new ReentrantLock(true)

  3. 可打断:public void lockInterruptibly()

  4. 锁超时:public boolean tryLock()、public boolean tryLock(long timeout, TimeUnit unit)

  5. 条件变量:

    synchronized 的条件变量,是当条件不满足时进入 WaitSet 等待;ReentrantLock 的条件变量比 synchronized 强大之处在于支持多个条件变量

    这就好比

    • synchronized 是那些不满足条件的线程都在一间休息室等消息
    • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
    • ReentrantLock 类获取 Condition 对象:public Condition newCondition()
    • Condition 类 API:
      • void await():当前线程从运行状态进入等待状态,释放锁
      • void signal():唤醒一个等待在 Condition 上的线程,但是必须获得与该 Condition 相关的锁

同步工具

Semaphore

Semaphore(信号量)控制同时访问某一资源的线程数量 ,实现限流(流量控制)或资源池管理

java 复制代码
   public static void main(String[] args) {
        // 1.创建Semaphore对象
        Semaphore semaphore = new Semaphore(3);

        // 2. 10个线程同时运行
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    // 3. 获取许可
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " running...");
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " end...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 4. 释放许可
                    semaphore.release();
                }
            }).start();
        }
    }

每次运行结果不同,但会保证最多3个线程同时执行。

CountDownLatch

CountDownLatch(倒计时门闩) ,让一个或多个线程等待 其他线程完成一系列操作后再继续执行。一次性使用

java 复制代码
    public static void main(String[] args) throws InterruptedException {
        // 创建CountDownLatch,初始计数为3(等待3个线程完成)
        CountDownLatch latch = new CountDownLatch(3);

        // 创建3个工作线程
        for (int i = 1; i <= 3; i++) {
            new Thread(() -> {
                try {
                    // 模拟工作(随机延迟)
                    TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 1000));
                    System.out.println(Thread.currentThread().getName() + " 完成任务");
                    
                    // 任务完成后计数减1
                    latch.countDown();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }, "Worker-" + i).start();
        }

        System.out.println("主线程等待所有工作线程完成...");
        
        // 主线程等待所有工作线程完成(阻塞)
        latch.await();
        
        System.out.println("所有工作线程已完成,主线程继续执行");
    }

每次运行结果不同,但主线程会等待所有线程完成

CyclicBarrier

CyclicBarrier(循环栅栏) :让一组线程互相等待 ,直至全部到达某个公共屏障点 后,再一起继续执行。支持重复使用

与 CountDownLatch 的区别:CyclicBarrier的屏障点可以多次使用(重置后可继续使用),而CountDownLatch只能用一次。

例子:可以实现多线程中,某个任务在等待其他线程执行完毕以后触发

java 复制代码
    public static void main(String[] args) {
        // 创建CyclicBarrier,指定3个线程,添加屏障动作
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            System.out.println("所有选手已准备就绪,比赛即将开始!");
        });

        // 创建3个选手线程
        for (int i = 1; i <= 3; i++) {
            new Thread(() -> {
                try {
                    // 模拟选手准备时间(随机延迟)
                    TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 1000));
                    System.out.println(Thread.currentThread().getName() + " 已准备就绪");
                    
                    // 等待其他选手到达屏障点
                    barrier.await();
                    
                    // 所有选手到达后,开始比赛
                    System.out.println(Thread.currentThread().getName() + " 开始比赛");
                } catch (InterruptedException | BrokenBarrierException e) {
                    Thread.currentThread().interrupt();
                }
            }, "选手-" + i).start();
        }
    }

每次运行结果不同,但会等待所有选手到达

Exchanger

Exchanger:交换器,是一个用于线程间协作的工具类,用于进行线程间的数据交换

工作流程:两个线程通过 exchange 方法交换数据,如果第一个线程先执行 exchange() 方法,它会一直等待第二个线程也执行 exchange 方法,当两个线程都到达同步点时,这两个线程就可以交换数据


ConcurrentHashMap

ConcurrentHashMap jdk1.7与jdk1.8区别

JDK1.7中

  • 数据结构

ConcurrentHashMap 1.7 采用 Segment 分段锁机制,其中 Segment 是 ReentrantLock 锁对象,每个 Segment 内部包含一个 HashEntry 数组,数组的每个元素是一个链表头节点,链表由 HashEntry 节点组成。当对数据进行修改时,必须先获取对应 Segment 的锁,实现高并发下的线程安全。

但是这么做的缺陷就是每次通过 hash 确认位置时需要 2 次才能定位到当前 key 应该落在哪个槽:

  1. 通过 hash 值和 段数组长度-1 进行位运算确认当前 key 属于哪个段,即确认其在 segments 数组的位置。
  2. 再次通过 hash 值和 table 数组(即 ConcurrentHashMap 底层存储数据的数组)长度 - 1进行位运算确认其所在桶。
java 复制代码
public class ConcurrentHashMap<K, V> {
    // Segment数组(默认16个)
    final Segment<K, V>[] segments;
    
    // Segment内部结构
    static final class Segment<K, V> extends ReentrantLock {
        // HashEntry数组(桶数组)
        transient volatile HashEntry<K, V>[] table;
        
        // 链表节点(HashEntry)
        static final class HashEntry<K, V> {
            final K key;
            final V value;
            final int hash;
            final HashEntry<K, V> next;
        }
    }
}
  • put 操作流程(需锁)

    1. 计算Segment索引(hash >>> 28) & 15
    2. 获取Segment锁segments[segmentIndex].lock()
    3. 计算桶索引hash & (table.length - 1)
    4. 遍历链表:
      • 找到key → 更新value
      • 未找到 → 链表头插入新节点
    5. 检查扩容count > threshold → 触发rehash()
    6. 释放锁segments[segmentIndex].unlock()
  • get 操作流程(无锁)

  1. 计算Segment索引(hash >>> 28) & 15
  2. 获取Segmentsegments[segmentIndex]
  3. 计算桶索引hash & (table.length - 1)
  4. 遍历链表table[index]next → ... → 找到key
  5. 返回value(或null)

关键特点完全无锁tablevolatile保证可见性

JDK1.8中

  • 数据结构

    1.8中放弃了Segment臃肿的设计,选择了和 HashMap 相同的 Node 数组 + 链表 + 红黑树结构,采用 CAS + Synchronized 来保证并发安全性,锁粒度细化到链表头节点级别。结构如下:

  • put 操作流程(CAS + synchronized)

    1. 初始化table为空 → initTable()

    2. 计算hashhash = spread(key.hashCode())

    3. 定位桶i = (n - 1) & hash

    4. CAS插入(桶为空):

      • casTabAt(table, i, null, new Node(...))
      • 失败 → 自旋重试
    5. 协助扩容 (桶头是MOVED):

      • helpTransfer(table, f)
    6. 同步插入(桶非空):

      java 复制代码
      synchronized (f) { // 锁住桶头节点
          if (tabAt(table, i) == f) {
              // 链表操作:遍历、更新、尾插
              // 红黑树操作:putTreeVal()
              // 链表长度≥8 → treeifyBin()
          }
      }
    7. 更新计数addCount(1L, binCount)

  • get 操作流程(无锁)

    1. 计算hashhash = spread(key.hashCode())
    2. 定位桶i = (n - 1) & hash
    3. 获取节点f = tabAt(table, i)
    4. 匹配查找:
      • 如果是ForwardingNode (标志正在扩容节点)→ 去nextTable查找
      • 匹配key → 返回value
      • 未匹配 → 遍历链表/红黑树
    java 复制代码
    static class Node<K, V> implements Map.Entry<K, V> {
        final int hash;
        final K key;
        volatile V value;        // value 使用 volatile 修饰
        volatile Node<K, V> next; // next 使用 volatile 修饰
    }

    全程无锁value 和 next 使用 volatile 修饰:确保内存可见性

ConcurrentHashMap和Hashtable的异同点

Hashtable 采用全局锁机制(只允许一个线程操作整个数据结构),性能差,已过时;ConcurrentHashMap 采用细粒度锁(JDK 8+ 为 CAS + synchronized),锁粒度细化到桶头节点级别,实现理论无限并发,是高并发场景下线程安全哈希表的首选。


JUC并发集合

CopyOnWrite

  • 写操作流程:
    1. 获取写锁
    2. 获取当前数组
    3. 复制新数组
    4. 修改新数组
    5. 原子替换setArray(newElements)
    6. 释放写锁
  • 读操作 :不加锁,在原数组上操作

SkipList :基于跳表(Skip List)数据结构

并发List

  • CopyOnWriteArrayList → 替代 ArrayList

并发Set

  • CopyOnWriteArraySet→ 替代 HashSet

  • ConcurrentSkipListSet→ 替代 TreeSet

并发Map

  • ConcurrentHashMap→ 替代 HashMap

  • ConcurrentSkipListMap→ 替代 TreeMap

并发Queue

  • 非阻塞队列

    • ConcurrentLinkedQueue
    • ConcurrentLinkedDeque
  • 阻塞队列BlockingQueue

    • ArrayBlockingQueue
    • LinkedBlockingQueue
    • SynchronousQueue

全程无锁value 和 next 使用 volatile 修饰:确保内存可见性

ConcurrentHashMap和Hashtable的异同点

Hashtable 采用全局锁机制(只允许一个线程操作整个数据结构),性能差,已过时;ConcurrentHashMap 采用细粒度锁(JDK 8+ 为 CAS + synchronized),锁粒度细化到桶头节点级别,实现理论无限并发,是高并发场景下线程安全哈希表的首选。


JUC并发集合

CopyOnWrite

  • 写操作流程:
    1. 获取写锁
    2. 获取当前数组
    3. 复制新数组
    4. 修改新数组
    5. 原子替换setArray(newElements)
    6. 释放写锁
  • 读操作 :不加锁,在原数组上操作

SkipList :基于跳表(Skip List)数据结构

并发List

  • CopyOnWriteArrayList → 替代 ArrayList

并发Set

  • CopyOnWriteArraySet→ 替代 HashSet

  • ConcurrentSkipListSet→ 替代 TreeSet

并发Map

  • ConcurrentHashMap→ 替代 HashMap

  • ConcurrentSkipListMap→ 替代 TreeMap

并发Queue

  • 非阻塞队列

    • ConcurrentLinkedQueue
    • ConcurrentLinkedDeque
  • 阻塞队列BlockingQueue

    • ArrayBlockingQueue
    • LinkedBlockingQueue
    • SynchronousQueue
相关推荐
哈哈不让取名字2 小时前
分布式日志系统实现
开发语言·c++·算法
3GPP仿真实验室2 小时前
【MATLAB源码】6G:感知辅助毫米波 MIMO 信道估计仿真平台
开发语言·matlab·智能电视
芬加达2 小时前
leetcode221 最大正方形
java·数据结构·算法
猿小羽2 小时前
深度实战:Spring AI 与 MCP(Model Context Protocol)构建下一代 AI Agent
java·大模型·llm·ai agent·spring ai·开发者工具·mcp
catchadmin2 小时前
Laravel12 + Vue3 的免费可商用 PHP 管理后台 CatchAdmin V5.1.1 发布
开发语言·php
曾几何时`2 小时前
二分查找(十)1146. 快照数组 pair整理
java·服务器·前端
编程(变成)小辣鸡2 小时前
JVM、JRE和JDK 的关系
java·开发语言·jvm
lbb 小魔仙2 小时前
【Java】Spring Cloud 微服务系统搭建:核心组件 + 实战项目,一步到位
java·spring cloud·微服务
a程序小傲2 小时前
得物Java面试被问:流批一体架构的实现和状态管理
java·开发语言·数据库·redis·缓存·面试·架构