Java多线程进阶-死锁与面试题解析

文章目录

  • Java多线程进阶:死锁与面试题解析
    • 一、并发编程的噩梦——死锁
      • 1. 什么是死锁?四个缺一不可的条件
      • 2. 如何避免死锁?从破坏循环等待开始
    • 二、并发编程面试题全景解析
      • 1. 锁与同步机制
      • 2. CAS 与原子操作
      • 3. JUC 工具与线程池
      • 4. 线程安全集合
      • 5. 综合问题
    • 本文核心要点总结 (Key Takeaways)

Java多线程进阶:死锁与面试题解析

学到这里,我们总算来到了 Java 多线程学习之旅的最后一站。在前面的笔记里,我们一起探索了锁策略、synchronized 的底层细节、JUC 工具包以及各种并发集合。现在,是时候挑战两个“终极 BOSS”了:

  1. 死锁:并发编程中最让人头疼的噩梦。
  2. 面试题:检验我们学习成果的试金石。

这篇笔记的目标很明确:首先,彻底搞懂死锁的成因和避免策略;然后,整理一份相对全面的并发面试题库。这既是对整个系列知识的最终巩固,也希望能帮助自己(以及其他可能读到这篇笔记的朋友)更自信地应对面试的挑战。


一、并发编程的噩梦——死锁

1. 什么是死锁?四个缺一不可的条件

死锁,简单来说,就是多个线程同时被阻塞,它们中的一个或全部都在等待对方持有的资源。因为大家都在等对方先放手,结果就导致所有线程都被无限期地阻塞,程序也就卡住无法正常结束了。

这里我的理解是,可以用一个“吃饺子”的场景来帮助消化这个概念:

滑稽老哥和不吃香菜一起去吃饺子,但桌上只有一瓶酱油和一瓶醋。

  • 滑稽老哥手快,先拿起了酱油瓶(这相当于持有了锁 A)。
  • 与此同时,不吃香菜拿起了醋瓶(持有了锁 B)。

接下来,滑稽老哥想蘸醋(请求锁 B),而不吃香菜想用酱油(请求锁 A)。如果两人谁也不肯先把自己的瓶子放下给对方用,那就僵持住了——这就是一个典型的死锁。

从这个例子可以总结出,死锁的产生必须同时满足以下四个缺一不可的条件:

  1. 互斥使用:一个资源一次只能被一个线程使用(酱油瓶一次只能一人拿)。
  2. 不可抢占:线程不能强行从另一个线程手中夺取资源,只能等待其主动释放(不能从对方手里抢瓶子)。
  3. 请求和保持:线程在持有至少一个资源的同时,又去请求其他资源(拿着酱油瓶,又想要醋瓶)。
  4. 循环等待:存在一个线程等待链,T1等T2,T2等T3,…,Tn等T1,形成环路(滑稽老哥等不吃香菜,不吃香菜又在等滑稽老哥)。

2. 如何避免死锁?从破坏循环等待开始

要避免死锁,理论上只需破坏上述四个条件中的任意一个即可。但在实际编程中,我们最容易、也最常入手的是破坏“循环等待”条件。

最常用的一种死锁预防技术就是锁排序。这个方法思路很简单:假设有 N 个线程尝试获取 M 把锁, 我们可以对这 M 把锁进行统一编号 (例如根据它们的 hashCode)。然后定下规矩,所有线程在需要获取多把锁时,都必须严格按照固定的、从小到大的编号顺序来获取。这样一来,请求锁的方向都是一致的,就从根本上避免了形成等待环路。

可能产生死lock的代码:
下面这个例子很典型,两个线程以相反的顺序申请锁,就可能导致死锁。

Object lock1 = new Object();
Object lock2 = new Object();// 线程1: 尝试先锁 lock1,再锁 lock2
new Thread(() -> {synchronized (lock1) {System.out.println("线程1 持有 lock1, 尝试获取 lock2...");try { Thread.sleep(100); } catch (InterruptedException e) {}synchronized (lock2) {System.out.println("线程1 成功获取两把锁");}}
}).start();// 线程2: 尝试先锁 lock2,再锁 lock1 (顺序相反,非常危险!)
new Thread(() -> {synchronized (lock2) {System.out.println("线程2 持有 lock2, 尝试获取 lock1...");synchronized (lock1) {System.out.println("线程2 成功获取两把锁");}}
}).start();

