Skip to content

Commit 5514569

Browse files
committed
update:优化完善java集合和并发部分面试题答案
1 parent 9172b6b commit 5514569

File tree

3 files changed

+59
-33
lines changed

3 files changed

+59
-33
lines changed

docs/java/collection/java-collection-questions-01.md

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -257,49 +257,75 @@ public interface RandomAccess {
257257

258258
### ⭐️集合中的 fail-fast 和 fail-safe 是什么?
259259

260+
`fail-fast`(快速失败)和 `fail-safe`(安全失败)是Java集合框架在处理并发修改问题时,两种截然不同的设计哲学和容错策略。
261+
260262
关于`fail-fast`引用`medium`中一篇文章关于`fail-fast``fail-safe`的说法:
261263

262264
> Fail-fast systems are designed to immediately stop functioning upon encountering an unexpected condition. This immediate failure helps to catch errors early, making debugging more straightforward.
263265
264266
快速失败的思想即针对可能发生的异常进行提前表明故障并停止运行,通过尽早的发现和停止错误,降低故障系统级联的风险。
265267

266-
`java.util`包下的大部分集合是不支持线程安全的,为了能够提前发现并发操作导致线程安全风险,提出通过维护一个`modCount`记录修改的次数,迭代期间通过比对预期修改次数`expectedModCount``modCount`是否一致来判断是否存在并发操作,从而实现快速失败,由此保证在避免在异常时执行非必要的复杂代码。
268+
`java.util`包下的大部分集合(如 `ArrayList`, `HashMap`)是不支持线程安全的,为了能够提前发现并发操作导致线程安全风险,提出通过维护一个`modCount`记录修改的次数,迭代期间通过比对预期修改次数`expectedModCount``modCount`是否一致来判断是否存在并发操作,从而实现快速失败,由此保证在避免在异常时执行非必要的复杂代码。
267269

268-
对应的我们给出下面这样一段在示例,我们首先插入`100`个操作元素,一个线程迭代元素,一个线程删除元素,最终输出结果如愿抛出`ConcurrentModificationException`
270+
**ArrayList (fail-fast) 示例:**
269271

270272
```java
271-
// 使用线程安全的 CopyOnWriteArrayList 避免 ConcurrentModificationException
272-
List<Integer> list = new CopyOnWriteArrayList<>();
273-
CountDownLatch countDownLatch = new CountDownLatch(2);
274-
275-
// 添加元素
276-
for (int i = 0; i < 100; i++) {
277-
list.add(i);
278-
}
279-
280-
Thread t1 = new Thread(() -> {
281-
// 迭代元素 (注意:Integer 是不可变的,这里的 i++ 不会修改 list 中的值)
282-
for (Integer i : list) {
283-
i++; // 这行代码实际上没有修改list中的元素
284-
}
285-
countDownLatch.countDown();
286-
});
273+
// 使用线程不安全的 ArrayList,它是一种 fail-fast 集合
274+
List<Integer> list = new ArrayList<>();
275+
CountDownLatch latch = new CountDownLatch(2);
276+
277+
for (int i = 0; i < 5; i++) {
278+
list.add(i);
279+
}
280+
System.out.println("Initial list: " + list);
281+
282+
Thread t1 = new Thread(() -> {
283+
try {
284+
for (Integer i : list) {
285+
System.out.println("Iterator Thread (t1) sees: " + i);
286+
Thread.sleep(100);
287+
}
288+
} catch (ConcurrentModificationException e) {
289+
System.err.println("!!! Iterator Thread (t1) caught ConcurrentModificationException as expected.");
290+
} catch (InterruptedException e) {
291+
e.printStackTrace();
292+
} finally {
293+
latch.countDown();
294+
}
295+
});
296+
297+
Thread t2 = new Thread(() -> {
298+
try {
299+
Thread.sleep(50);
300+
System.out.println("-> Modifier Thread (t2) is removing element 1...");
301+
list.remove(Integer.valueOf(1));
302+
System.out.println("-> Modifier Thread (t2) finished removal.");
303+
} catch (InterruptedException e) {
304+
e.printStackTrace();
305+
} finally {
306+
latch.countDown();
307+
}
308+
});
309+
310+
t1.start();
311+
t2.start();
312+
latch.await();
313+
314+
System.out.println("Final list state: " + list);
315+
```
287316

288-
Thread t2 = new Thread(() -> {
289-
System.out.println("删除元素1");
290-
list.remove(Integer.valueOf(1)); // 使用 Integer.valueOf(1) 删除指定值的对象
291-
countDownLatch.countDown();
292-
});
317+
输出:
293318

294-
t1.start();
295-
t2.start();
296-
countDownLatch.await();
319+
```
320+
Initial list: [0, 1, 2, 3, 4]
321+
Iterator Thread (t1) sees: 0
322+
-> Modifier Thread (t2) is removing element 1...
323+
-> Modifier Thread (t2) finished removal.
324+
!!! Iterator Thread (t1) caught ConcurrentModificationException as expected.
325+
Final list state: [0, 2, 3, 4]
297326
```
298327

299-
我们在初始化时插入了`100`个元素,此时对应的修改`modCount`次数为`100`,随后线程 2 在线程 1 迭代期间进行元素删除操作,此时对应的`modCount`就变为`101`
300-
线程 1 在随后`foreach`第 2 轮循环发现`modCount``101`,与预期的`expectedModCount(值为100因为初始化插入了元素100个)`不等,判定为并发操作异常,于是便快速失败,抛出`ConcurrentModificationException`
301-
302-
![](https://oss.javaguide.cn/github/javaguide/java/collection/fail-fast-and-fail-safe-insert-100-values.png)
328+
程序在线程 t2 修改列表后,线程 t1 的下一次迭代操作立刻就抛出了 `ConcurrentModificationException`。这是因为 ArrayList 的迭代器在每次 `next()` 调用时,都会检查 `modCount` 是否被改变。一旦发现集合在迭代器不知情的情况下被修改,它会立即“快速失败”,以防止在不一致的数据上继续操作导致不可预期的后果。
303329

304330
对此我们也给出`for`循环底层迭代器获取下一个元素时的`next`方法,可以看到其内部的`checkForComodification`具有针对修改次数比对的逻辑:
305331

@@ -324,7 +350,7 @@ final void checkForComodification() {
324350

325351
> Fail-safe systems take a different approach, aiming to recover and continue even in the face of unexpected conditions. This makes them particularly suited for uncertain or volatile environments.
326352
327-
该思想常运用于并发容器,最经典的实现就是`CopyOnWriteArrayList`的实现,通过写时复制的思想保证在进行修改操作时复制出一份快照,基于这份快照完成添加或者删除操作后,将`CopyOnWriteArrayList`底层的数组引用指向这个新的数组空间,由此避免迭代时被并发修改所干扰所导致并发操作安全问题,当然这种做法也存在缺点,即进行遍历操作时无法获得实时结果:
353+
该思想常运用于并发容器,最经典的实现就是`CopyOnWriteArrayList`的实现,通过写时复制(Copy-On-Write)的思想保证在进行修改操作时复制出一份快照,基于这份快照完成添加或者删除操作后,将`CopyOnWriteArrayList`底层的数组引用指向这个新的数组空间,由此避免迭代时被并发修改所干扰所导致并发操作安全问题,当然这种做法也存在缺点,即进行遍历操作时无法获得实时结果:
328354

329355
![](https://oss.javaguide.cn/github/javaguide/java/collection/fail-fast-and-fail-safe-copyonwritearraylist.png)
330356

docs/java/concurrent/java-concurrent-questions-02.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,7 +672,7 @@ public class SynchronizedDemo {
672672

673673
- **等待可中断** : `ReentrantLock`提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说当前线程在等待获取锁的过程中,如果其他线程中断当前线程「 `interrupt()` 」,当前线程就会抛出 `InterruptedException` 异常,可以捕捉该异常进行相应处理。
674674
- **可实现公平锁** : `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。`ReentrantLock`默认情况是非公平的,可以通过 `ReentrantLock`类的`ReentrantLock(boolean fair)`构造方法来指定是否是公平的。
675-
- **可实现选择性通知(锁可以绑定多个条件)**: `synchronized`关键字与`wait()``notify()`/`notifyAll()`方法相结合可以实现等待/通知机制。`ReentrantLock`类当然也可以实现,但是需要借助于`Condition`接口与`newCondition()`方法
675+
- **通知机制更强大**`ReentrantLock` 通过绑定多个 `Condition` 对象,可以实现分组唤醒和选择性通知。这解决了 `synchronized` 只能随机唤醒或全部唤醒的效率问题,为复杂的线程协作场景提供了强大的支持
676676
- **支持超时**`ReentrantLock` 提供了 `tryLock(timeout)` 的方法,可以指定等待获取锁的最长等待时间,如果超过了等待时间,就会获取锁失败,不会一直等待。
677677

678678
如果你想使用上述功能,那么选择 `ReentrantLock` 是一个不错的选择。

docs/java/concurrent/java-concurrent-questions-03.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,7 @@ new RejectedExecutionHandler() {
626626

627627
不同的线程池会选用不同的阻塞队列,我们可以结合内置线程池来分析。
628628

629-
- 容量为 `Integer.MAX_VALUE``LinkedBlockingQueue`有界阻塞队列):`FixedThreadPool``SingleThreadExecutor``FixedThreadPool`最多只能创建核心线程数的线程(核心线程数和最大线程数相等),`SingleThreadExecutor`只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满。
629+
- 容量为 `Integer.MAX_VALUE``LinkedBlockingQueue`无界阻塞队列):`FixedThreadPool``SingleThreadExecutor``FixedThreadPool`最多只能创建核心线程数的线程(核心线程数和最大线程数相等),`SingleThreadExecutor`只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满。
630630
- `SynchronousQueue`(同步队列):`CachedThreadPool``SynchronousQueue` 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,`CachedThreadPool` 的最大线程数是 `Integer.MAX_VALUE` ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。
631631
- `DelayedWorkQueue`(延迟队列):`ScheduledThreadPool``SingleThreadScheduledExecutor``DelayedWorkQueue` 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。`DelayedWorkQueue` 是一个无界队列。其底层虽然是数组,但当数组容量不足时,它会自动进行扩容,因此队列永远不会被填满。当任务不断提交时,它们会全部被添加到队列中。这意味着线程池的线程数量永远不会超过其核心线程数,最大线程数参数对于使用该队列的线程池来说是无效的。
632632
- `ArrayBlockingQueue`(有界阻塞队列):底层由数组实现,容量一旦创建,就不能修改。

0 commit comments

Comments
 (0)