Java编程之多线程&并发编程(上)

2017-07-21  本文已影响210人  vancent

1. 介绍-多任务和多线程

Java 支持单线程和多线程,

Java支持在一个程序中并发运行多个线程以实现并发编程。线程,也称作轻量进程,是一个具有确定起点和终点的单一顺序控制流。线程的生命周期中只有一个执行点。一个线程不能独立存在,它必须是进程的一部分。

下图所示为在一个单独CPU下运行的三线程程序:


单CPU下运行的三线程程序单CPU下运行的三线程程序

1.1 多任务(或多进程)

现在的操作系统(如Windows和 UNIX)都是多任务系统,多任务系统能通过共享计算资源如CPUs,主内存,I/O通道等并发执行多任务。在单CPU机器中同一时间(CPU时间片)只有一个任务被执行;而在多CPU机器中多任务能在CPU之间或者是在时间片上同时被执行。

多任务在当今的操作系统中被大量运用,通过充分利用并优化计算资源的使用能实现更高执行效率。通常有以下两种多任务操作系统:

  1. 协同式多任务系统(cooperative multitasking system):每个任务必须自发将资源控制权交与其他任务。这种系统的缺点是失控或没能实现协同操作的任务可能会挂起整个系统;
  2. 抢占式多任务系统(Pre-emptive multitasking systems):任务被分配CPU(s)时间片,一旦分配耗尽将强制将控制权交与其他任务。

1.2 多线程 (进程中)

在UNIX中用fork命令创建新进程,而在Windows中通过启动一个程序创建新进程。一个进程或程序有自己的地址空间和控制区块。之所以被称为重量级(heavyweight)是因为它消耗更多的系统资源。在一个进程或程序中,我们能并发运行多个线程以提高处理效率。

线程,不同于重量级进程,轻量并在进程中运行-它们共享相同的地址空间、资源和环境,之所以谓之轻量是因为它在重量级进程中运行且利用系统为该程序及其环境分配的资源。线程必须在运行的进程中划分出自己的资源,如:线程有自己的堆栈,寄存器和程序计数器。运行在线程中的代码仅在那个环境(context)中运行,因此线程(含有线性执行流)也称为执行上下文(Execution context)。

程序中的多线程通过优化系统资源的使用提升了程序的运行效率,如:当一个线程被阻塞(等待I/O操作的完成),另一个线程能利用CPU时间执行计算,这样能获得更高的效率和更大的吞吐量。

多线程在提供更好的用户交互方面也很重要。例如,在文本处理器中,当一个线程正在打印或保存文件的时候,另一个线程能继续打字。在图形用户界面(GUI)应用中,多线程对于提供响应式用户界面而言必不可少。

在这篇文章里我们用到Swing应用来进行详细说明,因为Swing应用依赖于多线程(执行特有的函数,重回并加工事件),比较适合用来说明多线程的应用。

典型的Java程序在单一进程中运行,一般不会用到多个进程,不过在该进程中经常会用到多线程来并行处理多个任务。一个独立的Java应用以关联main()方法的单一线程(主线程)开始,之后该主线程可以启动更多新线程。

2. "界面无响应"

计数程序计数程序

通过下面计数Swing程序能最好地说明这个常见的“界面无响应”(Unresponsive User Interface(UI))问题。

该GUI程序有两个按钮。点击“Start Counting”开始计数,点击“Stop Counting”停止(暂停)计数。这两个处理函数通过一个名为“stop”的布尔值通信。Stop按钮设置“stop”标记; Start按钮检查在继续下个计数的时候是否已经设置了“stop”标记。

在Eclipse或NetBeans下面编写程序来追踪这些线程的执行情况。

2.1 例1: 无响应 UI

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
 
/** Illustrate Unresponsive UI problem caused by "busy" Event-Dispatching Thread */
public class UnresponsiveUI extends JFrame {
    private boolean stop = false;  // start or stop the counter
    private JTextField tfCount;
    private int count = 1;
 