破坏循环等待后的安全代码:
只要我们约定好,所有线程都先获取 lock1, 再获取 lock2,问题就解决了。

Object lock1 = new Object();
Object lock2 = new Object();// 两个线程都遵循先锁1再锁2的顺序
new Thread(() -> {synchronized (lock1) {synchronized (lock2) {System.out.println("线程1 成功");}}
}).start();new Thread(() -> {synchronized (lock1) {synchronized (lock2) {System.out.println("线程2 成功");}}
}).start();

二、并发编程面试题全景解析

这部分内容既是对整个多线程系列学习的复习,也是对常见面试问题的检验。

1. 锁与同步机制

1. 如何理解乐观锁和悲观锁?具体如何实现?

  • 悲观锁:我的理解是,它非常“悲观”,总是假设会发生并发冲突。所以,在每次对数据进行操作前,它都会先加锁,确保在自己操作的这段时间里,别人碰不了数据。这种方式的实现,通常依赖于底层的同步机制,比如 Java 中的 synchronized 关键字和 ReentrantLock 类。
  • 乐观锁:它则非常“乐观”,总是假设不会发生并发冲突。所以,它操作数据时不加锁,而是在准备提交更新的时候,才去检查数据在操作期间有没有被其他线程修改过。如果没被改过,就成功更新;如果被改了,就放弃或者重试。它的典型实现是 CAS(Compare-and-Swap)机制,为了防止 ABA 问题,通常还会配合一个版本号字段来一起检查。

2. 介绍一下读写锁?

读写锁(ReadWriteLock)是一种很巧妙的锁,它把锁的功能一分为二:分为“读锁”和“写锁”,特别适合用在“读多写少”的场景。

  • 读锁 vs 读锁:不互斥。大家都是读数据,不会有冲突,所以允许多个线程同时持有读锁,并发读取。
  • 写锁 vs 写锁:互斥。为了保证数据写入的原子性,一次只能有一个线程持有写锁。
  • 读锁 vs 写锁:互斥。当有线程在写数据时,其他线程不能读,反之亦然。这保证了读线程不会读到修改了一半的“脏数据”。
    Java 中的 ReentrantReadWriteLock 就是它的标准实现。

3. 什么是自旋锁?为什么要使用它,缺点是什么?

  • 什么是自旋锁:当一个线程想获取锁但发现锁被占用了,它不会立刻放弃 CPU 进入阻塞状态,而是在原地进行一个忙等待的循环(“自旋”),不断地尝试获取锁。
  • 为什么使用:它基于一个假设——锁被占用的时间通常很短。如果这个假设成立,那么通过自旋等待,线程可以避免进入和退出阻塞状态的巨大开销(这涉及到用户态和内核态的切换以及线程调度),一旦锁被释放,就能立刻抢到,响应速度更快。
  • 缺点:如果锁被占用的时间很长,自旋的线程就会持续空转,白白浪费 CPU 资源。所以它是个双刃剑,用对地方才高效。

4. synchronized 是可重入锁吗?

是的,必须是。可重入锁(也叫递归锁)指的是,同一个线程在已经持有锁的情况下,可以再次成功获取该锁,而不会自己把自己锁死。

它的内部实现机制大致是:锁本身会记录下当前是哪个线程持有着它,并且还有一个计数器。当这个线程再次请求这把锁时,计数器会递增。每次执行完一次同步代码块释放锁时,计数器会递减。只有当计数器减到 0 时,这个锁才会被真正释放,其他线程才能获取。

5. 什么是偏向锁?

偏向锁是 JVM 对 synchronized 的一种极致优化。它的核心思想是,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。

为了处理这种情况,偏向锁并不会真的加锁,而是在对象头里记录一个“偏向”的线程 ID。如果后续访问该锁的始终是这一个线程,那么它进出同步块时就几乎没有任何开销,连 CAS 操作都不需要。这大大降低了无竞争情况下的同步成本。当然,一旦有其他线程参与竞争,偏向锁状态就会被撤销,升级为轻量级锁。

