《瘋狂Java講義》學習筆記(十二)多線程

1、線程概述

  • 操作系統中所有運行中的任務通常對應一個進程(Process),當一個程序進入內存運行時,即變成一個進程
  • 進程的三個特徵:
    獨立性:每一個進程都擁有自己私有的地址空間,沒有經過進程本身允許,不允許其他進程訪問地址空間
    動態性:相比起程序,進行加入了時間概念,具有自己的生命週期和各種不同的狀態,而程序不具備
    併發性:多個進行可以在單個處理器上併發執行,互不影響
  • 並行指在同一時刻,有多條指令在多個處理器上同時執行;併發指在同一個時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上具有多個進程同時執行的效果
  • 多線程則擴展了多進程的概念,使得同一個進程可以同時併發處理多個任務;線程也被稱作輕量級進程,線程是進程的執行單元
  • 多線程的優勢:
    進程之間不能共享內存,但線程之間共享內容非常容易
    系統創建進程時需要爲該進程重新分配系統資源,但創建線程則代價小得多,因此使用多線程來實現多任務併發比多進程的效率高
    Java語言內置了多線程功能支持,而不是單純地爲底層操作系統的調度方式,從而簡化了Java的多線程編程

2、線程的創建和啓動

  • 繼承Tread類創建線程類
    定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務
    調用線程對象的start()方法來啓動該線程
  • Thread.currentThread():currentThread()是Tread類的靜態方法,該方法總是返回當前正在執行的線程對象
  • getName():該方法是Tread類的實例方法,該方法返回調用該方法的線程名字
  • 使用繼承Tread類的方法來創建線程類時,多個線程之間無法共享線程類的實例變量
  • 實現Runnable接口創建線程類
    定義Runnable接口的實現類,並重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體
    創建Runnable實現類的實例,並以此實例作爲Tread的target來創建Tread對象,該Tread對象纔是真正的線程對象
    new Thread(new Runnable() {
    @Override
    public void run() {
    // TODO Auto-generated method stub
    }
    },”ThreadName”).start();;
    調用線程對象的start()方法來啓動該線程
  • 採用Runnable接口的方式創建的多個線程可以共享線程類的實例變量
  • 使用Callable和Futrue創建線程
    Callable接口提供了一個call()方法可以作爲線程執行體,但call()方法比run()方法功能更強大
    call()方法可以有返回值
    call()方法可以聲明拋出異常
    Callable接口不是Runnable接口的子接口,所以Callable對象不能直接作爲Tread的target,需要使用Future接口來代表Callable接口裏call()方法的返回值,併爲Future接口提供了一個FutureTask實現類,該實現類實現了Future接口,並實現了Runnable接口,所以可以作爲Tread類的target
  • 創建並啓動有返回值的線程步驟:
    創建Callable接口的實現類,並實現call()方法
    使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值
    使用FutureTask對象作爲Tread對象的target創建並啓動新線程
    調用FutureTask對象的get()方法來獲得子線程執行結束後的返回值
FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        return 10;
    }
});     
new Thread(task,"").start();;
try {
    System.out.println(task.get());
} catch (Exception e) {
    e.printStackTrace();
} 
  • 三種創建方式的對比,採用實現Runnable、Callable接口的方式歸爲一類:
    線程類只實現Runnable、Callable接口還可以繼承其他類
    在這種方式下,多個線程可以共享一個target對象,非常適合多個相同線程來處理同一份資源的情況
    如果要訪問當前線程,必須使用Tread.currentTread()方法,如果繼承Tread類的話直接使用this即可獲得當前線程

