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

6. Swing应用中的多线程问题

Swing应用运行于多个线程之上。特别是三类线程:
1.起始线程,或主线程,在main()方法中运行,启动GUI的构建和应用的退出;
2.EDT线程;
3.一些处理计算密集型任务及IO的后台线程。

所有的事件处理,绘制和界面刷新代码在单一线程(EDT)中进行,这是为了确保事件处理器在下个处理器启动之前完成执行,且绘制不被事件打断。如果因计算密集型任务造成EDT饥饿,用户界面将会挂起,程序对用户交互无响应。"理想情况下,任何需要花费30-100 ms任务不应该在EDT上运行。否则用户将感觉到输入和UI应答之间的停顿。

再者,所有访问GUI组件的代码应该在EDT上运行,因为这些组件中很多不能确保线程安全,而从相同线程中访问他们能避免多线程问题。

总之,
1.耗时和阻塞IO任务不应在EDT上运行,以避免造成EDT饥饿,进而影响用户交互的事件触发及界面刷新;
2.为了线程安全,Swing组件应仅在EDT上访问。

6.1 javax.Swing.SwingUtilities.invokeLater() 和invokeAndWait()

方法invokeLater(Runnable)和 invokeAndWait(Runnable)协调EDT中的Runnable任务。

为避免主线程(运行main()方法)和EDT之间线程问题,推荐使用javax.swing.SwingUtilities.invokeLater(Runnable)在EDT上创建GUI组件,如:

/** The entry main() method */
public static void main(String args[]) {
    // Run the GUI codes on the event-dispatching thread for thread-safety
    SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
            JFrame frame = new JFrame("My Swing Application");
            frame.setContentPane(new MyMainPanel());
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.pack();
            frame.setLocationRelativeTo(null); // center on screen
            frame.setVisible(true);            // show it
        }
    });
}

静态方法SwingUtilities.invokelater()以Runnable对象(作为匿名内部类执行)为参数,并在EDT上制定任务(具体在run()方法中)。可以从任何线程中调用invokeLater()来要求EDT执行特定代码,这些代码可以写在Runnable参数的run()方法中。invokeLater()会立即返回,而不等待EDT执行这些代码。

如果有需要可以使用invokeAndWait(), 这方法会等待EDT执行指定代码才返回。对于applet,推荐(在init())通过invokeAndWait()运行GUI创建代码。这是为了避免init()在GUI创建完成之前退出造成问题。如:

