问题症状
程序中通过遍历map生成简要的summary信息,但是该接口不定时发生异常,绝大部分时候正常,偶尔出现类似如下异常
2022-10-30 21:10:36,171 WARN [pool-3-thread-1] com.yq.demo.MultiThread: [55] ex when getting summary
java.lang.IllegalStateException: End size 0 is less than fixed size 20
at java.base/java.util.stream.Nodes$FixedNodeBuilder.end(Nodes.java:1232)
at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:575)
at java.base/java.util.stream.AbstractPipeline.evaluateToArrayNode(AbstractPipeline.java:260)
at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:616)
at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:622)
at java.base/java.util.stream.ReferencePipeline.toList(ReferencePipeline.java:627)
at com.yq.demo.MultiThread.getSummary(MultiThread.java:93)
at com.yq.demo.MultiThread.lambda$main$1(MultiThread.java:52)
at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:833)
分析
在网上搜以及分析对应源码后发现, map在stream遍历的时候, 其中entry正好发生了update导致toList出现异常。
参看https://stackoverflow.com/questions/57274369/java-8-streams-java-lang-illegalstateexception-end-size-84758-is-less-than-fix
经过简化的可以复现问题的代码(出问题的和解决问题的都包含在其中)
package com.yq.demo;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.yq.demo.NewResult;
import lombok.extern.slf4j.Slf4j;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.*;
@Slf4j
public class MultiThread
{
private Map<String, String> tableMap = new ConcurrentHashMap<String, String>();
int count = 1000;
LoadingCache<String, String> tables;
public static void main(String[] args) {
int maxLoop = 90000;
MultiThread demo = new MultiThread();
demo.initMap();
ExecutorService executorService1 = Executors.newFixedThreadPool(3);
ExecutorService executorService2 = Executors.newFixedThreadPool(3);
CompletableFuture<NewResult> future1 = CompletableFuture.supplyAsync(() -> {
log.info("supplyAsync2");
NewResult result = new NewResult(01, "name01");
for(int i=0; i < maxLoop; i++) {
try {
Thread.sleep(50);
demo.delRandom();
} catch(Exception ex) {
log.warn("ex deleting key", ex);
}
}
return result;
}, executorService1);
CompletableFuture<NewResult> future2 = CompletableFuture.supplyAsync(() -> {
log.info("supplyAsync2");
NewResult result = new NewResult(02, "name02");
for(int i=0; i < maxLoop; i++) {
try {
Thread.sleep(50);
List<String> stringList = demo.getSummary();
log.info("stringList:{}", stringList);
} catch(Exception ex) {
log.warn("ex when getting summary", ex);
}
}
return result;
}, executorService2);
CompletableFuture<NewResult> future3 = CompletableFuture.supplyAsync(() -> {
log.info("supplyAsync2");
NewResult result = new NewResult(03, "name03");
for(int i=0; i < maxLoop; i++) {
try {
Thread.sleep(50);
List<String> stringList = demo.getSummaryCopy();
log.info("size:{}, getSummaryCopy:{}", stringList.size(), stringList);
} catch(Exception ex) {
log.warn("ex when getting getSummaryCopy", ex);
}
}
return result;
}, executorService2);
try {
future2.get();
future1.get();
} catch(Exception ex) {
log.warn("ex when getting future", ex);
}
log.info("done");
}
public List<String> getSummary() {
return tableMap.values().stream().map(String::toLowerCase).toList();
}
public List<String> getSummaryCopy() {
Map<String, String> copyMap = new HashMap<>(tableMap);
// copy.values().StreamArray().toArray(n -> new VisitDataBE[n]);
return copyMap.values().stream().map(String::toLowerCase).toList();
}
public List<String> getSummaryCopyUnmodifiable() {
//https://stackoverflow.com/questions/57274369/java-8-streams-java-lang-illegalstateexception-end-size-84758-is-less-than-fix
//Map<...> copy = new HashMap<>(visits.getVisitDataMap());
//copy.values().stream().toArray(n -> new VisitDataBE[n]);
Map<String, String> copyMap = Collections.unmodifiableMap(tableMap);
// copy.values().StreamArray().toArray(n -> new VisitDataBE[n]);
return copyMap.values().stream().map(String::toLowerCase).toList();
}
public List<String> getSummaryArray() {
return Arrays.stream(tableMap.values().stream().map(String::toLowerCase).toArray(String[]::new)).toList();
}
public void initMap()
{
try {
// 这里为了快速复现问题,所以设置CacheBuilder中3秒就过期
tables = CacheBuilder.newBuilder()
.expireAfterAccess(3, TimeUnit.SECONDS)
.maximumSize(20)
.build(cacheLoader());
tableMap = tables.asMap();
for(int i=0; i< count; i++) {
//tableMap.put(i+"", "A" + i);
tables.get(i+"");
}
} catch (Exception ex) {
log.error("ex when getting tables", ex);
}
log.info("map size:{}", tableMap.size());
}
private CacheLoader<String, String> cacheLoader()
{
return new CacheLoader<>()
{
@Override
public String load(String key)
{
return "A" + key;
}
};
}
private void delRandom() {
SecureRandom secureRandom = new SecureRandom();
int randomInt = secureRandom.nextInt(count);
String key = randomInt + "";
if (tableMap.containsKey(key)) {
tableMap.remove(key);
log.info("rm key {}", key);
}
}
}
NewResult 类是之前CompletableFuture中用到的,就继续使用,复现问题可以不需要该类
package com.yq.demo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Value;
import lombok.experimental.Accessors;
@Value
@AllArgsConstructor
@Accessors(fluent = true)
public class NewResult {
int id;
String name;
}
解决办法
public List<String> getSummaryCopy() {
// 在遍历该map前,进行copy式创建一个新的map,这样当我们遍历的时候,原始map中entry发生变化也不会影响我们,代价就是多了一个对象,不过好在这个内容不多,并且一次使用就不使用了,jvm会自动回收。
Map<String, String> copyMap = new HashMap<>(tableMap);
return copyMap.values().stream().map(String::toLowerCase).toList();
}