繁体   English   中英

Swing GUI 不一致地冻结

[英]Swing GUI Inconsistently freezing

编辑:

我在 8 年前发布了这个问题,当时我还是一名新程序员。 这个问题本身并没有很好地形成,尽管我尽了最大的努力,但我认为这个问题本身没有什么可以挽救的; 但是,我确实认为有几个答案可以阐明一些重要主题。 我想我会尝试解决对我声誉造成的伤害,这个问题已经通过编译和阐述其中的一些主题证明了自己。

我和一个朋友一直在尝试在 Java 中编写一个简单的服务器/客户端聊天应用程序。当我们只使用命令行(例如 java.util.Scanner)时,一切都很好。 直到我们尝试构建一个 GUI 时才遇到了麻烦。

有时它会工作,有时它会冻结,有时什么也不会发生。

我们有点确定问题出在ask()方法中

public String ask(String string)
    {
        println(string);        
        hasInput = false;  

        while(true)
        { 
            //System.out.println("working");                
            if(hasInput)
            {
                println("done");
                return processLastInput();//removes the carot ">" from the input and returns it              
            }
        }        
    }

但是如果你取消注释 println 语句,它会不一致地工作......这是代码的 rest 供你查看

import java.awt.Color;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;


public class ConsoleGUI
{
    private  JFrame FRAME;
    private  JPanel PANEL;
    private  JTextArea CMD_TEXT;
    private  JTextArea CMD_HISTORY;
    private  JScrollPane CMD_HISTORY_SCROLLER;
    private String LAST_INPUT = "";
    private boolean hasInput = false;   

    private final  int screenX = (int)Toolkit.getDefaultToolkit().getScreenSize().getWidth();
    private final  int screenY = (int)Toolkit.getDefaultToolkit().getScreenSize().getHeight(); 

    public ConsoleGUI(String terminalHeader)
    {   
        FRAME = new JFrame(terminalHeader);
        PANEL = new JPanel();
        CMD_TEXT = new JTextArea(">");
        CMD_HISTORY = new JTextArea();
        CMD_HISTORY_SCROLLER = new JScrollPane(CMD_HISTORY_SCROLLER);

        FRAME.setBounds(screenX/2-250,screenY/2-150,500,300);        
        FRAME.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);        
        FRAME.getContentPane().add(PANEL);
        FRAME.setResizable(false);      

        CMD_TEXT.setBackground(Color.BLACK);
        CMD_TEXT.setForeground(Color.GREEN);
        CMD_TEXT.setFont(new Font("courier new",Font.PLAIN,15));
        CMD_TEXT.setBorder(BorderFactory.createTitledBorder("COMMAND:"));
        CMD_TEXT.setBounds(0,220,490,50);        

        CMD_TEXT.getInputMap().put(KeyStroke.getKeyStroke("ENTER"), "NEXT");       
        CMD_TEXT.getActionMap().put("NEXT",new ActivateInputAction());

        CMD_HISTORY.setEditable(false);
        CMD_HISTORY.setBackground(Color.BLACK);
        CMD_HISTORY.setForeground(Color.GREEN);
        CMD_HISTORY.setFont(new Font("courier new",Font.PLAIN,20));
        CMD_HISTORY.setBorder(BorderFactory.createTitledBorder("CONSOLE:")); 

        CMD_HISTORY_SCROLLER = new JScrollPane(CMD_HISTORY);
        CMD_HISTORY_SCROLLER.setBounds(0,0,490,220);         

        PANEL.setBackground(Color.GRAY);        
        PANEL.setFocusable(true);
        PANEL.setLayout(null);         
        PANEL.add(CMD_HISTORY_SCROLLER);
        PANEL.add(CMD_TEXT);          

        FRAME.setVisible(true);       
    }    

    public void print(Object ob)
    {
        CMD_HISTORY.append(ob.toString());
    }
    public void println(Object ob)
    {
        CMD_HISTORY.append(ob.toString()+"\n");
    }

    public String getLastInput(){return LAST_INPUT;}
    public String processLastInput()
    {
        String newString = LAST_INPUT.replace(">","");
        return newString;
    }
    public boolean hasInput(){return hasInput;}    

    public String ask(String string)
    {
        println(string);        
        hasInput = false;  

        while(true)
        {                 
            if(hasInput)
            {
                println("done");
                return processLastInput();                
            }
        }        
    }

    class ActivateInputAction extends AbstractAction
    {
        @Override
        public void actionPerformed(ActionEvent e)
        {            
            LAST_INPUT = CMD_TEXT.getText();
            println(LAST_INPUT);
            CMD_TEXT.setText(">");
            CMD_HISTORY.setCaretPosition(CMD_HISTORY.getText().length());  
            hasInput = true;  
        }
    }    
}

如果你愿意,服务器代码也在下面

import java.io.*;
import java.net.*;
import java.util.Scanner;

public class Server 
{
    private static ServerSocket service = null;
    private static String line;
    private static BufferedReader input;
    private static PrintStream output;
    private static Socket clientSocket = null;
    //private static final Scanner keyboard = new Scanner(System.in);
    private static ConsoleGUI Console = new ConsoleGUI("SERVER");