6. synchronized 的实现原理是什么?

synchronized 的实现核心是一个从偏向锁、轻量级锁到重量级锁的锁升级过程,这是 JVM 为了在不同竞争情况下都能有较好性能而做的优化。

  1. 偏向锁:当没有竞争时,锁会“偏向”于第一个获取它的线程。此时,仅在对象头记录线程ID,开销最低。
  2. 轻量级锁:当出现另一个线程竞争时,偏向锁会升级为轻量级锁。这时,线程会通过自旋+CAS的方式尝试获取锁,避免了线程阻塞带来的开销。
  3. 重量级锁:如果自旋一定次数后,竞争依然激烈,锁就会“膨胀”为重量级锁。此时它会依赖操作系统的 mutex 互斥量来实现。获取不到锁的线程会被挂起,进入等待队列,等待操作系统唤醒。

除此之外,JVM 还会进行锁消除(如果判断一段代码不可能有共享数据竞争,就直接去掉锁)和锁粗化(将多个连续的加解锁操作合并为一个更大的锁)等优化。

2. CAS 与原子操作

1. 讲解一下你自己理解的 CAS 机制。

我理解的 CAS(Compare-and-Swap,比较并交换)是一种非常底层的原子操作,很多无锁编程都依赖它。你可以把它想象成一个需要三个参数的指令:

  1. 要操作的内存地址 V
  2. 我预期的这个地址上的旧值 A
  3. 我想要更新成的新值 B

执行这条指令时,CPU 会原子性地做一件事:检查地址 V 上的当前值是否等于我预期的旧值 A。当且仅当它俩相等时,才会把地址 V 上的值更新为新值 B。整个“比较再更新”的过程是一步完成的,不会被其他线程中断,这就是它实现原子性的关键。

2. ABA问题怎么解决?

ABA 问题是 CAS 的一个经典漏洞。意思是,一个值原来是 A,被其他线程改成了 B,然后又改回了 A。我的 CAS 操作在检查时,会发现值仍然是 A,就误以为它没变过,然后执行更新。

要解决这个问题,光比较值是不够的,还需要引入一个版本号时间戳。每次更新值的时候,都把版本号加一。这样,CAS 操作就变成了比较“值 + 版本号”。

  • 原来的流程:A -> B -> A
  • 加入版本号后:(A, v1) -> (B, v2) -> (A, v3)
    当我拿着 (A, v1) 去做 CAS 时,发现现在是 (A, v3),版本号对不上,就知道中间发生过变化,从而避免了错误的操作。Java 中的 AtomicStampedReference 类就是用来解决这个问题的。

3. AtomicInteger 的实现原理是什么?

它的核心原理就是基于 CAS 的自旋循环。以 incrementAndGet() 方法为例,它内部并没有用锁,而是执行了这样一个循环:

  1. 读取:先读取 AtomicInteger 内部 volatile 修饰的当前值(oldValue)。
  2. 计算:在本地计算出新值(newValue = oldValue + 1)。
  3. 交换 (CAS):调用底层的 compareAndSet(oldValue, newValue) 方法,尝试原子性地用 newValue 替换 oldValue
  4. 判断与重试:如果 compareAndSet 返回 true,说明在我计算的这段时间里,没有其他线程修改过值,更新成功,退出循环。如果返回 false,说明值已经被其他线程改了,我的 oldValue 已经过时了。这时,循环会继续,重新执行第 1 步,直到 CAS 操作成功为止。

3. JUC 工具与线程池

1. 线程同步的方式有哪些?

我目前学到的主要有这几种:

  • 最基础的:使用 synchronized 关键字,简单粗暴。
  • 更灵活的:使用 JUC 包下的 Lock 接口实现,比如 ReentrantLock
  • 控制协作的:使用 JUC 包下的一些同步工具类,比如 Semaphore(信号量)、CountDownLatch(倒计时门闩)等。
  • 无锁方式:使用原子类,比如 AtomicInteger,通过 CAS 保证单个变量操作的线程安全。

2. 为什么有了 synchronized 还需要 JUC 下的 Lock

