Как освободить блокировку мьютекса, удерживаемую Thread, которая никогда не отпускает ее, постоянно прослушивая сокет

Есть два класса Client и ChatWindow, у клиента есть поля DatagramSocket, InetAddress и порта, а также методы для отправки, получения и закрытия сокета. Чтобы закрыть сокет, я использую анонимный поток socketCLOSE

Класс клиента

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 — это своего рода графический интерфейс, который расширяет JPanel и реализует Runnable. Внутри класса есть два потока — run и 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) {
    }
  }

Всякий раз, когда клиент закрывается, вызывается client.close(), который порождает поток socketCLOSE, но поток ничего не делает, он входит в заблокированное состояние, как показывает трассировка стека -

Имя: socketClOSE Состояние: ЗАБЛОКИРОВАНО на java.net.DatagramSocket@1de1602, владелец: Listen Всего заблокировано: 1 Всего ожидано: 0

Трассировка стека: app//com.server.Client$2.run(Client.java:90)

Имя: Listen Состояние: RUNNABLE Всего заблокировано: 0 Всего ожидано: 0

Трассировка стека:

[email protected]/java.net.DualStackPlainDatagramSocketImpl.socketReceiveOrPeekData(собственный метод) [email protected]/java.net.DualStackPlainDatagramSocketImpl.receive0(DualStackPlainDatagramSocketImpl.java:130)

  • заблокировано
  • заблокирован java.net.DualStackPlainDatagramSocketImpl@3dd26cc7 [email protected]/java.net.DatagramSocket.receive(DatagramSocket.java:864)
  • заблокирован java.net.DatagramPacket@6d21ecb
  • заблокированное приложение java.net.DatagramSocket@1de1602//com.thecherno.chernochat.Client.receive(Client.java:59) app//com.thecherno.chernochat.ClientWindow$5.run(ClientWindow.java:183)

Это не позволяет потоку SocketCLOSE закрыть сокет внутри блока synchronized, так как блокировка сокета удерживается потоком прослушивания. Как я могу заставить поток прослушивания снять свою блокировку, программа завершается без закрытия сокета, а отладчик показывает поток прослушивания как все еще работающий. Является ли реализация ошибочной или это можно решить?


person Crypto Kun    schedule 17.02.2021    source источник


Ответы (1)


Я могу воспроизвести проблему с JDK 14, но не с JDK 15 или новее.

Это кажется правдоподобным, поскольку JDK-8235674, JEP 373: повторная реализация устаревшего API DatagramSocket указывает, что реализация была переписана для JDK 15. В отчете даже говорится: «В реализации также есть несколько проблем с параллелизмом (например, с асинхронным закрытием), которые требуют капитального ремонта для правильного решения.».

Однако вы можете избавиться от проблемы и с JDK 14; просто удалите synchronized. Ничто в документации не говорит о том, что для вызова close() требовалась синхронизация, и когда я удалил ее, мой тесткейс заработал как положено.

Если вы хотите координировать многопоточный доступ к сокету вашего приложения, вы должны использовать объект блокировки, отличный от самого экземпляра сокета.

person Holger    schedule 24.02.2021
comment
Большое тебе спасибо. Я думал, что это, вероятно, останется без ответа, и да, удаление synchornized закрыло сокет, но это вызвало исключение SOCKETCLOSED в socket.recieved() внутри метода получения клиента. Если вы хотите координировать многопоточный доступ к сокету вашего приложения, вы должны использовать объект блокировки, отличный от самого экземпляра сокета. Какой объект, кроме сокета, я мог бы здесь использовать? Объект клиента? - person Crypto Kun; 24.02.2021
comment
Ожидается исключение. Поскольку переменная socket объявлена ​​в Client, синхронизация экземпляра Client была бы хорошим соглашением. Однако не ясно, нужен ли он вообще (вы не показали, куда присваивается socket). Здесь есть несколько странных вещей, например, поток "Running", единственным действием которого является создание потока "Listen". Далее, не стоит ловить IOException и действовать как ни в чем не бывало, особенно в receive, который потом возвращает неверные данные. - person Holger; 24.02.2021