同步器

java.util.concurrent 包包含了几个能帮助人们管理相互合作的线程集的类见表 14-5。这 些机制具有为线程之间的共用集结点模式(common rendezvous patterns) 提供的“ 预置功能” ( canned functionality)0 如果有一个相互合作的线程集满足这些行为模式之一, 那么应该直接 重用合适的库类而不要试图提供手工的锁与条件的集合。

信号量

概念上讲,一个信号量管理许多的许可证(permit)。为了通过信号量,线程通过调用 acquire请求许可。其实没有实际的许可对象, 信号量仅维护一个计数。许可的数目是固定 的,由此限制了通过的线程数量。其他线程可以通过调用 release 释放许可。而且,许可不是二 必须由获取它的线程释放。事实上,任何线程都可以释放任意数目的许可,这可能会增加许可数目以至于超出初始数目。

信号量在 1968 年由 Edsger Dijkstra 发明, 作为同步原语(synchronization primitive ) 0 Dijkstra 指出信号量可以被有效地实现, 并且有足够的能力解决许多常见的线程同步问题。

倒计时门栓

一个倒计时门栓(CountDownLatch) 让一个线程集等待直到计数变为 0。倒计时门栓是 一次性的。一旦计数为 0, 就不能再重用了。 一个有用的特例是计数值为 1 的门栓。实现一个只能通过一次的门。线程在门外等候直 到另一个线程将计数器值置为0。

障栅

CyclicBarrier 类实现了一个集结点(rendezvous) 称为障栅(barrier)。考虑大量线程运行 在一次计算的不同部分的情形。当所有部分都准备好时,需要把结果组合在一起。当一个线 程完成了它的那部分任务后, 我们让它运行到障栅处。一旦所有的线程都到达了这个障栅, 障栅就撤销,线程就可以继续运行。

。首先, 构造一个障栅,并给出参与的线程数:

CyclicBarrier barrier = new CydicBarrier(nthreads);

每一个线程做一些工作,完成后在障栅上调用 await :

public void run() { 
    doWork(); 
    bamer.await();
    ...
}

await 方法有一个可选的超时参数:

barrier.await(100, TineUnit.MILLISECONDS);

如果任何一个在障栅上等待的线程离开了障栅, 那么障栅就被破坏了(线程可能离开是 因为它调用 await 时设置了超时,或者因为它被中断了) 。在这种情况下,所有其他线程的 await 方法抛出 BrokenBarrierException 异常。那些已经在等待的线程立即终止 await 的调用。

可以提供一个可选的障栅动作(barrier action), 当所有线程到达障栅的时候就会执行这 一动作。

Runnable barrierAction = ...;

CyclicBarrier barrier = new Cyc1icBarrier(nthreads, barrierAction);

该动作可以收集那些单个线程的运行结果。 障栅被称为是循环的(cyclic), 因为可以在所有等待线程被释放后被重用。在这一点上, 有别于 CountDownLatch, CountDownLatch 只能被使用一次。

Phaser 类增加了更大的灵活性,允许改变不同阶段中参与线程的个数。

交换器

当两个线程在同一个数据缓冲区的两个实例上工作的时候, 就可以使用交换器 ( Exchanger) 典型的情况是, 一个线程向缓冲区填人数据, 另一个线程消耗这些数据。当它 们都完成以后,相互交换缓冲区。

同步队列

同步队列是一种将生产者与消费者线程配对的机制。当一个线程调用 SynchronousQueue 的 put 方法时,它会阻塞直到另一个线程调用 take方法为止,反之亦然。与 Exchanger 的情 况不同, 数据仅仅沿一个方向传递,从生产者到消费者。 即使 SynchronousQueue 类实现了 BlockingQueue 接口, 概念上讲, 它依然不是一个队 列。它没有包含任何元素,它的 size方法总是返回 0。

线程与 Swing

当程序需要做某些耗时的工作时,应该启动另一个工作器线程而不是阻塞用户接口。

但是,必须认真考虑工作器线程在做什么,因为这或许令人惊讶,Swing不是线程安全 的。如果你试图在多个线程中操纵用户界面的元素,那么用户界面可能崩溃。 要了解这一问题,运行程序清单 14-13 中的测试程序。当你点击 Bad 按钮时, 一个新的 线程将启动,它的 run方法操作一个组合框,随机地添加值和删除值。

