一不小心掉入了 Java Interface 的陷阱
阿里妹导读
public interface PostTask {
void process();
}
public interface BaseResult extends Serializable {
List<PostTask> postTaskList = Lists.newArrayList();
default void addPostTask(PostTask postTask) {
postTaskList.add(postTask);
}
default List<PostTask> getPostTaskList() {
return postTaskList;
}
}
public class SimpleResult implements BaseResult {
}
// 请求处理的一部分逻辑
SimpleResult result = new SimpleResult();
...
// 处理过程中,会往后置任务列表加入任务
result.addPostTask(() -> { ...发消息... });
...
// 在返回结果之前,会对所有的后置任务进行遍历执行后置任务
PostTaskUtil.process(result.PostTaskList());
...
public class PostTaskUtil {
public static void process(List<PostTask> postTasks) {
if(CollectionUtils.isEmpty(postTasks)){
return;
}
Iterator<PostTask> iterator = postTasks.iterator();
while (iterator.hasNext()){
PostTask postTask = iterator.next();
if (postTask == null) {
return;
}
postTask.process();
iterator.remove();
}
}
}
接口的属性是 public static final 修饰的
In addition, an interface can contain constant declarations. All constant values defined in an interface are implicitly public, static, and final. Once again, you can omit these modifiers.
另外,接口可以包含常量声明。接口中定义的所有常量值默认为 public 、 static 、 final 。再次说明,你可以省略这些修饰符。
上面出问题的代码在于接口 BaseResult 的属性 postTaskList,因为是接口的属性,那这个属性默认是静态的,也就是说所有实例化的 SimpleResult 所操作的后置任务列表,底层都是同一个队列,这个就是最大的问题了。
二、问题分析
后置任务列表的元素不确定性,可能包含历史或者其他请求任务;
存在并发修改异常 java.util.ConcurrentModificationException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
public class InterfaceBugTest {
public static void main(String[] args) throws InterruptedException {
List<CompletableFuture<Boolean>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int finalI = i;
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> {
Test test = new Test();
test.add(finalI);
System.out.println(STR."\{finalI}: \{test.list.toString()}");
return true;
}, Executors.newVirtualThreadPerTaskExecutor());
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
System.out.println(ITest.list);
}
static class Test implements ITest { }
interface ITest {
List<Integer> list = new ArrayList<>();
default void add(Integer num) {
list.add(num);
}
}
}
6: [0, 9, 6]
1: [0, 9, 6, 7, 8, 4, 5, 3, 2, 1]
9: [0, 9, 6]
7: [0, 9, 6, 7]
2: [0, 9, 6, 7, 8, 4, 5, 3, 2, 1]
3: [0, 9, 6, 7, 8, 4, 5, 3]
Exception in thread "main" java.util.concurrent.CompletionException: java.util.ConcurrentModificationException
at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:315)
at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:320)
at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1770)
at java.base/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:314)
at java.base/java.lang.VirtualThread.run(VirtualThread.java:329)
Caused by: java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050)
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:458)
at com.zh.next.test.InterfaceBugTest.lambda$main$0(InterfaceBugTest.java:16)
at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768)
这个很符合 ArrayList 线程不安全的特性,如果将 ArrayList 替换成 CopyOnWriteArrayList 就可以解决并发修改问题。
The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.
Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.
这段注释是在解释Java中`ArrayList`类提供的迭代器(`iterator()`和`listIterator(int)`方法返回的)的行为特性,特别是所谓的“快速失败”(fail-fast)机制。
### 快速失败 (Fail-Fast)
1. **定义**:当一个线程正在遍历列表时,如果另一个线程修改了这个列表的结构(添加、删除或替换元素),除了通过迭代器自身的`remove()`或`add()`方法进行的操作外,迭代器会抛出`ConcurrentModificationException`异常。这种行为被称为“快速失败”。
2. **目的**:避免在并发修改的情况下出现未定义的行为或者数据不一致的问题。它确保程序在检测到并发修改时立即失败,而不是在未来某个不确定的时间点以非确定性的方式失败。
3. **实现原理**:通常,迭代器会维护一个与容器相关的内部计数器(称为`modCount`)。每当容器被修改时,这个计数器就会增加。每次迭代器访问下一个元素之前都会检查这个计数器是否自上次调用以来发生了变化。如果有变化,则抛出`ConcurrentModificationException`。
4. **限制**:尽管尽力保证快速失败,但在存在无同步的并发修改情况下,不能做出绝对的保证。这是因为其他线程可能在两次检查之间修改了集合,导致迭代器无法检测到这些更改。
5. **使用建议**:不应该依赖于`ConcurrentModificationException`来保证程序的正确性。它的主要用途是帮助开发者在测试阶段发现潜在的并发问题。正确的做法是在多线程环境中对共享资源使用适当的同步手段,如`synchronized`关键字或显式锁等。
总之,“快速失败”机制是一种设计模式,用于提高多线程环境下程序的健壮性和可调试性,但不应被视为一种可靠的并发控制策略。
// List<Integer> list = new ArrayList<>();
List<Integer> list = new CopyOnWriteArrayList<>();
想要 BaseResult 的后置任务列表只属于实例化的 SimpleResult,可以将 BaseResult 从接口改成类,其他地方做相应的修改即可。
三、进一步分析
public interface BaseResult extends Serializable, IResult {}
public interface IResult {
String getResult();
}
四、总结
接口中定义的所有常量值默认为 public、static、final; ArrayList 线程不安全,想要线程安全请使用 CopyOnWriteArrayList; 接口可以多重继承多接口;
参考文档:
Defining an Interface (The Java™ Tutorials > Learning the Java Language > Interfaces and Inheritance) (oracle.com):https://docs.oracle.com/javase/tutorial/java/IandI/interfaceDef.html
Java Interfaces | Baeldung:https://www.baeldung.com/java-interfaces#overview
jclasslib Bytecode Viewer:https://plugins.jetbrains.com/plugin/9248-jclasslib-bytecode-viewer
多媒体数据存储与分发
视频、图文类多媒体数据量快速增长,内容不断丰富,多媒体数据存储与分发解决方案融合对象存储 OSS、内容分发 CDN 、智能媒体管理 IMM 等产品能力,解决客户多媒体数据存储、处理、加速、分发等业务问题,进而实现低成本、高稳定性的业务目标。本技术解决方案以搭建一个多媒体数据存储与分发服务为例,搭建一个多媒体数据存储与分发服务。快点击阅读原文参加部署吧~
微信扫码关注该文公众号作者