简介
- 多线程锁定同一资源会造成死锁
- 线程池中的任务使用当前线程池也可能出现死锁
- RxJava 或 Reactor 等现代流行库也可能出现死锁
死锁是两个或多个线程互相等待对方所拥有的资源的情形。举个例子,线程 A 等待 lock1,lock1 当前由线程 B 锁住,然而线程 B 也在等待由线程 A 锁住的 lock2。最坏情况下,应用程序将无限期冻结。让我给你看个具体例子。假设这里有个 Lumberjack
(伐木工) 类,包含了两个装备的锁:
import com.google.common.collect.ImmutableList; import lombok.RequiredArgsConstructor; import java.util.concurrent.locks.Lock; @RequiredArgsConstructor class Lumberjack { private final String name; private final Lock accessoryOne; private final Lock accessoryTwo; void cut(Runnable work) { try { accessoryOne.lock(); try { accessoryTwo.lock(); work.run(); } finally { accessoryTwo.unlock(); } } finally { accessoryOne.unlock(); } } }
每个 Lumberjack
(伐木工)需要两件装备:helmet
(安全帽) 和 chainsaw
(电锯)。在他开始工作前,他必须拥有全部两件装备。我们通过如下方式创建伐木工们:
import lombok.RequiredArgsConstructor; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @RequiredArgsConstructor class Logging { private final Names names; private final Lock helmet = new ReentrantLock(); private final Lock chainsaw = new ReentrantLock(); Lumberjack careful() { return new Lumberjack(names.getRandomName(), helmet, chainsaw); } Lumberjack yolo() { return new Lumberjack(names.getRandomName(), chainsaw, helmet); } }
可以看到,有两种伐木工:先戴好安全帽然后再拿电锯的,另一种则相反。谨慎派(careful()
)伐木工先戴好安全帽,然后去拿电锯。狂野派伐木工(yolo()
)先拿电锯,然后找安全帽。让我们并发生成一些伐木工:
private List<Lumberjack> generate(int count, Supplier<Lumberjack> factory) { return IntStream .range(0, count) .mapToObj(x -> factory.get()) .collect(toList()); }
generate()
方法可以创建指定类型伐木工的集合。我们来生成一些谨慎派伐木工和狂野派伐木工。
private final Logging logging; //... List<Lumberjack> lumberjacks = new CopyOnWriteArrayList<>(); lumberjacks.addAll(generate(carefulLumberjacks, logging::careful)); lumberjacks.addAll(generate(yoloLumberjacks, logging::yolo));
最后,我们让这些伐木工开始工作:
IntStream .range(0, howManyTrees) .forEach(x -> { Lumberjack roundRobinJack = lumberjacks.get(x % lumberjacks.size()); pool.submit(() -> { log.debug("{} cuts down tree, {} left", roundRobinJack, latch.getCount()); roundRobinJack.cut(/* ... */); }); });
这个循环让所有伐木工一个接一个(轮询方式)去砍树。实质上,我们向线程池(ExecutorService
)提交了和树木数量(howManyTrees
)相同个数的任务,并使用 CountDownLatch
来记录工作是否完成。
CountDownLatch latch = new CountDownLatch(howManyTrees); IntStream .range(0, howManyTrees) .forEach(x -> { pool.submit(() -> { //... roundRobinJack.cut(latch::countDown); }); }); if (!latch.await(10, TimeUnit.SECONDS)) { throw new TimeoutException("Cutting forest for too long"); }
其实想法很简单。我们让多个伐木工(Lumberjacks
)通过多线程方式去竞争一个安全帽和一把电锯。完整代码如下:
import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; @RequiredAr