1 回答

TA貢獻1858條經驗 獲得超8個贊
允許JVM假定其他線程pizzaArrived
在循環期間不更改變量。換句話說,它可以pizzaArrived == false
在循環外提升測試,優化:
while (pizzaArrived == false) {}
進入這個:
if (pizzaArrived == false) while (true) {}
這是一個無限循環。
要確保一個線程所做的更改對其他線程可見,您必須始終在線程之間添加一些同步。最簡單的方法是創建共享變量volatile
:
volatile boolean pizzaArrived = false;
創建變量可以volatile
保證不同的線程可以看到彼此對其的更改的影響。這可以防止JVM緩存pizzaArrived
循環外的測試值或將測試掛起。相反,它必須每次都讀取實變量的值。
(更正式地說,在訪問變量之間volatile
創建了一個先發生的關系。這意味著在傳遞披薩之前線程所做的所有其他工作對于接收披薩的線程也是可見的,即使那些其他更改不是volatile
變量。)
同步方法主要用于實現互斥(防止兩件事同時發生),但它們也具有所有相同的副作用volatile
。在讀取和寫入變量時使用它們是另一種使更改對其他線程可見的方法:
class MyHouse { boolean pizzaArrived = false; void eatPizza() { while (getPizzaArrived() == false) {} System.out.println("That was delicious!"); } synchronized boolean getPizzaArrived() { return pizzaArrived; } synchronized void deliverPizza() { pizzaArrived = true; }}
打印聲明的效果
System.out
是一個PrintStream
對象。方法是PrintStream
這樣同步的:
public void println(String x) { synchronized (this) { print(x); newLine(); }}
同步防止pizzaArrived
在循環期間緩存。嚴格地說,兩個線程必須在同一個對象上同步,以保證對變量的更改是可見的。(例如,println
在設置之后pizzaArrived
調用并在讀取之前再次調用它將pizzaArrived
是正確的。)如果只有一個線程在特定對象上同步,則允許JVM忽略它。在實踐中,JVM不夠智能,無法證明其他線程println
在設置后不會調用pizzaArrived
,因此它假定它們可能。因此,如果調用它,它不能在循環期間緩存變量System.out.println
。這就是為什么這樣的循環在有打印語句時起作用的原因,盡管它不是正確的解決方法。
使用System.out
不是導致這種效果的唯一方法,但它是人們最常發現的,當他們試圖調試為什么他們的循環不起作用時!
更大的問題
while (pizzaArrived == false) {}
是一個忙碌的等待循環。那很糟!當它等待時,它會占用CPU,從而減慢其他應用程序的速度,并增加系統的功耗,溫度和風扇速度。理想情況下,我們希望循環線程在等待時休眠,因此它不會占用CPU。
以下是一些方法:
使用wait / notify
一個低級解決方案是使用以下的wait / notify方法Object
:
class MyHouse { boolean pizzaArrived = false; void eatPizza() { synchronized (this) { while (!pizzaArrived) { try { this.wait(); } catch (InterruptedException e) {} } } System.out.println("That was delicious!"); } void deliverPizza() { synchronized (this) { pizzaArrived = true; this.notifyAll(); } }}
在這個版本的代碼中,循環線程調用wait()
,這使線程處于休眠狀態。睡覺時不會使用任何CPU周期。在第二個線程設置變量之后,它調用notifyAll()
喚醒正在等待該對象的任何/所有線程。這就像披薩家伙敲響了門鈴一樣,所以你可以坐下來休息,等待,而不是笨拙地站在門口。
在對象上調用wait / notify時,必須保持該對象的同步鎖,這就是上面的代碼所做的。你可以使用你喜歡的任何對象,只要兩個線程使用相同的對象:這里我使用this
(實例MyHouse
)。通常,兩個線程無法同時進入同一對象的同步塊(這是同步目的的一部分),但它在此處起作用,因為線程在wait()
方法內部時臨時釋放同步鎖。
BlockingQueue的
A BlockingQueue
用于實現生產者 - 消費者隊列?!跋M者”從隊列前面取物品,“生產者”在后面推動物品。一個例子:
class MyHouse { final BlockingQueue<Object> queue = new LinkedBlockingQueue<>(); void eatFood() throws InterruptedException { // take next item from the queue (sleeps while waiting) Object food = queue.take(); // and do something with it System.out.println("Eating: " + food); } void deliverPizza() throws InterruptedException { // in producer threads, we push items on to the queue. // if there is space in the queue we can return immediately; // the consumer thread(s) will get to it later queue.put("A delicious pizza"); }}
注意:可以拋出s 的put
和take
方法,它們是必須處理的已檢查異常。在上面的代碼中,為簡單起見,重新拋出了異常。您可能更喜歡捕獲方法中的異常并重試put或take調用以確保它成功。除了那一點丑陋之外,很容易使用。BlockingQueue
InterruptedException
BlockingQueue
這里不需要其他同步,因為BlockingQueue
確保在將項目放入隊列之前所做的所有線程對于將這些項目取出的線程是可見的。
執行人
Executor
s就像現成的BlockingQueue
執行任務一樣。例:
// A "SingleThreadExecutor" has one work thread and an unlimited queueExecutorService executor = Executors.newSingleThreadExecutor(); Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); }; // we submit tasks which will be executed on the work threadexecutor.execute(eatPizza);executor.execute(cleanUp); // we continue immediately without needing to wait for the tasks to finish
有關詳情請參閱該文檔Executor
,ExecutorService
和Executors
。
事件處理
在等待用戶在UI中單擊某些內容時循環是錯誤的。而是使用UI工具包的事件處理功能。例如,在Swing中:
JLabel label = new JLabel();JButton button = new JButton("Click me");button.addActionListener((ActionEvent e) -> { // This event listener is run when the button is clicked. // We don't need to loop while waiting. label.setText("Button was clicked");});
因為事件處理程序在事件派發線程上運行,所以在事件處理程序中執行長時間的工作會阻止與UI的其他交互,直到工作完成。可以在新線程上啟動慢速操作,或使用上述技術之一(wait / notify,a BlockingQueue
或Executor
)將調度分派給等待的線程。您還可以使用SwingWorker
專為此設計的a,并自動提供后臺工作線程:
JLabel label = new JLabel();JButton button = new JButton("Calculate answer");// Add a click listener for the buttonbutton. addActionListener((ActionEvent e) -> { // Defines MyWorker as a SwingWorker whose result type is String: class MyWorker extends SwingWorker<String,Void> { @Override public String doInBackground() throws Exception { // This method is called on a background thread. // You can do long work here without blocking the UI. // This is just an example: Thread.sleep(5000); return "Answer is 42"; } @Override protected void done() { // This method is called on the Swing thread once the work is done String result; try { result = get(); } catch (Exception e) { throw new RuntimeException(e); } label.setText(result); // will display "Answer is 42" } } // Start the worker new MyWorker().execute();});
計時器
要執行定期操作,您可以使用a java.util.Timer
。它比編寫自己的定時循環更容易使用,更容易啟動和停止。該演示每秒打印一次當前時間:
Timer timer = new Timer();TimerTask task = new TimerTask() { @Override public void run() { System.out.println(System.currentTimeMillis()); }};timer.scheduleAtFixedRate(task, 0, 1000);
每個java.util.Timer
都有自己的后臺線程,用于執行其調度的TimerTask
s。當然,線程在任務之間休眠,因此它不會占用CPU。
在Swing代碼中,還有一個javax.swing.Timer
類似的,但它在Swing線程上執行偵聽器,因此您可以安全地與Swing組件交互,而無需手動切換線程:
JFrame frame = new JFrame();frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);Timer timer = new Timer(1000, (ActionEvent e) -> { frame.setTitle(String.valueOf(System.currentTimeMillis()));});timer.setRepeats(true);timer.start();frame.setVisible(true);
其他方法
如果您正在編寫多線程代碼,那么值得探索這些包中的類以查看可用的內容:
另請參閱Java教程的Concurrency部分。多線程很復雜,但有很多幫助可用!
添加回答
舉報