Java相關內容:多線程

要想進階成Java中級開發工程師,有一些東西總是繞不過去的,Java的知識體系裏,像IO/NIO/AIO,多線程,JVM,網絡編程,數據庫,框架等必須有一定了解才行。最近在準備面試,所以對這些東西也做個記錄。本篇記錄的是多線程相關。

 

線程

進程/線程概念

​ 我們打開一個應用程序時,就會打開應用程序對應的進程。系統會給每個進程分配一定的資源(CPU和內存等)。分配資源後把該進程放入進程就緒隊列,進程調度器選中它的時候就會爲它分配CPU時間,程序開始真正運行。

​ 然後一個進程裏面就包括一到多個線程,線程是進程的一個執行流,也是CPU調度和分派的基本單位。每個線程上都有要執行的代碼,線程間共享進程的所有資源。然後每個線程又自己的堆棧和局部變量。在單核環境下,也允許開啓多個線程運行,不過並不是同時運行的的,只是CPU處理得太快讓多個線程看起來是在同時運行,在多核環境下線程能夠做到真正意義上的並行。

併發 != 並行。併發只是多個進程間快速交替的執行,並行是某個時刻真的有多個線程在同時執行。

 

進程/線程區別

1) 進程是操作系統分配資源的最小單位,而線程是程序執行(CPU調度)的最小單位;

2) 一個進程由一個或多個線程組成,線程是一個進程中代碼的不同執行路線;

3) 進程有自己的獨立的地址空間,而線程共享同一個進程的資源;線程之間的通信更方便,進程之間的通信比較複雜。

4) 線程的創建和切換花銷比進程要小得多。

 

線程的狀態/生命週期

線程有創建,可運行,運行中,阻塞,消亡五種狀態。

新建: 繼承Thread | 實現runnable()接口| 實現Callable接口 | ExecutorService

可運行: 新建的線程調用start() | 運行中的線程時間片用完 | 運行中的線程調用yield() | 從阻塞或鎖池狀態恢復

運行: 操作系統調度選中

阻塞: 等待用戶輸入 | sleep() | join()

消亡: run() | main() 方法運行結束

 

線程主要方法

sleep():讓調用這個方法的正在執行的線程休眠一段時間

join():讓調用這個方法的線程進入阻塞狀態,一直等到調用這個方法的線程執行完了再繼續運行。

yield():讓調用這個方法的線程暫停,然後讓操作系統重新調度。

interrupt():向調用這個方法的線程進入中斷狀態,注意,那個線程並不會被中斷。

interrupted():讓調用這個方法的線程停止中斷狀態。

isInterrupt():判斷當前線程的中斷狀態。是在中斷狀態返回true。

stop()/suspend()/resume():停止/掛起/恢復掛起三個過時的方法,不建議使用。

sleep()與wait()方法區別:

sleep()是Thread類的靜態方法,它的作用是讓當前線程從運行狀態轉入阻塞狀態,當一個線程通過sleep()方法暫停之後,該線程並不會釋放它對同步監視器的加鎖。

wait()是Object對象的方法,它的作用是讓當前線程釋放對該同步監視器的加鎖,然後該線程則會進入該同步監視器的等待池中,直到該同步監視器調用notify()或notifyAll()來通知該線程。

wait()、notify使用實例:

//假設兩個線程:想要第一個線程跑兩圈然後,然後讓第二個線程跑完,第一個線程再接着跑。main方法中:
TestThread tt = new TestThread();
new Thread(tt,"線程1").start();
new Thread(tt,"線程2").start();
​
class TestThread extends Thread{
    int num;
    @Override
    public void run(){
        synchronized (this) {
            try {
                for(int i = 0; i<3;i++){
                    System.out.println(String.format("線程名=[%s]調用了同步方法。%d",
                         Thread.currentThread().getName(),this.num++));
                    if(this.num==2){
                       //第一個線程進來跑兩次,this.num == 2;然後讓這個線程釋放鎖睡覺。下一個線程接過鎖;
                        this.wait();    
                    }
                    if(this.num==3){
                       //第二個線程接過鎖後跑,期間任意時候可叫醒第一個線程。(只兩個線程時測試)
                        this.notify();  
                    }
                }
            } catch (Exception e) {
            }
        }
}

 

線程啓動/停止

​ 我們創建線程之後,是通過線程的start()方法來啓動。如果是用線程池的話,就是調用線程池的excecute()方法或者submit()方法來啓動。

​ 當run() 或者 call() 方法執行完的時候線程會自動結束,主動停止線程的話,Java提供的終止方法只有一個stop(),但是不建議使用。因爲它是一個過時的方法,是一種惡意的中斷。可能會導致不可預知的錯誤,比如線程鎖沒有歸還,io流不能關閉。然後我一般是設置一個volatile 狀態變量,通過這個狀態變量來退出run()方法的循環,從而結束這個線程的。

如果run()方法的while循環裏面被阻塞的話,可以配合interrupt(),捕獲InterruptedException 來結束此線程。

 

Java內存模型(JMM)

​ Java內存模型就是一組規則,它規定了多個線程之間的通信方式與通信細節。

​ 首先,JMM規定了所有的變量都存儲在主內存中,每個線程都有自己的工作內存,線程需要使用到的變量,都會從主內存拷貝一份副本到自己的工作內存中,線程對變量的讀寫操作都是在自己的工作內存中進行,而不能直接讀寫主內存中的變量。不同線程之間無法直接訪問對方工作內存中的變量,線程間變量值的傳遞需要通過主內存來完成。

​ Java內存模型是圍繞着併發編程中原子性、可見性、有序性這三個特徵來建立的。舊的內存模型(在JDK5之前),Java主要是依靠規定了8種對內存訪問語句以及規則。比如如何執行變量的讀/寫,加鎖/解鎖,以及volatile變量的讀/寫等,來保證多線程程序的正確性。舊的內存模型很複雜,JDK5之後,JMM採用新的內存模型。新的內存模型使用happens-before的概念來闡述操作之間的內存可見性。happens-before就是先行發生原則,它定義了一些規則如:一個線程內寫在前面的代碼會先發生於寫在後面的代碼,還有一個鎖的解鎖操作會先發生於這個鎖的解鎖操作,還有volatile變量的寫操作會先發生於這個變量的讀操作,還有事件A如果先發生於事件B,事件B又先發生於事件C,那麼A一定先發生於C 諸如此類的一些規則。通過這些規則我們可以解決併發環境下兩個操作之間是否可能存在衝突的問題。

​ 總之,Java內存模型的提出,就是爲了保證線程之間的可見性,原子性和有序性,然後保證我們對程序的運行順序是可控的從而保證線程安全和提高程序運行效率。

JDK1.5前的JMM通過定義八種操作來完成內存交互。(unlock|lock|write<storage|assign|use|load<read

多線程間通信

​ 線程間通信是有兩種機制,一個是共享內存,一個是消息傳遞。

​ 共享內存是一種隱式的通信方式,就是JMM模型定義的那樣,線程之間共享一些公共狀態,線程通過讀或寫等操作來影響這些公共狀態,實現與其它線程的通信。

​ 消息傳遞則是一種顯示的通信方式,就是我們通過wait()、notify()/notifyAll()這種顯示的調用的方式發送消息,實現通信。這種顯示的通信方式除了wait()、notify()/notifyAll()外,還有thread.join(),CountdownLatch,CyclicBarrier,FutureTask/Callable,condition.await() / condition.signal()等等。目的就是爲了更直觀更方便地控制線程執行的順序,達到我們需要的效果。

一般說的synchronized用來做多線程同步功能,其實synchronized只是提供多線程互斥,而對象的wait()和notify()方法才提供線程的同步功能。

實現線程安全的辦法:第一,是採用原子變量,線程安全問題最根本上是由於全局變量和靜態變量引起的,定義變量用sig_atomic_t和volatile。第二,就是實現線程間同步,讓線程有序訪問變量

 

多線程關鍵字

synchronized  面試大概會問

要點: 出現原因/作用 使用場景 原理

​ 在JMM模型裏面,我們知道多個線程會共享同一個主內存上的數據,因爲多個線程都能夠對主內存上的數據進行讀或寫操作,所以就會存在一個數據如何同步的問題。因爲同時讀數據不會產生衝突,所以要解決同步問題,實際上就是要解決多個線程之間同時寫數據會衝突的問題。對於這個問題,java就提供了這個Synchronized關鍵字,synchronized就可以保證在併發情況下,同一時刻只有一個線程執行某個方法或某段代碼。只有一個線程執行寫操作的話,自然就沒有衝突的問題了。

​ synchronized的用法也很簡單,它可以修飾普通方法、靜態方法和代碼塊。這裏就要說一下鎖的概念了。Java中每一個對象都可以作爲鎖,這是synchronized實現同步的基礎。當synchronized修飾普通方法時,鎖對象就是當前調用這個方法的實例對象;當synchronized修飾靜態方法時,鎖對象就是當前類的class對象;當synchronized修飾代碼塊時,鎖對象就是括號裏面的對象。當一個線程想調用synchronized修飾的方法或代碼塊時,它必須要獲取對應的鎖才能夠執行。比如多個線程同時調用一個同步方法,那麼這多個線程就會去搶調用這個同步方法的實例對象對應的鎖,搶到這個鎖的線程就可以執行這個方法,沒搶到的線程就會進入一個同步隊列,等到搶到鎖的線程執行結束或者出現異常釋放鎖後,同步隊列中的線程就繼續爭搶對象鎖。如此循環,這就保證了同一時刻只有一個線程在執行這段代碼。也是通過這種方式實現同步和線程安全,也保證了線程之間的可見性和原子性。

  • 釋放鎖的情況還有一種就是調用鎖對象的wait()方法,釋放當前鎖,並將當前線程放入等待隊列,該線程將等待notify()喚醒,喚醒後進入同步隊列。 (wait()/notify()要在synchronized中使用)

  • synchronized是非公平鎖,新進入同步隊列的線程會先嚐試自旋獲取鎖,可能立即獲得鎖,而在隊列中等候已久的線程則可能再次等待。

  • JDK1.6後,當執行synchronized同步塊的時候jvm會根據啓用的鎖和當前線程的爭用情況,決定如何執行同步操作;偏向鎖-> 輕量級鎖->自旋鎖->重量級鎖。如果線程爭用激烈,那麼應該禁用偏向鎖。-XX:-UseBiasedLocking

 

volatile  面試大概會問

​ 首先volatile也是java中的一個關鍵字,加入volatile關鍵字時,編譯後的底層代碼會多出一個lock前綴指令。這個lock前綴指令實際上相當於一個內存屏障,內存屏障會提供3個功能:

1)它確保指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;

2)它會強制將對緩存的修改操作立即寫入主存;

