我对Java还很陌生,尤其是并发编程,所以如果这是一个新手问题,请原谅我。
我有一个线程(服务器(,它管理子线程的集合(每个线程代表客户端和服务器之间的会话(。服务器维护一个会话集合,当会话结束时,它会向父服务器发出结束的信号,以便服务器可以将其从会话集合中删除。
有人告诉我,如果你打算将ArrayLists与线程一起使用,就需要对其进行保护,而int也可能会出现问题,除非同步,所以使用两者的方法都是同步的。
接下来是服务器和会话对象的相关部分。
public class Server {
private int listenPort = 0;
private ServerSocket serverSocket = null;
private List<Session> sessions = new ArrayList ();
private int lastId = 0;
/**
* Start listening for clients to process
*
* @throws IOException
* @todo Maintain a collection of Clients so we can send global messages
* @todo Provide an escape condition for the loop
*/
synchronized public void run () throws IOException {
Session newSession;
// Client listen loop
while (true) {
//int sessionId = this.Sessions.
newSession = this.initSession (++this.lastId);
this.sessions.add (newSession);
//this.Sessions.add (newSession);
new Thread (newSession).start ();
}
}
/**
*
* @return
* @throws IOException
*/
public Socket accept () throws IOException {
return this.getSocket().accept ();
}
/**
*
* @param closedSession
*/
synchronized public void cleanupSession (Session closedSession) {
this.sessions.remove (closedSession);
}
}
这是会话类:
public class Session implements Runnable {
private Socket clientSocket = null;
private Server server = null;
private int sessionId = 0;
/**
* Run the session input/output loop
*/
@Override
public void run () {
CharSequence inputBuffer, outputBuffer;
BufferedReader inReader;
try {
this.sendMessageToClient ("Hello, you are client " + this.sessionId);
inReader = new BufferedReader (new InputStreamReader (this.clientSocket.getInputStream (), "UTF8"));
do {
// Parse whatever was in the input buffer
inputBuffer = this.requestParser.parseRequest (inReader);
System.out.println ("Input message was: " + inputBuffer);
// Generate a response for the input
outputBuffer = this.responder.respond (inputBuffer);
System.out.println ("Output message will be: " + outputBuffer);
// Output to client
this.sendMessageToClient (outputBuffer.toString ());
} while (!"QUIT".equals (inputBuffer.toString ()));
} catch (IOException e) {
Logger.getLogger (Session.class.getName ()).log (Level.SEVERE, null, e);
} finally {
this.cleanupClient ();
}
}
/**
* Terminate the client connection
*/
public void cleanupClient () {
try {
this.streamWriter = null;
this.clientSocket.close ();
this.server.cleanupSession (this);
} catch (IOException e) {
Logger.getLogger (Session.class.getName ()).log (Level.SEVERE, null, e);
}
}
/**
*
* @param clientSocket
*/
public Session (Server owner, int sessionId) throws IOException {
System.out.println ("Class " + this.getClass () + " created");
this.server = owner;
this.sessionId = sessionId;
this.clientSocket = this.server.accept ();
System.out.println ("Session ID is " + this.sessionId);
}
}
我遇到的问题是会话的CleanupClient方法。当服务器中的CleanupSession方法标记为"已同步"时,会话线程似乎不会终止。相反,根据Netbeans的说法,他们进入了一个名为"On Monitor"的状态。
我试图弄清楚这意味着什么以及该怎么办都无济于事。我确实发现,监视器就像一个只能由单个线程占用的空间,其他线程必须等待轮到他们使用它,这就是Java中实现并发的方式。然而,我找不到解释,为什么在父类中调用同步方法的子线程会触发线程明显永久进入这种状态,或者该怎么办
我确实发现,如果Server类中的cleanupSession方法没有标记为synchronized,那么线程确实会像我预期的那样终止。然而,如果我需要同步以维护线程安全,那么我不能让该方法不同步,并相信运气。
我显然错过了一些基本的东西,但我不确定是什么。如果有人能指出我在这里做错了什么,我将不胜感激
(补充:我希望我应该使用其他类别的Collection,而不是ArrayList,知道它是什么肯定会很好地解决这个特定的情况,但我也希望得到反馈,说明如何在唯一可用的选项是同步的一般情况下避免这个问题(
正如Antimony已经指出的,您会得到死锁,因为Server的两个方法在同一对象(即Server
实例(上同步,而run()
方法从不释放锁。
另一方面,您仍然需要某种线程间同步来正确更新sessions
列表(如果没有同步,您将遇到两个问题:缺乏更改可见性和数据竞争(。
因此,一种解决方案是只同步代码的尽可能小的部分:对sessions
的访问(您不需要在任何地方使用this.
,只需要在本地名称隐藏实例变量名称的地方(:
...
public void run () throws IOException {
Session newSession;
// Client listen loop
while (true) {
...
newSession = initSession (++lastId);
synchronized (this) {
sessions.add (newSession);
}
...
}
}
public void cleanupSession (Session closedSession) {
synchronized (this) {
sessions.remove (closedSession);
}
}
你知道List
不是最适合这里的,你需要HashMap
,因为你所做的只是添加新客户端和搜索客户端,而客户端在集合中的存储顺序并不重要(即使很重要,最好使用一些有序的Map
,如TreeMap
,以提高性能(。因此,您可以将Server
代码更改为:
private Map<Integer, Session> sessions = new HashMap<IntegerPatternConverter, Session>();
...
// Client listen loop
while (true) {
int key = ++lastId;
newSession = initSession (key);
synchronized (this) {
sessions.put (key, newSession);
}
new Thread (newSession).start ();
}
...
public void cleanupSession (int closedSessionKey) {
synchronized (this) {
sessions.remove (closedSessionKey);
}
}
在这个更改之后,您可以通过使用带有内置同步的Map
来完全摆脱synchronized
:ConcurrentHashMap
。
不过,最好在掌握Java并发编程的基础知识后再进行。为此,实践中的Java并发是一个很好的起点。我读过的最好的Java入门书(其中有一部分是关于并发的(是Gosling和Holmes的《Java编程语言》。
问题是出现了死锁。
您的Server.run
方法将永久保留在Server
监视器上。由于cleanupSession
也试图进入这个监视器,所以只要服务器正在运行,每次从不同线程调用它的尝试都会死锁。
不管怎样,同步并不能达到你想要的效果。我建议研究java.util.Concurrency
。