public class SwingTemplateJApplet extends JApplet {
    /** init() to setup the GUI components */
    @Override
    public void init() {
        // Run GUI codes in the Event-Dispatching thread for thread safety
        try {
            // Use invokeAndWait() to ensure that init() exits after GUI construction
            SwingUtilities.invokeAndWait(new Runnable() {
                @Override
                public void run() {
                    // Set the content-pane of JApplet to an instance of main JPanel
                    setContentPane(new SwingTemplateJPanel());
                }
            });
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

也可用java.awt.EventQueue.invokeLater()代替javax.swing.SwingUtilities.invokeLater(),javax.swing.SwingUtilities.invokeLater()是 java.awt.EventQueue.invokeLater()的进一步封装。

通过之前例子的追踪可以发现如果没有使用SwingUtilities.invokeLater(),EDT在setVisible()方法之后启动。另一方面,invokerLater()启动EDT。

6.2 javax.swing.Timer (JDK 1.2)

如果需要在一定时间后或一段频繁时间间隔内更新组件,使用时间类如 javax.swing.Timer(JDK 1.2)或java.util.Timer(JDK 1. 3)。

对于javax.swing.Timer,阅读"animation using javax.swing.Timer"。

6.3 javax.swing.SwingWorker (JDK 1.6)

如之前提到过的,在Swing应用中:

  1. 计算密集型任务不应在EDT上运行,以避免EDT在处理事件和重绘中出现饥饿;
  2. 为了线程安全,Swing组件应仅在EDT上访问。

类javax.swing.SwingWorkder 帮助管理唯一的EDT和几个后台工作者线程之间的交互。它可以用来协调后台线程中的计算密集型任务并返回EDT中产生的最终结果或中间结果。

定义 SwingWorker 类如:public abstract class SwingWorker implements RunnableFuture

SwingWorker是带两种类型参数的抽象类:T 指方法doInBackground()和get()最终结果类型,而V指 方法publish()和process()中间结果类型。

RunnableFuture 接口是两个接口的结合:Runnable 和Future。接口Runnable 声明了抽象方法run();而 Future 声明 get(), cancel(), isDone(), 和isCancelled()。

制定后台任务

protected abstract T doInBackground() throws Exception
    // Do this task in a background thread
protected void done()
    // Executes on the Event-Dispatching thread after the doInBackground() method finishes.
public final T get() throws InterruptedException, ExecutionException
    // Waits for doInBackground() to complete and gets the result.
    // Calling get() on the Event-Dispatching thread blocks all events, including repaints,
    //  until the SwingWorker completes.
public final void execute()
    // Schedules this SwingWorker for execution on one of the worker thread.
public final boolean cancel(boolean mayInterruptIfRunning)
    // Attempts to cancel execution of this task.
public final boolean isDone()
    // Returns true if this task has completed (normally or exception)
public final boolean isCancelled()
    // Returns true if this task was cancelled before it completed normally

为在工作者线程中制定一项任务,扩展 SwingWorker子类并重写:

  1. doInBackground()指定执行任务,在一个工作者线程中制定并返回类型为T的结果;
  2. done()方法在 doInBackground()完成后在EDT中运行,用get()方法取得doInBackground()结果 (类型 T)。
import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.ExecutionException;
import javax.swing.*;
 
/** Test SwingWorker on the counter application with a compute-intensive task */
@SuppressWarnings("serial")
public class SwingWorkerCounter extends JPanel {
    // For counter
    private JTextField tfCount;
    private int count = 0;
    // For SwingWorker
    JButton btnStartWorker;   // to start the worker
    private JLabel lblWorker; // for displaying the result
 
    /** Constructor to setup the GUI components */
    public SwingWorkerCounter () {
        setLayout(new FlowLayout());
 
        add(new JLabel("Counter"));
        tfCount = new JTextField("0", 10);
        tfCount.setEditable(false);
        add(tfCount);
 
        JButton btnCount = new JButton("Count");
        add(btnCount);
        btnCount.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                ++count;
                tfCount.setText(count + "");
            }
        });
 
        /** Create a SwingWorker instance to run a compute-intensive task 
            Final result is String, no intermediate result (Void) */
        final SwingWorker worker = new SwingWorker() {
            /** Schedule a compute-intensive task in a background thread */
            @Override
            protected String doInBackground() throws Exception {
                // Sum from 1 to a large n
                long sum = 0;
                for (int number = 1; number < 1000000000; ++number) {
                    sum += number;
                }
                return sum + "";
            }
 
            /** Run in event-dispatching thread after doInBackground() completes */
            @Override
            protected void done() {
                try {
                    // Use get() to get the result of doInBackground()
                    String result = get();
                    // Display the result in the label (run in EDT)
                    lblWorker.setText("Result is " + result);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        };
 
        btnStartWorker = new JButton("Start Worker");
        add(btnStartWorker);
        btnStartWorker.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                worker.execute();                 // start the worker thread
                lblWorker.setText("  Running...");
                btnStartWorker.setEnabled(false); // Each instance of SwingWorker run once
            }
        });
        lblWorker = new JLabel("  Not started...");
        add(lblWorker);
 
    }
 
    /** The entry main() method */
    public static void main(String[] args) {
        // Run the GUI construction in the Event-Dispatching thread for thread-safety
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame("SwingWorker Test");
                frame.setContentPane(new SwingWorkerCounter());
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        });
    }
}

SwingWorker的实例设计只能运行一次且不能重新启动,需要创建新实例重新执行任务。

发布并处理中间结果

不一定在done()中处理最终结果, 如有需要可以publish()并 process()中间结果。

@SafeVarargs
protected final void publish(V... chunks)
    // Sends data chunks to the process(java.util.List) method.
    // This method shall be called inside the doInBackground() to deliver intermediate results
    //  for processing on the Event-Dispatching thread inside the process() method.
protected void process(List chunks)
    // Receives data chunks from publish() asynchronously on the Event-Dispatching thread.

在doInBackground()中, 用publish(V...) 发布一个或更多类型V中间结果。重写process(List)方法来处理List中公布的结果,process()方法运行在EDT中。

import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.ExecutionException;
import javax.swing.*;
 
/** Test SwingWorker on the counter application with a compute-intensive task */
@SuppressWarnings("serial")
public class SwingWorkerCounterIntermediateResult extends JPanel {
    // For counter
    private JTextField tfCount;
    private int count = 0;
    // For SwingWorker
    JButton btnStartWorker;   // to start the worker
    private JLabel lblWorker; // for displaying the result
 