因为以 ReentrantLock 为代表的 Lock 接口,提供了 synchronized 不具备的、更高级和更灵活的功能,让我们可以对锁进行更精细的控制:

  • 可中断的锁获取lockInterruptibly() 允许在等待锁的过程中响应中断。
  • 可限时的锁获取tryLock(time, unit) 可以尝试在指定时间内获取锁,超时就放弃,避免死等。
  • 公平性选择:可以在创建时指定为公平锁,保证线程先来先得,避免饥饿。
  • 灵活的线程通信:可以绑定多个 Condition 对象,实现对不同条件的线程进行分组等待和精确唤醒,而 synchronized 只有一个等待队列。

3. 信号量听说过吗?之前都用在过哪些场景下?

信号量(Semaphore)我理解它是一个控制“许可证”数量的计数器,用来限制能同时访问某个特定资源的线程数量。

  • acquire():线程尝试获取一个许可证,成功则计数器减一。如果许可证发完了(计数器为0),线程就得阻塞等待。
  • release():线程在用完资源后,释放一个许可证,计数器加一,可能会唤醒一个正在等待的线程。

它最常见的用途就是流量控制资源池管理。比如,我们有一个数据库连接池,里面只有10个连接,那就可以用一个初始值为10的 Semaphore 来控制,确保最多只有10个线程能同时拿到连接。

4. 解释一下 ThreadPoolExecutor 构造方法的参数的含义。

这是创建线程池最核心的构造方法,每个参数都得搞清楚:

  • corePoolSize: 核心线程数。线程池创建后,即使线程是空闲的,也会长期保留这么多线程。
  • maximumPoolSize: 最大线程数。当任务队列满了,线程池最多能再创建多少个“临时”线程,总线程数不能超过这个值。
  • keepAliveTime: 临时线程的空闲存活时间。当线程数超过 corePoolSize 时,那些多出来的临时线程如果空闲了这么久还没接到新任务,就会被销毁。
  • unit: keepAliveTime 的时间单位。
  • workQueue: 任务阻塞队列。当核心线程都在忙时,新来的任务会先被存放在这个队列里排队。
  • threadFactory: 线程工厂。用来创建新线程,可以自定义线程的名字、是否为守护线程等。
  • handler: 拒绝策略。当任务队列已满,并且线程数也达到了 maximumPoolSize 时,用来处理新提交任务的策略(比如抛异常、丢弃任务等)。

5. Java创建线程池的接口是什么?参数LinkedBlockingQueue的作用是什么?

  • 创建线程池的核心接口是 ExecutorService。我们通常不直接实现它,而是通过 Executors 这个工厂类提供的静态方法(如 newFixedThreadPool)或者直接构造 ThreadPoolExecutor 类的实例来创建。
  • LinkedBlockingQueue 在线程池中通常被用作任务队列(workQueue)。它的特点是一个无界的链式阻塞队列。当所有核心线程都在忙碌时,新提交的任务就会被无限地添加到这个队列中,等待核心线程空闲后前来领取并执行。

4. 线程安全集合

1. ConcurrentHashMap的读是否要加锁,为什么?

读操作(get 方法)几乎不加锁,这也是它性能高的关键之一。

ConcurrentHashMap 为了最大化读操作的并发性能,采取了非常精巧的设计。它通过将共享变量(比如哈希桶中 Node 节点的 valnext 指针)声明为 volatile,来利用 volatile 的内存可见性语义。这确保了当一个线程修改了某个节点后,这个修改能立刻对其他读线程可见。这样,读线程总能看到最新的数据,从而实现了高效的无锁读取。

2. 介绍下 ConcurrentHashMap的锁分段技术?

锁分段技术是 Java 1.7 版本中 ConcurrentHashMap 提高并发度的核心策略。它的思路是,不对整个哈希表加一把大锁,而是将整个表在逻辑上分成若干个“段”(Segment),每个 Segment 本身就像一个小型的、线程安全的 Hashtable,拥有自己独立的锁。

当需要操作某个 key 时,只需根据 key 的哈希值定位到它所属的那个 Segment,然后只锁定这一个 Segment 即可,其他 Segment 的操作完全不受影响。这相当于把一把大锁拆分成了多把小锁,大大提高了并发写入的效率。