    public static void main(String[] args)
    {
        try
        {                   
            int port = Integer.parseInt(Console.ask("PORT:")); 
            Console.println("PORT = "+port);
            service = new ServerSocket(port);
            Console.println(service.getLocalSocketAddress());
            Console.println("WAITING FOR CLIENT TO CONNECT");            
            clientSocket = service.accept();
            input = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            output = new PrintStream(clientSocket.getOutputStream());
            Console.println("CLIENT CONNECTED");
            Console.println(service.getInetAddress());
            while(true)
            {

                line = input.readLine();
                output.println(line);
            }
        }
        catch(Exception e)
        {
            Console.println(e);
        }
    }
}

Swing是一个单线程环境,也就是说,期望所有与UI的交互都将从Event Dispatching Thread完成。 任何阻止EDT的东西都会阻止UI响应用户或处理新事件

你似乎正在犯这样的错误,试图在EDT的上下文之外更新textarea,这似乎是导致某种线程锁定(至少在Java 8下)

因此,我首先更新了您的print方法,以便在EDT的上下文中更新内容

public void print(final Object ob) {
    Runnable run = new Runnable() {
        @Override
        public void run() {
            CMD_HISTORY.append(ob.toString());
        }
    };
    if (EventQueue.isDispatchThread()) {
        run.run();
    } else {
        EventQueue.invokeLater(run);
    }
}

public void println(Object ob) {
    Runnable run = new Runnable() {
        @Override
        public void run() {
            CMD_HISTORY.append(ob.toString() + "\n");
        }
    };
    if (EventQueue.isDispatchThread()) {
        run.run();
    } else {
        EventQueue.invokeLater(run);
    }
}

我还更新了你的LAST_INPUThasInput变量是volatile ......

private volatile String LAST_INPUT = "";
private volatile boolean hasInput = false;

这将确保它们在线程边界之间正确更新...

而不是尝试使用boolean标志作为更多信息的指示器,最好使用对象监视器锁,其主要原因是它将使等待线程进入休眠状态,因此它不会占用任何CPU ...

private final Object inputLock = new Object();

//...

public String ask(String string) {
    println(string);
    hasInput = false;

    do {
        synchronized (inputLock) {
            try {
                System.out.println("Wait");
                inputLock.wait();
            } catch (InterruptedException ex) {
            }
        }
    } while (!hasInput);
    println("done");
    return processLastInput();
}

在强行推进之前,我强烈建议您阅读Swing中的Concurrency 上面的例子充其量只是hacks ...

在ask()中,hasInput初始化为false,因此您将立即进入无限循环。 这将导致奇怪的行为,因为它吸收任何可用的周期。

我在 8 年前发布了这个问题,当时我还是一名新程序员。 这个问题本身并没有很好地形成,尽管我尽了最大的努力,但我认为这个问题本身没有什么可以挽救的; 但是,我确实认为有几个答案可以阐明一些重要主题。 我想我会尝试解决对我声誉造成的伤害,这个问题已经通过编译和阐述其中的一些主题证明了自己。

Swing 是单线程环境。

(感谢@MadProgrammer 的回答)

为了向新程序员(或年轻的我)解释并发性,我首先要解释“线程”是操作系统用来同时执行指令的一种结构。

当一个程序由操作系统运行时,或者在这种情况下由 JVM 运行时,会产生一个新线程并运行直到完成。 这意味着“单线程”范式或多或少是大多数程序的默认模式,而且它恰好更直观,因为您可以可靠地推断程序的顺序执行 然而, “多线程”范式通常需要改变编写程序的方式,因为不再保证按顺序执行

虽然“Swing 是一个单线程环境”这一说法是正确的,但它确实略过了一些微妙的东西。 对于第一次学习 Swing 的人来说,不明显的是上面的程序没有一个线程,而是两个线程。 第一个是实际的程序线程,第二个是由 Swing 框架隐式管理的线程。

最终这意味着 Swing 的某些部分被严格设计为由第二个线程在内部调用,并且尝试从主线程内执行某些操作可能会产生灾难性的后果,因为它违反了 Swing 框架所依赖的某些预期。

与 Swing 交互的“正确”方式是:

  1. 使用事件监听器。
  2. (主要)使用事件队列向 Swing 提交操作。
  3. 扩展 Swing 组件并覆盖自定义行为的特定方法。

编译器知道得更多?

(感谢@Boann 和@CDahn 的回答)

该程序内部是一个while循环,该循环一直运行直到满足某些条件(在本例中等待hasInput为真)。 这通常被称为“忙循环”“微调器” ,并且由于多种原因,这种特定的实现恰好是有问题的。

通常,在实现微调器时,建议您在循环内调用Thread.sleep()或等效平台。 这通过通知 OS 调度程序此进程可以暂停一段时间来确保您的繁忙循环不会积极和浪费地消耗 CPU 周期。

此实现容易受到由于“线程缓存”而出现的奇怪行为的影响。 这是指线程喜欢在附近保留变量的本地副本以减少读/写时间。 在大多数单线程和多线程环境中,这没什么特别的; 但是,如果两个线程需要读/写同一个变量,则两个线程都可能看不到另一个线程在做什么。 解决此问题的推荐方法是通过将其声明为volatile来强制每个线程在执行读/写时检索原始变量。

将代码转换为可执行文件时,编译器通常会执行一系列积极的优化,这些优化旨在实现同构,但有时会导致奇怪的行为。 这些优化之一涉及一个称为“提升”的过程,编译器在该过程中检测它认为是常量表达式的内容并提取/替换它。 与人类一样,编译器非常擅长推理单线程执行,但是当从多个线程访问一个变量时,编译器很难识别这种交互。 同样,建议的修复方法是将这些共享变量声明为volatile ,以便编译器知道不要执行其中的一些优化。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM