Java——JUC
目录
4.调用start方法时会执行run方法,那怎么不直接调用run方法?
public final void join();//插入线程/插队线程
基础
1.你是如何理解线程安全的?
如果一段代码块或者一个方法被多个线程同时执行,还能够正确地处理共享数据,那么这段代码块或者这个方法就是线程安全的。
可以从三个要素确保线程安全:
- 原子性:一个操作要么完全执行,要么完全不执行,不会出现中间状态。
可以通过同步关键字synchronized或原子操作,如AtomicInteger来保证原子性
AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); // 原子操作2.可见性:当一个线程修改了共享变量,其他线程能够立即看到变化。
-
有序性:要确保线程不会因为死锁,饥饿,活锁等问题导致无法继续执行。

2.说说进程和线程的区别?
进程说简单点就是我们在电脑上启动的一个个应用。它是操作系统分配资源的最小单位。
线程是进程中的独立执行单元。多个线程可以共享一个进程的资源,如内存;每个线程都有自己独立的栈和寄存器。
如何理解协程
协程是一种用户态的轻量级线程,也被称为“微线程”。与传统线程相比有如下优势:
- 用户态调度:协程的调度完成由程序控制,不需要操作系统介入,开销极小(通常只需要保存少量上下文信息)
- 协作式任务:协程之间的切换是“协作式”的,而不是线程的“抢占式”。一个协程需要主动让出CPU资源,其他协程才能运行。
- 高效的并发处理:单个线程内可以运行多个协程,非常适合IO密集型任务(如网络请求,文件操作等)。
- 共享内存:同一线程内的协程共享内存空间,不需要复杂的同步机制。
可以用一个简单的比喻理解协程:传统线程像多个独立的工人在各自的工作间工作,协程像是同一个工人在多个任务之间灵活切换,哪个任务需要处理就先做哪个。
线程间是如何进行通信的?
消息传递(管道,消息队列)和共享内存两种方法来实现。Java采用的是共享内存的并发模型。
JMM决定了一个线程对共享变量的写入,何时对另外一个线程可见。
用一句话概括是:共享变量存储在主内存中,每个线程的私有本地内存,存储的是这个共享变量的副本。
3.说说线程的几种创建方式

一种需要重写父类 Thread 的 run() 方法,并且调用 start() 方法启动线程
第二种需要重写 Runnable 接口的 run() 方法,并将实现类的对象作为参数传递给 Thread 对象的构造方法,最后调用 start() 方法启动线程。
这种方法的优点是可以避免 Java 的单继承限制,并且更符合面向对象的编程思想,因为 Runnable 接口将任务代码和线程控制的代码解耦了。
第三种需要重写 Callable 接口的 call() 方法,然后创建 FutureTask 对象,参数为 Callable 实现类的对象;紧接着创建 Thread 对象,参数为 FutureTask 对象,最后调用 start() 方法启动线程。
这种方法的优点是可以获取线程的执行结果。
4.调用start方法时会执行run方法,那怎么不直接调用run方法?
调用start()会创建一个新的线程,并异步执行run()方法中的代码。
直接调用run()方法只是一个普通的同步方法调用。所有代码都在当前线程中执行,不会创建新线程。没有新的线程创建,也就达不到多线程并发的目的。
也就是说,调用start()方法会通知JVM,去调用底层的线程机制来启动新线程。

调用start()后,线程进入就绪状态,等待操作系统调度;一旦调度执行,线程会执行其run()方法中的代码。
6.线程的生命周期
具体方法

final void setDaemon(boolean on);设置守护线程
当其它的非守护线程执行完毕之后,守护线程会陆续结束
通俗易懂:
当女神线程结束了,备胎也没有存在的必要了
应用场景:
聊天一个线程,传输文件一个线程

public static void yield() 出让线程/礼让线程//出让当前cpu的执行权
public final void join();//插入线程/插队线程
- 是一种让线程同步的方法。让线程ab从并行执行,变为串行执行。
- join()方法的本质时wait()和notify()方法。会让当前线程进入阻塞等待状态,直到调用jion()方法的线程执行结束回来,当前线程会被notify()回来。
- 在main()线程中调用t1.join()底层在t1线程没有执行结束的时候,对主线程采用了等待唤醒机制,底层实际上调用的wait()方法,直到join的线程执行结束后被notify()【由JVM来唤醒】
sleep()方法