不过这里有个重点需要记一下:这个技术已经在 Java 1.8 中被优化替换了。在 Java 1.8 及以后的版本中,ConcurrentHashMap 取消了 Segment 的设计,改为使用 synchronized 锁住哈希桶的头节点,再加上 CAS 操作来辅助,实现了粒度更细的锁定,并发性能通常更好。

3. HashtableHashMapConcurrentHashMap 之间的区别?

这三个是面试老朋友了,它们的关键区别在于线程安全和性能:

  • HashMap: 线程不安全。如果用在多线程环境下,需要手动在外部加锁。它的优点是性能最高,并且 key 和 value 都允许为 null
  • Hashtable: 线程安全。它实现安全的方式非常粗暴,就是给几乎所有 public 方法都加上了 synchronized,锁住的是整个 Hashtable 对象。这导致并发效率极低,现在基本不推荐使用了。另外,它的 key 和 value 都不允许null
  • ConcurrentHashMap: 线程安全,并且是为高并发场景设计的。在 JDK 1.8 中,它通过 synchronized 锁住哈希桶的头节点 + CAS + volatile 变量来保证安全,实现了细粒度的锁,并发性能非常高。和 Hashtable 一样,它的 key 和 value 也都不允许null

5. 综合问题

1. 谈谈死锁是什么,如何避免死锁?

  • 死锁定义:我理解的死锁,就是两个或多个线程在执行过程中,因为争夺资源而陷入一种互相等待的僵局。如果没有外力干涉,它们谁也无法继续往下执行。
  • 四个必要条件:互斥使用、不可抢占、请求与保持、循环等待。这四个条件必须同时满足才会发生死锁。
  • 如何避免:最核心的方法就是想办法破坏这四个必要条件之一。在编程实践中,我们最常用的手段是通过锁排序来破坏“循环等待”条件,即规定好所有线程都必须以一个固定的、相同的顺序来获取一系列的锁。

2. volatile关键字的用法?

volatile 关键字在我看来主要有两个关键作用:

  1. 保证内存可见性:这是它最核心的功能。它能确保一个线程对 volatile 变量的修改,能够立刻被其他线程“看到”。底层原理是它会强制线程每次都从主内存中读取变量的值,而不是依赖自己工作内存中的缓存。
  2. 禁止指令重排序volatile 还能充当一个内存屏障,防止编译器和处理器为了优化性能而随意改变代码的执行顺序。这一点在实现某些底层并发算法时至关重要,比如经典的双重检查锁定(DCL)单例模式。

3. Java多线程是如何实现数据共享的?

JVM 的内存模型把内存分成了几个区域,其中**堆内存(Heap)方法区(Method Area)**是所有线程共享的区域。所以,只要我们把一个对象(无论是类的实例还是静态变量)创建在这两个共享区域里,那么这个对象的引用就可以被多个线程同时持有和访问,这样就实现了线程间的数据共享。

4. Java线程共有几种状态?状态之间怎么切换的?

根据 Thread.State 枚举,Java 线程有 6 种状态:

  • NEW (新建): 线程对象被创建,但还没调用 start() 方法。
  • RUNNABLE (可运行): 这是个复合状态,包含了操作系统线程状态中的“就绪”(Ready)和“运行中”(Running)。调用 start() 后线程就进入这个状态,等待 CPU 调度。
  • BLOCKED (阻塞): 线程在等待进入一个 synchronized 同步块时,因为获取不到监视器锁而被阻塞。
  • WAITING (无限期等待): 线程需要等待其他线程执行特定的唤醒动作。调用 Object.wait()Thread.join()LockSupport.park() 会进入此状态。
  • TIMED_WAITING (限时等待): 和 WAITING 类似,但它不会无限等下去,会在指定时间后自动唤醒。调用 Thread.sleep(time)Object.wait(time) 等方法会进入此状态。
  • TERMINATED (终止): 线程的 run() 方法执行完毕,线程生命周期结束。

5. 在多线程下,如果对一个数进行叠加,该怎么做?

要保证线程安全,主要有两种方法:

  • 加锁:最直接的方法,使用 synchronized 关键字或 ReentrantLock 来保护这个叠加操作,确保同一时间只有一个线程能执行 i++
  • 使用原子类:更推荐的方式是使用 java.util.concurrent.atomic 包下的原子类,比如 AtomicInteger。它的 addAndGet()incrementAndGet() 方法是基于 CAS 操作实现的,属于无锁操作,在并发量大的情况下性能通常比加锁要好。