    /** Constructor to setup the GUI components */
    public UnresponsiveUI() {
        Container cp = this.getContentPane();
        cp.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 10));
        cp.add(new JLabel("Counter"));
        tfCount = new JTextField(count + "", 10);
        tfCount.setEditable(false);
        cp.add(tfCount);

        JButton btnStart = new JButton("Start Counting");
        cp.add(btnStart);
        btnStart.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent evt) {
                stop = false;
                for (int i = 0; i < 100000; ++i) {
                    if (stop) break;  // check if STOP button has been pushed,
                                      //  which changes the stop flag to true
                    tfCount.setText(count + "");
                    ++count;
                }
            }
        });
        JButton btnStop = new JButton("Stop Counting");
        cp.add(btnStop);
        btnStop.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent evt) {
                stop = true;  // set the stop flag
            }
        });

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setTitle("Counter");
        setSize(300, 120);
        setVisible(true);
    }
 
    /** The entry main method */
    public static void main(String[] args) {
        // Run GUI codes in Event-Dispatching thread for thread safety
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new UnresponsiveUI();  // Let the constructor do the job
            }
        });
    }
}

运行程序可知:一旦点击START按钮,UI界面就像挂起了一样–计数值并未更新(即显示无刷新),且用户界面没有响应STOP按钮的点击以及任何其他的用户交互。

追踪线程(高级)

从程序追踪(通过Eclipse/NetBeans),我们观察到:

  1. 在主线程中启动Main()方法;
  2. JRE的窗口子系统通过SwingUtilities.invokeLater()启动三个线程: "AWT-Windows"(守护线程), "AWT-Shutdown"和"AWT-EventQueue-0"。"AWT-EventQueue-0"作为事件分发线程(Event-Dispatching Thread (EDT)),负责处理所有事件(如按钮的点击)及界面刷新显示以保证操作GUI和操控GUI组件线程安全。指定构造函数 UnresponsiveUI()在事件分发线程上通过invokeLater()运行,“主”线程在main()方法完成后退出。新线程 "DestroyJavaVM" 随之创建;
  3. 当点击START按钮的时候,actionPerformed()在EDT上运行,EDT现在完全被(计算密集型的)计数循环所占用。换言之,当计数正在运行的时候,EDT处于busy状态并且不能够处理任何事件(如:点击STOP按钮或关闭窗口按钮)及刷新显示-直到计数完成且EDT解除占用。其表现为显示会在计数循环完成之前一直处于冻结状态。

推荐在EDT上通过invokeLater()运行GUI创建代码,因为GUI组件中很多都不能保证线程安全。在单一线程中引导对GUI组件的访问能保证线程安全。假定直接在main()方法(在主线程下)中运行构造函数,如下:

public static void main(String[] args) {
    new UnresponsiveUI();
}

跟踪显示:

  1. 在主线程中启动Main()方法;
  2. 新线程“AWT-Windows”(守护线程)在进入构造函数“new UnresponsiveUI()”(因为“extends JFrame”)时启动;
  3. 在执行“setVisible(true)”之后,另外两个线程"AWT-Shutdown"和"AWT-EventQueue-0"(即EDT)被创建;
  4. 主线程在main()方法完成之后退出,新线程 "DestroyJavaVM" 随之创建;
  5. 此时有四个线程正在运行 - "AWT-Windows", "AWT-Shutdown", "AWT-EventQueue-0 (EDT)" 和 "DestroyJavaVM";
  6. 点击START按钮会在EDT上运行actionPerformed()。

在之前的例子里,EDT通过invokeLater()启动;而在后面的例子中EDT在setVisible()之后启动。

2.2 例2: 带线程而依然无响应UI

不使用EDT,而是新建一个线程来计数,如下:

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

/** Illustrate the Unresponsive UI problem caused by "starved" event-dispatching thread */
public class UnresponsiveUIwThread extends JFrame {
    private boolean stop = false;
    private JTextField tfCount;
    private int count = 1;
 
    /** Constructor to setup the GUI components */
    public UnresponsiveUIwThread() {
        Container cp = getContentPane();
        cp.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 10));
        cp.add(new JLabel("Counter"));
        tfCount = new JTextField(count + "", 10);
        tfCount.setEditable(false);
        cp.add(tfCount);
    
        JButton btnStart = new JButton("Start Counting");
        cp.add(btnStart);
        btnStart.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent evt) {
                stop = false;
                // Create our own Thread to do the counting
                Thread t = new Thread() {
                    @Override
                    public void run() {  // override the run() to specify the running behavior
                        for (int i = 0; i < 100000; ++i) {
                            if (stop) break;
                            tfCount.setText(count + "");
                            ++count;
                        }
                    }
                };
                t.start();  // call back run()
            }
        });
 
        JButton btnStop = new JButton("Stop Counting");
        cp.add(btnStop);
        btnStop.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent evt) {
                stop = true;
            }
        });
 
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setTitle("Counter");
        setSize(300, 120);
        setVisible(true);
    }
 
    /** The entry main method */
    public static void main(String[] args) {
        // Run GUI codes in Event-Dispatching thread for thread safety
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new UnresponsiveUIwThread();  // Let the constructor do the job
            }
        });
    }
}