public void run() ( 
    try { while (true) { 
        int i = Math.abs(generator.nextlnt()); 
        if (i X 2 = 0) 
            combo.insertltemAt(new Integer(i), 0); 
        else if (combo.getltemCountO > 0) 
            combo.renioveIteinAt(i % combo.getltemCountO);
        sleep(1);
   } catch (InterruptedException e) {}
        }
} 

试试看。点击 Bad 按钮。点击几次组合框, 移动滚动条, 移动窗口, 再次点击 Bad 按 钮, 不断点击组合框。最终,你会看到一个异常报告(见下图 )。

运行耗时的任务

将线程与 Swing—起使用时, 必须遵循两个简单的原则。

( 1 ) 如果一个动作需要花费很长时间,在一个独立的工作器线程中做这件事不要在事件 分配线程中做。

( 2 ) 除了事件分配线程,不要在任何线程中接触 Swing 组件。

不可以从自己的线程中调用 label.setText,而应该使用 EventQueue类的 invokeLater方法 和 invokeAndWait方法使所调用的方法在事件分配线程中执行。

应该将 Swing 代码放置到实现 Runnable 接口的类的 run方法中。然后,创建该类的一个 对象,将其传递给静态的 invokeLater或 invokeAndWait方法。例如, 下面是如何更新标签内 容的代码:

EventQueue.invokeLater(()->{ 
    label.setText(percentage + "56 complete"); 
});

当事件放人事件队列时,invokeLater方法立即返回,而run方法被异步执行。invokeAnd Wait方法等待直到 run方法确实被执行过为止。

在更新进度标签时, invokeLater方法更适宜。用户更希望让工作器线程有更快完成工作 而不是得到更加精确的进度指示器。

这两种方法都是在事件分配线程中执行 run 方法。没有新的线程被创建。

程序清单 14-13 演示了如何使用 invokeLater方法安全地修改组合框的内容。如果点击 Good 按钮, 线程插人或移除数字。但是,实际的修改是发生在事件分配线程中。

//程序清单 14-13 swing/SwingThreadTest.java 
package swing;

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;

/**
 * This program demonstrates that a thread that runs in parallel with the event dispatch thread can
 * cause errors in Swing components.
 * @version 1.23 2007-05-17
 * @author Cay Horstmann
 */
public class SwingThreadTest
{
   public static void main(String[] args)
   {
      EventQueue.invokeLater(new Runnable()
         {
            public void run()
            {
               JFrame frame = new SwingThreadFrame();
               frame.setTitle("SwingThreadTest");
               frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
               frame.setVisible(true);
            }
         });
   }
}

/**
 * This frame has two buttons to fill a combo box from a separate thread. The "Good" button uses the
 * event queue, the "Bad" button modifies the combo box directly.
 */
class SwingThreadFrame extends JFrame
{
   public SwingThreadFrame()
   {
      final JComboBox<Integer> combo = new JComboBox<>();
      combo.insertItemAt(Integer.MAX_VALUE, 0);
      combo.setPrototypeDisplayValue(combo.getItemAt(0));
      combo.setSelectedIndex(0);

      JPanel panel = new JPanel();

      JButton goodButton = new JButton("Good");
      goodButton.addActionListener(new ActionListener()
         {
            public void actionPerformed(ActionEvent event)
            {
               new Thread(new GoodWorkerRunnable(combo)).start();
            }
         });
      panel.add(goodButton);
      JButton badButton = new JButton("Bad");
      badButton.addActionListener(new ActionListener()
         {
            public void actionPerformed(ActionEvent event)
            {
               new Thread(new BadWorkerRunnable(combo)).start();
            }
         });
      panel.add(badButton);

      panel.add(combo);
      add(panel);
      pack();
   }
}

/**
 * This runnable modifies a combo box by randomly adding and removing numbers. This can result in
 * errors because the combo box methods are not synchronized and both the worker thread and the
 * event dispatch thread access the combo box.
 */
class BadWorkerRunnable implements Runnable
{
   private JComboBox<Integer> combo;
   private Random generator;

   public BadWorkerRunnable(JComboBox<Integer> aCombo)
   {
      combo = aCombo;
      generator = new Random();
   }

   public void run()
   {
      try
      {
         while (true)
         {
            int i = Math.abs(generator.nextInt());
            if (i % 2 == 0) combo.insertItemAt(i, 0);
            else if (combo.getItemCount() > 0) combo.removeItemAt(i % combo.getItemCount());
            Thread.sleep(1);
         }
      }
      catch (InterruptedException e)
      {
      }
   }
}