3)如果是寫操作,它會導致其他CPU中對應的緩存行無效。其它線程因爲緩存失效所以會重讀主內存拿到它修改後的值。

也就是說volatile能保證線程之間的可見性與原子性。值得注意的是它並不能保證複合操作的原子性。如自增自減操作。

 

Lock

​ Lock是一個接口。jdk1.5後出現的。我們瞭解到如果一個代碼塊被synchronized修飾了,當一個線程獲取了對應的鎖,並執行該代碼塊時,其他線程便只能一直等待,等待獲取鎖的線程釋放鎖,那麼如果這個獲取鎖的線程由於要等待IO或者其他原因(比如調用sleep方法)被阻塞了,但是又沒有釋放鎖,其它線程也只能繼續等待,而什麼也做不了。這樣就很影響了程序的效率。而Lock就可以通過tryLock()方法做到讓等待的線程只等待一定的時間,如果一定時間內沒有獲得鎖就可以繼續等或者可以做其他事情。Lock也可以通過lock.lockInterruptibly()方法直接中斷線程的等待過程。

​ 還有一種情況是,當有多個線程讀寫文件時,讀操作和寫操作會發生衝突現象,寫操作和寫操作會發生衝突現象,但是讀操作和讀操作不會發生衝突現象。但是採用synchronized關鍵字來實現同步的話,就會導致一個問題:如果多個線程都只是進行讀操作,當一個線程在進行讀操作時,其他線程也只能等待無法進行讀操作。而Lock能夠做到多個線程都進行讀操作而不會發生衝突。

​ 另外,通過Lock可以通過tryLock()方法知道線程有沒有成功獲取到鎖。這個是synchronized無法辦到的。

Lock類常用的有四個方法:lock()、unLock()、tryLock()、lockInterruptibly()。lock()就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。unLock()就是用來釋放鎖。一般放在finally塊中保證一定執行。避免死鎖。tryLock(long waittime)就是用來嘗試獲取鎖,無論如何都會立即返回。故拿不到鎖時也能做其他事。lockInterruptibly()就是獲取不到鎖是可以中斷線程的等待過程。而不是一直等待。

 