6. Servlet是否是线程安全的?

Servlet 本身的设计是单实例多线程的。也就是说,Web 容器(比如 Tomcat)通常只会为每个 Servlet 类创建一个实例。当多个 HTTP 请求同时访问这个 Servlet 时,容器会为每个请求分配一个独立的线程,这些线程会并发地执行同一个 Servlet 实例的 service() 方法。

因此,结论是:

  • 如果 Servlet 中定义了成员变量(实例变量),并且 service() 方法对这些成员变量进行了读写操作,那么就会存在线程安全问题。
  • 如果 Servlet 中只使用了局部变量(在 service() 方法内部定义的变量),那么它是线程安全的,因为每个线程都有自己独立的栈空间来存放局部变量。

7. ThreadRunnable的区别和联系?

  • Runnable 是一个接口,里面只有一个 run() 方法。它代表的是一个“任务”或“要做什么事”。
  • Thread 是一个类,它代表一个执行任务的线程实体,是真正“干活的人”。
  • 联系: Thread 类本身也实现了 Runnable 接口。创建线程时,可以将一个 Runnable 对象作为任务传递给 Thread 的构造函数。
  • 区别与选择: 推荐使用实现 Runnable 接口的方式。因为它将“任务”和“执行者”解耦了,一个 Runnable 任务可以被不同的 Thread 执行。而如果通过继承 Thread 类的方式,由于 Java 的单继承限制,这个类就不能再继承其他类了,灵活性较差。

8. 多次start一个线程会怎么样?

一个 Thread 对象的 start() 方法只能被调用一次。

  • 第一次调用 start() 会成功启动线程,使其进入 RUNNABLE 状态,并由 JVM 调度执行其 run() 方法。
  • 之后对同一个线程对象再次调用 start(),无论线程是否已经执行完毕,都会抛出 IllegalThreadStateException 异常。

9. 有synchronized两个方法,两个线程分别同时调用,请问会发生什么?

这要看 synchronized 修饰的是什么类型的方法,以及两个线程调用的是否是同一个对象的实例方法。

  • 修饰非静态方法:此时锁是当前对象实例 (this)。
    • 如果两个线程调用的是同一个对象的这两个同步方法,它们会互斥,一个线程执行时另一个必须等待锁。
    • 如果两个线程调用的是不同对象的这两个同步方法,它们之间没有关系,不会互斥,可以并发执行。
  • 修饰静态方法:此时锁是当前类的 Class 对象 (Xxx.class)。
    • 这种情况下,锁是全局唯一的。无论两个线程操作的是不是同一个对象实例,只要它们调用的是这个类的任何静态 synchronized 方法,都会互斥。

10. 进程和线程的区别?

这是个基础但非常重要的问题,我的理解是:

  • 资源单位 vs 调度单位:进程是操作系统进行资源分配的最小单位(比如内存空间)。线程是 CPU 调度的最小单位,是真正执行计算的。
  • 包含关系:一个进程可以包含一个或多个线程。线程是进程的一部分,不能独立存在。
  • 内存共享:进程之间的内存空间是相互独立的。而同一进程内的所有线程共享该进程的内存空间(如堆、方法区),但每个线程有自己独立的栈和程序计数器。
  • 开销:创建、销毁和切换进程的开销远大于线程,因此线程也被称为“轻量级进程”。

