進程 & 線程
進程:是系統進行資源分配的和調度的一個獨立單位。舉例:將公司的一個部門看做一個進程,公司分配項目獎金的時候就是以部門爲基本單位分配1萬塊錢,部門下面的十幾我的就分這1萬塊錢,這時就能夠將部門中的每一個人看做是一個線程。這就是進程與線程之間的關係。java
線程:線程是進程的執行單元,一個進程能夠有多個線程。編程
進程間能夠併發,線程間也能夠併發。數組
什麼是併發,什麼是並行?????緩存
併發(concurrency)和並行(parallellism)是: 解釋一:並行是指兩個或者多個事件在同一時刻發生;而併發是指兩個或多個事件在同一時間間隔發生。 解釋二:並行是在不一樣實體上的多個事件,併發是在同一實體上的多個事件。 解釋三:在一臺處理器上「同時」處理多個任務,在多臺處理器上同時處理多個任務。如hadoop分佈式集羣 因此併發編程的目標是充分的利用處理器的每個核,以達到最高的處理性能。-- 併發編程網安全
所以,多線程編程解決的是併發問題,並行的問題能夠經過分佈式部署來解決。???多線程
建立線程的3種方式
繼承Thread類
步驟:
一、 定義Thread類的子類,並重寫run()方法。
二、 建立Thread子類的實現
三、 調用線程對象的start()方法啓動線程併發
代碼片斷:dom
// 經過繼承Thread類來建立線程類 public class FirstThread extends Thread { // 重寫run方法,run方法的方法體就是線程執行體 public void run() { } public static void main(String[] args) { // 建立、並啓動第一條線程 new FirstThread().start(); // 建立、並啓動第二條線程 new FirstThread().start(); } }
實現Runnable接口
步驟:
一、定義Runnable接口的實現類,並重寫該接口的run()方法
二、建立該實現類的實例,並以此實例做爲Thread的target來建立Thread對象
三、調用該線程對象的start()方法來啓動線程分佈式
代碼片斷:ide
// 經過實現Runnable接口來建立線程類 public class SecondThread implements Runnable { // run方法一樣是線程執行體 public void run() { } public static void main(String[] args) { SecondThread st = new SecondThread(); // ① // 經過new Thread(target , name)方法建立新線程 new Thread(st , "新線程1").start(); new Thread(st , "新線程2").start(); } }
Callable和Future
步驟:
一、建立Callable接口實現類,並實現call()方法
二、使用FutureTask類來包裝Callable對象
三、使用FutureTask對象做爲Thread對象的target建立並啓動線程
四、經過FutureTask對象的get()方法獲取線程的返回值
代碼片斷:
@Test public void test() throws Exception { FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() { public Integer call() throws Exception { return 1; } }); new Thread(task, "新的線程1").start(); Integer i = task.get(); System.out.println("i:" + i); }
線程生命週期
線程的生命週期有:新建、就緒、運行、阻塞、死亡。
線程對象調用start()方法後就處於就緒狀態。
線程生命週期的狀態轉換圖以下:
總結:
- wait(),進入阻塞狀態,釋放同步監視器
- sleep(),進入阻塞狀態,給其它進程(無論線程優先級)執行的機會,不會釋放同步監視器
- join(),調用線程進入阻塞狀態,直到加入的線程執行完成爲止
- yield(),不進入阻塞狀態,給其它線程(只給優先級高)執行的機會,不會釋放同步監視器
控制線程
join
join:讓一個線程等待另外一個線程完成。
setDaemon(true)
設置爲後臺線程,調用線程實例自己的setDaemon()方法:
MyThread myThread = new MyThread(); myThread.setDaemon(true);
sleep
線程睡眠,並進入阻塞狀態。
調用線程Thread類的靜態sleep()方法,而不是某個線程的實例方法:
Thread.sleep(1000);
yield
線程暫停,但不進入阻塞狀態。
調用線程Thread類的靜態yield()方法,而不是某個線程的實例方法:
Thread.yield();
sleep()與yield()區別
- sleep()與yield()都會讓當前線程暫停,sleep()會給其它線程執行的機會,不理會其它線程的優先級;但yield()只給優先級相同或更高的線程執行的機會。
- sleep()會讓線程進入阻塞狀態,yield()則不會進入阻塞狀態。
線程優先級
priority,1-10,數值越大優先級越高:
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
myThread.setPriority(Thread.MAX_PRIORITY);
注:servlet中的 load-on-startup 參數,值越大優先級越低
線程同步
傳統線程同步
同步方法
同步一個易發生線程安全的方法,這裏的鎖就是this,即當前類對象:
// 提供一個線程安全draw()方法來完成取錢操做 public synchronized void draw(double drawAmount) { }
同步代碼塊
同步一個易發生線程安全的字段,這裏的鎖就是此可能發生線程安全的字段:
public class DrawThread extends Thread { // 模擬用戶帳戶 private Account account; // 當多條線程修改同一個共享數據時,將涉及數據安全問題。 public void run() { // 使用account做爲同步監視器,任何線程進入下面同步代碼塊以前, // 必須先得到對account帳戶的鎖定——其餘線程沒法得到鎖,也就沒法修改它 // 這種作法符合:「加鎖 → 修改 → 釋放鎖」的邏輯 synchronized (account) { } // 同步代碼塊結束,該線程釋放同步鎖 } }
什麼時候釋放同步監視器
- wait()方法會釋放同步監視器
- Thread.sleep(),Thread.yield()不會釋放同步監視器
同步鎖(Lock)
java5開始提供了同步鎖,經常使用的同步鎖是ReentrantLock。
同步鎖繼承關係以下:
- Lock |-ReentrantLock - ReadWriteLock |-ReentrantReadWriteLock
同步鎖使用代碼片斷:
import java.util.concurrent.locks.*; public class Account { // 定義鎖對象 private final ReentrantLock lock = new ReentrantLock(); // 提供一個線程安全draw()方法來完成取錢操做 public void draw(double drawAmount) { // 加鎖 lock.lock(); try { } finally { // 修改完成,釋放鎖 lock.unlock(); } } }
ThreadLocal
原理:爲每個使用該變量的線程都提供一個變量值的副本,使每個線程均可以獨立改變本身的副本,而不會和其餘線程的副本衝突,就好像每個線程都徹底擁有該變量同樣。代碼內部使用ThreadLocal.ThreadLocalMap來存儲變量的鍵和值,也就是內部使用一個相似Map的結構來存儲name和name對應的值。
ThreadLocal只提供三個public方法:
- T get():返回此線程局部變量中當前線程副本中的值。
- void remove():刪除此線程局部變量中當前線程副本中的值。
- void set(T value):設置此線程局部變量中當前線程副本中的值。
示例:
class Acount { private ThreadLocal<String> name = new ThreadLocal<>(); public Acount(String str){ this.name.set(str); } public String getName(){ return name.get(); } public void setName(String str){ this.name.set(str); } //...... }
線程間通訊
傳統線程間通訊
適用條件:若是程序中使用的是同步代碼塊、同步方法來控制線程安全,則線程間的通訊是使用Object類的 wait(),notify(),notifyAll()控制線程間通訊。
public class Account { // 封裝帳戶編號、帳戶餘額的兩個成員變量 private String accountNo; private double balance; // 標識帳戶中是否已有存款的旗標 private boolean flag = false; public Account(){} // 構造器 public Account(String accountNo , double balance) { this.accountNo = accountNo; this.balance = balance; } // accountNo的setter和getter方法 public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public String getAccountNo() { return this.accountNo; } // 所以帳戶餘額不容許隨便修改,因此只爲balance提供getter方法, public double getBalance() { return this.balance; } public synchronized void draw(double drawAmount) { try { // 若是flag爲假,代表帳戶中尚未人存錢進去,取錢方法阻塞 if (!flag) { wait(); } else { // 執行取錢 System.out.println(Thread.currentThread().getName() + " 取錢:" + drawAmount); balance -= drawAmount; System.out.println("帳戶餘額爲:" + balance); // 將標識帳戶是否已有存款的旗標設爲false。 flag = false; // 喚醒其餘線程 notifyAll(); } } catch (InterruptedException ex) { ex.printStackTrace(); } } public synchronized void deposit(double depositAmount) { try { // 若是flag爲真,代表帳戶中已有人存錢進去,則存錢方法阻塞 if (flag) //① { wait(); } else { // 執行存款 System.out.println(Thread.currentThread().getName() + " 存款:" + depositAmount); balance += depositAmount; System.out.println("帳戶餘額爲:" + balance); // 將表示帳戶是否已有存款的旗標設爲true flag = true; // 喚醒其餘線程 notifyAll(); } } catch (InterruptedException ex) { ex.printStackTrace(); } } }
使用Condition控制線程間通訊
適用條件:若是程序中使用的是同步鎖(Lock)來控制線程安全,則線程間通訊是使用Condition來控制線程間通訊。
獲取Condiion實例代碼片斷:經過鎖去獲取
private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition();
Condition類提供以下方法以實現線程間通訊:
- await()
- signal(),喚醒單個線程
- signalAll(),喚醒此Lock上的全部線程
示例:
import java.util.concurrent.*; import java.util.concurrent.locks.*; public class Account { // 顯式定義Lock對象 private final Lock lock = new ReentrantLock(); // 得到指定Lock對象對應的Condition private final Condition cond = lock.newCondition(); public void draw(double drawAmount) { // 加鎖 lock.lock(); try { if () { } else { // 喚醒其餘線程 cond.signalAll(); } } catch (InterruptedException ex) { ex.printStackTrace(); } // 使用finally塊來釋放鎖 finally { lock.unlock(); } } }
使用阻塞隊列(BockingQueue)控制線程間通訊
todo...
線程組與異常處理
todo...
線程池
普通線程池
java 5 新增了一個 Executors 工廠類來產生線程池,經常使用如下幾個靜態方法:
- ExecutorService newCachedThreadPool():系統根據須要建立一個具備緩存功能的線程池。
- ExecutorService newFixedThreadPool(int nThreads):建立一個可重用的、固定線程數的線程池。
- ExecutorService newSingleThreadExector():建立一個只有單個線程的線程池。
- ScheduledExecutorService newScheduledThreadPool(int corePoolSize):建立指定線程數的線程池,它能夠指定延遲後執行線程任務。
- ScheduledExecutorService newSingleThreadScheduledExecutor():建立一個只有一個線程和線程池,它能夠在指定延遲後執行線程任務。
- ExecutorService newWorkStealingPool(int parallelism):建立持有足夠的線程的線程池來支持給定的並行級別,該方法還會使用多個隊列來減小競爭。(java 8)
- ExecutorService newWorkStealingPool():該方法是前一個方法的簡化版本。若是當前機器有4個CPU,則目標並行級別被設置爲4,也就是至關於前一個方法傳入4做爲參數。(java 8)
ExecutorService 表明儘快執行線程的線程池,程序只需把一個 Runnable 對象或 Callable 對象提交給指定的線程池便可。
ExecutorService 經常使用如下方法:
- Future<?> submit(Runnable task):將一個線程任務提交給線程池,Future表明返回值。
- <T> Future<T> submit(Runnable task,T result):同上,其中result顯式指定線程執行執行結束後返回的值。
- <T> Future<T> submit(Callable<T> task):將一個Calable對象提交給指定的線程池,Future表明返回值。
線程池使用步驟:
一、調用Executors類的靜態工廠方法建立一個ExecutorService對象。
二、建立Runnable實現類或Callable實現類的實例。
三、調用ExecutorService對象的submit()方法提交Runnable或Callable實例。
四、調用ExecutorService對象的shutdown()關閉線程池。
示例代碼片斷:
import java.util.concurrent.*; public class ThreadPoolTest { public static void main(String[] args) throws Exception { // 建立足夠的線程來支持4個CPU並行的線程池 // 建立一個具備固定線程數(6)的線程池 ExecutorService pool = Executors.newFixedThreadPool(6); // 使用Lambda表達式建立Runnable對象 Runnable target = () -> { for (int i = 0; i < 100 ; i++ ) { System.out.println(Thread.currentThread().getName() + "的i值爲:" + i); } }; // 向線程池中提交兩個線程 pool.submit(target); pool.submit(target); // 關閉線程池 pool.shutdown(); } }
ForkJoinPool
jdk 7 提供了ForkJoinPool來支持將一個任務拆分紅多個「小任務」並行計算,再把多個「小任務」的結果合併成總的計算結果。
java.util.concurrent.Executor |-ExecutorService |-AbstractExecutorService |-ForkJoinPool
建立ForkJoinPool實例後,就能夠調用ForkJoinPool的submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法來提交任務。
其中ForkJoinTask有兩個抽象子類:
java.util.concurrent.Future |-ForkJoinTask 表示一個可執行、合併的任務 |-RecursiveAction 表示沒有返回值的任務 |-RecursiveTask 表示有返回值的任務
完整示例:
import java.util.concurrent.*; // 繼承RecursiveAction來實現"可分解"的任務 class PrintTask extends RecursiveAction { // 每一個「小任務」只最多隻打印50個數 private static final int THRESHOLD = 50; private int start; private int end; // 打印從start到end的任務 public PrintTask(int start, int end) { this.start = start; this.end = end; } @Override protected void compute() { // 當end與start之間的差小於THRESHOLD時,開始打印 if(end - start < THRESHOLD) { for (int i = start ; i < end ; i++ ) { System.out.println(Thread.currentThread().getName() + "的i值:" + i); } } else { // 若是當end與start之間的差大於THRESHOLD時,即要打印的數超過50個 // 將大任務分解成兩個小任務。 int middle = (start + end) / 2; PrintTask left = new PrintTask(start, middle); PrintTask right = new PrintTask(middle, end); // 並行執行兩個「小任務」 left.fork(); right.fork(); } } } public class ForkJoinPoolTest { public static void main(String[] args) throws Exception { ForkJoinPool pool = new ForkJoinPool(); // 提交可分解的PrintTask任務 pool.submit(new PrintTask(0 , 300)); pool.awaitTermination(2, TimeUnit.SECONDS); // 關閉線程池 pool.shutdown(); } }
import java.util.concurrent.*; import java.util.*; // 繼承RecursiveTask來實現"可分解"的任務 class CalTask extends RecursiveTask<Integer> { // 每一個「小任務」只最多隻累加20個數 private static final int THRESHOLD = 20; private int arr[]; private int start; private int end; // 累加從start到end的數組元素 public CalTask(int[] arr , int start, int end) { this.arr = arr; this.start = start; this.end = end; } @Override protected Integer compute() { int sum = 0; // 當end與start之間的差小於THRESHOLD時,開始進行實際累加 if(end - start < THRESHOLD) { for (int i = start ; i < end ; i++ ) { sum += arr[i]; } return sum; } else { // 若是當end與start之間的差大於THRESHOLD時,即要累加的數超過20個時 // 將大任務分解成兩個小任務。 int middle = (start + end) / 2; CalTask left = new CalTask(arr , start, middle); CalTask right = new CalTask(arr , middle, end); // 並行執行兩個「小任務」 left.fork(); right.fork(); // 把兩個「小任務」累加的結果合併起來 return left.join() + right.join(); // ① } } } public class Sum { public static void main(String[] args) throws Exception { int[] arr = new int[100]; Random rand = new Random(); int total = 0; // 初始化100個數字元素 for (int i = 0 , len = arr.length; i < len ; i++ ) { int tmp = rand.nextInt(20); // 對數組元素賦值,並將數組元素的值添加到sum總和中。 total += (arr[i] = tmp); } System.out.println(total); // 建立一個通用池 ForkJoinPool pool = ForkJoinPool.commonPool(); // 提交可分解的CalTask任務 Future<Integer> future = pool.submit(new CalTask(arr , 0 , arr.length)); System.out.println(future.get()); // 關閉線程池 pool.shutdown(); } }