/**
 * This runnable modifies a combo box by randomly adding and removing numbers. In order to ensure
 * that the combo box is not corrupted, the editing operations are forwarded to the event dispatch
 * thread.
 */
class GoodWorkerRunnable implements Runnable
{
   private JComboBox<Integer> combo;
   private Random generator;

   public GoodWorkerRunnable(JComboBox<Integer> aCombo)
   {
      combo = aCombo;
      generator = new Random();
   }

   public void run()
   {
      try
      {
         while (true)
         {
            EventQueue.invokeLater(new Runnable()
               {
                  public void run()
                  {
                     int i = Math.abs(generator.nextInt());
                     if (i % 2 == 0) combo.insertItemAt(i, 0);
                     else if (combo.getItemCount() > 0) combo.removeItemAt(i
                           % combo.getItemCount());
                  }
               });
            Thread.sleep(1);
         }
      }
      catch (InterruptedException e)
      {
      }
   }
}

使用 Swing 工作线程

SwingWorker 类使后台任务的实现不那么繁琐。 程序清单 14-14 中的程序有加载文本文件的命令和取消加载过程的命令。应该用一个长 的文件来测试这个程序, 例如 The Count of Monte Cristo 的全文, 它在本书的附赠代码的 gutenberg 目录下。该文件在一个单独的线程中加载。在读取文件的过程中, Open 菜单项被 禁用, Cancel 菜单项为可用。读取每一行后,状态条中的线性计数器被更新。 读取过程完成之后, Open 菜单项重新变为可用, Cancel 项被禁用,状态行文本置为 Done。

这个例子展示了后台任务的典型 UI 活动:

•在每一个工作单位完成之后,更新 UI 来 显示进度。

•整个工作完成之后, 对 U丨做最后的更新。

SwingWorker 类使得实现这一任务轻而易 举。 覆盖 doInBackground 方法来完成耗时的工 作, 不时地调用 publish 来报告工作进度。这 一方法在工作器线程中执行。publish 方法使得 process 方法在事件分配线程中执行来处理进度数据。当工作完成时, done方法在事件分配线程中被调用以便完成 UI 的更新。

为了向用户展示进度,要在状态行中显示读入的行数。因此, 进度数据包含当前行号以 及文本的当前行。将它们打包到一个普通的内部类中:

private class ProgressData { 

	public int number; 

	public String line; 

} 

最后的结果是已经读人 StringBuilder 的文本。因此, 需要一个 SwingWorker<StringBuilder, ProgressData〉。

在 doInBackground方法中, 读取一个文件, 每次一行。在读取每一行之后, 调用 publish方法发布行号和当前行的文本。

@Override public StringBuilder doInBackground() throws IOException, InterruptedException { 
    int lineNumber = 0; 
    Scanner in = new Scanner(new FilelnputStream(file), "UTF-8"); 
    while (in.hasNextLine()) { 
        String line = in.nextLine(); 
        lineNumber++; text.append(line).append("\n"); 
        ProgressData data = new ProgressDataO: data.number = lineNumber; 
        data.line = line; publish(data); Thread.sleep(l ); // to test cancellation; no need to do this in your programs
    }
        return text;
}

在读取每一行之后休眠 1 毫秒, 以便不使用重读就可以检测取消动作, 但是, 不要使用 休眠来减慢程序的执行速度。如果对这一行加注解, 会发现 The Count of Monte Cristo 的加 载相当快, 只有几批用户接口更新。

//程序清单 14-14 swingWorker/SwingWorkerTest.java
package swingWorker;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.util.List;
import java.util.concurrent.*;

import javax.swing.*;

/**
 * This program demonstrates a worker thread that runs a potentially time-consuming task.
 * @version 1.1 2007-05-18
 * @author Cay Horstmann
 */
public class SwingWorkerTest
{
   public static void main(String[] args) throws Exception
   {
      EventQueue.invokeLater(new Runnable()
         {
            public void run()
            {
               JFrame frame = new SwingWorkerFrame();
               frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
               frame.setVisible(true);
            }
         });
   }
}

/**
 * This frame has a text area to show the contents of a text file, a menu to open a file and cancel
 * the opening process, and a status line to show the file loading progress.
 */
class SwingWorkerFrame extends JFrame
{
   private JFileChooser chooser;
   private JTextArea textArea;
   private JLabel statusLine;
   private JMenuItem openItem;
   private JMenuItem cancelItem;
   private SwingWorker<StringBuilder, ProgressData> textReader;
   public static final int TEXT_ROWS = 20;
   public static final int TEXT_COLUMNS = 60;

