[英]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_INPUT
和hasInput
變量是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 年前發布了這個問題,當時我還是一名新程序員。 這個問題本身並沒有很好地形成,盡管我盡了最大的努力,但我認為這個問題本身沒有什么可以挽救的; 但是,我確實認為有幾個答案可以闡明一些重要主題。 我想我會嘗試解決對我聲譽造成的傷害,這個問題已經通過編譯和闡述其中的一些主題證明了自己。
(感謝@MadProgrammer 的回答)
為了向新程序員(或年輕的我)解釋並發性,我首先要解釋“線程”是操作系統用來同時執行指令的一種結構。
當一個程序由操作系統運行時,或者在這種情況下由 JVM 運行時,會產生一個新線程並運行直到完成。 這意味着“單線程”范式或多或少是大多數程序的默認模式,而且它恰好更直觀,因為您可以可靠地推斷程序的順序執行。 然而, “多線程”范式通常需要改變編寫程序的方式,因為不再保證按順序執行。
雖然“Swing 是一個單線程環境”這一說法是正確的,但它確實略過了一些微妙的東西。 對於第一次學習 Swing 的人來說,不明顯的是上面的程序沒有一個線程,而是兩個線程。 第一個是實際的程序線程,第二個是由 Swing 框架隱式管理的線程。
最終這意味着 Swing 的某些部分被嚴格設計為由第二個線程在內部調用,並且嘗試從主線程內執行某些操作可能會產生災難性的后果,因為它違反了 Swing 框架所依賴的某些預期。
與 Swing 交互的“正確”方式是:
(感謝@Boann 和@CDahn 的回答)
該程序內部是一個while
循環,該循環一直運行直到滿足某些條件(在本例中等待hasInput
為真)。 這通常被稱為“忙循環”或“微調器” ,並且由於多種原因,這種特定的實現恰好是有問題的。
通常,在實現微調器時,建議您在循環內調用Thread.sleep()
或等效平台。 這通過通知 OS 調度程序此進程可以暫停一段時間來確保您的繁忙循環不會積極和浪費地消耗 CPU 周期。
此實現容易受到由於“線程緩存”而出現的奇怪行為的影響。 這是指線程喜歡在附近保留變量的本地副本以減少讀/寫時間。 在大多數單線程和多線程環境中,這沒什么特別的; 但是,如果兩個線程需要讀/寫同一個變量,則兩個線程都可能看不到另一個線程在做什么。 解決此問題的推薦方法是通過將其聲明為volatile
來強制每個線程在執行讀/寫時檢索原始變量。
將代碼轉換為可執行文件時,編譯器通常會執行一系列積極的優化,這些優化旨在實現同構,但有時會導致奇怪的行為。 這些優化之一涉及一個稱為“提升”的過程,編譯器在該過程中檢測它認為是常量表達式的內容並提取/替換它。 與人類一樣,編譯器非常擅長推理單線程執行,但是當從多個線程訪問一個變量時,編譯器很難識別這種交互。 同樣,建議的修復方法是將這些共享變量聲明為volatile
,以便編譯器知道不要執行其中的一些優化。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.