通过Thread类创建一个新线程,重写 run ()方法来进行计数。创建一个实例,调用实例的start ()会在自身线程上执行run ()方法(创建新线程细节会在后面介绍)。

响应较之前有轻微的改善。但是计数值还是没有显示,而且STOP按钮响应有延迟。(在双核处理器下可能看不到这种差异)

这是因为计数线程本身不会自动将资源控制权交与EDT,EDT便不能更新显示并响应STOP按钮。再者,JVM会根据调度算法强制计数线程放弃对资源的控制, 这会导致在更新显示上的延迟 (尚未确证).

跟踪和线程 (高级)

当点击START按钮时,一个名为Thread-n(n是一个流水号)的线程被创建运行计数循环,此线程不会将资源控制权交与其他线程,尤其是EDT。

而这个程序比之前好一点:显示会更新,且STOP按钮在一些延时之后起作用。

2.3 例3: 带线程响应式UI

对程序作以下修改:调用计数线程的sleep ()方法以使计数线程将资源控制权交与EDT来更新显示并响应STOP按钮。计数线程现在可以按照预期运行。Sleep ()方法也提供必需的延迟。

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
 
/** Resolve the unresponsive UI problem by running the compute-intensive task
    in this own thread, which yields control to the EDT regularly */
public class UnresponsiveUIwThreadSleep extends JFrame {
   private boolean stop = false;
   private JTextField tfCount;
   private int count = 1;
 
   /** Constructor to setup the GUI components */
   public UnresponsiveUIwThreadSleep() {
      Container cp = getContentPane();
      cp.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 10));
      cp.add(new JLabel("Counter"));
      tfCount = new JTextField(count + "", 10);
      tfCount.setEditable(false);
      cp.add(tfCount);
 
      JButton btnStart = new JButton("Start Counting");
      cp.add(btnStart);
      btnStart.addActionListener(new ActionListener() {
         @Override
         public void actionPerformed(ActionEvent evt) {
            stop = false;
            // Create a new Thread to do the counting
            Thread t = new Thread() {
               @Override
               public void run() {  // override the run() for the running behaviors
                  for (int i = 0; i < 100000; ++i) {
                     if (stop) break;
                     tfCount.setText(count + "");
                     ++count;
                     // Suspend this thread via sleep() and yield control to other threads.
                     // Also provide the necessary delay.
                     try {
                        sleep(10);  // milliseconds
                     } catch (InterruptedException ex) {}
                  }
               }
            };
            t.start();  // call back run()
         }
      });
 
      JButton btnStop = new JButton("Stop Counting");
      cp.add(btnStop);
      btnStop.addActionListener(new ActionListener() {
         @Override
         public void actionPerformed(ActionEvent evt) {
            stop = true;
         }
      });
 
      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      setTitle("Counter");
      setSize(300, 120);
      setVisible(true);
   }
 
   /** The entry main method */
   public static void main(String[] args) {
      // Run GUI codes in Event-Dispatching thread for thread safety
      javax.swing.SwingUtilities.invokeLater(new Runnable() {
         @Override
         public void run() {
            new UnresponsiveUIwThreadSleep();  // Let the constructor do the job
         }
      });
   }
}

sleep()方法暂停当前线程并将其置于等待特定时间的状态,另一个线程开始执行(在单CPU环境中)。调用线程interrupt()方法能中断sleep(),但是会引发InterruptedException 。

在这个例子中,创建的计数线程("Thread-n")在每次计数之后通过sleep(10)将资源控制权交与其他线程,EDT就可以更新界面显示并在每次计数之后响应“STOP”按钮。

2.4 例4: SwingWorker

JDK 1.6 提供了一个新的javax.swing.SwingWorker类,它能用来在后台线程中执行计算密集型任务 并将最后结果或中间结果传递给在EDT上运行的方法。关于SwingWorker 的讨论会在后面的章节中进行。

翻译自:
https://www3.ntu.edu.sg/home/ehchua/programming/java/j5e_multithreading.html

Java编程之多线程&并发编程(中)
Java编程之多线程&并发编程(下)

上一篇下一篇

猜你喜欢

热点阅读