第 十四 章 并 发

什么是线程

线程Thread

• 进程内部的一个执行单元,它是程序中一个单一的顺序控制流程。

• 线程又被称为轻量级进程(lightweight process)

• 如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为多线程

线程特点

• 轻量级进程

• 独立调度的基本单位

• 可并发执行

• 共享进程资源

线程和进程的区别

线程的创建

•方式1:继承Java.lang.Thread类,并覆盖run() 方法

• 方式2:实现Java.lang.Runnable接口,并实现run() 方法

• 方法run( )称为线程体。

线程的启动

• 新建的线程不会自动开始运行,必须通过start( )方法启动

• 不能直接调用run()来启动线程,这样run()将作为一个普通方法立即执行,执行完毕前其他线 程无法兵法执行

• Java程序启动时,会立刻创建主线程,main就是在这个线程上运行。当不再产生新线程时, 程序是单线程的

两种线程创建方式的比较

• 继承Thread类方式的多线程

• 优势:编写简单

• 劣势:无法继承其它父类 • 实现Runnable接口方式的多线程

• 优势:可以继承其它类,多线程可共享同一个Runnable对象

• 劣势:编程方式稍微复杂,如果需要访问当前线程,需要调用Thread.currentThread()方 法

• 实现Runnable接口方式要通用一些。

Thread类常用方法

 

当点击 Start 按钮时, 程序将从屏幕的左上角弹出一个球,这个球便开始弹跳。Start 按 钮的处理程序将调用 addBall 方法。这个方法循 环运行 1000 次 move。 每调用一次 move, 球就 会移动一点, 当碰到墙壁时, 球将调整方向, 并重新绘制面板。

Ball ball = new Ball(); 
panel.add(ball); 
for (int i = 1 ;i <= STEPS;i++) {
    ball.move(panel.getBounds()); 
    panel,paint(panel.getCraphics()); 
    Thread.sleep(DELAY); 
} 

 

调用 Threadsleep 不会创建一个新线程, sleep 是 Thread 类的静态方法,用于暂停当前线 程的活动。 sleep方法可以抛出一个 IntermptedException 异常。稍后将讨论这个异常以及对它的处 理。现在,只是在发生异常时简单地终止弹跳。

 

程序清单14-1 ~ 程序清单 14-3 给出了这个程序的代码。

//程序清单 14-1 bounce/Bounce.java 
package bounce;
​
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
​
/**
 * Shows an animated bouncing ball.
 * @version 1.33 2007-05-17
 * @author Cay Horstmann
 */
public class Bounce
{
   public static void main(String[] args)
   {
      EventQueue.invokeLater(new Runnable()
         {
            public void run()
            {
               JFrame frame = new BounceFrame();
               frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
               frame.setVisible(true);
            }
         });
   }
}
​
/**
 * The frame with ball component and buttons.
 */
class BounceFrame extends JFrame
{
   private BallComponent comp;
   public static final int STEPS = 1000;
   public static final int DELAY = 3;
​
   /**
    * Constructs the frame with the component for showing the bouncing ball and Start and Close
    * buttons
    */
   public BounceFrame()
   {
      setTitle("Bounce");
​
      comp = new BallComponent();
      add(comp, BorderLayout.CENTER);
      JPanel buttonPanel = new JPanel();
      addButton(buttonPanel, "Start", new ActionListener()
         {
            public void actionPerformed(ActionEvent event)
            {
               addBall();
            }
         });
​
      addButton(buttonPanel, "Close", new ActionListener()
         {
            public void actionPerformed(ActionEvent event)
            {
               System.exit(0);
            }
         });
      add(buttonPanel, BorderLayout.SOUTH);
      pack();
   }
​
   /**
    * Adds a button to a container.
    * @param c the container
    * @param title the button title
    * @param listener the action listener for the button
    */
   public void addButton(Container c, String title, ActionListener listener)
   {
      JButton button = new JButton(title);
      c.add(button);
      button.addActionListener(listener);
   }
​
   /**
    * Adds a bouncing ball to the panel and makes it bounce 1,000 times.
    */
   public void addBall()
   {
      try
      {
         Ball ball = new Ball();
         comp.add(ball);
​
         for (int i = 1; i <= STEPS; i++)
         {
            ball.move(comp.getBounds());
            comp.paint(comp.getGraphics());
            Thread.sleep(DELAY);
         }
      }
      catch (InterruptedException e)
      {
      }
   }
}
​
//程序清单 14-2 bounce/Ball.java 
package bounce;
​
import java.awt.geom.*;
​
/**
 * A ball that moves and bounces off the edges of a rectangle
 * @version 1.33 2007-05-17
 * @author Cay Horstmann
 */
