JUC并发编程知识二(待完善) 多线程Java并发常见面试题总结上 | JavaGuide1.什么是多线程多线程是指一个进程中创建了多个线程多个线程共享进程资源并且各自执行独立的任务。2.多线程的好处多线程能够提高CPU的利用率当一个线程进入阻塞状态时其他线程能够可以继续使用CPU避免CPU闲置。CPU是计算机的核心硬件购买和运行是存在成本的让CPU闲置意味着 “付费却不用”提高利用率才能够对得起CPU的高成本3.多线程带来的问题并发编程并不总是能提高程序运行速度的而且并发编程可能会遇到很多问题比如内存泄漏、死锁、线程不安全等等并发、并行1.什么是并发、并行并发一个处理器处理多个任务多个任务交替执行。并行多个处理器同时处理多个不同任务2.并发的三大特性原子性一个操作或一系列操作过程要么全部执行成功要么全部执行失败。期间不会被其他线程中断。可见性 一个线程修改一个共享变量其他线程能够立即看到有序性程序执行顺序和代码先后顺序一致如何理解线程安全线程不安全是指在多线程环境下多个线程对同一份数据进行访问或操作时会出现数据不一致问题。例如两个线程进行i操作i分为三个步骤读取、计算、写入。由于i并不是原子性两个线程同时进行i是会发生112 情况。public class TicketTest { //多个线程共享的资源售票数 private int ticketCount 10; //售票方法 public void ticket(){ if (ticketCount0){ System.out.println(Thread.currentThread().getName() 卖出一张表. 剩余票数 ticketCount-- ); try { Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } } else { System.out.println(票数已售馨); } } public static void main(String[] args) { TicketTest test new TicketTest(); Thread thread1 new Thread(new Runnable() { Override public void run() { for (int i 0; i 5; i) { test.ticket(); } } },窗口A); Thread thread2 new Thread(new Runnable() { Override public void run() { for (int i 0; i 5; i) { test.ticket(); } } },窗口B); Thread thread3 new Thread(new Runnable() { Override public void run() { for (int i 0; i 5; i) { test.ticket(); } } },窗口C); thread1.start(); thread2.start(); thread3.start(); } }1.线程不安全的原因线程不安全的核心原因多线程访问共享资源时因为原子性、有序性、可见性被破坏导致代码逻辑混乱数据出现不一致。2.原子性问题1.什么是原子性问题原子性问题是在多线程环境下一个线程在执行一个操作过程中被其他线程打断导致数据出现错误。2.产生原子性问题的原因CPU 时间片随机切换多线程环境下CPU会频繁切换不同的线程导致一个线程的操作被强行中断。执行的操作本身不是原子性如i操作分为“读值→修改→写回”3 条 CPU 指令由于这些指令不是原子的无法一次性执行完毕一旦在指令间隙发生 CPU 切换其他线程就可能读取到 “中间状态” 的值或修改未完成的结果最终导致数据错误。3.如何保证原子性可以通过是使用synchronized关键字、lock接口、原子类如AtomicInteger类synchronized只有一个线程能够获取锁进入代码块中其他线程若未获取到锁会阻塞等待避免操作被打断。lock接口提供了比synchronized更灵活的锁机制如可中断、超时获取、公平锁等通过lock()获取锁、unlock()释放锁保证操作原子性。原子类无锁机制高效基于CAS机制实现无锁原子操作直接操作内存比较内存中的值与预期值如果一致则更新为新值否则重试。CAS 不会阻止线程中断但能处理中断后的冲突3.可见性问题1.什么是可见性问题可见性问题是指一个线程对共享变量进行修改时其他线程无法立即看到2.产生可见性问题原因1. JMM 规定所有共享变量存放于主内存每个线程都有自己工作内存线程仅能操作工作内存中的变量副本无法直接读写主内存。一个线程在修改变量副本后不会实时同步到主内存其他线程加载的仍是旧副本。2. CPU 多级缓存架构不同核心缓存无法即时同步是该问题的物理基础。3.如何保证可见性volatile、synchronized、lock、原子类可以保证可见性volatile当线程修改volatile变量时先在工作内存中进行修改然后立即通过内存屏障将新值强制写入到主内存中。当线程读取volatile变量时先在工作内存中将变量副本标记为“失效”然后从主内存加载最新值到工作内存最后读取本地内存中的新值。synchronized释放锁时同步线程释放锁前会将工作内存中修改的变量副本同步到主内存。获取锁时刷新线程获取锁后会清空工作内存中的变量副本从主内存重新加载最新值。原子类底层基于 CAS 操作实现CAS 操作是直接与主内存交互每次读写都针对主内存中的变量值因此天然保证可见性。4.有序性问题1.什么是有序性问题有序性问题是指多线程环境下因代码指令执行顺序与代码书写顺序不一致导致的数据错误。“代码指令” 本质是CPU 可执行的最小操作单元—— 我们编写的高级语言代码如 Java、C最终会被编译器 / 解释器翻译成 CPU 能直接执行的底层指令机器指令2.产生有序性问题原因编译器和处理器为了优化性能在不改变单线程的执行结果下会对代码指令进行重新排序。但在多线程环境下线程间不知道对方是否进行了重排序导致执行顺序与代码书写顺序不一致从而出现数据错误。高级语言代码如Java→ 编译器翻译 → 汇编指令 → CPU优化可能重排→ 机器指令最终执行// 共享变量 volatile int x 0; volatile boolean flag false; // 线程A void taskA() { x 1; // 操作1 flag true;// 操作2与x无数据依赖允许重排序 } // 线程B void taskB() { if (flag) { System.out.println(x); } }x1和flagtrue编译器 / CPU 会交换顺序可能重排执行顺序先flagtrue再x1多线程异常场景线程 B 读到flagtrue时x 还未赋值打印结果为 0违背预期。3.如何保证有序性使用volatile、synchronized实现有序性。volatile通过内存屏障禁止指令重排序synchronizedsynchronized 具备互斥性同一时间只有一个线程执行同步块必须完整执行完同步块所有代码才会释放锁。 就算块内指令交换顺序线程 B 必须等 A 完全退出同步块、全部变量刷新到主存后才能进入同步块读取。int x 0; boolean flag false; // 线程A void taskA() { synchronized (this) { // 加锁插入Load屏障 x 1; flag true; } // 释放锁插入Store屏障 } // 线程B void taskB() { synchronized (this) { if (flag) { System.out.println(x); } } }synchronized1.什么是synchronized关键字synchronized是Java中的一个关键字也叫同步锁。被它修饰的方法或者代码块在多线程环境下同一时间只有一个线程能够获取锁进入代码块执行其他线程会被阻塞。2.synchronized如何使用加在代码块上时锁是的括号的里的对象只有持有该对象锁的线程才能执行同步代码块。加在普通方法上时锁的是当前对象只有一个线程可以执行该对象的同步方法不同对象可以同时获取到各自的锁加在静态方法上时锁的是当前类对象所有对象共享一个类锁 只有一个线程可以执行该类的静态同步方法。1️⃣ 修饰代码块同步代码块 public class SynchronizedBlockExample { private final Object lock new Object(); public void method() { // 非同步代码 System.out.println(Non-synchronized code); // 同步代码块 synchronized (lock) { System.out.println(Synchronized code block); } } } 2️⃣ 修饰普通方法同步方法 public synchronized void methodA() { // 同一对象的不同线程互斥执行 } 3️⃣ 修饰静态方法类锁 public static synchronized void methodB() { // 所有对象共享一把锁 }public class SimpleSyncBlock { // 锁对象 private Object lock new Object(); private int num 0; public void add() { // 非同步部分多个线程可同时执行 System.out.println(Thread.currentThread().getName() 准备加锁); // 同步代码块需要获取lock锁才能进入 synchronized (lock) { num; System.out.println(Thread.currentThread().getName() 持有锁num num); try { Thread.sleep(500); // 模拟操作耗时 } catch (InterruptedException e) { e.printStackTrace(); } } // 自动释放锁 System.out.println(Thread.currentThread().getName() 释放锁\n); } public static void main(String[] args) { SimpleSyncBlock demo new SimpleSyncBlock(); // 3个线程竞争同一把锁 new Thread(demo::add, 线程1).start(); new Thread(demo::add, 线程2).start(); new Thread(demo::add, 线程3).start(); } }3.synchronized是如何实现的需要先了解在 JVM 中Java对象保存在堆中时由以下三部分组成1、对象头对象头由两个部分组成Mark Word标记字段根据锁状态保存不同的锁信息Klass Point类型指针告诉 JVM创建的对象是啥类型的2、实例数据3、对齐填充synchonized基于JVM的Monitor (监视器锁)和对象头实现的。4.synchonized锁升级过程无锁状态对象刚创建没有任何线程竞争此时为无锁状态。偏向锁当第一个线程第一次获取锁时锁升级为偏向锁。 JVM 会在对象头的 Mark Word 里记录该线程 ID。 之后这个线程再次获取锁时不需要 CAS而是直接进入业务逻辑。轻量级锁当有其他线程尝试竞争时偏向锁会升级为轻量级锁。每个线程会在自己栈帧中创建锁记录对象头的 Mark Word用来存储锁记录指针。其他线程通过CAS修改 Mark Word的方式获取锁修改成功则加锁失败的线程自旋重试。重量级锁当锁竞争激烈线程自旋多次仍无法获取锁时轻量级锁升级为重量级锁。JVM 会为对象关联 Monitor 监视器然后修改对象头 Mark Word 用来存储监视器指针。线程获取锁的方式是通过抢占监视器中的互斥量抢不到锁的线程放入到阻塞队列中直到持有锁的线程释放锁时操作系统会唤醒阻塞队列中线程来重新竞争锁。JUC常见的工具类1.lock锁1.什么是lock接口Lock是JUC.lock包下的接口他比synchronized更加灵活用来代替synchronized。与synchronized不同的是需要手动加锁、解锁。Lock lock new ReentrantLock(); try { lock.lock(); 手动加锁 操作共享资源 } finally { lock.unlock(); 手动解锁必须在 finally 中确保执行 }2. lock和synchronized的区别lock是Java中的一个接口synchronized是Java中的关键字lock只给代码块加锁synchronized可以给代码块、方法lock是显示锁需要手动加锁、解锁synchronized是隐式锁加锁、解锁有JVM自动完成2.lock锁常用实现类ReentrantLock (可重入锁)1、ReentrantLock是什么同一个线程可以重复多次获取同一把锁每获取一次锁锁计数 1 释放时必须调用unlock()将锁计数进行归0当计数归 0 才算真正释放锁。2、ReentrantLock 锁具备以下特性:可中断除了lock()方式加锁还有可中断加锁lockInterruptibly()。如果线程获取锁失败线程进入阻塞队列等待等待期间是可以被其他线程中断通过Thread.interrupt()中断ReentrantLock lock new ReentrantLock(); try { lock.lockInterruptibly(); // 可被中断的加锁 try { // 临界区代码 } finally { lock.unlock(); } } catch (InterruptedException e) { // 处理中断例如恢复中断状态、记录日志等 Thread.currentThread().interrupt(); // 可选恢复中断标记 }可以设置超时时间允许线程在指定时间内获取锁若在指定时间内获取锁则返回true若超过指定时间未获取锁则返回false不再继续等待锁释放。通过tryLock(long timeout, TimeUnit unit)方法设置超时时间。可以设置为公平锁和非公平锁公平锁锁释放后按线程等待的时间顺序来获取锁也就是先到先得。非公平锁锁释放后线程可以插队获取锁随机或者按照其他优先级排序。ReentrantLock fairLock new ReentrantLock(true); 公平锁 ReentrantLock nonFairLock1 new ReentrantLock(); 默认非公平锁 ReentrantLock nonFairLock2 new ReentrantLock(false); 显式指定非公平锁支持多个条件变量支持通过Condition接口创建多个条件变量条件队列。通过newCondition()方法创建多个独立的Condition对象每个Condition对应一个单独的条件队列实现线程按不同条件等待 / 唤醒精细化控制线程协作private final ReentrantLock lock new ReentrantLock(); 两个条件变量队列满生产者等待、队列空消费者等待 private final Condition notFull lock.newCondition(); private final Condition notEmpty lock.newCondition();3、ReentrantLock实现原理(ReentrantLock内部类Sync继承了AQS实现了加锁、公平锁、非公平锁)ReentrantLock是基于AQS实现的可重入锁ReentrantLock用AQS的state变量表示重入次数用 AQS 的CLH队列管理竞争锁失败的线程。ReentrantLock两种加锁方式非公平锁加锁线程先通过CAS尝试将state变量值从0修改为1如果修改成功则获取锁修改失败线程封装成一个Node节点放入CLH 队列中。公平锁加锁先检查 CLH队列是否有等待线程如果有则将当前线程封装为 Node节点放入队列中如果没有则尝试通过CAS修改state变量值为1成功则线程持有锁失败再放入CLH队列。4. 释放锁是调用unlock()将state的值进行递减递减为0时锁释放。4、与synchronized有什么区别用法不同synchronized 是关键字可直接用来修饰普通方法、静态方法和代码块ReentrantLock是一个类 只能用在代码块里获取锁和释放锁方式不同synchronized 会自动加锁和释放锁ReentrantLock 需要手动加锁和释放锁锁类型不同synchronized 属于非公平锁ReentrantLock 既可以是公平锁也可以是非公平锁底层实现不同synchronized 是通过监视器和对象头实现的ReentrantLock 是基于 AQS 实现的5、与synchronized有什么性能差异JDK1.6 之前synchronized 只有重量级锁当线程阻塞时会切换到内核态然后将线程放入操作系统内核阻塞队列线程唤醒时又要切回用户态两次上下文切换性能开销大 ReentrantLock 基于 AQS、CAS 自旋实现的是用户态操作所以性能远高于synchronized。JDK1.6 及之后synchronized 进行了优化新增了偏向锁和轻量级锁。在低竞争或单线程场景中synchronized 的偏向锁和轻量级锁性能接近甚至高于 ReentrantLock 。但在高竞争环境下ReentrantLock 通常性能更高因为他的非公平锁机制和自旋等待减少了线程阻塞的开销。ReentrantReadWriteLock (可重入读写锁)ReentrantReadWriteLock是一个可重入的读写锁他实现了ReadWriteLock接口将锁分为读锁共享锁和写锁排他锁。允许多个线程同时读取共享资源但修改资源时只允许一个线程能够获取到写锁。3.atomic原子类1.atomic原子类是什么atomic原子类简单来说就是具有原子性操作特征的类。atomic原子类提供了一种线程安全的方式来操作单个变量。2.常见的atomic原子类分类基本数据类型原子类AtomicInteger整型原子类AtomicLong长整型原子类AtomicBoolean布尔型原子类数组类型原子类AtomicIntegerArray整形数组原子类AtomicLongArray长整形数组原子类AtomicReferenceArray引用类型数组原子类引用类型原子类AtomicReference引用类型原子类AtomicStampedReference原子更新带有版本号的引用类型。3.atomic类底层实现原理原子类的实现依赖两大机制CAS使用Unsafe类的原子指令实现变量的比较并更新volatile确保内存可见性防止线程缓存变量造成数据不同步java内存模型1.前置知识计算机在执行程序时每条指令都是在CPU中执行的。而执行指令的过程中势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存物理内存当中的这时就存在一个问题由于CPU执行速度很快而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多硬盘 内存 缓存cache CPU。因此如果任何时候对数据的操作都要通过和内存的交互来进行会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。CPU进行计算时就可以直接从它的高速缓存中读取数据或向其写入数据了。当运算结束之后再将高速缓存中的数据刷新到主存当中。该知识点解释了 CPU 高速缓存存在的原因这也是理解 JMMJava 内存模型和多线程可见性问题的底层基础。2.JMM核心概念JMM 规定共享变量存于主内存每个线程拥有独立的工作内存。线程不能直接读写主内存必须把共享变量拷贝到工作内存所有操作读、改、赋值都在工作内存中完成操作完后再决定何时同步回主内存。volatile关键字1.volatile关键字的作用volatile的作用是保证变量的可见性和禁止指令重排。1、保证变量的可见性当一个共享变量被volatile修饰一个线程对共享变量进行修改其他线程会立即看修改后的值。2、禁止指令重排指令重排是指编译器和处理器为了优化性能对代码指令的执行顺序进行重新排序volatile通过内存屏障限制 “特定范围内” 的指令进行重新排序保证代码指令顺序与编写代码顺序一致避免因为代码指令重新排序导致数据错误。2.如何保证变量的可见性当线程修改volatile变量时先在工作内存中进行修改然后立即通过内存屏障将新值强制写入到主内存中。当线程读取volatile变量时先在工作内存中将变量副本标记为“失效”然后从主内存重新加载最新值到工作内存 最后读取最新值。3.如何禁止指令重排序JMM 实际定义了 4 种内存屏障LoadLoad、StoreStore、LoadStore、StoreLoad。在 volatile 写操作之前插入StoreStore屏障防止前面的普通写被重排序到 volatile 写之后。在 volatile 写操作之后插入StoreLoad屏障防止 volatile 写与后面可能出现的 volatile 读/写发生重排序这是开销最大的屏障。在 volatile 读操作之后插入LoadLoad屏障防止 volatile 读与后面的普通读发生重排序。在 volatile 读操作之后插入LoadStore屏障防止 volatile 读与后面的普通写发生重排序。// 声明变量加volatile volatile boolean flag; volatile int num; // volatile 写 a1 【StoreStore屏障】 flagtrue // volatile写 【StoreLoad屏障】 后续代码4.volatile和 synchronized的区别volatile是修饰变量synchronized是修饰代码块、方法volatile能保证变量的可见性但不能保证原子性synchronized都可以保证volatile不会造成线程阻塞synchronized会造成线程阻塞CAS1.CAS是什么CAS(compareAndSetState)全称为比较并交换CAS 是一种乐观锁机制它包含三个操作数内存位置V、预期值A和新值B。CAS 操作的逻辑是如果内存位置 V 的值等于预期值 A则将其更新为新值 B否则不做任何操作。而且整个过程是原子性的。2.CAS工作原理线程修改共享变量时先从主内存读取变量存入工作内存作为预期值旧值比较预期值是否与主内存中的实际值相等如果相等说明变量没有被其他线程修改过将实际值更新为准备的新值如果不相等说明变量被其他线程修改过线程修改执行失败。失败后会进行自旋重复执行相同操作直到成功例如线程A要修改一个变量值XX值为5修改为10。会先读取内存的5作为预期值。线程B此时把X值修改为88为X的实际值。线程A开始执行比较比较后预期值与实际值不相等线程A执行失败。3.CAS是如何实现的CAS是基于一个Unsafe类实现Unsafe的方法被native关键字修饰通过“JNI”调用Unsafe类中的方法最终映射到硬件级别的原子指令从而保证操作的原子性。(native关键字修饰意味着这些方法的具体实现不是用 Java 代码写的而是由底层的本地代码实现通常是C或者C。使用Unsafe类相当于直接对底层进行操作不推荐直接使用)4.CAS存在的问题1. ABA问题ABA问题是指变量A修改为变量B再修改为变量A这过程CAS无法检测如何解决ABA问题解决思路在变量修改前加是版本号或者时间戳。当一个线程修改变量前先读取版本号的值比较版本号值是否与当前版本号的值是否相等如果相等则修改成功如果不想相等个则回滚当前操作。2. 自旋开销大多个线程竞争同一个共享变量时大量线程会因 CAS 失败而进行自旋一直重复之前的操作导致CPU资源被占用。如何解决自旋开销大问题限制自旋次数设定最大自旋次数如 10 次超过次数后放弃自旋3.只能保证一个变量的原子操作CAS 操作仅能对单个共享变量有效。当需要操作多个共享变量时CAS 就显得无能为力如何解决原子性加锁使用synchronized使用AtomicReference类该类通过将多个变量封装在一个对象中然后通过AtomicReference进行操作保证对象的原子操作。乐观锁、悲观锁1.什么是乐观锁、悲观锁乐观锁假设不会发生冲突认为每次读取数据时不会有人对数据进行修修改所以不会加锁。在提交数据时检查数据是否被修改。如果没有被修改则提交成功如果被修改则提交失败进行重试。悲观锁假设会发生冲突认为每次读取数据时会有人对数据进行修修改所以每次读取数据时都加锁。2、如何实现乐观锁版本号或时间戳一般是在数据表中加上一个版本号version字段表示数据被修改的次数。线程A更新数据时在读取数据的同时也会读取版本号的字段值。当更新要提前时比较之前读取到的版本号的字段值与当前版本号的字段相等时才会更新不相等则一直重复操作直到更新成功

相关新闻

最新新闻

NCM加密音乐格式本地解密与跨平台播放完整解决方案

NCM加密音乐格式本地解密与跨平台播放完整解决方案

1. 项目概述:当音乐被“锁”在格式里 作为一名折腾过无数音频格式和播放方案的老玩家,我最近发现一个现象:身边不少朋友,尤其是那些习惯使用特定音乐平台的朋友,常常会遇到一个尴尬——辛辛苦苦下载或收藏的音乐&#…

2026/7/4 12:06:08
Metasploit新增模块解析:FreePBX、Cacti、SmarterMail漏洞利用与防御实战

Metasploit新增模块解析:FreePBX、Cacti、SmarterMail漏洞利用与防御实战

1. 项目概述:一次聚焦企业软件安全的“弹药”补充 如果你是一名渗透测试工程师或者红队成员,那么最近Metasploit框架的更新,绝对值得你花上十分钟仔细研究一下。这次更新不是什么底层架构的大改,而是实打实的“弹药”补充——一口…

2026/7/4 12:06:08
PyQt+dlib+CNN实现课堂随机抽问系统开发

PyQt+dlib+CNN实现课堂随机抽问系统开发

1. 项目概述与背景 作为一名长期从事教育技术开发的工程师,我最近完成了一个基于PyQt框架的课堂随机抽问系统。这个项目结合了dlib人脸识别库和CNN卷积神经网络技术,旨在解决传统课堂提问方式中存在的公平性和效率问题。 在传统课堂上,提问往…

2026/7/4 12:06:08
基于CNN的蝴蝶识别系统开发与实践

基于CNN的蝴蝶识别系统开发与实践

1. 项目背景与核心价值蝴蝶识别作为计算机视觉领域的经典课题,在生物多样性研究、生态监测和科普教育中具有重要应用价值。传统人工识别方法效率低下且依赖专家经验,而基于CNN的深度学习方案能够实现自动化、高精度的物种分类。这个毕设项目完美融合了学…

2026/7/4 12:06:08
Windows 11文件资源管理器启动速度优化:告别预加载,实现底层性能提升

Windows 11文件资源管理器启动速度优化:告别预加载,实现底层性能提升

🚀 30款热门AI模型一站整合,DeepSeek/GLM/Claude 随心用,限时 5 折。 👉 点击领海量免费额度 如果你在 Windows 11 上打开文件资源管理器时,曾经历过那令人烦躁的短暂卡顿——点击图标后,鼠标指针转圈&a…

2026/7/4 12:06:08
AI Orchestration:企业级AI落地的精密调度系统

AI Orchestration:企业级AI落地的精密调度系统

1. 项目概述:当企业级集成遇上大模型,为什么需要一场“精密调度”? 在真实的企业现场跑过三年以上AI落地项目的人都知道,最让人头皮发麻的从来不是模型效果差,而是——数据根本拿不到、API调不通、权限卡死、结果格式对…

2026/7/4 12:01:08

周新闻

月新闻