    /** Constructor to setup the GUI components */
    public SwingWorkerCounterIntermediateResult () {
        setLayout(new FlowLayout());
 
        add(new JLabel("Counter"));
        tfCount = new JTextField("0", 10);
        tfCount.setEditable(false);
        add(tfCount);
 
        JButton btnCount = new JButton("Count");
        add(btnCount);
        btnCount.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                ++count;
                tfCount.setText(count + "");
            }
        });
 
        /** Create a SwingWorker instance to run a compute-intensive task */
        final SwingWorker worker = new SwingWorker() {
 
            /** Schedule a compute-intensive task in a background thread */
            @Override
            protected String doInBackground() throws Exception {
                long sum = 0;
                for (int number = 0; number < 10000000; ++number) {
                    sum += number;
                    publish(sum + ""); // Send "every" intermediate result to process()
                                  // You might not publish every intermediate result
                }
                return sum + "";
            }
 
            /** Run in event-dispatching thread after doInBackground() completes */
            @Override
            protected void done() {
                try {
                    // Use get() to get the result of doInBackground()
                    String finalResult = get();
                    // Display the result in the label (run in EDT)
                    lblWorker.setText("Final Result is " + finalResult);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
 
            /** Run in event-dispatching thread to process intermediate results
                send from publish(). */
            @Override
            protected void process(java.util.List chunks) {
                // Get the latest result from the list
                String latestResult = chunks.get(chunks.size() - 1);
                lblWorker.setText("Result is " + latestResult);
            }
        };
 
        btnStartWorker = new JButton("Start Worker");
        add(btnStartWorker);
        btnStartWorker.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                worker.execute();  // start the worker thread
                lblWorker.setText("  Running...");
                btnStartWorker.setEnabled(false);  // SwingWorker can only run once
            }
        });
        lblWorker = new JLabel("  Not started...");
        add(lblWorker);
 
    }
 
    /** The entry main() method */
    public static void main(String[] args) {
        // Run the GUI construction in the Event-Dispatching thread for thread-safety
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame("SwingWorker Test");
                frame.setContentPane(new SwingWorkerCounterIntermediateResult());
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        });
    }
}

属性变化事件

doInBackground()发送PropertyChangeEvent给其所有的PropertyChangeListeners 关于边界属性变化。有两个边界属性:"state" 和"progress"。"state"在封装枚举类型SwingWorker.StateValue中定义,StateValue取值为PENDING (SwingWorker 实例创建), START (doInBackground启动)和DONE(doInBackground 完成) 。"progress" 是一个整数型,取值范围从0到100。可以在doInBackground()中 通过setProgress()方法改变progress值来发送PropertyChangeEvent给其所有的PropertyChangeListeners。

例子

在本例 doInBackground()方法内,调用setProgess()来改变progress边界特征值(0-100之间),然后发送PropertyChangeEvent。用SwingWorker定义并注册 PropertyChangeListener , 在进度条显示progress值。事件处理器在EDT中运行。

import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.concurrent.ExecutionException;
import javax.swing.*;
 
/** Test SwingWorker on the counter application with a compute-intensive task */
@SuppressWarnings("serial")
public class SwingWorkerCounterProgress extends JPanel {
    // For counter
    private JTextField tfCount;
    private int count = 0;
    // For SwingWorker
    JButton btnStartWorker;   // to start the worker
    private JLabel lblWorker; // for displaying the result
    JProgressBar pbWorker;    // progress bar for the worker task
 
    /** Constructor to setup the GUI components */
    public SwingWorkerCounterProgress () {
        setLayout(new FlowLayout());
 
        add(new JLabel("Counter"));
        tfCount = new JTextField("0", 10);
        tfCount.setEditable(false);
        add(tfCount);
 
        JButton btnCount = new JButton("Count");
        add(btnCount);
        btnCount.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                ++count;
                tfCount.setText(count + "");
            }
        });
 
        /** Create a SwingWorker instance to run a compute-intensive task */
        final SwingWorker worker = new SwingWorker() {
 
            /** Schedule a compute-intensive task in a background thread */
            @Override
            protected String doInBackground() throws Exception {
                long sum = 0;
                int maxNumber = 10000000;
                for (int number = 0; number < maxNumber; ++number) {
                    sum += number;
                    publish(sum + ""); // send intermediate result to process()
                    // Fire PropertyChangeEvent for the bound-property "progress"
                    setProgress(100 * (number + 1) / maxNumber);
                }
                return sum + "";
            }
 
            /** Run in event-dispatching thread after doInBackground() completes */
            @Override
            protected void done() {
                try {
                    // Use get() to get the result of doInBackground()
                    String finalResult = get();
                    // Display the result in the label (run in EDT)
                    lblWorker.setText("Final Result is " + finalResult);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
 
            /** Run in event-dispatching thread to process intermediate results
                send from publish(). */
            @Override
            protected void process(java.util.List chunks) {
                // Get the latest result from the list
                String latestResult = chunks.get(chunks.size() - 1);
                lblWorker.setText("Result is " + latestResult);
            }
        };
 
        /** Event handler for the PropertyChangeEvent of property "progress" */
        worker.addPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if (evt.getPropertyName().equals("progress")) {  // check the property name
                    pbWorker.setValue((Integer)evt.getNewValue());  // update progress bar
                }
            }
        });
 
        btnStartWorker = new JButton("Start Worker");
        add(btnStartWorker);
        btnStartWorker.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                worker.execute();  // start the worker thread
                lblWorker.setText("  Running...");
                btnStartWorker.setEnabled(false);  // SwingWorker can only run once
            }
        });
        lblWorker = new JLabel("  Not started...");
        add(lblWorker);
        pbWorker = new JProgressBar();
        add(pbWorker);
    }
 
    /** The entry main() method */
    public static void main(String[] args) {
        // Run the GUI construction in the Event-Dispatching thread for thread-safety
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame("SwingWorker Test");
                frame.setContentPane(new SwingWorkerCounterProgress());
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setSize(300, 150);
                frame.setVisible(true);
            }
        });
    }
}

