共计 2599 个字符,预计需要花费 7 分钟才能阅读完成。
最近项目需要 web 客户端与服务器保持长链接的场景并需要服务器向所有链接的客户端推送消息,于是自然使用了 WebSocket 技术,自然要考虑到服务器于多个客户端线程安全的问题。于是乎,想当然的在 WebSocket 服务器端通过一个线程安全的队列来保持所有客户端的 Session.
private volatile static List<Session> sessions = Collections.synchronizedList(new ArrayList());
private Session session;
/*
* 客户端链接成功后讲其保存在线程安全的集合中
*/
@OnOpen
public void onOpen(Session session) throws IOException {this.session = session;
sessions.add(this);
}
/*
* 客户端断开链接后将其从线程安全的集合中移除
*/
@OnClose
public void onClose() {sessions.remove(this);
}
// 给所有客户端发送消息
public static void sendMessage(String clientInfoJson) {try {if (sessions.size() != 0) {for (session s : sessions) {if (s != null) {s.getBasicRemote().sendText(clientInfoJson);
}
}
}
} catch (IOException e) {// TODO Auto-generated catch block
e.printStackTrace();}
}
上述代码感觉上好像没问题。Session 信息是保存在线程安全的集合,又通过 volatile 变量来修饰保证了内存可见性,但实际运行时却发现并没有想象的那么好。当客户端断开链接,时服务器需要发送消息给客户端时. 服务端抛出异常:
IllegalStateException: The WebSocket session [0] has been closed and no method (apart from close()) may be called on a closed session
不难看出, 是服务端在关闭 Session 即将 Session 从线程安全的队列移除时,在发送消息的方法里应该被移除的 Session 消息却进入了发送消息的环节,在执行 getBasicRemote().sendText(clientInfoJson); 操作时发生了异常。
解决方法:
Google 了大量资料后发现如果要解决这种线程安全的问题, 不能通过线程安全的集合来保存 Session 解决。而应该保存整个类,并通过 CopyOnWriteArraySet 容器来操作。
@ServerEndpoint("/getLocation")
@Component
public class TransmissionLocationWebSocket {
@Autowired
public TerminalService terminalServiceInWebSocket;
private static CopyOnWriteArraySet<TransmissionLocationWebSocket> sessions = new CopyOnWriteArraySet<TransmissionLocationWebSocket>();
/*
* 线程不安全
*/
//private volatile static List<Session> sessions = Collections.synchronizedList(new ArrayList());
private Session session;
/*
* 链接成功后的回掉
*/
@OnOpen
public void onOpen(Session session) throws IOException {System.out.println("链接成功");
this.session = session;
sessions.add(this);
}
public static void sendUserLocal(String clientInfoJson,) {try {if (sessions.size() != 0) {for (TransmissionLocationWebSocket s : sessions) {if (s != null) {// 判断是否为终端信息。如果是终端信息则查询数据库获取 detail
s.session.getBasicRemote().sendText(clientInfoJson);
}
}
}
} catch (IOException e) {// TODO Auto-generated catch block
e.printStackTrace();}
}
@OnClose
public void onClose() {System.out.println("设置离线");
sessions.remove(this);
}
}
完美解决
备注:虽然我上面贴出来的代码是在 COW 中保存了整个类,但我测试的时候发生,保存 Session 也是可以的。
Copy-On-Write 简称 COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容 Copy 出去形成一个新的内容然后再改,这是一种延时懒惰策略。从 JDK1.5 开始 Java 并发包里提供了两个使用 CopyOnWrite 机制实现的并发容器, 它们是 CopyOnWriteArrayList 和 CopyOnWriteArraySet。CopyOnWrite 容器非常有用,可以在非常多的并发场景中使用到。
CopyOnWrite 容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。
本文永久更新链接地址:http://www.linuxidc.com/Linux/2016-10/136356.htm