Java Socket「饥饿死锁」问题

前言

Socket是指网络上端到端的通信机制,一般基于传输层协议。本文以Java为例,专门讨论TCP协议的Socket,UDP协议的Socket也可以参考。

饥饿-读

考虑这样一个情况:一个简单的HTTP客户端访问服务器资源的程序在运行了一段时间后,莫名的停住了;或者是一个简单的JDBC查询数据库记录的程序在运行了一段时间后,没有动静了。此时,dump下Java目前的运行状态,基本上能看到如下代码片段:

Thread %d: (state = IN_NATIVE)
 - java.net.SocketInputStream.socketRead0(java.io.FileDescriptor, byte[], int, int, int) @bci=0 (Compiled frame; information may be imprecise)
 - java.net.SocketInputStream.read(byte[], int, int, int) @bci=87 (Compiled frame)
 - java.net.SocketInputStream.read(byte[], int, int) @bci=11 (Compiled frame)
 - java.net.SocketInputStream.read(byte[]) @bci=5 (Compiled frame)
 - ... ... ...

多次dump发现,进程一直阻塞在socketRead0函数,也就是说进程已经「饿死」了。至于为什么会「饥饿」到死,这个原因就复杂了,可能是服务端自己产生了死锁,也可能是服务端运行异常,跳过了往客户端发送数据的代码。但是作为客户端,读不到数据可能不是最糟糕的,最糟糕的是阻塞导致其它代码不能执行。
这就需要打破这种阻塞状态。Java的Socket API 提供了这个机制。

void setSoTimeout(int timeout) 

Socket的setSoTimeout方法,用于设置读取超时,单位是毫秒。默认情况下,值为0,即无限等待直到读取数据。如果设置为一个合理的值,比如30000,则读取超时为30秒,即30秒之后,如果数据还未到达,则抛出一个超时异常。

HTTP和JDBC底层都是基于TCP的Socket机制实现,所以不管是HTTP客户端,还是JDBC客户端,一般都会提供这个参数的配置。

死锁-写

死锁-写有一个经典的场景。
Server端 ↓

public class Server {

    public static void main(String[] args) throws Exception {
        int count = 0;
        ServerSocket ss = new ServerSocket(6060);
        Socket s = ss.accept();
        while (true) {
            s.getOutputStream().write(new byte[1024 * 8]);
            s.getInputStream().read();
            System.out.println("server: " + ++count);
        }
    }
}

Client端 ↓

public class Client {

    public static void main(String[] args) throws Exception {
        int count = 0;
        Socket s = new Socket("localhost", 6060);
        while (true) {
            s.getOutputStream().write(new byte[1024 * 8]);
            s.getInputStream().read();
            System.out.println("client: " + ++count);
        }
    }
}

两个程序运行几次后,就不再打印任何消息了,即两个进程处于死锁状态了。造成死锁的原因自然是满足了死锁的四个必要条件:

  • 互斥;Server端和Client端都既是生产者,也是消费者,互斥的使用两个TCP缓冲队列。
  • 请求与保持;由于write方法每次写8KB,而read方法每次只读1B,所以两个缓冲队列都很快被填满。此时,两端都请求空的缓冲区,并保持着对方空缓冲的释放权利。
  • 不剥夺;这两个程序请求资源是非抢占式的,显然不会被剥夺。
  • 环路等待;Server端和Client端的程序都阻塞在write,互相等待对方释放空的缓冲区。

解决死锁的办法也很简单,将write和read分离在不同的线程,写归写,读归读,打破环路等待的条件。那样虽然读的比较慢,但死锁是不存了。

将读写分离成不同线程时,需要特别注意:不要在Socket对象上加同步锁。这样跟不分离没有实质上的区别。

结论

Socket编程一般都是多进程编程(服务端和客户端),多线程编程(Socket发送接收一般都是独立线程)的综合。所以,多进程、多线程的各种编程问题层出不穷,最突出的就是饥饿和死锁,需要足够多的代码来沉淀。

    原文作者:java锁
    原文地址: https://blog.csdn.net/Foxir/article/details/43898677
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