synchronized volatile Lock三者區別:

synchronized和volatile區別。

1.1在說他們的區別之前,得先說一下JMM。JMM就是java內存模型,它設計是用來解決併發過程中如何處理可見性、原子性和有序性的問題。JMM定義了主內存和線程之間的關係。主內存就是堆內存。然後主內存中存儲着所有實例/靜態變量/數組。每個線程都有自己的工作內存,然後線程會把它要用到的共享對象從主內存拷貝到自己的工作內存緩存起來。線程對共享變量的寫操作,並不是直接作用在主內存上,而是隻在自己的工作內存進行操作。寫操作結束之後纔會把結果傳給主內存,然後主內存上的值就修改成對應結果。然後之後其它線程再用到這個值的時候就會從主內存中拿到新修改的值。這個就是JMM定義的主內存和工作內存之間交互的規則。

這樣就有兩個問題,第一個是,線程什麼時候刷新本地內存上共享變量的值到主內存,第二個是,其它線程又是什麼時候把主內存的值同步到本地內存。這兩個都是不確定的,就會導致一個可見性的問題。爲了解決這個問題,就有了Synchronized和volatile這兩個關鍵字。

使用Synchronized的話,線程a得到鎖後會鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住。然後線程a釋放鎖以後會把數據同步到主內存。然後另一個線程b獲得鎖以後會從主內存獲得數據到本地內存。這就完成了一個同步,實現了可見性。

使用volatile修飾變量時,對這個變量進行寫操作時,JVM會向處理器發送一條lock指令,將這個變量所在的緩存行的數據寫回到主內存,然後其他線程工作內存上這個變量的緩存就變爲了失效狀態,會重新把這個變量從主內存讀取到自己的工作內存。

所以,synchronized和volatile都能實現可見性。volatile本質是告訴jvm當前變量在工作內存中的值是不確定的,需要從主內存讀取,它不會阻塞其它線程; synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住。這是他們的一個區別。關於可見性的。

然後對於原子性。Volatile和Synchronized都能保證原子性,但是volatile不能保證複合操作的原子性,比如對一個共享對象進行自增自減操作,就涉及到從主內存中獲取值,對這個值進行自增自減操作,然後把值寫會主內存這三個操作。這三個操作是不能保證原子性的。synchronized可以,因爲只有拿到鎖的線程纔可以對這個共享變量進行操作嘛。這是他們的第二個區別。關於原子性的。

然後對於有序性,volatile標記的變量不會被編譯器優化;synchronized標記的變量可以被編譯器優化。就是說volatile不允許重排序的,它會有一個內存屏障,屏障前的代碼一定比屏障後的代碼先執行。但synchronized可以重排序。因爲它是獨佔線程,不會有安全問題。這是他們的第三個區別,關於有序性的。

還有一個區別就是volatile僅能使用在變量級別;synchronized則可以使用在變量、方法、和類級別的。然後volatile的優點在於它的花銷比較小。比較適合作爲狀態標誌。

// 結合使用 volatile 和 synchronized 實現 “開銷較低的讀-寫鎖”
private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
    return value++;
}

synchronized和Lock區別:

①synchronized是java的關鍵字,而ReentrantLock是一個類。都能通過鎖來控制同步。Synchronized的鎖更重量級一些,Lock類的鎖更輕量級。

②然後synchronized會自動釋放鎖,Lock需要主動調用unlock()方法釋放鎖。特別是異常時,如果沒有主動unlock()釋放鎖,很可能造成死鎖,所以unlock一般都是放在finally塊中執行。

③然後synchronized沒有獲得鎖時會一直等待,不能中斷,相當於是阻塞式的。Lock可以讓等待鎖的線程響應中斷。

④然後synchronized不能知道當前線程有沒有獲得鎖,而lock通過tryLock()可以知道有沒有獲得鎖。

⑤還有Lock可以提高多個線程進行讀操作的效率。ReadWriteLock

  • 可重入鎖就是一個線程得到一個對象鎖後再次請求該對象鎖,是永遠可以拿到鎖的。這時候僅僅是把狀態值進行累加。synchronized 和 ReentrantLock都是可重入鎖。

  • 死鎖實例:

//死鎖
synchronized(DeadLock.obj1){    //線程1中持有obj1的鎖後,又去拿obj2的鎖.
  Thread.sleep(1000);//獲取obj1後先等一會兒,讓Lock2有足夠的時間鎖住obj2
  synchronized(DeadLock.obj2){  //這時obj2已經被線程2拿着。所以這裏線程1會陷入等待。
    System.out.println("Lock1 lock obj2");
  }
}
synchronized(DeadLock.obj2){    //線程2中持有obj2的鎖後,又去拿obj1的鎖。  
  System.out.println("Lock2 lock obj2");
  Thread.sleep(1000); //獲取obj2後先等一會兒,讓Lock1有足夠的時間鎖住obj1
  synchronized(DeadLock.obj1){  //這時obj1已經被線程1拿着。所以這裏線程2也會陷入等待。
    System.out.println("Lock2 lock obj1");
  }
}
//兩個線程都陷入了等待,都等對方釋放鎖。就死鎖了。所以鎖不要亂嵌套,會死掉的!!

線程池

要點: 概念 作用 類型

在沒用線程池之前,我們是需要使用線程就去創建一個,實現起來也很簡單,但是問題如果併發的線程數量很多,而且每個線程執行的時間又比較短的話,系統就會很頻繁地創建,切換和銷燬線程,這都是很耗時間的,會降低系統的效率。然後這個問題就跟我們數據庫的連接一樣,然後數據庫有對應的數據庫連接池。線程對應就有線程池。目的都是爲了實現一個資源複用的效果。

使用Java線程池的好處:1) 重用存在的線程,減少對象創建、消亡的開銷,提升性能。2)可有效控制最大併發線程數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞。3)提供定時執行、定期執行、單線程、併發數控制等功能。

Java中常用的線程池有四種,每種線程池有不同的適用場景:

newCachedThreadPoolnewFixedThreadPoolScheduledThreadPoolnewSingleThreadPool

newCachedThreadPool

概念:創建一個可緩存線程池,調用 execute() 執行任務時它可以重用之前構造的並且還可用的線程,然後如果沒有可用的線程的話,它就會池裏面添加一個新的線程去執行任務。默認這個線程池能存在Integer.MAX_VALUE個線程,然後默認會終止和從從緩存中移除那些60s都沒有被使用的線程。就是說,長時間保持這個空閒的線程池也不會佔用資源。

適用:適合執行大量短暫異步的程序,或者希望提交的任務儘快分配線程執行的情況。

ExecutorService cacheThreadPool = Executors.newCachedThreadPool();  //可緩存線程池。

補充:使用CachedThreadPool,要非常注意控制任務的數量,否則由於大量線程同時運行,很有會造成系統癱瘓

 

newFixedThreadPool

概念:創建一個指定工作線程數量的線程池,這樣就能控制最大併發數。然後池中的線程數小於核心線程數時,每提交一個任務,線程池就會創建一個工作線程去執行這個任務。然後如果池中的線程數超過核心線程數了,那麼這些新提交的任務就會被放到池隊列中。然後如果你還要繼續提交任務,把池隊列也給放滿了,就是把池隊列的21億個位置都放滿了。那麼這時候就判斷,如果池中的線程數量小於線程池的最大線程數量,線程池的最大線程數也是21億,那就繼續創建線程去執行。如果池中的線程數量等於線程池的最大線程數量了,就會拋出異常,拒絕任務,至於如何拒絕處理新增的任務,取決於線程池的飽和策略RejectedExecutionHandler了。

適用:對於需要保證所有提交的任務都要被執行的情況,它的性能好很多 

補充:

  • 如果在關閉前的執行期間由於失敗而導致任何線程終止,那麼一個新線程將代替它執行後續的任務

  • 當線程池中線程數超過corePoolSize,空閒時間達到keepAliveTime時,關閉空閒線程,直到=corePoolSize

  • 當設置allowCoreThreadTimeOut(true)時,線程池中corePoolSize線程空閒時間達到keepAliveTime也將關閉

 

ScheduledThreadPool

概念:創建一個線程池,它可安排在給定延遲後運行命令或者定期地執行。池中保存的線程數,即使線程是空閒的也包括在內。

