我们写了一个以这种方式工作的传入反应堆:
反应堆的关闭程序正在迭代selector.keys()
,并且每个关闭相应的通道并取消关键。
我们为关机程序编写了以下单元测试:
测试导致ConcurrentModificationException指向循环迭代套接字并关闭它们(在主线程上下文中)。
我们的假设是当一个Sender读取方法得到-1时,它关闭了套接字并以某种方式唤醒了选择器选择方法,然后选择器访问它的密钥集,该密钥集由关闭循环迭代,因此是异常。 / p>
我们通过使用选择器的所有键创建新列表来解决此问题。通过迭代此列表来取消这些键可防止两个对象修改相同的键集。
我们的问题是:
编辑:添加了一些代码片段以供澄清 (我们尽可能缩小代码范围)
IncomingReactor:
public boolean startAcceptingIncomingData() {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open());
serverSocketChannel.bind(new InetSocketAddress(incomingConnectionsPort));
serverSocketChannel.configureBlocking(false);
SelectionKey acceptorSelectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
acceptorSelectionKey.attach((Worker) this::acceptIncomingSocket);
startSelectionLoop(selector);
return true;
}
private boolean acceptIncomingSocket() {
try {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_READ);
selectionKey.attach(new WorkerImpl() /*Responsible for reading data and tranferring it into a parsing thread*/);
return true;
} catch (IOException e) {
return false;
}
}
private void startSelectionLoop(Selector selector) {
shouldLoop = true;
while (shouldLoop) {
try {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
if (!shouldLoop) {
break;
}
selectedKeys.forEach((key) -> {
boolean workSuccess = ((Worker) key.attachment()).work();
if (!workSuccess) {
key.channel().close();
key.cancel();
}
});
selectedKeys.clear();
} catch (ClosedSelectorException ignore) {
}
}
}
public void shutDown() {
shouldLoop = false;
selector.keys().forEach(key -> { /***EXCEPTION - This is where the exception points to (this is line 129) ***/
key.channel().close();
key.cancel();
});
try {
selector.close();
} catch (IOException e) {
}
}
单元测试:
@Test
public void testMaximumConnectionsWithMultipleThreads() {
final int PORT = 24785;
final int MAXINUM_CONNECTIONS = 10;
IncomingReactor incomingReactor = new IncomingReactor(PORT);
Callable<Boolean> acceptorThread = () -> {
incomingReactor.startAcceptingIncomingData();
return true;
};
ExecutorService threadPool = Executors.newFixedThreadPool(MAXIMUM_CONNECTIONS + 1);
Future<Boolean> acceptorFuture = threadPool.submit(acceptorThread);
List<Future<Boolean>> futureList = new ArrayList<>(MAXIMUM_CONNECTIONS);
for (int currentSenderThread = 0; currentSenderThread < MAXIMUM_CONNECTIONS; currentSenderThread++) {
Future<Boolean> senderFuture = threadPool.submit(() -> {
Socket socket = new Socket(LOCALHOST, PORT);
int bytesRead = socket.getInputStream().read();
if (bytesRead == -1) { //The server has closed us
socket.close();
return true;
} else {
throw new RuntimeException("Got real bytes from socket.");
}
});
futureList.add((senderFuture));
}
Thread.sleep(1000); //We should wait to ensure that the evil socket is indeed the last one that connects and the one that will be closed
Socket shouldCloseSocket = new Socket(LOCALHOST, PORT);
Assert.assertEquals(shouldCloseSocket.getInputStream().read(), -1);
shouldCloseSocket.close();
incomingReactor.shutDown();
for (Future<Boolean> senderFuture : futureList) {
senderFuture.get();
}
acceptorFuture.get();
threadPool.shutdown();
}
例外:
java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
at java.util.HashMap$KeyIterator.next(HashMap.java:1461)
at java.lang.Iterable.forEach(Iterable.java:74)
at java.util.Collections$UnmodifiableCollection.forEach(Collections.java:1080)
at mypackage.IncomingReactor.shutDown(IncomingReactor.java:129)
at mypackage.tests.TestIncomingReactor.testMaximumConnectionsWithMultipleThreads(TestIncomingReactor.java:177)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:85)
at org.testng.internal.Invoker.invokeMethod(Invoker.java:659)
at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:845)
at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1153)
at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:125)
at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:108)
at org.testng.TestRunner.privateRun(TestRunner.java:771)
at org.testng.TestRunner.run(TestRunner.java:621)
at org.testng.SuiteRunner.runTest(SuiteRunner.java:357)
at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:352)
at org.testng.SuiteRunner.privateRun(SuiteRunner.java:310)
at org.testng.SuiteRunner.run(SuiteRunner.java:259)
at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52)
at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:86)
at org.testng.TestNG.runSuitesSequentially(TestNG.java:1199)
at org.testng.TestNG.runSuitesLocally(TestNG.java:1124)
at org.testng.TestNG.run(TestNG.java:1032)
at org.testng.IDEARemoteTestNG.run(IDEARemoteTestNG.java:74)
at org.testng.RemoteTestNGStarter.main(RemoteTestNGStarter.java:124)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
答案 0 :(得分:2)
反应堆的关闭程序正在遍历selector.keys(),并且每个关闭相应的通道并取消该键。
它应该从停止选择器循环开始。注意关闭频道会取消该键。你不必自己取消它。
我们为关机程序编写了以下单元测试:
打开运行选择循环的反应器线程。 打开几个Sender线程。每个都打开一个到反应器的插座并读取。 读取阻塞直到它变为-1(意味着反应器关闭了套接字)。
反应堆关闭其接受的套接字。您的客户端套接字保持打开状态。
读取返回-1后,发件人关闭套接字并完成。
我希望这意味着发件人关闭了它的客户端套接字。
测试导致
ConcurrentModificationException
指向循环迭代套接字并关闭它们(在主线程上下文中)。
真的?我的问题中没有看到任何堆栈跟踪。
我们的假设是当一个Sender读取方法得到-1时,它关闭了套接字并以某种方式唤醒了选择器选择方法
除非反应堆没有关闭通道,否则不可能,在这种情况下你不会从读取等中得到-1。
然后,选择器访问其密钥集,该密钥集由关闭循环迭代,因此异常。
异常是在迭代期间修改密钥集引起的。服务器代码中的错误。
我们通过使用选择器的所有键创建新列表来解决此问题。通过迭代此列表来取消这些键可防止两个对象修改相同的键集。
您需要修复实际问题,为此您需要发布实际代码。
我们的问题是:
我们的假设是否正确?当客户端套接字调用close方法时 - 它是否真的唤醒了选择器?
除非选择器端通道仍处于打开状态。
创建新列表是否是合适的解决方案,还是只是一种解决方法?
对于尚未确定的问题,这只是一个讨厌的解决方法。
答案 1 :(得分:1)
您无法从for循环内部修改selector.keys()
Set<SelectionKey>
,因为Set
无法进行并发修改。 (调用channel.close()
会修改Set
内的Set
{/ 1}}
https://docs.oracle.com/javase/7/docs/api/java/util/HashSet.html
此类的迭代器方法返回的迭代器是快速失败的: 如果在创建迭代器后的任何时间修改了该集,则 除了通过迭代器自己的删除方法,Iterator之外的任何方式 抛出ConcurrentModificationException。因此,面对 并发修改,迭代器快速干净地失败, 而不是冒着任意的,非确定性的行为冒险 未来不确定的时间。
SelectionKey[] keys = selector.keys().toArray(new SelectionKey[0]);
for( SelectionKey k : keys )
{
try
{
k.channel().close();
}
catch(Throwable x )
{
// print
}
}
try
{
selector.close();
}
catch(IoException e )
{
// print
}