Java Socket一对多通信实现之并发处理方式
作者:从北码到南
效果图
场景描述
多台传感器连接至服务端时,保存每台传感器最初的登录顺序和 socket 信息,然后根据登录顺序进行页面排序,当设备掉线或者主动断开时移除 socket 连接信息
代码设计
1. 创建一个GlobalCommonUtil的工具类
存放全局静态集合
//存放设备连接信息 eg: mac 登录状态 初始登录顺序等 public static List<TcpObject> list = new LinkedList<TcpObject>(); //存放设备初始登录顺序(累计排序) public static List<FileObject> fileInfo = new LinkedList<FileObject>(); //存放 socket 连接对象,mac 为key 此处为线程安全的 map 集合 public static Map<String,Socket> map = new ConcurrentHashMap<String, Socket>();
2.创建线程通信类SocketThread
初始化 ServerSocket 和 Socket 对象
public class SocketThread extends Thread{ ServerSocket server; Socket client; public SocketThread(Socket socket){ this.client = socket; } @Override public void run() { try { reader = new BufferedReader(new InputStreamReader(client.getInputStream())); // 读取reader中的信息... } catch (Exception e) { //捕获socket强制断开异常信息,移除socket } } }
3.创建线程启动类
public class TcpServer { public static void main(String[] args) { start(); } public void start() { try { //记录链接过的客户端个数 //1、创建一个服务器端Socket,即ServerSocket,绑定指定的端口,进行监听 ServerSocket serverSocket = new ServerSocket(9000); log.info("服务器即将启动,等待客户端连接"); //2、循环监听等待客户端的连接 while(true){ //调用accept方法 等待客户端的连接 Socket socket = serverSocket.accept(); if(!GlobalCommonUtil.isStart) { //创建一个新的线程 SocketThread serverThread = new SocketThread(socket); //启动线程 serverThread.start(); GlobalCommonUtil.count++; log.info("连接过的客户端数量为:" + GlobalCommonUtil.count); } } } catch (IOException e) { e.printStackTrace(); } } }
测试环境中,由于真实场景和真实设备均没有见过,也无法考虑设备车间是怎么回事,只能老套路。
用tcp连接工具模拟登录,登录,数据收发时均无问题,代码运行良好,开发测试都 ok ,然后交付了,再然后>跪了< 。。。
问题一:
全局静态资源的并发操作引起的线程不安全:
for(TcpObject tcpObject : GlobalCommonUtil.list) { if(tcpObject.getMac().equals(mac)) { GlobalCommonUtil.list.remove(tcpObject); break; } } GlobalCommonUtil.count--; log.info("远程客户端已关闭连接!");
但客户端异常断开时,GlobalCommonUtil.list 的 remove 其他线程的读取很容易就会 java.lang.NullPointerException,多线程之间共享变量,从而导致的线程不安全问题,如果我们让每个线程依次去读写这个变量,这样应该可以避免不安全问题了
加 synchronized 锁
分类 | 具体分类 | 被锁的对象 | 伪代码 |
---|---|---|---|
方法 | 实例方法 | 类的实例对象 | // 实例方法 锁住的是该类的实例对象 public synchronized void method(){ // action ... } |
静态方法 | 类对象 | // 静态方法,锁住的是类对象 public static synchronized void method(){ // action ... } | |
代码块 | 实例对象 | 类的实例对象 | // 同步代码块,锁住的是该类的实例对象 synchronized (this){ // action ... } |
class对象 | 类对象 | // 同步代码块,锁住的是该类的类对象 synchronized (Dermo.class){ // action ... } | |
任意实例对象Object | 实例对象 | // 同步代码块,锁住的是配置的实例对象 //String 对象作为锁 String lock = “” synchronized (lock){ // action ... } |
注:
如果锁的是类的实例对象的话,每次 new 的操作都是创建一个新的对象,就出现 synchronized 锁不住对象的现象,如果锁的是类对象的话,无论new多少个实例对象,他们仍然会被锁住,即可保证线程之间的同步关系,synchronized 底层原理是使用了对象持有的监视器(monitor),但是同步代码块和同步方法的原理存在一点差异:
- 同步代码块使用的 monitorenter 和 monitorexit 指令实现的
- 同步方法是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZRED 标识隐式实现,实际上还是调用了 monitorenter 和 monitorexit 指令
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。