適用:週期性執行任務的場景

 

newSingleThreadPool

概念:創建一個使用單個 worker 線程的 Executor,以無界隊列方式來運行該線程。可保證順序地執行各個任務,並且在任意給定的時間不會有多個線程是活動的。與其他等效的 newFixedThreadPool(1)不同,可保證無需重新配置此方法所返回的執行程序即可使用其他的線程。

適用:一個任務一個任務執行的場景

補充:

  • 常用的線程池模式:半同步/半異步模式(生產者消費者模式)、領導者跟隨者模式。

  • new SingleThreadPool與new FixedThreadPool(1)的區別: 二者都能順序地執行任務,但SingleThreadExecutor不能配置去重新加入線程。也就是說new SingleThreadPool只會有一個線程執行任務,new FixedThreadPool(1)可以通過配置改變線程數量,這時候可能就不是一個線程在執行任務而已了。

final ExecutorService fixed = Executors.newFixedThreadPool(1);
ThreadPoolExecutor executor = (ThreadPoolExecutor) fixed;
executor.setCorePoolSize(4);    //如果通過這樣可以改變fix的任務數:FinalizableDelegatedExecutorService 

 

J.U.C下的常見類

ConcurrentHashMap,Lock,volatile,ThreadPoolExecute,Callable,

CountDownLatch,CyclicBarrier,Atomic,Future和FutureTask,Semaphore,

很多都是基於一個CAS算法:

  • CAS的全稱是Compare And Swap 即比較交換,當多個線程同時使用CAS操作一個變量時,只有一個會勝出,併成功更新,其餘均會失敗,但失敗的線程並不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的線程放棄操作;CAS 是一種無鎖的非阻塞算法的實現,無鎖天生就免疫死鎖。同時CAS是一種系統原語,原語屬於操作系統用語範疇,是由若干條指令組成的,用於完成某個功能的一個過程,並且原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的數據不一致問題。

  • CAS 包含了三個操作數:需要讀寫的內存值: V,進行比較的預估值: A,擬寫入的更新值: B。當且僅當 V == A 時, V = B, 否則,將不做任何操作;

 

ConcurrentHashMap

​ 在1.8版本以前,ConcurrentHashMap採用分段鎖的概念,默認一個ConcurrentHashMap中有16個子HashMap,所以相當於一個二級哈希。對於所有的操作都是先定位到子HashMap,然後每次操作對子HashMap加鎖,使鎖更加細化,再作相應的操作。避免多線程鎖得機率,提高併發效率。1.8之後已經改變了這種思路,而是利用CAS+Synchronized來保證併發更新的安全,當然底層採用數組+鏈表+紅黑樹的存儲結構。

按照1.8源碼,可以確定put整個流程如下:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 1)判空;ConcurrentHashMap的key、value都不允許爲null
    if (key == null || value == null) throw new NullPointerException();
    // 2)計算hash。利用方法計算hash值
    int hash = spread(key.hashCode()); //兩次hash,減少hash衝突,可以均勻分佈
    int binCount = 0;
    // 3)遍歷table,進行節點插入操作。插入過程如下:
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
      // 3.1)如果沒有初始化就先調用initTable() 方法來進行初始化過程
      if (tab == null || (n = tab.length) == 0)
                  tab = initTable();
      // 3.2)/如果插入位置沒有hash衝突就直接CAS插入
      else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
          break;                 
      }
      // 3.2)如果在進行擴容,則先進行擴容操作
      else if ((fh = f.hash) == MOVED)
        tab = helpTransfer(tab, f);
      // 3.3)如果以上條件都不滿足,那就要進行加鎖操作,也就是存在hash衝突,鎖住鏈表或者紅黑樹的頭結點
      else {
        V oldVal = null;
        //如果以上條件都不滿足,那就要進行加鎖操作,也就是存在hash衝突,鎖住鏈表或者紅黑樹的頭結點
        synchronized (f) {if (tabAt(tab, i) == f) { // JDK1.8是樂觀鎖,當有衝突的時候才進行併發處理
         ... //最後一個如果該鏈表的數量大於閾值8,就要先轉換成黑紅樹的結構
        }}
      }
      // 3.4)如果添加成功就調用addCount()方法統計size,並且檢查是否需要擴容
      addCount(1L, binCount);//統計size,並且檢查是否需要擴容
      return null;
    }
}