本文核心要点总结 (Key Takeaways)

  • 锁的本质是权衡:并发编程中的所有锁策略,都是在性能开销数据安全之间寻找平衡点。没有普适的“最优解”,只有“最适合”特定场景的方案。
  • synchronized 是智能的:现代 JVM 中的 synchronized 远非一个简单的互斥锁,它内部集成了偏向锁、轻量级锁(自旋)、重量级锁的动态升级机制,以及锁消除、锁粗化等优化,努力在各种场景下都提供接近最优的性能。
  • CAS是无锁并发的基石:CAS(比较并交换)作为一种 CPU 级别的原子指令,是 JUC 中许多高性能并发类(如 AtomicInteger, ConcurrentHashMap)的实现基础,它用乐观的非阻塞方式,在很多场景下替代了传统的悲观阻塞式加锁。
  • JUC是并发编程的瑞士军刀java.util.concurrent 包提供了一套丰富、模块化的并发工具(如 ReentrantLock, Semaphore, CountDownLatch, 线程池等),它们是解决复杂并发问题的强大武器库。
  • 死锁可防可控:深刻理解死锁产生的四个必要条件,并在编码实践中(最常见的是通过“锁排序”)主动破坏其中之一,是预防这个严重并发问题的关键。
    是 JUC 中许多高性能并发类(如 AtomicInteger, ConcurrentHashMap)的实现基础,它用乐观的非阻塞方式,在很多场景下替代了传统的悲观阻塞式加锁。
  • JUC是并发编程的瑞士军刀java.util.concurrent 包提供了一套丰富、模块化的并发工具(如 ReentrantLock, Semaphore, CountDownLatch, 线程池等),它们是解决复杂并发问题的强大武器库。
  • 死锁可防可控:深刻理解死锁产生的四个必要条件,并在编码实践中(最常见的是通过“锁排序”)主动破坏其中之一,是预防这个严重并发问题的关键。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/94214.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/94214.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ZYNQ启动流程——ZYNQ学习笔记11

ZYNQ SoC 的启动由片上的 BootROM 开始。片上 BootROM 是 ZYNQ 芯片上的一块非易失性存储器,它包含了 ZYNQ 所支持的配置器件的驱动, 而且里面的代码是不可修改的。 BootROM 中的代码首先会在片外的非易失性存储器中寻找一个头文件, 头文件里…

C++利用CerateProcess创建WPF进程并通过命名管道通讯

引言原因是我需要在C程序中调用另外一个WPF窗体打开或则关闭,进程之前通过通讯协议进行交互。由于使用不同语言开发,两者都比较复杂不方便重写,最方便的方法就是使用进程间通信,WPF窗体应用程序根据消息进行Show/Hide/Exit操作。函…

Seaborn数据可视化实战

1. Seaborn基础与实践:数据可视化的艺术 2. Seaborn入门:环境搭建与基础操作 3. Seaborn基础图表绘制入门 4. Seaborn数据可视化基础:从内置数据集到外部数据集的应用 5. Seaborn颜色与样式定制教程 6. Seaborn数据可视化入门:绘制…

BIM+写实数字孪生落地实战指南

🌟 正文 在智慧城市与工业4.0的浪潮中,BIM与数字孪生的深度碰撞正在重塑建筑的生命周期。基于Revit(RVT)模型构建的超写实数字孪生体,不仅实现物理空间的毫米级镜像,更通过实时数据驱动,赋予建…

[Git] 如何拉取 GitHub 仓库的特定子目录

作为开发者,我们经常遇到只需要克隆大型仓库中某个子目录的场景。 Git 本身并不支持直接克隆子目录,但通过一些技巧可以实现类似效果。本文将介绍几种实用的方法,帮助获取目标代码。 为什么需要局部拉取? 节省时间和带宽&#xff…

修复Simulink到UE5丢包时被控船体的残影问题

提问 simulink 有一个和UE5协同的模块,叫做Simulation 3D Scence Configuration,还有一个发送来自simulink到UE5数据的模块叫做Simulation 3D Message。 现在遇到的问题是,这两个模块的优先级设置是正确的,且sample time都设置为0…

嵌入式第三十五课!!Linux下的网络编程