3、線程的生命週期

  • 當線程被創建並啓動以後,它既不是一啓動就進入執行狀態,也不是一直處於執行狀態,它要經過新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocked)和死亡(Dead)5種狀態
  • 當線程啓動以後,CPU需要在多條線程之間切換,於是線程狀態也會多次在雲信、阻塞之間切換
  • 新建和就緒狀態
    new一個線程之後,該線程處於新建狀態,此時和其他Java對象一樣,僅僅由Java虛擬機爲其分配內存,並初始化其成員變量的值,此時線程對象沒有表現出任何線程的動態特徵,程序也不會執行線程的執行體
    當調用start()方法之後,該線程處於就緒狀態,Java虛擬機會爲其創建方法調用棧和程序計數器,處於這個狀態的線程並沒有開始運行,只是表示該線程可以運行了,至於何時開始運行,取決於JVM裏線程調度器的調度
  • 運行和阻塞狀態
    如果處於就緒狀態的線程獲得了CPU,開始執行run()方法的線程執行體,則該線程處於運行狀態。
    當發生如下情況時,線程將會進入阻塞狀態:
    線程調用了sleep()方法主動放棄所佔用的處理器資源
    線程調用了一個阻塞式IO方法,在該方法返回之前,該線程被阻塞
    線程試圖獲得一個同步監視器,但該同步監視器正在被其他線程所持有
    線程在等待某個通知
    程序調用了線程的suspend()方法將線程掛起
    當正在執行的線程被阻塞之後,其他線程就可以獲得執行的機會
    當發生如下特定的情況時可以解除上面的阻塞,讓該線程重新進入就緒狀態:
    調用sleep()方法的線程經過了指定時間
    線程調用的阻塞式IO方法已經返回
    線程成功地獲得了試圖取得的同步監視器
    線程正在等待某個通知時,其他線程發出了一個通知
    處於掛起狀態的線程被調用了resume()恢復方法
  • 線程死亡
    線程會以如下三種方式結束,結束後就處於死亡狀態
    run()或call()方法執行完成,線程正常結束
    線程拋出一個未捕獲的Exception或Error
    直接調用該線程的stop()方法來結束該線程-該方法容易導致死鎖,不推薦使用
    isAlive()方法可以測試某個線程是否已經死亡,當線程處於就緒、運行、阻塞三種狀態時,該方法返回true;當線程處於新建、死亡狀態時,返回false
    若對已死亡的線程使用start()方法將會引發IllegalThreadStateException異常

4、線程控制

  • join線程
    當在某個程序執行流中調用其它線程的join()方法時,調用線程將被阻塞,直到被join的線程執行完畢爲止
    jion()的三種重載方法:
    jion():等待被jion的線程執行完成
    jion(long milis):等待被jion的線程的時間最長爲millis毫秒
    jion(long millis,int nanos):等待被jion的線程的時間最長爲millis毫微秒
  • 後臺線程(Deamon Thread)
    後臺線程在後臺運行,任務是爲其它的線程提供服務
    如果所有的前臺線程都死亡,後臺線程會自動死亡
    setDeamon(true)方法可將指定線程設置成後臺線程
    isDeamon()方法可以判斷指定線程是否爲後臺線程
  • 線程睡眠:sleep
    Thread.sleep()方法可以讓正在執行的線程暫停一段時間,並進入阻塞狀態
    sleep()的兩種重載形式:
    static void sleep(long millis)
    static void sleep(long millis,int nanos)
    在線程睡眠期間,將不會獲得執行機會,即使系統中沒有其它可執行的線程也不會執行
  • 線程讓步:yield
    Thread.yield()方法可以讓正在執行的線程暫停,但它不會阻塞該線程,只是讓線程轉入就緒狀態,可能很快又會繼續執行
  • 改變線程優先級
    Tread.setPriority(int newPriority)、getPriority()方法可以設置和返回指定線程的優先級,範圍是1~10之間

5、線程同步

  • 可以使用關鍵字synchronized修飾代碼塊
synchronize(account){
}
  • 可以使用關鍵字synchronize修飾方法
public synchronize void draw(){
}
  • 釋放同步監視器的鎖定
    當前線程的同步方法、同步代碼塊執行結束時釋放
    遇到break、return終止了該代碼塊、該方法的繼續執行時釋放
    出現了未處理Error或Exception,導致異常結束時釋放
    程序執行了同步監視器對象的wait()方法時釋放
  • 不釋放
    程序調用Tread.sleep()、Thread.yield()方法來暫停當前線程的執行
    其它線程調用了該線程的suspend()方法將該線程掛起
  • 死鎖
    當兩個線程相互等打死對方釋放同步監視器時就會發生死鎖,Java虛擬機沒有監測,也沒有採取措施來處理死鎖情況
    由於Tread類的suspend()方法也很容易導致死鎖,所以不推薦使用

6、線程通信

  • 傳統的線程通信
    通過成員變量、synchronized、wait()、notify()、notifyAll()方法實現
    假設有存錢和取錢動作,要求每當存款者存款後取款者就立即取出該筆錢,不允許存款者連續兩次存錢,也不允許取款者連續取錢
    實現思路:通過booelan flag判斷是否應該存錢或取錢,如果賬戶已存款flag=true,並調用notifyAll()方法喚醒其它線程,其它當存款者調用wait()方法進入等待,此時取款者可以順利取款,取款後將flag=false,同樣調用notifyAll()方法喚醒其它線程,如此類推

7、使用Condition控制線程通信