與1.7的ConcurrentHashMap區別:

  • JDK1.8的實現降低鎖的粒度,JDK1.7版本鎖的粒度是基於Segment的,包含多個HashEntry,而JDK1.8鎖的粒度就是首節點。

  • JDK1.8使用內置鎖synchronized來代替重入鎖ReentrantLock。

  • JDK1.8使用紅黑樹來優化鏈表

按照1.8源碼,可以確定get整個流程如下:

  public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //1)首先根據key進行hash計算,如果table爲空,直接返回null。否則定位到table[]中的i。
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            //2)若table[i]存在,則繼續查找
            if ((eh = e.hash) == h) {// 2.1)首先比較鏈表頭部,如果匹配則返回對應值。
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)// 2.2)然後若爲紅黑樹,查找樹。否則就循環鏈表查找。
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {// 循環鏈表查找
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;// 都找不到就返回爲null
}

AtomicInteger

java8 提供的包 java.util.concurrent.atomic 包含了許多有用的類實現原子操作。原子操作是多個線程同時執行,確保其是安全的,且並不需要synchronized 關鍵字。這裏介紹 AtomicInteger、AtomicBoolean, AtomicLong 和 AtomicReference,這裏主要演示AtomicInteger類。

本質上,原子操作嚴重依賴於CAS,即比較與交換算法,如果只需要併發修改單個可變變量的情況下,優先使用原子類。

public static void main(String[] args){
  AtomicInteger ai = new AtomicInteger(10);
  for(int i = 0; i < 100; i++){
      new Thread( () -> ai.addAndGet(1) ).start();  
  }
  System.out.println(ai);   //是110。線程安全。
}

Callable

代碼實現:

public static void main(String[] args){
  System.out.println("開始執行");
  //1.實現Callable接口,並通過Thread類啓動
  FutureTask<String> result = new FutureTask<>(new Callable<String>() {
      @Override
      public String call() throws Exception {
          Thread.sleep(2000);
          return "睡了2s後...";
      }
  });
  new Thread(result).start();
​
  //2.get()等待線程執行結束,接收線程運算後的結果
  try {
      //也可以 result.canclel(true|false);
      String sum = result.get();    //此時main下面的方法是阻塞的   
      System.out.println(sum);
  } catch (Exception e) {
      e.printStackTrace();
  }
  System.out.println("執行完了");   //不get()的話,就直接執行完了
}

Callable與Runnable區別:

綜上例子可以看到: Callable 和 Future接口的區別

(1)Callable是類似於Runnable的接口,實現Callable接口的類和實現Runnable的類都是可被其它線程執行的任務。

(2)Callable規定的方法是call(),有返回值,可以拋出異常;而Runnable規定的方法是run(),沒有返回值,也不能拋出異常。

(3)運行Callable任務可拿到一個Future對象, 通過這個對象的isDone()方法可以檢查call()是否完成,可以用cancel()方法取消任務的執行,還可以用get()方法獲取任務執行的結果。

 

CountDownLatch

CountDownLatch概念

CountDownLatch是一個同步工具類,用來協調多個線程之間的同步,或者說起到線程之間的通信的作用。它能夠讓一個線程在等待另外一些線程完成各自工作之後,再繼續執行。

它是使用一個計數器進行實現。計數器初始值爲線程的數量。當每一個線程完成自己任務後,計數器的值就會減一。當計數器的值爲0時,表示所有的線程都已經完成了任務,然後在CountDownLatch上等待的線程就可以恢復執行任務。

然後CountDownLatch是一次性的,計數器的值只能在構造方法中初始化一次,之後沒有任何機制再次對其設置值,當CountDownLatch使用完畢後,它不能再次被使用。

CountDownLatch的用法

CountDownLatch典型用法1:某一線程在開始運行前等待n個線程執行完畢。將CountDownLatch的計數器初始化爲n new CountDownLatch(n) ,每當一個任務線程執行完畢,就將計數器減1 countdownlatch.countDown(),當計數器的值變爲0時,在CountDownLatch上 await() 的線程就會被喚醒。一個典型應用場景就是啓動一個服務時,主線程需要等待多個組件加載完畢,之後再繼續執行。