6.4 总结

线程对构建响应性GUI很有必要,有一些应该使用新线程的典型情形:

  • 在主线程中创建一新线程来处理耗时初始化任务(如:磁盘I/O)以便GUI加载更快;
  • 在EDT中创建一新线程来处理耗时任务(在事件处理器中)以便GUI保持响应;
  • 用计时器处理重复任务,在一定时间间隔或延时一段时间执行;
  • 如果操作需要等待其他线程或程序的信息,创建一个新线程。

此外,计算密集型线程必须协作并将控制交与其他线程。

7. Thread Pool, Executor, Callable/Future (JDK 1.5)

线程池支持类在JDK1.5 java.lang.concurrent包中引入:


Java编程之多线程&并发编程(下)_第1张图片

7.1 线程池(Thread Pool)

线程池是一个能执行任务线程的管理集合。当使用线程池执行大量任务时,效率会因线程循环执行任务而提高,这会降低每个任务调用开销。

可以实现接口ExecutorService来使用线程池,如ThreadPoolExecutor 或ScheduledThreadPoolExecutor,而在Executors 类中提供了更多便捷工厂方法,列举如下:

  • Executors.newSingleThreadExecutor(): 创建单一后台线程;
  • Executors.newFixedThreadPool(int numThreads): 创建固定大小的线程池;
  • Executors.newCachedThreadPool(): 创建带自动线程回收机制的无界线程池。

使用线程池的步骤:

  1. 写一个实现Runnable接口的工作者线程类,run()方法制定运行线程的行为;
  2. 用Executors 类提供的工厂方法创建线程池(ExecutorService),该线程池可以只有一个线程,固定数量线程或无限数量线程;
  3. 创建工作者线程类实例。用线程池的方法execute(Runnable r)来添加Runnable 任务到线程池,该任务将被协调并在有空闲线程时得到执行。

7.2 接口java.util.concurrent.Executor

Executor对象能执行提交的Runnable 任务,该接口声明了下面抽象方法:
public void execute(Runnable r)

在未来某个时候执行给定任务,任务因Executor执行情况可能在新线程,线程池或当前线程中执行。

7.3 接口java.util.concurrent.ExecutorService

接口ExecutorService 声明很多抽象方法,其中比较重要的有:

public void shutdown();
    // Initiates an orderly shutdown of the thread pool.
    // The previously executed/submitted tasks are allowed to complete,
    // but no new tasks will be scheduled.
public  Future submit(Callable task);
    // Submit or schedule the callable task for execution, which returns a Future object.

7.4 类java.util.concurrent.Executors

类Executors 提供创建Executor对象的工厂方法。如:

static ExecutorService newSingleThreadExecutor()
static ExecutorService newFixedThreadPool(int nThreads)
static ExecutorService newCachedThreadPool()
static ScheduledExecutorService newSingleThreadScheduledExecutor()
static ScheduledExecutorService newScheduledThreadPool(int size)
public class WorkerThread implements Runnable {
    private int workerNumber;

    WorkerThread(int workerNumber) {
        this.workerNumber = workerNumber;
    }