public class Ball
{
   private static final int XSIZE = 15;
   private static final int YSIZE = 15;
   private double x = 0;
   private double y = 0;
   private double dx = 1;
   private double dy = 1;
​
   /**
    * Moves the ball to the next position, reversing direction if it hits one of the edges
    */
   public void move(Rectangle2D bounds)
   {
      x += dx;
      y += dy;
      if (x < bounds.getMinX())
      {
         x = bounds.getMinX();
         dx = -dx;
      }
      if (x + XSIZE >= bounds.getMaxX())
      {
         x = bounds.getMaxX() - XSIZE;
         dx = -dx;
      }
      if (y < bounds.getMinY())
      {
         y = bounds.getMinY();
         dy = -dy;
      }
      if (y + YSIZE >= bounds.getMaxY())
      {
         y = bounds.getMaxY() - YSIZE;
         dy = -dy;
      }
   }
​
   /**
    * Gets the shape of the ball at its current position.
    */
   public Ellipse2D getShape()
   {
      return new Ellipse2D.Double(x, y, XSIZE, YSIZE);
   }
}

 

使用线程给其他任务提供机会

下面是在一个单独的线程中执行一个任务的简单过程:

1 ) 将任务代码移到实现了 Runnable 接口的类的 run方法中。这个接口非常简单,只有 一个方法:

public interface Runnable {

  void run();

}

 

由于 Runnable 是一个函数式接口,可以用 lambda 表达式建立一个实例:

Runnable r = ()-> { taskcode};

2 ) 由 Runnable 创建一个 Thread 对象:

Thread t = new Thread(r);

3 ) 启动线程:

t.start();

要想将弹跳球代码放在一个独立的线程中, 只需要实现一个类 BallRunnable, 然后,将 动画代码放在 nm 方法中,如同下面这段代码:

Runnable r = ()-> { 
    try { 
        for (int i = 1 ; i <=: STEPS; i++) {
            ball.move(comp.getBounds()); 
            comp.repaint(); 
            Thread.sieep(DELAY); 
        } 
    } catch (InterruptedException e) {
    
    } 
} ; 
Thread t = new Thread(r); 
t.start();

 

同样地, 需要捕获 sleep方法可能抛出的异常 InterruptedException。下一节将讨论这个异常。在 一般情况下, 线程在中断时被终止。因此,当发生 InterruptedException 异常时, run方法将结束执行。

完整的代码见程序清单 14-4。

[警告]  不要调用 Thread 类或 Runnable 对象的 run 方法。直接调用 run 方法, 只会执行同 一个线程中的任务, 而不会启动新线程。应该调用 Thread.start 方法。这个方法将创建一 个执行 ran 方法的新线程。

 

//程序清单 14-4 bounceThread/BounceThread.java 
package bounceThread;
​
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
​
/**
 * Shows animated bouncing balls.
 * @version 1.33 2007-05-17
 * @author Cay Horstmann
 */
public class BounceThread
{
   public static void main(String[] args)
   {
      EventQueue.invokeLater(new Runnable()
         {
            public void run()
            {
               JFrame frame = new BounceFrame();
               frame.setTitle("BounceThread");
               frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
               frame.setVisible(true);
            }
         });
   }
}
​
/**
 * A runnable that animates a bouncing ball.
 */