一、目的网络编程的目的实际上也是进程通信的一种方式,不过它可以在不同的主机上进行通信;二、需要解决的问题1. 主机与主机之间物理层面必须互联互通。指的是参与通信的计算机(主机)需要通过物理设备建立连接(光纤、网…

遥感机器学习入门实战教程|Sklearn案例⑦:特征选择与重要性分析

很多同学问:波段/特征一多就“维度灾难”,训练慢、过拟合,且很难解释“哪些特征最关键”。本篇用 sklearn 给出一套能跑、可视化、可比较的最小工作流,并配上方法论速记,帮助你在高光谱/多特征任务里做出稳健筛选。 &a…

地理数据制备:蔚蓝地图空气质量数据的获取、清洗与坐标匹配指南

【📊】手把手攻略:如何从“蔚蓝地图”挖宝——获取济南市可用空气质量数据全记录 一份不需要写代码也能搞定环境数据获取的实用指南 ✨ 引言:为什么选择蔚蓝地图? 作为一名环境数据爱好者,我经常需要获取准确、可靠、…

Unreal Engine USceneComponent

Unreal🏛 Unreal Engine - USceneComponent📚 定义🏷 类继承⚡ 关键特性⚙️ 常见配置🛠️ 使用方法🔗 创建与挂载🔄 获取与修改 Transform🧩 附加/分离组件🏊 典型应用场景&#x1…

2025年9月5090工作站、

在深度学习与大模型训练领域,算力是决定研发效率与模型性能的核心要素,而显卡作为算力输出的核心硬件,其性能参数直接影响着训练任务的速度、稳定性与成本控制。对于企业与科研机构而言,选择一套适配自身需求且性价比优异的显卡及…

亚矩阵云手机:亚马逊第三方店铺多账号安全合规运营的核心技术支撑

亚矩阵云手机在亚马逊第三方店铺多账号安全合规运营的技术支持,通过硬件级虚拟化、AI 行为建模、动态资源调度三大核心技术模块,构建了覆盖设备、网络、行为、数据的四维防御体系,确保账号在亚马逊平台规则下的长期稳定运行。以下从技术架构、…

使用C++11改进工厂方法模式:支持运行时配置的增强实现

在软件开发中,工厂方法模式是一种常用的设计模式,用于创建对象。通过使用C11的新特性,我们可以进一步改进工厂方法模式,使其更加灵活和高效。本文将详细介绍如何使用C11的std::function、lambda表达式和智能指针来实现一个支持运行…

小程序插件使用

插件介绍 插件是对一组 js 接口、自定义组件 或页面的封装,用于嵌入到小程序中使用。插件不能独立运行,必须嵌入在其他小程序中才能被用户使用;而第三方小程序在使用插件时,也无法看到插件的代码。因此,插件适合用来封…

要区分一张图片中的网状图(如网格结构或规则纹理)和噪点(随机分布的干扰像素),比如电路的方法 计算机视觉

要区分一张图片中的网状图(如网格结构或规则纹理)和噪点(随机分布的干扰像素),需结合图像预处理、特征提取和分割算法。以下是系统化的解决方案,分阶段说明关键技术和算法选择: 🔍 一…

06_并发编程高级特性

第6课:并发编程高级特性 课程目标 掌握context包的使用 理解sync包中的同步原语 学会处理并发安全问题 掌握性能优化技巧 1. Context包 1.1 Context基础 import ("context""fmt""time" )// 基本Context使用 func basicContext()

X00238-非GNSS无人机RGB图像卫星图像视觉定位python

获取方式见文末,可开发票随着无人机在工业和科研领域应用的加速发展,在非城市环境中使用无gnss、基于视觉的方法进行无人机定位的需求日益增长。本文提出了一种基于视觉的定位算法,利用深度特征计算无人机在野外飞行的地理坐标。该方法基于匹…

Eino 开源框架全景解析 - 以“大模型应用的搭积木指南”方式理解

Eino 开源框架全景解析 - 大模型应用的搭积木指南 🎯 什么是 Eino?一句话概括 Eino 是字节跳动开源的大语言模型应用开发框架,就像是一个专门为 AI 应用设计的"搭积木工具箱",让开发者能够像搭乐高一样轻松构建复杂的 A…

嵌入式开发中,usb通信中输出端点和输入端点

一. 简介本文简单学习一下,嵌入式开发中,usb的输出端点和输入端点。在嵌入式开发的 USB 通信场景中,输出端点(OUT Endpoint) 和 输入端点(IN Endpoint) 是 USB 设备与主机(如电脑、嵌…

【自用】Maven常用依赖

【自用】Maven常用依赖 工具类 Guava Guava(Google Guava)是由Google团队开发的一套Java开源工具库,旨在简化和增强Java开发者的日常工作。它提供了许多实用的工具和基础设施,覆盖了集合、并发、字符串处理、I/O、数学运算等多个…