    public void run() {
        // The thread simply prints 1 to 5
        for (int i = 1; i <= 5; ++i) {
            System.out.printf("Worker %d: %d\n", workerNumber, i);
            try {
                // sleep for 0 to 0.5 second
                Thread.sleep((int)(Math.random() * 500));
            } catch (InterruptedException e) {}
        }
    }
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolTest {
    public static void main(String[] args) {
        int numWorkers = Integer.parseInt(args[0]);
        int threadPoolSize = Integer.parseInt(args[1]);

        ExecutorService pool = 
                Executors.newFixedThreadPool(threadPoolSize);
        WorkerThread[] workers = new WorkerThread[numWorkers];
        for (int i = 0; i < numWorkers; ++i) {
            workers[i] = new WorkerThread(i+1);
            pool.execute(workers[i]);
        }
        pool.shutdown();
    }
}
//2 threads, 5 workers
> java ThreadPoolTest 5 2
Worker 1: 1
Worker 2: 1
Worker 1: 2
Worker 1: 3
Worker 2: 2
Worker 1: 4
Worker 2: 3
Worker 1: 5
Worker 3: 1
Worker 3: 2
Worker 2: 4
Worker 3: 3
Worker 2: 5
Worker 3: 4
Worker 3: 5
Worker 4: 1
Worker 4: 2
Worker 5: 1
Worker 4: 3
Worker 5: 2
Worker 5: 3
Worker 4: 4
Worker 5: 4
Worker 5: 5
Worker 4: 5

首先安排工作者1和2用线程池中的两个线程执行,接着是工作者3,4和5。占用线程任务完成后,线程返回线程池,然后安排另外一个任务并开始执行。

可以调用pool.shutdown()来关闭线程池中所有线程。

7.5 接口java.util.concurrent.Callable 和Future

Callable 类似于Runnable,不同的是 Callable提供方法返回结果或Exception到相应的线程中。Callable声明了抽象方法call()(不同于Runnable中的run())。
public V call() // Call() returns a result of type , or throws an exception if unable to do so.

在线程池中,使用submit(Callable r)而非execute(Runnable r),这会返回一个Future 对象(在ExecutorService接口中声明)。当需要结果时,可以对Future对象调用get()方法加以获取。结果一旦完成便会返回,否则当前线程一直阻塞直到获取结果。

接口Future 声明了下面的抽象方法:

V get()           // wait if necessary, retrieve result
V get(long timeout, TimeUnit unit)
boolean cancel(boolean mayInterruptIfRunning)
boolean isCancelled()
boolean isDone()  // return true if this task completed
import java.util.concurrent.Callable;

public class CallableWorkerThread implements Callable {
    private int workerNumber;

    CallableWorkerThread(int workerNumber) {
        this.workerNumber = workerNumber;
    }

    public String call() {    // use call() instead of run()
        for (int i = 1; i <= 5; ++i) {    // just print 1 to 5
            System.out.printf("Worker %d: %d\n", workerNumber, i);
            try {
                Thread.sleep((int)(Math.random() * 1000));
            } catch (InterruptedException e) {}
        }
        return "worker " + workerNumber;
    }
}
import java.util.concurrent.*;

public class CallableThreadPoolTest {
    public static void main(String[] args) {
        int numWorkers = Integer.parseInt(args[0]);

        ExecutorService pool = Executors.newCachedThreadPool();
        CallableWorkerThread workers[] = new CallableWorkerThread[numWorkers];
        Future[] futures = new Future[numWorkers];

        for (int i = 0; i < numWorkers; ++i) {
            workers[i] = new CallableWorkerThread(i + 1);
            futures[i] = pool.submit(workers[i]);
        }
        for (int i = 0; i < numWorkers; ++i) {
            try {
                System.out.println(futures[i].get() + " ended");
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            } catch (ExecutionException ex) {
                ex.printStackTrace();
            }
        }
    }
}
三个工作者线程的输出:
> java CallableThreadPoolTest 3
Worker 1: 1
Worker 3: 1
Worker 2: 1
Worker 3: 2
Worker 1: 2
Worker 2: 2
Worker 2: 3
Worker 2: 4
Worker 2: 5
Worker 1: 3
Worker 3: 3
Worker 3: 4
Worker 3: 5
Worker 1: 4
Worker 1: 5
worker 1 ended
worker 2 ended
worker 3 ended

7.6 JDK 1.5中其他新线程特性

  • Lock
  • ReadWriteLock
  • Semaphores
  • Atomics
  • Blocking Queue

Java参考及资源链接

更多参考文献及资源

  1. Swing Tutorial's "Concurrency in Swing".
  2. John O'Conner, "Improve Application Performance With SwingWorker in Java SE 6" @ http://java.sun.com/developer/technicalArticles/javase/swingworker/.

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

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

你可能感兴趣的:(Java编程之多线程&并发编程(下))