class BallRunnable implements Runnable
{
   private Ball ball;
   private Component component;
   public static final int STEPS = 1000;
   public static final int DELAY = 5;
​
   /**
    * Constructs the runnable.
    * @param aBall the ball to bounce
    * @param aComponent the component in which the ball bounces
    */
   public BallRunnable(Ball aBall, Component aComponent)
   {
      ball = aBall;
      component = aComponent;
   }
​
   public void run()
   {
      try
      {
         for (int i = 1; i <= STEPS; i++)
         {
            ball.move(component.getBounds());
            component.repaint();
            Thread.sleep(DELAY);
         }
      }
      catch (InterruptedException e)
      {
      }
   }
​
}
​
/**
 * The frame with panel and buttons.
 */
class BounceFrame extends JFrame
{
   private BallComponent comp;
​
   /**
    * Constructs the frame with the component for showing the bouncing ball and Start and Close
    * buttons
    */
   public BounceFrame()
   {
      comp = new BallComponent();
      add(comp, BorderLayout.CENTER);
      JPanel buttonPanel = new JPanel();
      addButton(buttonPanel, "Start", new ActionListener()
         {
            public void actionPerformed(ActionEvent event)
            {
               addBall();
            }
         });
​
      addButton(buttonPanel, "Close", new ActionListener()
         {
            public void actionPerformed(ActionEvent event)
            {
               System.exit(0);
            }
         });
      add(buttonPanel, BorderLayout.SOUTH);
      pack();
   }
​
   /**
    * Adds a button to a container.
    * @param c the container
    * @param title the button title
    * @param listener the action listener for the button
    */
   public void addButton(Container c, String title, ActionListener listener)
   {
      JButton button = new JButton(title);
      c.add(button);
      button.addActionListener(listener);
   }
​
   /**
    * Adds a bouncing ball to the canvas and starts a thread to make it bounce
    */
   public void addBall()
   {
      Ball b = new Ball();
      comp.add(b);
      Runnable r = new BallRunnable(b, comp);
      Thread t = new Thread(r);
      t.start();
   }
}

 

•Thread(Runnable target )

构造一个新线程, 用于调用给定目标的 nm() 方法。

•void start( )

启动这个线程,将引发调用 mn() 方法。这个方法将立即返回,并且新线程将并发运行。 參 void run( ) 调用关联 Runnable 的 run 方法。

•void run( )

必须覆盖这个方法, 并在这个方法中提供所要执行的任务指令。

中断线程

当线程的 run 方法执行方法体中最后一条语句后, 并经由执行 return 语句返回时, 或者出现了在方法中没有捕获的异常时,线程将终止。

没有可以强制线程终止的方法。然而,interrupt 方法可以用来请求终止线程。

当对一个线程调用 interrupt 方法时,线程的中断状态将被置位。这是每一个线程都具有 的 boolean 标志。每个线程都应该不时地检査这个标志, 以判断线程是否被中断。

要想弄清中断状态是否被置位,首先调用静态的 Thread.currentThread方法获得当前线 程,然后调用 islnterrupted方法:

while (!Thread.currentThread().islnterrupted() && more work todo) {
     domorework 
 } 

 

但是, 如果线程被阻塞, 就无法检测中断状态。这是产生 InterruptedExceptioii 异常的地 方。当在一个被阻塞的线程(调用 sleep 或 wait) 上调用 interrupt方法时,阻塞调用将会被 Interrupted Exception 异常中断。)

 

[注]  有两个非常类似的方法,interrupted 和 islnterrupted。Interrupted 方法是一个静态 方法, 它检测当前的线程是否被中断。 而且, 调用 interrupted 方法会清除该线程的中断 状态。另一方面,islnterrupted 方法是一个实例方法,可用来检验是否有线程被中断。调 用这个方法不会改变中断状态。

在很多发布的代码中会发现 InterruptedException 异常被抑制在很低的层次上, 像这样:

 void mySubTask() {
     . . .
     try { 
         sleep(delay); 
     } catch (InterruptedException e){
         
     } // Don't ignore!
     . . .
} 

 

不要这样做! 如果不认为在 catch 子句中做这一处理有什么好处的话,仍然有两种合理 的选择:

•在 catch 子句中调用 Thread.currentThread().interrupt() 来设置中断状态。于是,调用者 可以对其进行检测。

void mySubTask() {
    . . .
    try { 
        sleep(delay); 
    } catch (InterruptedException e) { 
        Thread.currentThread().interrupt(); 
    }
    . . .
}

 

•或者,更好的选择是,用 throws InterruptedException标记你的方法, 不采用 try语句 块捕获异常。于是,调用者(或者,最终的 run 方法)可以捕获这一异常。

 void mySubTask() throws InterruptedException{
     . . .
     sleep(delay);
     . . .
} 

 

线程状态

• 新生状态:

用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start进入就绪状态

• 就绪状态:

处于就绪状态线程具备了运行条件,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU

当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称之为“cpu调度”。

• 运行状态:

在运行状态的线程执行自己的run方法中代码,直到等待某资源而阻塞或完成任务而死亡。

如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。

• 阻塞状态:

处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进 入阻塞状态。

在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入 就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。

• 死亡状态:

死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有三个。一个是正常运行的线程完成了它的全部工作;另一个是线 程被强制性地终止,如通过执行stop方法来终止一个线程【不推荐使用】,三是线程抛出未捕获的异常

要确定一个线程的当前状态, 可调用 getState 方法。

线程属性

线程优先级

每一个线程有一个优先级。默认情况下,一+线程继承它的父 线程的优先级。可以用 setPriority 方法提高或降低任何一个线程的优先级。可以将优先级设 置为在 MIN_PRIORITY (在 Thread类中定义为 1 ) 与 MAX_PRIORITY (定义为 10 ) 之间的 任何值。NORM_PRIORITY 被定义为 5。

每当线程调度器有机会选择新线程时, 它首先选择具有较高优先级的线程。但是,线程 优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时, Java 线程的优 先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少。

[警告]  如果确实要使用优先级, 应该避免初学者常犯的一个错误。如果有几个高优先级 的线程没有进入非活动状态, 低优先级的线程可能永远也不能执行。每当调度器决定运 行一个新线程时,首先会在具有高优先级的线程中进行选择, 尽管这样会使低优先级的 线程完全饿死。

守护线程

可以通过调用 t.setDaemon(true); 将线程转换为守护线程(daemon thread)。这样一个线程没有什么神奇。守护线程的唯一用途 是为其他线程提供服务。计时线程就是一个例子,它定时地发送“ 计时器嘀嗒” 信号给其他 线程或清空过时的高速缓存项的线程。当只剩下守护线程时, 虚拟机就退出了,由于如果只 剩下守护线程, 就没必要继续运行程序了。

守护线程有时会被初学者错误地使用, 他们不打算考虑关机(shutdown) 动作。但是, 这是很危险的。守护线程应该永远不去访问固有资源, 如文件、 数据库,因为它会在任何时 候甚至在一个操作的中间发生中断。

•void setDaemon( boolean isDaemon)

标识该线程为守护线程或用户线程。这一方法必须在线程启动之前调用。

未捕获异常处理器

线程的 run方法不能抛出任何受查异常, 但是,非受査异常会导致线程终止。在这种情 况下,线程就死亡了。

但是,不需要任何 catch子句来处理可以被传播的异常。相反,就在线程死亡之前, 异 常被传递到一个用于未捕获异常的处理器。

该处理器必须属于一个实现 Thread.UncaughtExceptionHandler 接口的类。这个接口只有— 个方法。

void uncaughtException(Thread t, Throwable e)

可以用 setUncaughtExceptionHandler方法为任何线程安装一个处理器。也可以用 Thread 类的静态方法 setDefaultUncaughtExceptionHandler 为所有线程安装一个默认的处理器。替换 处理器可以使用日志 API 发送未捕获异常的报告到日志文件。

如果不安装默认的处理器, 默认的处理器为空。但是, 如果不为独立的线程安装处理 器,此时的处理器就是该线程的 ThreadGroup 对象。

ThreadGroup 类实现 Thread.UncaughtExceptionHandler 接口。它的 uncaughtException方 法做如下操作:

