线程池自引发死锁
强烈推介IDEA2020.2破解激活,IntelliJ IDEA 注册码,2020.2 IDEA 激活码
线程池自引发死锁
- 死锁是由许多线程锁定相同资源引起的
- 如果在该池中运行的任务内使用线程池,也会发生死锁
- 像RxJava / Reactor这样的现代图书馆也很容易受到影响
死锁是两个或多个线程正在等待彼此获取的资源的情况。例如,线程A等待lock1线程B锁定,而线程B等待lock2,由线程A锁定。在最坏的情况下,应用程序冻结无限期的时间。让我向您展示一个具体的例子。想象一下,有一个Lumberjack类可以保存对两个附件锁的引用:
代码语言:javascript复制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();
}
}
}
每个伐木工人都需要两个配件:头盔和电锯。在他接近之前work,他必须对这两者保持独占锁定。我们按如下方式创建伐木工人:
代码语言:javascript复制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);
}
}
正如你所看到的那样,有两种伐木工人:首先是头盔,然后是电锯,反之亦然。小心翼翼的伐木工人首先尝试获得头盔,然后等待电锯。YOLO型的伐木工人首先拿电锯然后寻找头盔。让我们生成一些伐木工人并同时运行它们:
代码语言:javascript复制rivate List<Lumberjack> generate(int count, Supplier<Lumberjack> factory) {
return IntStream
.range(0, count)
.mapToObj(x -> factory.get())
.collect(toList());
}
generate()是一种创建给定类型的伐木工人集合的简单方法。然后我们生成了一堆细心和yolo伐木工人:
代码语言:javascript复制private final Logging logging;
//...
List<Lumberjack> lumberjacks = new CopyOnWriteArrayList<>();
lumberjacks.addAll(generate(carefulLumberjacks, logging::careful));
lumberjacks.addAll(generate(yoloLumberjacks, logging::yolo));
最后让我们把这些伐木工人投入使用:
代码语言:javascript复制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(/* ... */);
});
});
这个循环以循环方式一个接一个地接受伐木工人并要求他们砍树。基本上我们将howManyTrees一些任务提交给一个线程pool(ExecutorService)。为了弄清楚工作何时完成,我们使用CountDownLatch:
代码语言:javascript复制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");
}
这个想法很简单 - 让一群伐木工人通过头盔和电锯在多个线程上竞争。完整的源代码如下:
代码语言:javascript复制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;
@RequiredArgsConstructor
class Forest implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(Forest.class);
private final ExecutorService pool;
private final Logging logging;
void cutTrees(int howManyTrees, int carefulLumberjacks, int yoloLumberjacks) throws InterruptedException, TimeoutException {
CountDownLatch latch = new CountDownLatch(howManyTrees);
List<Lumberjack> lumberjacks = new ArrayList<>();
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(latch::countDown);
});
});
if (!latch.await(10, TimeUnit.SECONDS)) {
throw new TimeoutException("Cutting forest for too long");
}
log.debug("Cut all trees");
}
private List<Lumberjack> generate(int count, Supplier<Lumberjack> factory) {
return IntStream
.range(0, count)
.mapToObj(x -> factory.get())
.collect(Collectors.toList());
}
@Override
public void close() {
pool.shutdownNow();
}
}
现在有趣的部分。如果您只创建careful伐木工人,应用程序几乎立即完成,例如:
代码语言:javascript复制ExecutorService pool = Executors.newFixedThreadPool(10);
Logging logging = new Logging(new Names());
try (Forest forest = new Forest(pool, logging)) {
forest.cutTrees(10_000, 10, 0);
} catch (TimeoutException e) {
log.warn("Working for too long", e);
}
但是,如果你玩一些伐木工人的数量,例如10小心和一个yolo,系统经常会失败。发生了什么?细心的团队中的每个人都会先尝试拿起头盔。如果其中一名伐木工人拿起头盔,其他人都会等待。然后幸运的家伙拿起一个必须可用的电锯。为什么?其他人在拿起电锯之前都在等头盔。到现在为止还挺好。但如果团队中有一名yolo伐木工人怎么办?当每个人都争夺头盔时,他偷偷地抓住了电锯。但是有一个问题。一个仔细的伐木工人得到了他的安全头盔。然而,他不能拿起电锯,因为它已经被别人拿走了。更糟糕的是,电锯的当前所有者(yolo家伙)在他拿到头盔之前不会释放他的电锯。这里没有超时。细心的家伙用头盔无限地等待,无法找到电锯。yolo家伙永远无所事事,因为他无法获得头盔。陷入僵局。
现在,如果所有伐木工人都是yolo,会发生什么事,即他们都试图先挑选电锯?原来避免死锁的最简单方法是始终以相同的顺序获取和释放锁。例如,您可以根据某些任意条件对资源进行排序。如果一个线程获得锁定A跟随B,而第二个线程首先获得B,则它是死锁的配方。
线程池自引发死锁
这是一个僵局的例子,相当简单。但事实证明,如果使用不正确,单个线程池可能会导致死锁。想象一下,你有一个ExecutorService(就像在前面的例子中一样)你这样使用:
代码语言:javascript复制ExecutorService pool = Executors.newFixedThreadPool(10); pool.submit(() - > {
try {
log.info(“First”);
pool.submit(() - > log.info(“Second”))。get();
log.info(“Third”) ;
} catch(InterruptedException | ExecutionException e){
log.error(“Error”,e);
} });
这看起来很好,所有消息都按预期显示在屏幕上:
代码语言:javascript复制INFO [pool-1-thread-1]: First
INFO [pool-1-thread-2]: Second
INFO [pool-1-thread-1]: Third
请注意,我们阻止(请参阅get())Runnable在显示之前等待内部完成"Third"。这是一个陷阱!等待内部任务完成意味着它必须从线程获取一个线程pool才能继续。但是我们已经获得了一个线程,因此内部将阻塞,直到它可以得到第二个。我们的线程池目前足够大,所以它工作正常。让我们稍微改变一下代码,将线程池缩小到一个线程。此外,我们将删除get(),这是至关重要的:
代码语言:javascript复制ExecutorService pool = Executors.newSingleThreadExecutor(); pool.submit(() - > {
log.info(“First”);
pool.submit(() - > log.info(“Second”));
log.info(“Third”); }
代码工作正常,但有一个扭曲:
代码语言:javascript复制INFO [pool-1-thread-1]: First
INFO [pool-1-thread-1]: Third
INFO [pool-1-thread-1]: Second
有两点需要注意:
- 一切都在一个线程中运行(不出所料)
- 在"Third"之前出现的消息"Second"
订单的变化是完全可预测的,并不是来自线程之间的某些竞争条件(事实上,我们只有一个)。仔细观察会发生什么:我们向线程池提交一个新任务(一次打印"Second")。但是,这次我们不等待完成该任务。伟大的,因为在一个线程池非常单一线程已经被任务所占用的印刷"First"和"Third"。因此,外部任务继续,打印"Second"。当此任务完成时,它将单个线程释放回线程池。内部任务终于可以开始执行,打印"Second"。现在陷入僵局的地方?尝试向get()内部任务添加阻止:
代码语言:javascript复制ExecutorService pool = Executors.newSingleThreadExecutor(); pool.submit(() - > {
try {
log.info(“First”);
pool.submit(() - > log.info(“Second”))。get();
log.info(“Third”) ;
} catch(InterruptedException | ExecutionException e){
log.error(“Error”,e);
} });
僵局!一步步:
- 任务打印"First"提交到空闲的单线程池
- 此任务开始执行并打印 “First”
- 我们向"Second"线程池提交内部任务打印
- 内部任务落在待处理任务队列中 - 没有线程可用,因为当前只有一个线程被占用
- 我们阻止等待内部任务的结果。不幸的是,在等待内部任务时,我们持有唯一可用的线程
- get() 将永远等待,无法获得线程
- 僵局
这是否意味着单线程池是坏的?并不是的。任何大小的线程池都可能出现同样的问题。但在这种情况下,只有在高负载下才会出现死锁,从维护的角度来看,这种情况要糟糕得多。从技术上讲,你可以拥有一个无限制的线程池,但情况更糟。
反应堆/ RxJava
请注意,像Reactor这样的高级库可能会出现此问题:
代码语言:javascript复制Scheduler pool = Schedulers.fromExecutor(Executors.newFixedThreadPool(10));
Mono
.fromRunnable(() -> {
log.info("First");
Mono
.fromRunnable(() -> log.info("Second"))
.subscribeOn(pool)
.block(); //VERY, VERY BAD!
log.info("Third");
})
.subscribeOn(pool);
一旦你订阅,这似乎工作,但非常非惯用。基本问题是一样的。外部Runnable从pool(subscribeOn()从最后一行)获取一个线程,同时内部Runnable尝试获取线程。用单线程池替换底层线程池,这会产生死锁。至少使用RxJava / Reactor,解决方法很简单 - 只需编写异步操作而不是相互阻塞:
代码语言:javascript复制Mono
.fromRunnable(() - > {
log.info(“First”);
log.info(“Third”);
})。
then(
Mono .fromRunnable(() - > log.info(“Second”))
。 subscribeOn(pool)).
subscribeOn(pool)
预防
没有100%的方法来防止死锁。一种技术是避免可能导致死锁的情况,例如共享资源或专门锁定。如果那是不可能的(或者死锁不明显,就像线程池一样),请考虑正确的代码卫生。监视线程池并避免无限期阻塞。当你愿意无限期地等待完成时,我很难想象这种情况。这就是如何get()或者block()不超时工作正常。
原文链接:https://gper.club/articles/7e7e7f7ff2g51gce