14 多线程
多线程与多进程的区别在于,每个进程都有自己的一整套变量,而线程则共享数据。共享数据使得线程之间的通信比进程之间的通信更有效,更容易。与进程相比,线程更轻量级,创建、撤销一个线程比启动新进程的开销要小得多。
14.1 什么是线程
package com.company.bounce; import java.awt.geom.Ellipse2D; import java.awt.geom.Rectangle2D; 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; public void move(Rectangle2D bounds) { x += dx; y += dy; if (x < bounds.getMinX()) { x = bounds.getMinY(); 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; } } public Ellipse2D getShape() { return new Ellipse2D.Double(x, y, XSIZE, YSIZE); } }
package com.company.bounce; import javax.swing.*; import java.awt.*; import java.util.ArrayList; public class BallComponent extends JPanel { private static final int D_W = 450; private static final int D_H = 350; private java.util.List<Ball> balls = new ArrayList<>(); public void add(Ball ball) { balls.add(ball); } public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; for (Ball b : balls) { g2.fill(b.getShape()); } } public Dimension gerPreferredSize() { return new Dimension(D_W, D_H); } }
package com.company.bounce; import org.omg.CORBA.PUBLIC_MEMBER; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class Bounce { public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { @Override public void run() { JFrame frame = new BounceFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } }); } } class BounceFrame extends JFrame { private BallComponent comp; public static final int STEPS = 1000; public static final int DELAY = 3; public BounceFrame() { setTitle("Bounce"); comp = new BallComponent(); add(comp, BorderLayout.CENTER); JPanel buttonPanel = new JPanel(); addButton(buttonPanel, "Start", new ActionListener() { @Override public void actionPerformed(ActionEvent e) { addBall(); } }); addButton(buttonPanel, "Close", new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.exit(0); } }); add(buttonPanel, BorderLayout.SOUTH); pack(); } public void addButton(Container c, String title, ActionListener listener) { JButton button = new JButton(title); c.add(button); button.addActionListener(listener); } public void addBall() { try { Ball ball = new Ball(); comp.add(ball); for (int i = 0; i <= STEPS; i++) { ball.move(comp.getBounds()); comp.paint(comp.getGraphics()); Thread.sleep(DELAY); } } catch (InterruptedException e) { } } }
运行这个程序,球会自如地来回弹跳,但是,这个程序完全控制了整个应用程序。在球自己结束弹跳之前无法与程序进行交互。
可以将球移动的代码放置在一个独立的线程里。发起多个球,每个球都在自己的线程里运行。在球运行期间,当用户点击Close按钮时,事件调度程序将有机会关注到这个事件,并处理“关闭”这一动作。
package com.company.bounce; import java.awt.geom.Ellipse2D; import java.awt.geom.Rectangle2D; 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; public void move(Rectangle2D bounds) { x += dx; y += dy; if (x < bounds.getMinX()) { x = bounds.getMinY(); 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; } } public Ellipse2D getShape() { return new Ellipse2D.Double(x, y, XSIZE, YSIZE); } }
package com.company.bounce; import javax.swing.*; import java.awt.*; import java.util.ArrayList; public class BallComponent extends JPanel { private static final int D_W = 450; private static final int D_H = 350; private java.util.List<Ball> balls = new ArrayList<>(); public void add(Ball ball) { balls.add(ball); } public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; for (Ball b : balls) { g2.fill(b.getShape()); } } public Dimension gerPreferredSize() { return new Dimension(D_W, D_H); } }
import java.awt.*; import java.awt.event.*; import javax.swing.*; 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) { } } } class BounceFrame extends JFrame { private BallComponent comp; 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(); } public void addButton(Container c, String title, ActionListener listener) { JButton button = new JButton(title); c.add(button); button.addActionListener(listener); } public void addBall() { Ball b = new Ball(); comp.add(b); Runnable r = new BallRunnable(b, comp); Thread t = new Thread(r); t.start(); } }
14.2 中断线程
有一种可以强制线程终止的方法。然而,interrupt方法可以用来请求终止线程。
对一个线程调用interrupt方法时,线程的中断状态将置位。每个线程都具有boolean标志,每个线程都应该不时地检查这个标志,以判断线程是否被中断。
但是,如果线程被阻塞,就无法检测中断状态。这是产生InterruptException异常的地方。当一个被阻塞的线程上调用interrupt方法,阻塞调用将会被Interrupted Exception异常中断。
14.3 线程状态
线程可以有以下6种状态
- new 新创建
- runnable 可运行
- blocked 被阻塞
- waiting 等待
- timed waiting 记时等待
- terminated 被终止
14.3.1 新创建线程
14.3.2 可运行线程
一旦调用start方法,线程处于runnable状态。可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。
14.3.3 被阻塞线程和等待线程
- 当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。
- 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。
- 当几个方法有一个超时参数。调用它们导致线程进入计时等待状态。
14.3.4 被终止线程
- 因为run方法正常退出而自然死亡
- 因为一个没有捕获的异常终止了run方法而意外死亡
14.4 线程属性
14.4.1 线程优先级
线程优先级是高度依赖于系统的。
14.4.2 守护线程
守护线程的唯一用途是为其他线程提供服务。
14.4.3 未捕获异常处理器
线程的run方法不能抛出任何被检测的异常,但是,不被检测的异常会导致线程终止。
14.5 同步
在实际的多线程应用中,两个或两个以上的线程需要共享对同一个数据的存取。根据各线程访问数据的次序,可能会发生讹误的对象。这样的一个情况,通常被称为竞争条件。
14.5.1 竞争条件的一个例子
为了避免多线程引起的对共享数据的讹误,必须学习如何同步存取。
14.5.2 竞争条件详解
14.5.3 锁对象
有两种机制防止代码块受并发访问的干扰。Java提供了一个synchronized关键字达到这一目的,并在Java se5.0 引入了ReentrantLock类。synchronized关键字自动提供一个锁及相关的“条件”。
用ReentrantLock保护代码块的基本结构如下:
myLock.lock(); try { critical section } finally { myLock.unlock(); }
锁是可重入的,因为线程可以重复地获得已经持有的锁。
14.5.4 条件对象
通常,线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。
14.5.5 synchronized关键字
- 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码
- 锁可以管理试图进入被保护代码段的线程
- 锁可以拥有一个或多个相关的条件对象
- 每个条件对象管理那些进入被保护的代码段但还不能运行的线程。
如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
可以简单地声明Bank类的transfer方法为synchronized,而不是使用一个显示的锁。
内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法接触等待线程的阻塞状态。
内部锁和条件存在一些局限
- 不能中断一个正在试图获得锁的线程
- 试图获得锁时不能设定超时
使用建议
- 最好既不使用Lock/Condition也不使用synchronized关键字。在许多情况下你可以使用java.util.concurrent包中的一种机制。
- 如果synchronized关键字适合你的程序,那么请尽量使用它。
- 如同特别需要Lock/Condition结构提供的独有特性时,才使用Lock/Condtion。
14.5.6 同步阻塞
14.5.7 监视器概念
14.5.8 volatile域
14.5.9 final变量
14.5.10 原子性
14.5.11 死锁
14.5.12 线程局部变量
14.5.13 锁测试与超时
14.5.14 读写锁
14.6 阻塞队列
对于实际编程来说,应该尽可能远离底层结构。使用由并发处理的专业人士实现的较高层次的结构要方便得多,要安全得多。
import java.io.*; import java.util.*; import java.util.concurrent.*; public class BlockingQueueTest { public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.print("Enter base directory (e.g. /usr/local/jdk1.6.0/src): "); String directory = in.nextLine(); System.out.print("Enter keyword (e.g. volatile): "); String keyword = in.nextLine(); final int FILE_QUEUE_SIZE = 10; final int SEARCH_THREADS = 100; BlockingQueue<File> queue = new ArrayBlockingQueue<>(FILE_QUEUE_SIZE); FileEnumerationTask enumerator = new FileEnumerationTask(queue, new File(directory)); new Thread(enumerator).start(); for (int i = 1; i <= SEARCH_THREADS; i++) new Thread(new SearchTask(queue, keyword)).start(); } } class FileEnumerationTask implements Runnable { public static File DUMMY = new File(""); private BlockingQueue<File> queue; private File startingDirectory; public FileEnumerationTask(BlockingQueue<File> queue, File startingDirectory) { this.queue = queue; this.startingDirectory = startingDirectory; } public void run() { try { enumerate(startingDirectory); queue.put(DUMMY); } catch (InterruptedException e) { } } /** * Recursively enumerates all files in a given directory and its subdirectories. * @param directory the directory in which to start */ public void enumerate(File directory) throws InterruptedException { File[] files = directory.listFiles(); for (File file : files) { if (file.isDirectory()) enumerate(file); else queue.put(file); } } } class SearchTask implements Runnable { private BlockingQueue<File> queue; private String keyword; public SearchTask(BlockingQueue<File> queue, String keyword) { this.queue = queue; this.keyword = keyword; } public void run() { try { boolean done = false; while (!done) { File file = queue.take(); if (file == FileEnumerationTask.DUMMY) { queue.put(file); done = true; } else search(file); } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { } } public void search(File file) throws IOException { try (Scanner in = new Scanner(file)) { int lineNumber = 0; while (in.hasNextLine()) { lineNumber++; String line = in.nextLine(); if (line.contains(keyword)) System.out.printf("%s:%d:%s%n", file.getPath(), lineNumber, line); } } } }
14.7 线程安全的集合
14.7.1 高效的映射表、集合和队列
java.util.concurrent包提供了映射表、有序集和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。
这些集合使用复杂的算法,通过允许并发地访问数据结构和不同部分来使竞争极小化。
14.7.2 写数组的拷贝
CopyOnWriteArrayList和CopyOnWriteArraySet是线程安全的集合,其中所有的修改线程对底层数组进行复制。
14.7.3 较早的线程安全集合
14.8 Callable与Future
Runnable封装一个异步运行的任务,可以把它想象成一个么有参数和返回值的异步方法。Callable与Runnable类似,但是有返回值。Callable接口是一个参数化的类型,只有一个方法call。
public interface Callable<V> { V call() throws Exception; }
Future保存异步计算的结果。可以启动一个运算,将Future对象交给某个线程,然后忘掉它。Future对象的所有者在结果计算好之后就可以获得它。
Future接口具有下面的方法:
public interface Future<V> { V get() throws...; V get(long timeout, TimeUnit unit) throws...; void cancel(boolean mayInterrupt); boolean isCancelled(); boolean isDone(); }
import java.io.*; import java.util.*; import java.util.concurrent.*; public class FutureTest { public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.print("Enter base directory (e.g. /usr/local/jdk5.0/src): "); String directory = in.nextLine(); System.out.print("Enter keyword (e.g. volatile): "); String keyword = in.nextLine(); MatchCounter counter = new MatchCounter(new File(directory), keyword); FutureTask<Integer> task = new FutureTask<>(counter); Thread t = new Thread(task); t.start(); try { System.out.println(task.get() + " matching files."); } catch (ExecutionException e) { e.printStackTrace(); } catch (InterruptedException e) { } } } class MatchCounter implements Callable<Integer> { private File directory; private String keyword; private int count; public MatchCounter(File directory, String keyword) { this.directory = directory; this.keyword = keyword; } public Integer call() { count = 0; try { File[] files = directory.listFiles(); List<Future<Integer>> results = new ArrayList<>(); for (File file : files) if (file.isDirectory()) { MatchCounter counter = new MatchCounter(file, keyword); FutureTask<Integer> task = new FutureTask<>(counter); results.add(task); Thread t = new Thread(task); t.start(); } else { if (search(file)) count++; } for (Future<Integer> result : results) try { count += result.get(); } catch (ExecutionException e) { e.printStackTrace(); } } catch (InterruptedException e) { } return count; } public boolean search(File file) { try { try (Scanner in = new Scanner(file)) { boolean found = false; while (!found && in.hasNextLine()) { String line = in.nextLine(); if (line.contains(keyword)) found = true; } return found; } } catch (IOException e) { return false; } } }
14.9 执行器
构建一个新的线程是有一定的代价的,因为涉及与操作系统的交互。如果程序中创建了大量的生命期很短的线程,应该使用线程池。一个线程池中包含许多准备运行的空闲线程。另外一个使用线程池的理由是减少并发线程的数目。
执行器类Executor类有许多静态工厂方法用来构建线程池。
14.9.1 线程池
import java.io.*; import java.util.*; import java.util.concurrent.*; public class ThreadPoolTest { public static void main(String[] args) throws Exception { Scanner in = new Scanner(System.in); System.out.print("Enter base directory (e.g. /usr/local/jdk5.0/src): "); String directory = in.nextLine(); System.out.print("Enter keyword (e.g. volatile): "); String keyword = in.nextLine(); ExecutorService pool = Executors.newCachedThreadPool(); MatchCounter counter = new MatchCounter(new File(directory), keyword, pool); Future<Integer> result = pool.submit(counter); try { System.out.println(result.get() + " matching files."); } catch (ExecutionException e) { e.printStackTrace(); } catch (InterruptedException e) { } pool.shutdown(); int largestPoolSize = ((ThreadPoolExecutor) pool).getLargestPoolSize(); System.out.println("largest pool size=" + largestPoolSize); } } class MatchCounter implements Callable<Integer> { private File directory; private String keyword; private ExecutorService pool; private int count; public MatchCounter(File directory, String keyword, ExecutorService pool) { this.directory = directory; this.keyword = keyword; this.pool = pool; } public Integer call() { count = 0; try { File[] files = directory.listFiles(); List<Future<Integer>> results = new ArrayList<>(); for (File file : files) if (file.isDirectory()) { MatchCounter counter = new MatchCounter(file, keyword, pool); Future<Integer> result = pool.submit(counter); results.add(result); } else { if (search(file)) count++; } for (Future<Integer> result : results) try { count += result.get(); } catch (ExecutionException e) { e.printStackTrace(); } } catch (InterruptedException e) { } return count; } public boolean search(File file) { try { try (Scanner in = new Scanner(file)) { boolean found = false; while (!found && in.hasNextLine()) { String line = in.nextLine(); if (line.contains(keyword)) found = true; } return found; } } catch (IOException e) { return false; } } }
14.9.2 预定执行
14.9.3 控制任务组
14.9.4 Fork-Join框架
Java se7 中新引入了Fork-Join框架,用于支持计算密集型任务,比如图像或者视频处理。
import java.util.concurrent.*; public class ForkJoinTest { public static void main(String[] args) { final int SIZE = 10000000; double[] numbers = new double[SIZE]; for (int i = 0; i < SIZE; i++) numbers[i] = Math.random(); Counter counter = new Counter(numbers, 0, numbers.length, new Filter() { public boolean accept(double x) { return x > 0.5; } }); ForkJoinPool pool = new ForkJoinPool(); pool.invoke(counter); System.out.println(counter.join()); } } interface Filter { boolean accept(double t); } class Counter extends RecursiveTask<Integer> { public static final int THRESHOLD = 1000; private double[] values; private int from; private int to; private Filter filter; public Counter(double[] values, int from, int to, Filter filter) { this.values = values; this.from = from; this.to = to; this.filter = filter; } protected Integer compute() { if (to - from < THRESHOLD) { int count = 0; for (int i = from; i < to; i++) { if (filter.accept(values[i])) count++; } return count; } else { int mid = (from + to) / 2; Counter first = new Counter(values, from, mid, filter); Counter second = new Counter(values, mid, to, filter); invokeAll(first, second); return first.join() + second.join(); } } }
14.10 同步器
14.11 Swing与线程

浙公网安备 33010602011771号