Search code examples
javamultithreadingsocketssynchronizedjava-14

How to release a mutex lock held by Thread which never lets it go, by continuously listening to a socket


There are two classes Client and ChatWindow, client has DatagramSocket, InetAddress and port fields along with methods for sending, receiving, and closing the socket. To close a socket I use an anonymous thread "socketCLOSE"

Client class

public class Client {
private static final long serialVersionUID = 1L;

private DatagramSocket socket;

private String name, address;
private int port;
private InetAddress ip;
private Thread send;
private int ID = -1;

private boolean flag = false;
public Client(String name, String address, int port) {
    this.name = name;
    this.address = address;
    this.port = port;
}


public String receive() {
    byte[] data = new byte[1024];
    DatagramPacket packet = new DatagramPacket(data, data.length);
    try {
        
        socket.receive(packet);
    
    } catch (IOException e) {
        e.printStackTrace();
    }

    String message = new String(packet.getData());
    return message;
}

public void send(final byte[] data) {
    send = new Thread("Send") {
        public void run() {
            DatagramPacket packet = new DatagramPacket(data, data.length, ip, port);
            try {
                
                socket.send(packet);
                
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    };
    send.start(); 
}

public int close() {
    System.err.println("close function called");
     new Thread("socketClOSE") {
        public void run() {
            synchronized (socket) {
                socket.close();
                System.err.println("is socket closed "+socket.isClosed());
               }
        }
    }.start(); 
    
    return 0;
}

ChatWindow class is sort of a GUI which extends JPanel and implements Runnable, There are two threads inside the class - run and Listen.

public class ClientWindow extends JFrame implements Runnable {
private static final long serialVersionUID = 1L;
private Thread run, listen;
private Client client;

private boolean running = false;

public ClientWindow(String name, String address, int port) {
    
    client = new Client(name, address, port);
    
    createWindow();
    console("Attempting a connection to " + address + ":" + port + ", user: " + name);
    String connection = "/c/" + name + "/e/";
    client.send(connection.getBytes());
    
    running = true;
    run = new Thread(this, "Running");
    run.start();
}

private void createWindow() {
             
            {
             //Jcomponents and Layouts here
            }
    
    addWindowListener(new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
            String disconnect = "/d/" + client.getID() + "/e/";
            send(disconnect, false);
            running = false;
            client.close();
            dispose();
        }
    });

    setVisible(true);

    txtMessage.requestFocusInWindow();
}

public void run() {
    listen();
}

private void send(String message, boolean text) {
    if (message.equals("")) return;
    if (text) {
        message = client.getName() + ": " + message;
        message = "/m/" + message + "/e/";
        txtMessage.setText("");
    }
    client.send(message.getBytes());
}

public void listen() {
    listen = new Thread("Listen") {
        public void run() {
            while (running) {
                String message = client.receive();
                if (message.startsWith("/c/")) {
                    client.setID(Integer.parseInt(message.split("/c/|/e/")[1]));
                    console("Successfully connected to server! ID: " + client.getID());
                } else if (message.startsWith("/m/")) {
                    String text = message.substring(3);
                    text = text.split("/e/")[0];
                    console(text);
                } else if (message.startsWith("/i/")) {
                    String text = "/i/" + client.getID() + "/e/";
                    send(text, false);
                } else if (message.startsWith("/u/")) {
                    String[] u = message.split("/u/|/n/|/e/");
                    users.update(Arrays.copyOfRange(u, 1, u.length - 1));
                }
            }
            
        }
    };
    listen.start();
}

public void console(String message) {
    }
  }

Whenever the client is closed, the client.close() is called which spawns socketCLOSE thread, but the thread does nothing, it enters the blocked state, as revealed by stack trace -

Name: socketClOSE State: BLOCKED on java.net.DatagramSocket@1de1602 owned by: Listen Total blocked: 1 Total waited: 0

Stack trace: app//com.server.Client$2.run(Client.java:90)

Name: Listen State: RUNNABLE Total blocked: 0 Total waited: 0

Stack trace:

[email protected]/java.net.DualStackPlainDatagramSocketImpl.socketReceiveOrPeekData(Native Method) [email protected]/java.net.DualStackPlainDatagramSocketImpl.receive0(DualStackPlainDatagramSocketImpl.java:130)

  • locked java.net.DualStackPlainDatagramSocketImpl@3dd26cc7 [email protected]/java.net.AbstractPlainDatagramSocketImpl.receive(AbstractPlainDatagramSocketImpl.java:181)
  • locked java.net.DualStackPlainDatagramSocketImpl@3dd26cc7 [email protected]/java.net.DatagramSocket.receive(DatagramSocket.java:864)
  • locked java.net.DatagramPacket@6d21ecb
  • locked java.net.DatagramSocket@1de1602 app//com.thecherno.chernochat.Client.receive(Client.java:59) app//com.thecherno.chernochat.ClientWindow$5.run(ClientWindow.java:183)

This doesn’t let SocketCLOSE thread close the socket inside the synchronized block as lock on socket is held by Listen thread. How can I make the listen thread release its lock, the program terminates without closing the socket and debugger shows Listen thread as still runnable. Is the implementation flawed itself or can this be solved ?


Solution

  • I can reproduce the problem with JDK 14, but not with JDK 15 or newer.

    This seems plausible, as JDK-8235674, JEP 373: Reimplement the Legacy DatagramSocket API indicates that the implementation has been rewritten for JDK 15. The report even says “The implementation also has several concurrency issues (e.g., with asynchronous close) that require an overhaul to address properly.

    However, you can get rid of the problem with JDK 14 too; just remove the synchronized. Nothing in the documentation says that synchronization was required to call close() and when I removed it, my testcase worked as intended.

    When you want to coordinate the multithreaded access to the socket of your application, you should use a different lock object than the socket instance itself.