   public SwingWorkerFrame()
   {
      chooser = new JFileChooser();
      chooser.setCurrentDirectory(new File("."));

      textArea = new JTextArea(TEXT_ROWS, TEXT_COLUMNS);
      add(new JScrollPane(textArea));

      statusLine = new JLabel(" ");
      add(statusLine, BorderLayout.SOUTH);

      JMenuBar menuBar = new JMenuBar();
      setJMenuBar(menuBar);

      JMenu menu = new JMenu("File");
      menuBar.add(menu);

      openItem = new JMenuItem("Open");
      menu.add(openItem);
      openItem.addActionListener(new ActionListener()
         {
            public void actionPerformed(ActionEvent event)
            {
               // show file chooser dialog
               int result = chooser.showOpenDialog(null);

               // if file selected, set it as icon of the label
               if (result == JFileChooser.APPROVE_OPTION)
               {
                  textArea.setText("");
                  openItem.setEnabled(false);
                  textReader = new TextReader(chooser.getSelectedFile());
                  textReader.execute();
                  cancelItem.setEnabled(true);
               }
            }
         });

      cancelItem = new JMenuItem("Cancel");
      menu.add(cancelItem);
      cancelItem.setEnabled(false);
      cancelItem.addActionListener(new ActionListener()
         {
            public void actionPerformed(ActionEvent event)
            {
               textReader.cancel(true);
            }
         });
      pack();
   }

   private class ProgressData
   {
      public int number;
      public String line;
   }

   private class TextReader extends SwingWorker<StringBuilder, ProgressData>
   {
      private File file;
      private StringBuilder text = new StringBuilder();

      public TextReader(File file)
      {
         this.file = file;
      }

      // The following method executes in the worker thread; it doesn't touch Swing components.

      @Override
      public StringBuilder doInBackground() throws IOException, InterruptedException
      {
         int lineNumber = 0;
         try (Scanner in = new Scanner(new FileInputStream(file)))
         {
            while (in.hasNextLine())         
            {
               String line = in.nextLine();
               lineNumber++;
               text.append(line);
               text.append("\n");
               ProgressData data = new ProgressData();
               data.number = lineNumber;
               data.line = line;
               publish(data);
               Thread.sleep(1); // to test cancellation; no need to do this in your programs
            }
         }
         return text;
      }

      // The following methods execute in the event dispatch thread.

      @Override
      public void process(List<ProgressData> data)
      {
         if (isCancelled()) return;
         StringBuilder b = new StringBuilder();
         statusLine.setText("" + data.get(data.size() - 1).number);
         for (ProgressData d : data)
         {
            b.append(d.line);
            b.append("\n");
         }
         textArea.append(b.toString());
      }

      @Override
      public void done()
      {
         try
         {
            StringBuilder result = get();
            textArea.setText(result.toString());
            statusLine.setText("Done");
         }
         catch (InterruptedException ex)
         {
         }
         catch (CancellationException ex)
         {
            textArea.setText("");
            statusLine.setText("Cancelled");
         }
         catch (ExecutionException ex)
         {
            statusLine.setText("" + ex.getCause());
         }

         cancelItem.setEnabled(false);
         openItem.setEnabled(true);
      }

   };
}

单一线程规则

每一个 Java 应用程序都开始于主线程中的 main方法。在 Swing 程序中,main方法的 生命周期是很短的。它在事件分配线程中规划用户界面的构造然后退出。在用户界面构造之 后,事件分配线程会处理事件通知, 例如调用 actionPerformed 或 paintComponent。其他线程 在后台运行, 例如将事件放入事件队列的进程,但是那些线程对应用程序员是不可见的。

对于单一线程规则存在一些例外情况。

•可在任一个线程里添加或移除事件监听器。 当然该监听器的方法会在事件分配线程中 被触发。 •只有很少的 Swing方法是线程安全的。在 API 文档中用这样的句子特别标明: “ 尽管 大多数 Swing 方法不是线程安全的, 但这个方法是。 ” 在这些线程安全的方法中最有 用的是:

JTextComponent.setText 
JTextArea.insert 
JTextArea.append 
JTextArea.replaceRange 
JCouponent.repaint 
JComponent.revalidate
posted on 2020-08-22 21:43  ♌南墙  阅读(320)  评论(0)    收藏  举报