1 ) 如果该线程组有父线程组, 那么父线程组的 uncaughtException方法被调用。

2 ) 否则, 如果 Thread.getDefaultExceptionHandler方法返回一个非空的处理器, 则调用 该处理器。

3 ) 否则,如果 Throwable 是 ThreadDeath 的一个实例, 什么都不做。

4 ) 否则,线程的名字以及 Throwable 的栈轨迹被输出到 System.err 上。

同 步

在大多数实际的多线程应用中, 两个或两个以上的线程需要共享对同一数据的存取。如果两个线程存取相同的对象, 并且每一个线程都调用了一个修改该对象状态的方法,将会发 生什么呢? 可以想象,线程彼此踩了对方的脚。根据各线程访问数据的次序,可能会产生i化 误的对象。这样一个情况通常称为竞争条件(race condition)。

 

竞争条件的一个例子

为了避免多线程引起的对共享数据的说误,必须学习如何同步存取。

程序清单 14-5 和程序清单 14-6 中的程序提供了完整的源代码。 看看是否可以从代码中 找出问题。下一节将解说其中奥秘。

//程序清单 14-5 unsynch/UnsynchBankTest.java
package unsynch;
​
/**
 * This program shows data corruption when multiple threads access a data structure.
 * @version 1.30 2004-08-01
 * @author Cay Horstmann
 */
public class UnsynchBankTest
{
   public static final int NACCOUNTS = 100;
   public static final double INITIAL_BALANCE = 1000;
​
   public static void main(String[] args)
   {
      Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
      int i;
      for (i = 0; i < NACCOUNTS; i++)
      {
         TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
         Thread t = new Thread(r);
         t.start();
      }
   }
}

 

 

//程序清单 14-6 unsynch/Bank.java
package unsynch;
​
/**
 * A bank with a number of bank accounts.
 * @version 1.30 2004-08-01
 * @author Cay Horstmann
 */
public class Bank
{
   private final double[] accounts;
​
   /**
    * Constructs the bank.
    * @param n the number of accounts
    * @param initialBalance the initial balance for each account
    */
   public Bank(int n, double initialBalance)
   {
      accounts = new double[n];
      for (int i = 0; i < accounts.length; i++)
         accounts[i] = initialBalance;
   }
​
   /**
    * Transfers money from one account to another.
    * @param from the account to transfer from
    * @param to the account to transfer to
    * @param amount the amount to transfer
    */
   public void transfer(int from, int to, double amount)
   {
      if (accounts[from] < amount) return;
      System.out.print(Thread.currentThread());
      accounts[from] -= amount;
      System.out.printf(" %10.2f from %d to %d", amount, from, to);
      accounts[to] += amount;
      System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
   }
​
   /**
    * Gets the sum of all account balances.
    * @return the total balance
    */
   public double getTotalBalance()
   {
      double sum = 0;
​
      for (double a : accounts)
         sum += a;
​
      return sum;
   }
​
   /**
    * Gets the number of accounts in the bank.
    * @return the number of accounts
    */
   public int size()
   {
      return accounts.length;
   }
}

 

竞争条件详解

上一节中运行了一个程序,其中有几个线程更新银行账户余额。一段时间之后, 错误不 知不觉地出现了,总额要么增加, 要么变少。当两个线程试图同时更新同一个账户的时候, 这个问题就出现了。假定两个线程同时执行指令

accounts[to] += amount;

问题在于这不是原子操作。该指令可能被处理如下:

1 ) 将 accounts[to] 加载到寄存器。

2 ) 增加 amount。

3 ) 将结果写回 accounts[to]。

现在,假定第 1 个线程执行步骤 1 和 2, 然后, 它被剥夺了运行权。假定第 2 个线程被 唤醒并修改了 accounts 数组中的同一项。然后,第 1 个线程被唤醒并完成其第 3 步。

这样, 这一动作擦去了第二个线程所做的更新。于是, 总金额不再正确。(见图 14-4)

 

posted on 2020-08-19 21:14  ♌南墙  阅读(171)  评论(0)    收藏  举报