CountDownLatch典型用法2:實現多個線程開始執行任務的最大並行性。注意是並行性,不是併發,強調的是多個線程在某一時刻同時開始執行。類似於賽跑,將多個線程放到起點,等待發令槍響,然後同時開跑。做法是初始化一個共享的CountDownLatch(1),將其計數器初始化爲1,多個線程在開始執行任務前首先 coundownlatch.await(),當主線程調用 countDown() 時,計數器變爲0,多個線程同時被喚醒。

典型用法:new CountDownLatch(n) -> countdownlatch.countDown() -> countdownlatch.await() 代碼如下

public static void main(String[] args){
  System.out.println("開始執行");
  final CountDownLatch latch = new CountDownLatch(1);
  new Thread(() -> {
      try {
          System.out.println("任務執行");
          Thread.sleep(2000);
          latch.countDown();
      } catch (Exception e) {
          e.printStackTrace();
      }
  }).start();
  //await()阻塞,知道latch調用countDown()至0.
  latch.await();
  System.out.println("執行完了");
}

CyclicBarrier

字面意思循環柵欄,通過它可以實現讓一組線程等待至某個狀態之後再全部同時執行。叫做循環是因爲當所有等待線程都被釋放以後,CyclicBarrier可以被重用。

第一個版本比較常用,用來掛起當前線程,直至所有線程都到達barrier狀態再同時執行後續任務;

第二個版本是讓這些線程等待至一定的時間,如果還有線程沒有到達barrier狀態就直接讓到達barrier的線程執行後續任務。

public static void main(String[] args){
  CyclicBarrier cb = new CyclicBarrier(3);
  for(int i=0;i<3;i++){
      final int taskNum = i;
      new Thread(() -> {
          try {
              System.out.println("任務"+taskNum+"開始等待");
              Thread.sleep((taskNum+2)*1000);
              cb.await();
          } catch (Exception e) {
              e.printStackTrace();
          }
          System.out.println("任務"+taskNum+"繼續執行其他事情");
      }).start();
  }
}

Semaphore

Semaphore翻譯成字面意思爲 信號量,Semaphore可以控制並行執行的線程個數,就是可以在Semaphore實例化的時候就指定線程的最大並行數。然後每個線程可以通過 acquire() 方法獲取一個許可,如果沒有獲得線程就會等待,獲得許可的線程執行完之後,使用release() 釋放一個許可。然後處於等待狀態的線程就可以繼續拿到這個許可了。

有意思的是,如果沒有acquire()就release()的話,就相當於最大並行數會隨之調用而多了一個。

Semaphore semaphore = new Semaphore(3); //最多3個線程同時進行
for (int i = 0; i < 5; i++) {
    final int taskNum = i;
    new Thread(()-> {
        try {
            semaphore.acquire();    //取得1個信號。
            System.out.println("線程"+taskNum+"開工");
            Thread.sleep(3000);     //這裏會看到,第三個線程之後,進不來,要等待釋放了信號才能進來。
            semaphore.release();    //讓出1個信號。
        } catch (Exception e) {
            e.printStackTrace();
        }
    }).start();;
}

ThreadLocal

ThreadLocal修飾的變量,會在每個線程中都創建一個副本,即每個線程內部都會有一個該變量,且在線程內部任何地方都可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會嚴重影響程序執行性能。對於同一個static ThreadLocal,不同線程只能從中get,set,remove自己的變量,而不會影響其他線程的變量。所以特別適用於各個線程依賴不同的變量值完成操作的場景。最常見的ThreadLocal使用場景爲 用來解決 數據庫連接、Session管理等。每個ThreadLocal只能保存一個變量副本,如果想要上線一個線程能夠保存多個副本以上,就需要創建多個ThreadLocal。它有四個方法:initialValue()、set()、 get()、 remove()。

ThreadLocal內部的ThreadLocalMap鍵爲弱引用,會有內存泄漏的風險。

適用於無狀態,副本變量獨立後不影響業務邏輯的高併發場景。如果如果業務邏輯強依賴於副本變量,則不適合用ThreadLocal解決,需要另尋解決方案。

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
  public Connection initialValue() {
      return DriverManager.getConnection(DB_URL);
  }
};
//如此依賴,每一個線程中獲取到的connection都是它自己獨享的。不會有線程安全問題。
public static Connection getConnection() {
    return connectionHolder.get();
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章