Thinking in Java -- 並發(一)
並發的多面性
基本的並發定義任務
線程可以驅動任務,我們通過實現 Runnable 介面來提供,需要實現 Runnable 介面的 run() 方法。
package concurrency;/** * Created by wwh on 16-3-24. */public class LiftOff implements Runnable { protected int countDown = 10; private static int taskCount = 0; private final int id = taskCount++; public LiftOff() {} public LiftOff(int countDown) { this.countDown = countDown; } public String status() { return "#" + id + "(" + (countDown > 0 ? countDown : "LiftOff!") + "), "; } public void run() { while (countDown-- > 0) { System.out.println(status()); /* Thread.yield() 是對線程調度器的一種建議,可以將CPU從一個線程轉移給另一個線程 */ Thread.yield(); } }}
package concurrency;/** * Created by wwh on 16-3-24. */public class MainThread { public static void main(String[] args) { LiftOff lauch = new LiftOff(); lauch.run(); }}
我們也可以通過繼承 Thread 類覆蓋 run() 方法來實現線程類,但繼承Thread類有一個缺點就是單繼承,而實現Runnable介面則彌補了它的缺點,可以實現多繼承。而且實現 Runnable 介面適合多線程共用資源,繼承 Thread 類適合各個線程完成自己的任務,因為繼承 Thread 類相當於每個線程有一份各自的資源,而實現 Runnable 還可以讓多個線程共用一份代碼。
Thread 類
將 Runnable 對象轉變為工作任務的傳統方式是將它提交給一個 Thread 構造器。
public class BasicThreads { public static void main(String[] args) { for (int i = 0; i < 5; ++i) { new Thread(new LiftOff()).start(); } System.out.println("Waiting for LiftOff"); }}
Thread 構造器只需要一個 Runnable 對象。調用 Thread 對象的 start() 方法為該線程執行必須的初始化操作,然後內部調用 Runnable 的 run() 方法。
使用 Executor
Java SE5 並發包中引入執行器可以為我們管理線程 Thread 對象。
package concurrency;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/** * Created by wwh on 16-3-24. */public class CachedThreadPool { public static void main(String []args) { /* ExecutorServive 是具有聲明周期的 Executor */ ExecutorService exec = Executors.newCachedThreadPool(); for (int i = 0; i < 5; ++i) { exec.execute(new LiftOff()); } /* shutdown 被用來防止新任務被提交給 Executor */ exec.shutdown(); }}
ThreadPool 種類很多。如:
包括
CacheThreadPool:為每個任務都建立一個線程池
FixedThreadPool:一次性預先分配好固定大小的線程
SingleThreadExecutor:線程數唯一,提交多個任務會排隊等候
ScheduledThreadPool:建立一個定長線程池,支援定時及週期性任務執行。<喎?http://www.bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPjwvcD4NCjxociAvPg0KPGgyIGlkPQ=="從任務中產生傳回值">從任務中產生傳回值
如果我們希望任務完成時能夠返回一個值,那麼可以實現 Callable 介面而不是 Runnable 介面,實現 Callable 介面要求覆蓋 Call() 方法。
package concurrency;import java.util.ArrayList;import java.util.concurrent.*;/** * Created by wwh on 16-3-24. */class TaskWithResult implements Callable { private int id; public TaskWithResult(int id) { this.id = id; } public String call() throws Exception { return "reslut of TaskWithResult " + id; }}public class CallableDemo { public static void main(String []args) { ExecutorService exec = Executors.newCachedThreadPool(); /* 定義 Future 對象,在將來擷取 */ ArrayList> results = new ArrayList>(); for (int i = 0; i < 10; ++i) { /* submit() 方法會產生 Future 對象,可以通過 Future 對象的 isDone() 方法來判斷查詢 Future 是否已經完成,當完成時,可以使用 get() 方法擷取結果 */ results.add(exec.submit(new TaskWithResult(i))); } for (Future fs : results) { try { System.out.println(fs.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } finally { exec.shutdown(); } } }}
優先順序
線程的優先順序將該線程的重要性傳遞給了調度器。調度器會傾向於讓優先順序高的線程先運行。
package concurrency;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/** * Created by wwh on 16-3-24. */public class SimplePriorities implements Runnable { private int countDown = 5; private volatile double d; private int priority; public SimplePriorities(int priority) { this.priority = priority; } public String toString() { return Thread.currentThread() + ": " + countDown; } public void run() { Thread.currentThread().setPriority(priority); while (true) { /* 這裡只有迴圈次數比較大才能看出優先順序的優勢 */ for (int i = 0; i < 100000000; ++i) { d += (Math.PI + Math.E) / (double)i; if (i % 1000 == 0) { Thread.yield(); } } System.out.println(this); if (--countDown == 0) { return; } } } public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); for (int i = 0; i < 5; ++i) { exec.execute(new SimplePriorities(Thread.MIN_PRIORITY)); } exec.execute(new SimplePriorities(Thread.MAX_PRIORITY)); exec.shutdown(); }}
加入一個線程
一個線程可以在其他線程之上調用 join() 方法,其效果是等待一段時間直到第二個線程結束才繼續執行。join() 調用時可以攜帶逾時參數。
package concurrency;/** * Created by wwh on 16-3-24. */class Sleeper extends Thread { private int duration; public Sleeper(String name, int sleepTime) { super(name); duration = sleepTime; start(); } public void run() { try { sleep(duration); } catch (InterruptedException e) { e.printStackTrace(); System.out.println(getName() + " was interrupted. " + "isInterrupted(): " + isInterrupted()); return; } System.out.println(getName() + " has awakened"); }}class Joiner extends Thread { private Sleeper sleeper; public Joiner(String name, Sleeper sleeper) { super(name); this.sleeper = sleeper; start(); } public void run() { try { /* 等待線程 */ sleeper.join(); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("Interrupted"); } System.out.println(getName() + " join completed"); }}public class Joining { public static void main(String[] args) { Sleeper sleepy = new Sleeper("Sleepy", 1500), grumpy = new Sleeper("Grumpy", 1500); Joiner drpey = new Joiner("Dopey", sleepy), doc = new Joiner("Doc", grumpy); grumpy.interrupt(); }}
共用受限資源
多個線程可能出現訪問共用資源的情況。
package concurrency;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/** * Created by wwh on 16-4-7. */public class Counter implements Runnable { private static int counter = 0; public static int getCounter() { return counter; } public void run() { for (int i = 0; i < 1000000; ++i) { counter++; } } public static void test(int n) { ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < n; ++i) { executorService.execute(new Counter()); } executorService.shutdown(); } public static void main(String[] args) throws InterruptedException { Counter.test(10); System.out.println(Counter.getCounter()); }}
以上代碼沒有進行同步,多個線程同時增加計數器。所以導致結果不正確。
Java 為我們提供了幾種方式。
synchronzied:包括兩種用法,synchronzied 方法和 synchronized 塊。對於有 synchronzied 關鍵字修飾的類方法或代碼塊,執行時首先要擷取該類執行個體的鎖,執行完畢後釋放。在執行過程中要有其它線程等請求該 synchronzied 方法或代碼塊則被阻塞。
Lock:互斥鎖,顯示鎖對象。Lock 對象必須被顯式建立、鎖定和釋放。
ReentrantLock:可重新進入鎖,允許嘗試著去擷取鎖。
這幾種方式後續會詳細解釋。