如果程序不使用synchronized關鍵字來保證同步,而是直接使用Lock對象來保證同步,則系統中不存在隱式同步監視器,也就不能使用wait、notify、notifyAll方法進行線程通信了,這個時候可以使用Condition對象替代

Lock lock = new ReentrantLock();
Condition cond = lock.newCondition();
cond.await():類似wait()
cond.signal():類似notify()
cond.signalAll():類似notifyAll()

8、使用阻塞隊列(BlockingQueue)控制線程通信

  • BlockingQueue是Queue的子接口,主要用途並不是作爲容器,而是作爲線程同步工具
  • 當線程試圖往BlockingQueue中放入元素時,如果已滿則線程被阻塞;當從BlockingQueue中取出元素時,如果已空則線程被阻塞
  • 兩個阻塞方法:
    put(E e)
    take()
  • 常用方法:
  • BlockingQueue與實現類之間的類圖
  • BlockingQueue的5個實現類
ArrayBlockingQueue:基於數組
LinkedBlockingQueue:基於鏈表
PriorityBlockingQueue:首先取出的元素是最小的元素
SynchronousQueue:同步隊列,存和取必須是交替進行
DelayQueue:根據getDalay()方法的返回值進行排序

9、線程組

  • Java使用ThreadGroup來表示線程組,它可以對一批線程進行分類管理,如果程序沒有顯式指定線程屬於哪個線程組則默認屬於默認線程組
  • 如果A線程創建了B線程,則默認B線程屬於A線程所在的線程組
  • 一旦某個線程加入了指定線程組之後,就一直屬於該線程組,直到該線程死亡

10、線程異常

  • 如果線程執行過程中拋出一個未處理異常,JVM在結束該線程之前會自動查找是否有對應的Thread.UncaughtExceptionHandler對象,如果找到該處理器對象,則會調用該對象的uncaughtException(Thread t,Throwable e)方法來處理該異常
  • 例子:
public class ExHandler {
    public static void main(String[] args) {
        Thread.currentThread().setUncaughtExceptionHandler(new MyExHandler());
        int a = 5/0;
        // 雖然制定了異常處理器對未捕獲的異常進行處理,但程序依然不會正常結束,這和catch不同
        System.out.println("程序正常結束!");
    }
}
class MyExHandler implements Thread.UncaughtExceptionHandler{
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println(t+"線程出現了異常:"+e);
    }
}

11、線程池

  • 當程序中需要創建大量生存期很短的線程時,應當要考慮使用線程池
  • Java5開始內建線程池,新增了一個Executors工廠類來產生線程池,常用API:
ExecutorService newCachedThreadPool():創建一個具有緩存功能的線程池,系統根據需要創建線程,這些線程將會被緩存在線程池中
ExecutorService newFixedThreadPool(int nThreads):創建一個可重用的、具有固定線程數的線程池
ExecutorService newSingleThreadExecutor():創建一個只有單線程的線程池,相當於調用newFixedThreadPool()方法時傳入參數
ScheduledExcutorService newScheduledThreadPool(int corePoolSize):創建具有指定線程數的線程池,它可以在指定延遲後執行線程任務,corePoolSize指池中所保存的線程數,即使線程是空閒的也被保存在線程池內
ScheduledExcutorService newSingleThreadScheduledExecutor():創建只有一個線程的線程池,它可以在指定延遲後執行線程任務
ExecutorService newWorkStealingPool(int parallelism):創建持有足夠的線程的線程池來支持給定的並行級別,該方法還會使用多個隊列來減少競爭
ExecutorService newWorkStealingPool():同上,如果當前機器有4個CPU,則目標並行級別被設置爲4,也就是相當於上一個方法傳入4作爲參數
  • ExecutorService代表儘快執行線程的線程池(只要線程池中有閒置線程,就立即執行線程任務),程序只要將一個
