머리말
최근 테스트에서 예외가 발생 java.lang.InterruptedException
했으며 StackTrace는 다음과 같습니다.
java.lang.InterruptedException
at java.base/java.lang.Object.wait(Native Method)
at java.base/java.lang.Object.wait(Object.java:328)
at bt.torrent.messaging.MetadataConsumer.waitForTorrent(MetadataConsumer.java:265)
at bt.processor.magnet.FetchMetadataStage.doExecute(FetchMetadataStage.java:92)
at bt.processor.magnet.FetchMetadataStage.doExecute(FetchMetadataStage.java:42)
at bt.processor.TerminateOnErrorProcessingStage.doExecute(TerminateOnErrorProcessingStage.java:38)
at bt.processor.RoutingProcessingStage.execute(RoutingProcessingStage.java:39)
at bt.processor.ChainProcessor.doExecute(ChainProcessor.java:112)
at bt.processor.ChainProcessor.executeStage(ChainProcessor.java:96)
at bt.processor.ChainProcessor.executeStage(ChainProcessor.java:98)
at bt.processor.ChainProcessor.lambda$process$0(ChainProcessor.java:81)
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run$$$capture(CompletableFuture.java:1736)
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
디버그가 예외를 throw하기 위해 찾는 소스 코드는 다음과 같습니다.
의 정의는 torrent.wait();
입니다 . 따라서 이 기사에서는 이 문제의 문제 해결 프로세스를 결합하여 모든 측면 을 깊이 이해할 것입니다. 기사 마지막에는 발생한 문제 해결 프로세스에 대한 간략한 분석과 요약이 있습니다.torrent
private final AtomicReference<Torrent> torrent;
AtomicReference
그것이 당신이 더 잘 이해하는 데 도움이 될 수 있기를 바랍니다 AtomicReference
.
원자성
소개 하기 전에 먼저 AtomicReference
이해해야 하는 핵심 개념이 있습니다. 바로 원자성 입니다. 간단한 예부터 시작하겠습니다.
프로그램이 변수를 업데이트할 때 여러 스레드가 동시에 변수를 업데이트하면 변수 i=1
, A 스레드 업데이트 i+1
, B 스레드도 업데이트 와 같은 예기치 않은 값을 얻을 수 있습니다. i+1
두 개의 스레드 작업 후에 i는 3과 같지 않을 수 있습니다. , 그러나 2와 같습니다.
스레드 A와 B 모두 변수 i를 업데이트할 때 i 1을 얻기 때문에 이것은 스레드 안전하지 않은 업데이트 작업입니다. 일반적으로 synchronized
이 문제를 해결하기 위해 이를 사용 synchronized
하여 여러 스레드가 변수 i를 업데이트하지 않도록 할 것입니다. 동시.
JDK 1.5부터 Java는 java.util.concurrent.atomic
원자성 연산 클래스가 변수를 업데이트하는 간단하고 성능 효율적이며 스레드로부터 안전한 방법을 제공하는 패키지를 제공했습니다. 이 기사는 AtomicReference
전형적인 대표자 중 하나에 초점을 맞춥니다.
원자 참조
먼저 JDK에서 쌍의 정의를 살펴보십시오 AtomicReference
.
An object reference that may be updated atomically. See the java.util.concurrent.atomic package specification for description of the properties of atomic variables.
JavaDoc:docs.oracle.com/javase/8/do…
AtomicReference
的作用是对对象
进行原子操作
。它提供了一种读和写都是原子性
的对象引用变量。
同时,原子性
意味着多个线程试图改变同一个 AtomicReference(例如比较和交换操作)将不会使得 AtomicReference 处于不一致的状态。
atomic 包
提到AtomicReference
就一定会提到原子包:java.util.concurrent.atomic
,在这里 JDK 提供了众多的针对原子操作的类。包括:
-
Number
AtomicInteger
- 提供对int类型的原子操作,是对基本类型封装的一个代表;
AtomicBoolean
- 提供对boolean类型的原子操作
AtomicLong
- 提供对long类型的原子操作
DoubleAdder
- 一个或多个变量一起保持最初为零的总和为double 。
- 当跨线程争用更新(方法add(double) )时,变量集可以动态增长以减少争用。
- 方法sum() (或等效地, doubleValue() )返回保持总和的变量的当前总和。
- 线程内或线程之间的累积顺序无法保证。
- 因此,如果需要数值稳定性,则该类可能不适用,尤其是在组合基本上不同数量级的值时。
LongAdder
- 一个或多个变量,它们共同维持最初的零long总和。
- 当跨线程争用更新(方法add(long) )时,变量集可以动态增长以减少争用。
- 方法sum() (或等效地, longValue() )返回保持总和的变量的当前总和。
- 当多个线程更新用于收集统计信息但不用于细粒度同步控制的目的的公共和时,此类通常优于AtomicLong 。
- 在低更新争用下,这两个类具有相似的特征。 但在高争用的情况下,这一类的预期吞吐量明显更高,但代价是空间消耗更高。
Striped64
DoubleAccumulator
- 使用提供的函数一起维护运行的double值的一个或多个变量。
- 当更新(方法accumulate(double) )跨线程争用时,变量集可以动态增长以减少争用。
- 方法get() (或等效地, doubleValue() )返回维护更新的变量的当前值。
- 当多个线程更新公共值时,此类通常优于备选方案,该公共值用于诸如经常更新但读取频率较低的摘要统计信息之类的目的。
- 提供的累加器功能应该是无副作用的,因为当尝试的更新由于线程之间的争用而失败时可以重新应用它。
- 对于可预测的结果,累加器函数应该在使用上下文中所需的浮点容差内是可交换的和关联的。
- 该函数应用现有值(或标识)作为一个参数,给定更新作为另一个参数。 例如,要保持运行的最大值,您可以提供Double::max以及Double.NEGATIVE_INFINITY作为标识。
- 线程内或线程之间的累积顺序无法保证。 因此,如果需要数值稳定性,则该类可能不适用,尤其是在组合基本上不同数量级的值时。
LongAccumulator
- 使用提供的函数一起维护运行的long值的一个或多个变量。 当跨线程争用更新(方法accumulate(long) )时,变量集可以动态增长以减少争用。 方法get() (或等效地, longValue() )返回维护更新的变量的当前值。
- 当多个线程更新用于收集统计信息但不用于细粒度同步控制的目的的公共值时,此类通常优于AtomicLong 。 在低更新争用下,这两个类具有相似的特征。 但在高争用的情况下,这一类的预期吞吐量明显更高,但代价是空间消耗更高。
- 线程内或线程之间的累积顺序无法保证且不能依赖,因此该类仅适用于累积顺序无关紧要的函数。 提供的累加器功能应该是无副作用的,因为当尝试的更新由于线程之间的争用而失败时可以重新应用它。 对于可预测的结果,累加器函数应该是关联的和可交换的。 该函数应用现有值(或标识)作为一个参数,给定更新作为另一个参数。 例如,要保持运行的最大值,您可以提供Long::max以及Long.MIN_VALUE作为标识。
- 类LongAdder提供了此类功能的类比,用于维护计数和总和的常见特殊情况。 电话new LongAdder()相当于new LongAccumulator((x, y) -> x + y, 0L) 。
AtomicIntegerFieldUpdater
- 原子地更新一个对象的int类型的field,这个int类型的field必须是被volatile修饰的;
- 基于反射的实用程序,可以对指定类的指定volatile int字段进行原子更新。 此类设计用于原子数据结构,其中同一节点的多个字段独立地受原子更新的影响。
- 请注意, compareAndSet方法的保证比其他原子类弱。 因为此类无法确保该字段的所有使用都适用于原子访问的目的,所以它只能保证在同一更新程序上对compareAndSet和set其他调用的原子性。
AtomicLongFieldUpdater
- 原子地更新一个对象的long类型的field,这个long类型的field必须是被volatile修饰的;
- 基于反射的实用程序,可以对指定类的指定volatile long字段进行原子更新。 此类设计用于原子数据结构,其中同一节点的多个字段独立地受原子更新的影响。
- 请注意, compareAndSet方法的保证比其他原子类弱。 由于此类无法确保该字段的所有使用都适用于原子访问,因此只能在同一更新程序上对compareAndSet和set其他调用保证原子性。
-
Array
AtomicIntegerArray
- 提供对int数组的原子操作,可以原子地更新数组里的某个index上的值;
AtomicLongArray
- 提供对long数组的原子操作,可以原子地更新数组里的某个index上的值;
-
Reference
AtomicReference
- 原子更新引用类型,提供对引用类型的原子操作
- 但是并不是说可以原子地操作引用的对象里的字段,可以将引用原子地指向两一个对象;
AtomicReferenceArray
- 原子地更新一个reference类型的数组。
AtomicReferenceFieldUpdater
- 原子更新引用类型里的字段
AtomicMarkableReference
- 原子更新带有标记位的引用类型。
- 需要传入一个reference和一个boolean类型的标志位,可以原子地更新reference和标志位;
AtomicStampedReference
- 与AtomicMarkableReference类似,不过把标志位换成了一个int值,原子的更新reference和int值;
继承自Number
的原子类,如AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用Reference
等原子更新的引用类型提供的类来完成了。
这里只是简单介绍些每个类的作用,关于java.util.concurrent.atomic
包并不是本文的重点,有兴趣的话,可以通过翻阅JDK源码进一步的了解。
源码
package java.util.concurrent.atomic;
import java.util.function.UnaryOperator;
import java.util.function.BinaryOperator;
import sun.misc.Unsafe;
public class AtomicReference<V> implements java.io.Serializable {
private static final long serialVersionUID = -1848883965231344442L;
// 空歌白石:基于Unsafe实现原子操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 空歌白石:基于volatile实现原子操作
private volatile V value;
public AtomicReference(V initialValue) {
value = initialValue;
}
public AtomicReference() {
}
// 空歌白石:基于volatile实现原子操作
public final V get() {
return value;
}
public final void set(V newValue) {
value = newValue;
}
// 空歌白石:基于Unsafe实现原子操作
public final void lazySet(V newValue) {
unsafe.putOrderedObject(this, valueOffset, newValue);
}
// 空歌白石:基于Unsafe实现原子操作
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
// 空歌白石:基于Unsafe实现原子操作
public final boolean weakCompareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
// 空歌白石:基于Unsafe实现原子操作
@SuppressWarnings("unchecked")
public final V getAndSet(V newValue) {
return (V)unsafe.getAndSetObject(this, valueOffset, newValue);
}
public final V getAndUpdate(UnaryOperator<V> updateFunction) {
V prev, next;
do {
prev = get();
next = updateFunction.apply(prev);
} while (!compareAndSet(prev, next));
return prev;
}
public final V updateAndGet(UnaryOperator<V> updateFunction) {
V prev, next;
do {
prev = get();
next = updateFunction.apply(prev);
} while (!compareAndSet(prev, next));
return next;
}
public final V getAndAccumulate(V x, BinaryOperator<V> accumulatorFunction) {
V prev, next;
do {
prev = get();
next = accumulatorFunction.apply(prev, x);
} while (!compareAndSet(prev, next));
return prev;
}
public final V accumulateAndGet(V x, BinaryOperator<V> accumulatorFunction) {
V prev, next;
do {
prev = get();
next = accumulatorFunction.apply(prev, x);
} while (!compareAndSet(prev, next));
return next;
}
public String toString() {
return String.valueOf(get());
}
}
原子操作
通过AtomicReference
的源码可以看出,AtomicReference
是基于volatile
和sun.misc.Unsafe
来实现对于引用的原子操作的。
volatile
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
sun.misc.Unsafe
通过上文可以看出一点,即java.util.concurrent.atomic
包中的类基本上都是使用sun.misc.Unsafe
实现的包装类,换句话说,java.util.concurrent.atomic
是对sun.misc.Unsafe
的封装。那么sun.misc.Unsafe
就是何方神圣呢?
简单讲一下这个类。Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。
如果对优秀开源项目有过深入分析研究的同学应该对sun.misc.Unsafe
并不陌生,为了追求极致性能,绝大部分的高性能框架或项目基本上都离不开sun.misc.Unsafe
类,Unsafe
并不是说这个类中的方法不安全,而是说想要使用好sun.misc.Unsafe
并不容易,需要对sun.misc.Unsafe
和底层原理有深刻的理解,因此并不推荐在一般项目中直接使用,这也从一个侧面说明了为何Unsafe
类并不是在java
包下,而是在sun
包下。
这个类尽管里面的方法都是public的,但是并没有办法使用它们,JDK API文档也没有提供任何关于这个类的方法的解释。总而言之,对于Unsafe类的使用都是受限制的,只有授信的代码才能获得该类的实例,当然JDK库里面的类是可以随意使用的。
Unsafe提供了硬件级别的操作,比如说获取某个属性在内存中的位置,比如说修改对象的字段值,即使它是私有的。不过Java本身就是为了屏蔽底层的差异,对于一般的开发而言也很少会有这样的需求。正因为Unsafe的硬件级别的操作,使得Unsafe的性能极高,在追求高性能的场景下使用极为广泛。
CAS
CAS,Compare and Swap
即比较并交换
,设计并发算法时常用到的一种技术,java.util.concurrent
包完全建立在CAS之上,没有CAS也就没有此包,在上文的AtmoicReference
中也可以看到众多的compareAndSwap方法。
此外,当前的处理器基本都支持CAS,只不过不同的厂家的实现不一样罢了。CAS有三个操作数:
- 内存值V
- 旧的预期值A
- 要修改的值B
当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。
使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层硬件实现来保证线程安全。相对于其他锁的实现没有现场切换和阻塞,也就没有了额外的开销,并且可以支持较大的并发性。(当然CAS也有一个缺点就是忙等,如果一直没有获取到将会处于死循环中。)
AtomicReference的使用
AtomicReference
的初始化过程。
// 空歌白石:在获取和验证元数据后立即设置原子集
private final AtomicReference<Torrent> torrent;
public MetadataConsumer(IMetadataService metadataService,
TorrentId torrentId,
Config config,
EventSource eventSource) {
// 空歌白石:略去无关代码
this.torrent = new AtomicReference<>();
// 空歌白石:略去无关代码
}
以下代码为实际开发中AtomicReference的代码:
private void processMetadataBlock(int pieceIndex, int totalSize, byte[] data) {
// 空歌白石:略去无关代码
synchronized (torrent) {
torrent.set(fetchedTorrent);
states.clear();
torrent.notifyAll();
}
// 空歌白石:略去无关代码
}
@Produces
public void produce(Consumer<Message> messageConsumer, MessageContext context) {
// 空歌白石:如果元数据已经被获取,则停止到此。
if (torrent.get() != null) {
return;
}
// 空歌白石:略去无关代码
}
/**
* 空歌白石:如果Torrent还没有被获取,便阻塞调用线程,等待获取到Torrent。
*/
public Torrent waitForTorrent() {
while (torrent.get() == null) {
synchronized (torrent) {
if (torrent.get() == null) {
try {
torrent.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
return torrent.get();
}
new AtomicReference<>() is null
为更好的展示原理,仅仅将最核心代码编写了一个demo,如下,可以看出AtomicReference的值是null,这是又为什么呢?
原因是以上代码仅仅是将new AtomicReference<>();
,实际上并没有真正将具体的reference传递给AtomicReference,因此,看到的integer的值仍为null。
我们更换一种写法,在创建AtomicReference时,随即将引用对象赋值,代码如下:
// 创建一个对象
String str = "testAtomicReference";
// 将引用传给AtomicReference对象
AtomicReference<String> atomicReference = new AtomicReference<String>(str);
// 获取到AtomicReference中的引用对象
String value = atomicReference.get();
// 创建一个新的对象
String newStr = "new testAtomicReference";
// compareAndSet方法会比较当前AtomicReference对象中的引用的是否是str;如果是,则会更新为newStr,如果不是,则不会更新。
boolean exchanged = atomicReference.compareAndSet(str, newStr);
当new AtomicReference<String>(str);
时,实际只是将str的引用传递给了AtomicReference,因此debug看到的仍然为null,实际在get方法是可以获取到具体的value,通过debug可以看出符合我们的预期,如下图:
java.lang.Object
在上述waitForTorrent
方法中,会使用到torrent.wait()
方法,wait
方法是在Object
类中定义的。对于Object
类相信大家都并不陌生,特别是其中的equals
、hashCode
、toString
等方法,但是如果不涉及到原子性操作或多线程开发,对于wait
、notify
、notifyAll
等方法可能使用的并不多。
package java.lang;
import jdk.internal.HotSpotIntrinsicCandidate;
public class Object {
private static native void registerNatives();
static {
registerNatives();
}
@HotSpotIntrinsicCandidate
public Object() {}
@HotSpotIntrinsicCandidate
public final native Class<?> getClass();
@HotSpotIntrinsicCandidate
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
@HotSpotIntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
@HotSpotIntrinsicCandidate
public final native void notify();
@HotSpotIntrinsicCandidate
public final native void notifyAll();
public final void wait() throws InterruptedException {
wait(0L);
}
public final native void wait(long timeoutMillis) throws InterruptedException;
public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
if (timeoutMillis < 0) {
throw new IllegalArgumentException("timeoutMillis value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeoutMillis++;
}
wait(timeoutMillis);
}
@Deprecated(since="9")
protected void finalize() throws Throwable { }
}
wait
这里特别将Object
的wait
方法单独介绍,通过以下源码可以看出,多个重载的wait
方法最终都会调用native
标注的wait(long timeoutMillis)
方法,此方法可能会抛出InterruptedException
中断异常。
public final void wait() throws InterruptedException {
wait(0L);
}
public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
if (timeoutMillis < 0) {
throw new IllegalArgumentException("timeoutMillis value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeoutMillis++;
}
wait(timeoutMillis);
}
public final native void wait(long timeoutMillis) throws InterruptedException;
那么何时会抛出异常呢?
如果任何线程在当前线程等待通知之前或期间中断了当前线程,便抛出此异常时清除当前线程的中断状态。
InterruptedException
本小节着重介绍InterruptedException
,我们从InterruptedException
的源码入手。
package java.lang;
/**
* Thrown when a thread is waiting, sleeping, or otherwise occupied,
* and the thread is interrupted, either before or during the activity.
* Occasionally a method may wish to test whether the current
* thread has been interrupted, and if so, to immediately throw
* this exception. The following code can be used to achieve
* this effect:
* <pre>
* if (Thread.interrupted()) // Clears interrupted status!
* throw new InterruptedException();
* </pre>
*
* @author Frank Yellin
* @see java.lang.Object#wait()
* @see java.lang.Object#wait(long)
* @see java.lang.Object#wait(long, int)
* @see java.lang.Thread#sleep(long)
* @see java.lang.Thread#interrupt()
* @see java.lang.Thread#interrupted()
* @since 1.0
*/
public class InterruptedException extends Exception {
private static final long serialVersionUID = 6700697376100628473L;
/**
* Constructs an <code>InterruptedException</code> with no detail message.
*/
public InterruptedException() {
super();
}
/**
* Constructs an <code>InterruptedException</code> with the
* specified detail message.
*
* @param s the detail message.
*/
public InterruptedException(String s) {
super(s);
}
}
InterruptedException
线程堵塞异常,加上上文中已经介绍过的Object的wait方法,这里再列举几个可能会抛出InterrptdeException异常方法:
java.lang.Object.wait()
及其重载方法- 会进入等待区等待。
java.lang.Thread.sleep()
及其重载方法- 会睡眠执行参数内所设置的时间。
java.lang.Thread.join()
及其重载方法- 会等待到指定的线程结束为止。
以上这些方法执行结束后,该线程会重新启动,因此可能会出现线程堵塞。故这些方法需要处理可能抛出的InterrptdeException异常。
Thread.interrupt()
wait()
、sleep()
、join()
等方法都会使得当前线程进入阻塞状态,若另外的一个线程调用被阻塞线程的Thread.interrupt()
,则会打断这种阻塞,抛出InterruptedException
。InterruptedException
就像一个signal(信号)一样通知当前线程被打断了。但是打断一个线程并不等于该线程的生命周期结束,仅仅是打断了当前线程的阻塞状态。
问题排查
进一步分析
通过上次对AtomicReference
相关逻辑的理解,回到最初的问题,可以确定一定是某种原因导致中断的发生,表现就是当AtomicReference
调用wait()
方法后引起了InterruptedException
。
经过进一步分析log,发现了新的StackTrace,如下:
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.CompletableFuture$AsyncSupply@3267902e rejected from java.util.concurrent.ThreadPoolExecutor@d4ab1ed[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2055)
at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:825)
at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1355)
at java.base/java.util.concurrent.CompletableFuture.asyncSupplyStage(CompletableFuture.java:1714)
at java.base/java.util.concurrent.CompletableFuture.supplyAsync(CompletableFuture.java:1931)
at bt.net.ConnectionSource.getConnectionAsync(ConnectionSource.java:132)
at bt.torrent.messaging.TorrentWorker.onPeerDiscovered(TorrentWorker.java:412)
at bt.torrent.messaging.TorrentWorker.lambda$new$0(TorrentWorker.java:108)
at bt.event.EventBus.lambda$addListener$3(EventBus.java:249)
at bt.event.EventBus.doFireEvent(EventBus.java:183)
at bt.event.EventBus.fireEvent(EventBus.java:171)
at bt.event.EventBus.firePeerDiscovered(EventBus.java:62)
at bt.peer.PeerRegistry.addPeer(PeerRegistry.java:214)
at bt.processor.magnet.FetchMetadataStage.lambda$doExecute$0(FetchMetadataStage.java:81)
at java.base/java.lang.Iterable.forEach(Iterable.java:75)
at bt.processor.magnet.FetchMetadataStage.doExecute(FetchMetadataStage.java:80)
at bt.processor.magnet.FetchMetadataStage.doExecute(FetchMetadataStage.java:42)
at bt.processor.TerminateOnErrorProcessingStage.doExecute(TerminateOnErrorProcessingStage.java:38)
at bt.processor.RoutingProcessingStage.execute(RoutingProcessingStage.java:39)
at bt.processor.ChainProcessor.doExecute(ChainProcessor.java:112)
at bt.processor.ChainProcessor.executeStage(ChainProcessor.java:96)
at bt.processor.ChainProcessor.executeStage(ChainProcessor.java:98)
at bt.processor.ChainProcessor.lambda$process$0(ChainProcessor.java:81)
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run$$$capture(CompletableFuture.java:1736)
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
抛出异常的源码:
private <E extends BaseEvent> void addListener(Class<E> eventType, TorrentId torrentId, Consumer<E> listener) {
Collection<Consumer<? extends BaseEvent>> listeners;
if (torrentId == null) {
listeners = this.listeners.computeIfAbsent(eventType, key -> ConcurrentHashMap.newKeySet());
} else {
listeners = this.listenersOnTorrent.computeIfAbsent(torrentId, key -> new ConcurrentHashMap<>())
.computeIfAbsent(eventType, key -> ConcurrentHashMap.newKeySet());
}
eventLock.writeLock().lock();
try {
Consumer<E> safeListener = event -> {
try {
listener.accept(event);
} catch (Exception ex) {
LOGGER.error("Listener invocation failed", ex);
}
};
listeners.add(safeListener);
} finally {
eventLock.writeLock().unlock();
}
}
Debug断点截图:
猜想
以上堆栈可以看到[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
这句描述,说明当前的使用的ExecutorService
并未创建线程或者创建的线程数量为0。这里我们可以大胆猜测,由于可用线程数量都是0,AtomicReference
调用wait()
方法后引起了InterruptedException
。
这里有进一步产生一个问题,我们并没有在实际使用中特意指定ExecutorService
,而是用的组件默认的ExecutorService
。那为什么为有Thread数量为0的情况呢?
通过分析源码发现,通过系统使用Google的guice这个IOC框架实现DI,其中@Inject
注解注入了TorrentProcessorFactory
,使用@ClientExecutor
指定了具体的ExecutorService
。
private ExecutorService executor;
@Inject
public TorrentProcessorFactory( // 空歌白石:略去无关的参数
@ClientExecutor ExecutorService executor,
// 空歌白石:略去无关的参数
) {
// 空歌白石:略去无关代码
this.executor = executor;
// 空歌白石:略去无关代码
}
使用guice
的Binder
绑定ClientExecutor
,代码如下。
binder.bind(ExecutorService.class).annotatedWith(ClientExecutor.class)
.toProvider(ExecutorServiceProvider.class).in(Singleton.class);
从上述代码可以看出在ExecutorServiceProvider
完成ExecutorService
的创建。
package bt.service;
import bt.runtime.Config;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @since 1.0
*/
public class ExecutorServiceProvider implements Provider<ExecutorService> {
private volatile ExecutorService executorService;
private final Object lock;
private final Config config;
@Inject
public ExecutorServiceProvider(Config config) {
this.lock = new Object();
this.config = config;
}
@Override
public ExecutorService get() {
if (executorService == null) {
synchronized (lock) {
if (executorService == null) {
executorService = Executors.newCachedThreadPool(new ThreadFactory() {
private AtomicInteger threadId = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, Objects.requireNonNull(getNamePrefix()) + "-" + threadId.getAndIncrement());
}
});
}
}
}
return executorService;
}
/**
* @since 1.6
*/
protected String getNamePrefix() {
return String.format("%d.bt.service.executor-thread", config.getAcceptorPort());
}
}
由于newCachedThreadPool
实现的ThreadPool的keepAliveTime
为60s
,猜测可能是这个时间过短引起。于是重新实现了newCachedThreadPool
但是问题仍然没有解决。
/**
* Creates a thread pool that creates new threads as needed, but
* will reuse previously constructed threads when they are
* available, and uses the provided
* ThreadFactory to create new threads when needed.
* @param threadFactory the factory to use when creating new threads
* @return the newly created thread pool
* @throws NullPointerException if threadFactory is null
*/
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
继续分析
上述思路受阻后,重新回到最原始的问题点,即java.lang.InterruptedException
的异常,再次查看wait()
方法的JDK注释,如下:
Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object. In other words, this method behaves exactly as if it simply performs the call wait(0).
The current thread must own this object's monitor. The thread releases ownership of this monitor and waits until another thread notifies threads waiting on this object's monitor to wake up either through a call to the notify method or the notifyAll method. The thread then waits until it can re-obtain ownership of the monitor and resumes execution.
As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}
This method should only be called by a thread that is the owner of this object's monitor. See the notify method for a description of the ways in which a thread can become the owner of a monitor.
Throws:
IllegalMonitorStateException – if the current thread is not the owner of the object's monitor.
InterruptedException – if any thread interrupted the current thread before or while the current thread was waiting for a notification. The interrupted status of the current thread is cleared when this exception is thrown.
See Also:
notify(), notifyAll()
如果torrent.wait();
不抛出InterruptedException
,那么根据wait()
方法的定义可以看出,可以使用notify()
或notifyAll()
停止wait。因此,决定重新分析MetadataConsumer
类,将相对完整的源码贴出来,同时去掉非相关的部分代码。
public class MetadataConsumer {
// 空歌白石:略去无关代码
private final TorrentId torrentId;
// set immediately after metadata has been fetched and verified
private final AtomicReference<Torrent> torrent;
// 空歌白石:略去无关代码
public MetadataConsumer(IMetadataService metadataService,
TorrentId torrentId,
Config config,
EventSource eventSource) {
// 空歌白石:略去无关代码
this.torrent = new AtomicReference<>();
// 空歌白石:略去无关代码
}
@Consumes
public void consume(UtMetadata message, MessageContext context) {
Peer peer = context.getPeer();
// being lenient herer and not checking if the peer advertised ut_metadata support
StateContext stateContext = states.get(peer);
switch (message.getType()) {
case DATA: {
int totalSize = message.getTotalSize().get();
if (totalSize >= metadataExchangeMaxSize) {
throw new IllegalStateException("Declared metadata size is too large: " + totalSize +
"; max allowed is " + metadataExchangeMaxSize);
}
if (stateContext != null) {
stateContext.requestedIndexes.remove(message.getPieceIndex());
if (stateContext.state != State.HAS_METADATA) {
stateContext.state = State.HAS_METADATA;
}
}
processMetadataBlock(message.getPieceIndex(), totalSize, message.getData().get());
}
break;
case REJECT: {
stateContext.rejectedTime = System.currentTimeMillis();
stateContext.state = State.REJECTED;
}
break;
default: {
// ignore
}
}
}
private void processMetadataBlock(int pieceIndex, int totalSize, byte[] data) {
if (metadata == null) {
synchronized (this) {
if (metadata == null) {
metadata = new ExchangedMetadata(totalSize, metadataExchangeBlockSize);
blocksNeedRequest = createBlockIndexQueue(metadata.getBlockCount());
}
}
}
if (!metadata.isBlockPresent(pieceIndex)) {
metadata.setBlock(pieceIndex, data);
if (metadata.isComplete()) {
byte[] digest = metadata.getSha1Digest();
if (Arrays.equals(digest, torrentId.getBytes())) {
Torrent fetchedTorrent = null;
try {
fetchedTorrent = metadataService.fromByteArray(metadata.getBytes());
} catch (Exception e) {
LOGGER.error("Processing of metadata failed: " + torrentId, e);
metadata = null;
}
if (fetchedTorrent != null) {
synchronized (torrent) {
torrent.set(fetchedTorrent);
states.clear();
torrent.notifyAll();
}
}
} else {
// 空歌白石:略去无关代码
}
}
}
}
@Produces
public void produce(Consumer<Message> messageConsumer, MessageContext context) {
// stop here if metadata has already been fetched
if (torrent.get() != null) {
return;
}
// 空歌白石:略去无关代码
}
/**
* @return Torrent, blocking the calling thread if it hasn't been fetched yet
*/
public Torrent waitForTorrent() {
while (torrent.get() == null) {
synchronized (torrent) {
if (torrent.get() == null) {
try {
torrent.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
return torrent.get();
}
// 空歌白石:略去无关代码
}
MetadataConsumer
클래스 를 재분석 하여 AtomicReference<Torrent>
전체 코드에서 notifyAll
다른 스레드 상태에 대한 알림을 사용하는 위치가 한 곳뿐임을 발견했습니다. 가로채기 코드는 다음과 같습니다.
Torrent fetchedTorrent = null;
try {
fetchedTorrent = metadataService.fromByteArray(metadata.getBytes());
} catch (Exception e) {
LOGGER.error("Processing of metadata failed: " + torrentId, e);
metadata = null;
}
if (fetchedTorrent != null) {
synchronized (torrent) {
torrent.set(fetchedTorrent);
states.clear();
torrent.notifyAll();
}
}
이 부분의 로그에 주의를 기울인 결과 다음과 같은 NPE 문제가 발견되었습니다.
java.lang.NullPointerException
at bt.torrent.MetadataService.fromByteArray(MetadataService.java:31)
at bt.torrent.messaging.MetadataConsumer.processMetadataBlock(MetadataConsumer.java:157)
at bt.torrent.messaging.MetadataConsumer.consume(MetadataConsumer.java:125)
at bt.torrent.messaging.DefaultMessageRouter$CollectingCompilerVisitor$1.consume(DefaultMessageRouter.java:167)
at bt.torrent.messaging.DefaultMessageRouter.lambda$doConsume$2(DefaultMessageRouter.java:129)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
at bt.torrent.messaging.DefaultMessageRouter.doConsume(DefaultMessageRouter.java:126)
at bt.torrent.messaging.DefaultMessageRouter.consume(DefaultMessageRouter.java:118)
at bt.torrent.messaging.RoutingPeerWorker.accept(RoutingPeerWorker.java:67)
at bt.torrent.messaging.RoutingPeerWorker.accept(RoutingPeerWorker.java:41)
at bt.torrent.messaging.TorrentWorker$PieceAnnouncingPeerWorker.accept(TorrentWorker.java:380)
at bt.torrent.messaging.TorrentWorker.lambda$consume$8(TorrentWorker.java:183)
at java.base/java.util.Optional.ifPresent(Optional.java:183)
at bt.torrent.messaging.TorrentWorker.consume(TorrentWorker.java:183)
at bt.torrent.messaging.TorrentWorker.lambda$addPeer$6(TorrentWorker.java:177)
at bt.MultiThreadMessageDispatcher$MessageDispatchingLoop.processConsumer(MultiThreadMessageDispatcher.java:148)
at bt.MultiThreadMessageDispatcher$MessageDispatchingLoop.run(MultiThreadMessageDispatcher.java:108)
at bt.MultiThreadMessageDispatcher.lambda$createAndSubmitTask$3(MultiThreadMessageDispatcher.java:292)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:264)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
추가 분석은 fromByteArray
클래스에서 경계 값을 처리하는 데 버그가 있다는 것입니다. 수정 후 더 이상 나타나지 InterruptedException
않고 문제가 해결되었습니다.
위에서 문제를 언급했는데 언제 예외가 발생합니까? , 당시의 대답은 다음과 같습니다. 현재 스레드가 알림을 기다리는 동안 또는 이전에 스레드가 현재 스레드를 인터럽트하는 경우 이 예외가 throw될 때 현재 스레드의 인터럽트된 상태를 지웁니다. 대기 메서드를 호출할 때 NullPointerException
AtomicReference에서 예외가 발생하는 것은 실행 중인 스레드가 중단 되었기 때문 입니다.InterruptedException
요약하다
이 논문 은 처음부터 InterruptedException
모든 관련 측면을 자세히 분석 AtomicReference
하고 마지막으로 실제 작업에서 직면하는 문제를 해결합니다.