interrupt()方法
在Java中interrupt()方法是线程中断机制的核心,用于请求中断一个线程的执行。需要注意的是,它并非直接终止线程,而是给线程发送一个中断信号,由线程自行决定如何处理这个信号(继续执行,停止执行或其他处理)。
工作原理
每个线程都有一个内部的中断标记(boolean),interrupt()方法的注意作用是:
- 将目标线程的中断状态设置未true。
- 如果目标线程处于阻塞状态(如调用sleep(),wait(),join()等方法时),会抛出InterruptedException并清除中断状态(将标记重置为false)。
使用场景
- 1.中断阻塞状态的线程
- 2.中断运行中的线程
- 当线程处于阻塞状态时调用interrupt()会导致线程退出阻塞并抛出异常:
- 对于正在运行的线程,interrupt()仅设置中断标记,需线程主动检查并标记处理:
关键方法
interrupt():设置线程的中断标记为true,若线程阻塞则唤醒并抛异常。isInterrupted():检查线程的中断标记(不会清除标记)。Thread.interrupted():静态方法,检查当前线程的中断标记(会清除标记)。
注意事项
- 非阻塞线程需主动配合:对于未阻塞的线程,
interrupt()仅设置标记,需线程通过isInterrupted()主动检查并退出。 - 异常会清除标记:在
InterruptedException被捕获后,中断标记会被重置为false,如需继续传递中断信号,需手动调用interrupt()重新设置。 - 不可中断的操作:某些 I/O 操作(如
InputStream.read())或synchronized锁等待无法被interrupt()中断。
interrupt() 机制体现了 Java 线程设计的协作式中断理念,避免了强制终止线程可能导致的资源泄露等问题,更安全可控。
从“JavaAPI”层面来描述(6种):
根据Thread。State一种枚举类来分的:


各状态与转换路径详解
- 1.NEW(新建状态)
- 含义:线程被创建但未调用start()方法,此时线程未分配CPU执行资源。
- 转换:调用start()方法后,进入RUNNABLE状态
- 2.RUNNABLE(可运行状态)
- 含义:线程可被CPU调度执行,细分为两部分:
- 运行状态:线程正在占用CPU执行代码
- 可运行状态:线程已就绪,等待CPU调度(如刚从其他状态唤醒,或与其他线程竞争到 CPU 前)
- 转换:
- 到BLOCKED:线程尝试获取synchronized锁但失败,进入BLOCKED状态(路径 9,竞争锁失败后进入 EntryList 等待)。
- 到WAITING:
- 调用
Object.wait()(无超时)、Thread.join()(无超时)、LockSupport.park()后,进入WAITING状态(路径 2、3、4)。
- 调用
- 到
TIMED_WAITING:- 调用
Thread.sleep(n)、Object.wait(n)、Thread.join(n)、LockSupport.parkNanos()/parkUntil()后,进入TIMED_WAITING状态(路径 5、6、7、8)。
- 调用
- 3.BLICKED(阻塞状态)
- 含义:线程因竞争synchronized锁失败,在EntryList(锁的等待队列)中等待,直到获取锁。
- 转换:当持有锁的线程释放锁,并通过notify()/notifyAll()唤醒等待线程后,BLOCKED状态的线程会重新竞争锁。若竞争成功,进入RUNNABLE状态;若竞争失败继续留在BLOCKED。
- 4.WAITING(无线等待状态)
- 含义:线程无超时地等待被唤醒,若不被主动唤醒,会一直阻塞。
- 转换:
- 到RUNNABLE:
- 被其他线程调用
Object.notify()/notifyAll()唤醒(路径 2,需竞争锁后进入RUNNABLE)。 - 被等待的线程执行完毕(如
join()的目标线程终止,路径 3)。 - 被其他线程调用
LockSupport.unpark()唤醒(路径 4)。
- 被其他线程调用
- 到RUNNABLE:
-
5.
TIMED_WAITING(超时等待状态) - 含义:线程在指定超时时间内等待,超时后会自动唤醒。
- 转换:
- 到
RUNNABLE:- 超时时间到,自动唤醒(如
sleep(n)时间到、wait(n)超时,路径 5、7)。 - 被其他线程提前唤醒(如
notify()/unpark(),路径 6、8 等场景)。
- 超时时间到,自动唤醒(如
- 到
-
6.
TERMINATED(终止状态) - 含义:线程执行完毕(
run()方法正常结束,或因异常退出),生命周期结束,无法再回到其他状态。
- 含义:线程可被CPU调度执行,细分为两部分:
三、关键机制总结
- 锁竞争与
BLOCKED:synchronized锁的竞争失败会导致线程进入BLOCKED,成功获取锁后回到RUNNABLE。 - 等待 / 唤醒与
WAITING/TIMED_WAITING:wait()/notify()、join()、park()/unpark()等方法用于线程间同步,触发线程在这两种等待状态间流转。 - 状态的不可逆性:
TERMINATED是最终状态,线程一旦终止,无法再回到RUNNABLE或其他状态重新执行。
7.怎么保证线程安全
临界区
一段代码块内如果存在对共享资源的多线程读写操作,则称这段代码块为临界区!
竞态条件

解决方案
为了避免出现临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized, Lock。
- 非阻塞式:原子变量,concurrent包下的变量的一些api

8.Java内存模型
说说你对Java内存模型的理解?
JMM即Java Memory Model,它定义了主存(线程共享),工作内存(线程私有)的抽象概念,底层对应者CPU寄存器,缓存,硬件内存,CPU指令优化等。
JMM体现在以下几个方面
- 原子性-保证指令不会受到线程上下文切换的影响
- 可见性-保证指令不会受到CPU缓存的影响。
- 有序性-保证指令不会受到CPU指令并行优化的影响。
1.可见性
JIT编译器会将run的值缓存到自己的工作内存中的高速缓存中,减少对run的访问,提高效率

解决办法
- volatile
- 可以修饰成员变量和静态成员变量,避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存
- synchronized
- 既可以保证原子性也可以保证代码块内变量的可见性。确定是重量级操作,性能更低
2.有序性
JVM会在不影响正确性的前提下,调整语句的执行顺序
3.volatile实现原理
volatile的底层实现依赖于内存屏障(一种CPU指令),通过插入特定的内存屏障指令,强制实现变量的可见性和指令有序性
- 1.内存屏障的作用
- 阻止屏障两侧的指令重排(保证指令执行顺序);
- 强制刷新CPU缓存,使缓存中的数据与主内存同步(保证可见性)
- 2.volatile变量的读写过程与内存屏障插入规则


- 可见性:StoreLoad屏障会触发CPU的“缓存刷新”,使b=2的最新值立即写入主内存,其他线程读取b时会从主内存加载(而非使用旧缓存值)
- 禁止重排:StoreStore屏障保证a=1一定在b=2之前执行,避免编译器或CPU将两者顺序颠倒。

- 可见性:LoadLoad 屏障会强制线程 2 从主内存读取
b的最新值(而非使用本地缓存的旧值)。 - 禁止重排:LoadStore 屏障保证
temp = b一定在c=3之前执行,避免重排导致逻辑错误。
与CPU缓存一致性协议的关系
- 现代CPU通过MESI协议(缓存一致性协议)保证多核心之间的缓冲同步,而volatile的可见性实现也依赖于此:
- 当一个 CPU 核心修改了
volatile变量,会通过 MESI 协议通知其他核心 “该变量的缓存行失效”; - 其他核心读取该变量时,发现缓存行失效,会主动从主内存重新加载最新值。
- 当一个 CPU 核心修改了
内存屏障的作用是强制触发 MESI 协议的同步行为,确保修改后的 volatile 变量能立即被其他线程感知。
9.锁
synchronized
1.对象的组成:
对象头 + 实例数据 + 对齐填充字节
1.1Java对象头
普通对象

其中MarkWord结构为

1.Monitor(锁)
Monitor被翻译为监视器或管程
每个Java对象都可以关联一个Monitor对象,Monitor也是class,其实例存储在堆中,如果使用synchronized给对象上锁之后,该对象头的Mark Word中被设置指向Monitor对象的指针,也就是重量级锁
Monitor对象是操作系统提供的一个对象!
Monitor结构如下


2.轻量级锁
- 轻量级锁对于重量级锁的一种优化。轻量级锁一定是处于不存在多个线程竞争下的场景。
- 轻量级锁提升性能的依据是“对于绝大部分锁,在同一个同步周期内是不存在线程竞争的!”。
- 所以一旦两条或两条以上的线程竞争同一把锁的情况,轻量级锁会进行锁膨胀升级为重量级锁。将锁对象Mark Word中锁标志位从00转变为10。
- 如果不存在竞争的话,轻量级锁便通过CAS操作避免了使用互斥量而消耗的性能。
工作流程:
- 代码进入同步同步代码块后,看对象的锁标志状态。如果是无锁状态,虚拟机会在当前栈帧中创建一个Lock Record锁记录的空间
- 该空间object reference指向对象地址,lock record存储对象头中的MarkWord的拷贝
- 虚拟机常用CAS操作.将共享对象的对象头中Mark Word中前30个bit替换成指向锁记录的指针.如果操作成功,说明当前线程获取到了对象的锁.并将Mark Word中锁标识位的01变为00。
- 如果操作失败了,说明一定有存在竞争该锁对象的其他的线程,会先检查Mark Word中指向的是否是当前线程栈帧中的锁记录,如果是说明是当前线程获取到了锁,就直接执行代码块.
- 如果不是说明当前对象锁是被其他线程获取到了. 此时就会进行锁膨胀升级为重量级锁.且与其竞争的线程进入阻塞状态!

锁重入

锁膨胀
- CAS操作失败后,会为当前锁申请一个monitor对象,修改锁对象中MarkWord前30个bit,由原先指向所记录改为指向monitor对象。
- 拿到锁的线程执行结束后,要解锁这个重量级锁,根据锁记录中存储锁对象MarkWord的拷贝,定位到当前的锁。
- 对象中前30bit所指向的monitor对象,然后设置monitor对象中owner为null,且唤醒EntryList中等在阻塞来获取所,解锁结束。

3.自旋锁
4.偏向锁
每次锁的重入都需要CAS操作。每次锁重入时都会产生一个锁记录,然后尝试用CAS将MarkWord替换成指向锁记录的指针。
偏向锁:
- 只有第一次使用CAS将线程ID存储到锁对象MarkWord中,之后如果一直是当前线程来获取锁对象,该锁会偏向这个线程,不需要进行任何同步操作,
- 线程来获取锁,将对象前23个bit存储该线程ID,对象锁标识设置为01,后续其他线程来获取锁,偏向锁模式结束,转换为00(轻量级锁)
偏向锁的撤销
当另一个线程尝试获取已偏向的锁时,JVM需要撤销偏向锁,这个过程需要:
- 暂停持有偏向锁的线程
- 检查该线程是否还在执行同步块
- 如果线程已经退出同步块,则将对象头设置为无锁状态
- 如果线程仍在执行,则升级为轻量级锁
ReentrantLock
ReentrantLock的底层依赖于AbstractQueuedSynchronizer(AQS)框架。核心围绕状态管理,线程排队和锁获取/释放机制展开:
一,核心基础:AQS框架
AQS是一个抽象同步器,通过内置的双向队列(CLH队列变种)管理等待线程,并通过volatile修饰的state变量控制同步状态。ReentrantLock中:
- state表示锁的重入次数(0 表示无锁,>0 表示被持有,数值为重入次数)。
- 队列用于存储竞争锁失败的线程(等待队列)
二,锁的实现核心逻辑
- 获取锁(lock()方法):

- 释放锁(unlock()方法):

- 公平锁的差异
- 公平锁的tryAcquire方法会多一步检查:当前线程是否位于等待队列的头部,确保按FIFO顺序获取锁:
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 公平性关键:hasQueuedPredecessors() 检查是否有前驱线程在等待 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 重入逻辑(与非公平锁一致) else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
- 公平锁的tryAcquire方法会多一步检查:当前线程是否位于等待队列的头部,确保按FIFO顺序获取锁:
三,等待队列(CLH队列变种)
AQS 的等待队列是一个 双向链表,每个节点(Node)包含:
- 线程引用(
thread)。 - 等待状态(
waitStatus:如0初始态、SIGNAL需唤醒后继、CANCELLED取消等待等)。 - 前驱(
prev)和后继(next)指针。
队列操作逻辑:
- 线程竞争锁失败时,被包装为
Node加入队列尾部(通过 CAS 保证原子性)。 - 队列头部(
head)是已获取锁的线程节点。 - 当锁释放时,头部节点唤醒后继节点(
SIGNAL状态的节点),后继节点尝试获取锁。
四、可重入性的实现
ReentrantLock 的可重入性通过以下逻辑保证:
- 当线程再次获取锁时,
tryAcquire检查当前线程是否为锁的持有者(current == getExclusiveOwnerThread())。 - 若是,则直接增加
state计数(state += 1),无需竞争。 - 释放时需对应减少
state,直到state == 0才真正释放锁。
ReentrantLock 的底层是 AQS 框架,通过 state 变量控制锁状态,利用双向队列管理竞争线程,实现了可重入、公平 / 非公平选择、条件变量等功能。
CAS
CAS(Compare And Swap,比较并交换)是并发编程中一种实现无锁同步的核心机制,它是许多原子类(如AtomicInteger)和并发工具底层实现的基础。其核心思想是通过原子操作实现多线程对共享资源的安全访问,无需使用锁。
一、CAS 的基本原理
CAS 操作包含三个核心参数:
- 内存地址 V:存储共享变量的内存位置
- 预期值 A:线程认为变量当前应该的值
- 新值 B:线程希望将变量更新为的值
操作逻辑:
- 读取内存地址 V 中的当前值,与预期值 A 比较
- 如果相等(说明值未被其他线程修改),则将 V 中的值更新为 B
- 如果不相等(说明值已被其他线程修改),则不做任何操作
- 无论是否更新成功,都返回 V 中的旧值
整个过程是原子操作(由 CPU 指令直接支持),不会被其他线程中断,因此能保证并发安全。
二,CAS的底层实现
CAS并非Java语言层面的特性,而是依赖硬件指令实现:
- x86 架构下通过
cmpxchg指令 - ARM 架构下通过
ldrex/strex指令对
JVM将Java中的CAS操作(如Unsafe类的compateAndSwapInt)映射为对应的CPU指令,确保操作的原子性。
三,CAS的核心优势
- 1.无锁机制:避免了锁带来的线程阻塞,上下文切换开销,在低竞争场景下性能更优
- 2.原子性保证:由硬件指令确保操作不可分割,比synchronized更轻量
- 3.并发友好:多个线程竞争是,失败的线程可以立即重试,而非进入阻塞状态
四,CAS的局限性
-
ABA 问题
- 问题描述:线程 1 准备将变量从 A 改为 B,期间线程 2 将变量改为 C 又改回 A,线程 1 的 CAS 会误认为值未变而成功更新,可能导致逻辑错误。
- 解决方案:使用版本号机制(如
AtomicStampedReference),给变量增加版本号,更新时同时检查值和版本号。
-
循环开销
- 当并发激烈时,CAS 可能长期失败并循环重试,导致 CPU 资源浪费(“自旋” 消耗)。
-
只能保证单个变量的原子操作
- CAS 仅支持对单个变量的原子操作,无法直接实现多个变量的原子性(需结合其他机制)。
五、CAS 的典型应用
- 原子类:
java.util.concurrent.atomic包下的类(如AtomicInteger、AtomicReference)均基于 CAS 实现。 - 并发工具:
ReentrantLock的 AQS 框架中,状态变量state的修改依赖 CAS。 - 乐观锁:数据库的乐观锁实现(如通过版本号字段)与 CAS 思想一致。
线程池
线程池代码实现
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。

1.2线程池的优点:
- 1.资源复用,减少资源消耗
- 2.提高响应速度,提高效率
- 3.增强线程资源的可管理性。
自定义线程池
1.核心线程数量
2.线程池中最大线程的数量
3.空闲时间(值)
4.空闲时间(单位)
5.阻塞队列
6.创建线程的方式
7.要执行的任务过多的解决方案
- 1.核心线程都在忙,队伍已满才会去创建临时线程
- 2.先提交的任务不一定先执行
- 3.核心线程使用,队伍排满,临时线程全部工作,后面的会执行任务拒绝策略
任务拒绝策略

线程池多大合适


- +1保证当前项目由于缺失故障或者由于其他原因导致线程暂停,额外的线程可以顶上去
- 可以使用工具thread dump测试到CPU计算时间
ConcurrentHashMap
ConcurrentHashMap是Java集合框架中用于高并发场景的hash表实现,为多线程环境设计,支持高并发的读写操作,同时保证线程安全。实现在jdk1.7和jdk1.8中有较大差异:
一,JDK1.7实现:分段锁(Segment)机制
JDK1.7中,ConcurrentHashMap采用“分段锁”设计,核心思想是将整个哈希表分为多个独立的段(Segment),每个段本质上是一个小的hash表(类似HashMap),并自带一把锁。
1.数据结构:
- 整体是一个Segment数组,每个Segment包含一个HashEntry数组(链表结构)。
- Segment继承自ReentrantLock,具备锁的功能
2.核心特性:
- 锁粒度细化:不同线程操作不同Segment时,无需竞争同一把锁,大大减少锁冲突。
- 并发度:默认并发度为16(即默认16个Segment),理论上支持16个线程同时写入。
- 读写分离:读操作无需加锁(通过volatile保证可见性),写操作仅锁定当前Segment。
3.操作逻辑:
定位元素:通过哈希计算确定元素所在的Segment,再在该Segment内部定位具体位置。
写操作(put/remove等):锁定目标Segment,完成操作后释放锁。
读操作(get等):直接访问目标Segment,无需加锁(依赖volatile保证最新值可见)。
二,JDK1.8实现:CAS + synchronized优化
JDK1.8摒弃了分段锁,采用“数组+链表+红黑树”的结构(于HashMap类似),并通过CAS原子操作和synchronized关键字实现线程安全,进一步提升并发性能。
1.数据结构:
- 底层是Node数组,每个Node可能是链表节点或红黑树节点(但链表长度超过8时转为红黑树)。
- 引入ForwardingNode等特殊节点,用于支持扩容时的并发操作。
2.核心改进:
- 移除分段锁:降低锁粒度到单个链表/红黑树的头节点,减少锁竞争。
- CAS操作:对数组元素的初始化,插入等操作使用CAS实现无锁并发。
- synchronized锁定头节点:当CAS失败(存在并发冲突)时,使用synchronized锁定链表/红黑树的头节点,保证操作的原子性。
- volatile保证可见性:Node数组和节点的val,next等字段用volatile修饰,确保多线程下的内存可见性。
-
操作逻辑:
- put 操作:
- 计算哈希值,定位数组索引。
- 若数组为空,初始化数组(CAS 保证线程安全)。
- 若目标位置为空,直接 CAS 插入新节点。
- 若目标位置存在节点,且处于扩容中(遇 ForwardingNode),协助扩容。
- 否则,用 synchronized 锁定头节点,插入节点(链表或红黑树)。
- get 操作:
- 计算哈希值,定位数组索引。
- 遍历链表 / 红黑树查找元素,无需加锁(依赖 volatile 可见性)。
- 扩容机制:支持多线程并发扩容,通过 ForwardingNode 标记已迁移的桶,其他线程可协助迁移数据,提升扩容效率。
- put 操作:
四,核心优势
- 1.高并发支持:读操作几乎无锁,写操作仅锁定局部节点,适合多线程频繁读写场景。
- 2.线程安全:通过锁机制和volatile保证数据一致性,无需外部同步。
- 3.性能优化:JDK1.8取消分段锁后,在低并发场景下性能接近HashMap,高并发场景下优于Hashtable(全表锁)。
更多推荐



所有评论(0)