Runnable對象或Callable對象(代表線程任務)提交給該線程池,該線程池就會盡快執行該任務,常用API:
Future<?> submit(Runnable task):將一個Runnable對象提交指定的線程池,線程池將在有空閒線程時執行Runnable對象代表的任務;其中Future對象代表Runnable任務的返回值-但run()方法沒有返回值,所以Future對象將在run()方法執行結束後返回null;但可以調用Future的isDone()、isCancelled()方法來獲得Runnable對象的執行狀態
<T> Future<T> submit(Runnable task,T result):同上,其中result顯式指定線程執行結束後的返回值
<T> Future<T> submit(Callable<T> task):同上,參數換成了Callable對象,其中Future代表Callable對象裏call()方法的返回值
ScheduledFuture<V> schedule(Callable<V> callable,long delay,TimeUnit unit):指定callable任務將在delay延遲後執行
ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unit):指定command任務將在delay延遲後執行
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit):同上,而且以設定頻率重複執行,在initialDelay後開始執行,依次在initialDelay+periodinitialDelay+2*period...處重複執行,依次類推
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit):創建並執行一個在給定初始延遲後首次啓動的定期操作,隨後在每一次執行終止和下一次執行開始之間都存在給定的延遲。如果任務在任一次執行時遇到異常,就會取消後續執行;否則,只能通過程序來顯式取消或終止該任務
  • 用完一個線程池後,應該調用線程池的shutdown()方法,該方法將啓動線程池的關閉序列,調用shutdown()方法後的線程不再接收新任務,但會將以前所有已提交任務執行完成。當線程池中的所有任務都執行完成後,池中的所有線程都會死忙;另外也可以調用線程池的shutdownNow()方法來關閉線程池,該方法試圖停止所有正在執行的活動任務,暫停處理正在等待的任務,並返回等待執行的任務列表
  • 使用線程池來執行線程任務的步驟:
    調用Executors類的靜態工廠方法創建一個ExecutorService對象,該對象代表一個線程池
    創建Runnable實現類或Callable實現類的實例,作爲線程執行任務
    調用ExecutorService對象或Callable實現類的實例,作爲線程執行任務
    當不想提交任何任務時,調用ExecutorService對象的shutdown()方法來關閉線程池
ExecutorService pool = Excutors.newFixedThreadPool(6);
Runnable target = ()->{
    system.out.println();
}
pool.submit(target);
pool.submit(target);
pool.shutdown();
  • Java7提供了ForkJoinPool來支持將一個任務拆分成多個小人物並行計算,在把多個小人物的結果合併成總的計算結果,構造器:
ForkJoinPool(int parallelism):創一個一個包含parallelism個並行線程的ForkJoinPool
ForkJOinPool():以Runtime.availableProcessors()方法的返回值作爲parallelism參數來創建ForkJoinPookl
Java8進一步擴展了ForkJoinPool功能:
ForkJoinPool commonPool():返回一個通用池,運行狀態不會受shutdown()或shutdownNow()方法的影響
int getCommonPoolParallelism():該方法返回通用池的並行級別
創建了ForkJoinPool實例之後,uji可以調用ForkJoinPool的submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法來執行指定任務。其中ForkJoinTask代表一個可以並行、合併的任務;ForkJoinTask是一個抽象類,它還有兩個抽象子類:RecursiveAction和RecursiveTask;其中RecursiveTask代表有返回值的任務,而RecursiveAction代表沒有返回值的任務
例:
class PrintTask extends RecursiveAction{
    int middle = (start + end)/2;
    PrintTask left = new PrintTask(start,middle);
    PrintTask right = new PrintTask(middle,end);
    left.fork();
    right.fork();
}
class CalTask extends RecursiveTask<Integer>{
    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();
}
  • ThreadLocal是線程局部變量的意思,就是爲每一個使用該變量的線程都提供一個變量值的副本,使每一個線程都可以獨立地改變的副本,而不會和其它線程的副本衝突
T get():返回此線程局部變量中當前線程副本中的值
void remove():刪除此線程局部變量中當前線程的值
void set(T value):設置此線程局部變量中當前線程副本中的值
ThreadLocal並不能替代同步機制,兩者面向的問題領域不同。同步機制是爲了同步多個線程相同資源的併發訪問,是多個線程之間進行通信的有效方式;而ThreadLocal是爲了隔離多個線程的數據共享,從根本上避免了多個線程之間對共享資源(變量)的競爭,也就不需要多多個線程進行同步了
  • 包裝線程不安全的集合
    Java集合中ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是線程不安全的,當多個併發線程向這些集合中存、取元素時,就會破壞這些集合的數據完整性
    Colection提供的類方法可以把這些集合包裝成線程安全的集合:
<T> Collection<T> synchronizedCollection(Collection<T> c):返回指定collection對應的線程安全的collection
static <T> List<T> synchronizedList(List<T> list):返回指定List對象對應的線程安全的List對象
static <K,V> Map<K,V> synchronizedMap(Map<K,V> m):類似上
static <T> Set<T> synchronizedSet(Set<T> s):類似上
static <K,V> SortedMap<K,V>synchronizedSortedMap(SortedMap<K,V> m):類似上
static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s):類似上
HashMap m = Collections.synchronizedMap(new HashMap());
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章