Java并發(fā)編程,你需要知道的

本文大綱

1.并發(fā)編程三要素

  • 原子性
    原子,即一個(gè)不可再被分割的顆粒。在Java中原子性指的是一個(gè)或多個(gè)操作要么全部執(zhí)行成功要么全部執(zhí)行失敗。
  • 有序性
    程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。(處理器可能會(huì)對(duì)指令進(jìn)行重排序)
  • 可見性
    當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),如果其中一個(gè)線程對(duì)其作了修改,其他線程能立即獲取到最新的值。

2. 線程的五大狀態(tài)

  • 創(chuàng)建狀態(tài)
    當(dāng)用 new 操作符創(chuàng)建一個(gè)線程的時(shí)候
  • 就緒狀態(tài)
    調(diào)用 start 方法,處于就緒狀態(tài)的線程并不一定馬上就會(huì)執(zhí)行 run 方法,還需要等待CPU的調(diào)度
  • 運(yùn)行狀態(tài)
    CPU 開始調(diào)度線程,并開始執(zhí)行 run 方法
  • 阻塞狀態(tài)
    線程的執(zhí)行過程中由于一些原因進(jìn)入阻塞狀態(tài)
    比如:調(diào)用 sleep 方法、嘗試去得到一個(gè)鎖等等??
  • 死亡狀態(tài)
    run 方法執(zhí)行完 或者 執(zhí)行過程中遇到了一個(gè)異常

3.悲觀鎖與樂觀鎖

  • 悲觀鎖:每次操作都會(huì)加鎖,會(huì)造成線程阻塞。
  • 樂觀鎖:每次操作不加鎖而是假設(shè)沒有沖突而去完成某項(xiàng)操作,如果因?yàn)闆_突失敗就重試,直到成功為止,不會(huì)造成線程阻塞。?

4.線程之間的協(xié)作

4.1 wait/notify/notifyAll

這一組是 Object 類的方法
需要注意的是:這三個(gè)方法都必須在同步的范圍內(nèi)調(diào)用?

  • wait
    阻塞當(dāng)前線程,直到 notify 或者 notifyAll 來喚醒????

    wait有三種方式的調(diào)用
    wait()
    必要要由 notify 或者 notifyAll 來喚醒????
    wait(long timeout)
    在指定時(shí)間內(nèi),如果沒有notify或notifAll方法的喚醒,也會(huì)自動(dòng)喚醒。
    wait(long timeout,long nanos)
    本質(zhì)上還是調(diào)用一個(gè)參數(shù)的方法
    public final void wait(long timeout, int nanos) throws InterruptedException {
          if (timeout < 0) {
                 throw new IllegalArgumentException("timeout value is negative");
           }
          if (nanos < 0 || nanos > 999999) {
                  throw new IllegalArgumentException(
                 "nanosecond timeout value out of range");
           }
           if (nanos > 0) {
                 timeout++;
           }
           wait(timeout);
    }
                  ?
    
    • notify
      只能喚醒一個(gè)處于 wait 的線程
    • notifyAll
      喚醒全部處于 wait 的線程
      ?

4.2 sleep/yield/join

這一組是 Thread 類的方法

  • sleep
    讓當(dāng)前線程暫停指定時(shí)間,只是讓出CPU的使用權(quán),并不釋放鎖

  • yield
    暫停當(dāng)前線程的執(zhí)行,也就是當(dāng)前CPU的使用權(quán),讓其他線程有機(jī)會(huì)執(zhí)行,不能指定時(shí)間。會(huì)讓當(dāng)前線程從運(yùn)行狀態(tài)轉(zhuǎn)變?yōu)榫途w狀態(tài),此方法在生產(chǎn)環(huán)境中很少會(huì)使用到,???官方在其注釋中也有相關(guān)的說明

          /**
          * A hint to the scheduler that the current thread is willing to yield
          * its current use of a processor. The scheduler is free to ignore this
          * hint.
          *
          * <p> Yield is a heuristic attempt to improve relative progression
          * between threads that would otherwise over-utilise a CPU. Its use
          * should be combined with detailed profiling and benchmarking to
          * ensure that it actually has the desired effect.
          *
          * <p> It is rarely appropriate to use this method. It may be useful
          * for debugging or testing purposes, where it may help to reproduce
          * bugs due to race conditions. It may also be useful when designing
          * concurrency control constructs such as the ones in the
          * {@link java.util.concurrent.locks} package.
          */??
          ????
    
  • join
    等待調(diào)用 join 方法的線程執(zhí)行結(jié)束,才執(zhí)行后面的代碼
    其調(diào)用一定要在 start 方法之后(看源碼可知)?
    使用場景:當(dāng)父線程需要等待子線程執(zhí)行結(jié)束才執(zhí)行后面內(nèi)容或者需要某個(gè)子線程的執(zhí)行結(jié)果會(huì)用到 join 方法?

5.valitate 關(guān)鍵字

5.1 定義

java編程語言允許線程訪問共享變量,為了確保共享變量能被準(zhǔn)確和一致的更新,線程應(yīng)該確保通過排他鎖單獨(dú)獲得這個(gè)變量。Java語言提供了volatile,在某些情況下比鎖更加方便。如果一個(gè)字段被聲明成volatile,java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的。

valitate是輕量級(jí)的synchronized,不會(huì)引起線程上下文的切換和調(diào)度,執(zhí)行開銷更小。

5.2 原理

1. 使用volitate修飾的變量在匯編階段,會(huì)多出一條lock前綴指令
2. 它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成
3. 它會(huì)強(qiáng)制將對(duì)緩存的修改操作立即寫入主存
4. 如果是寫操作,它會(huì)導(dǎo)致其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效

5.3 作用

內(nèi)存可見性
多線程操作的時(shí)候,一個(gè)線程修改了一個(gè)變量的值 ,其他線程能立即看到修改后的值
防止重排序
即程序的執(zhí)行順序按照代碼的順序執(zhí)行(處理器為了提高代碼的執(zhí)行效率可能會(huì)對(duì)代碼進(jìn)行重排序)

并不能保證操作的原子性(比如下面這段代碼的執(zhí)行結(jié)果一定不是100000)

    public class testValitate {
    public volatile int inc = 0;
    public void increase() {
        inc = inc + 1;
    }
    public static void main(String[] args) {
        final testValitate test = new testValitate();
        for (int i = 0; i < 100; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 1000; j++)
                        test.increase();
                }
            }.start();
        }
        while (Thread.activeCount() > 2) {  //保證前面的線程都執(zhí)行完
            Thread.yield();
        }
        System.out.println(test.inc);
     }
   }

6. synchronized 關(guān)鍵字

確保線程互斥的訪問同步代碼

6.1 定義

synchronized 是JVM實(shí)現(xiàn)的一種鎖,其中鎖的獲取和釋放分別是
monitorenter 和 monitorexit 指令,該鎖在實(shí)現(xiàn)上分為了偏向鎖、輕量級(jí)鎖和重量級(jí)鎖,其中偏向鎖在 java1.6 是默認(rèn)開啟的,輕量級(jí)鎖在多線程競爭的情況下會(huì)膨脹成重量級(jí)鎖,有關(guān)鎖的數(shù)據(jù)都保存在對(duì)象頭中

6.2 原理

加了 synchronized 關(guān)鍵字的代碼段,生成的字節(jié)碼文件會(huì)多出 monitorenter 和 monitorexit 兩條指令(利用javap -verbose 字節(jié)碼文件可看到關(guān),關(guān)于這兩條指令的文檔如下:

  • monitorenter
    Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
    ? If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
    ? If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
    ? If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.?

  • monitorexit
    The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
    The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.??

加了 synchronized 關(guān)鍵字的方法,生成的字節(jié)碼文件中會(huì)多一個(gè) ACC_SYNCHRONIZED 標(biāo)志位,當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間,其他任何線程都無法再獲得同一個(gè)monitor對(duì)象。 其實(shí)本質(zhì)上沒有區(qū)別,只是方法的同步是一種隱式的方式來實(shí)現(xiàn),無需通過字節(jié)碼來完成。

6.3 關(guān)于使用

  • 修飾普通方法
    同步對(duì)象是實(shí)例對(duì)象
  • 修飾靜態(tài)方法
    同步對(duì)象是類本身
  • 修飾代碼塊
    可以自己設(shè)置同步對(duì)象?

6.4 缺點(diǎn)

會(huì)讓沒有得到鎖的資源進(jìn)入Block狀態(tài),爭奪到資源之后又轉(zhuǎn)為Running狀態(tài),這個(gè)過程涉及到操作系統(tǒng)用戶模式和內(nèi)核模式的切換,代價(jià)比較高。Java1.6為 synchronized 做了優(yōu)化,增加了從偏向鎖到輕量級(jí)鎖再到重量級(jí)鎖的過度,但是在最終轉(zhuǎn)變?yōu)橹亓考?jí)鎖之后,性能仍然較低。

7. CAS

AtomicBoolean,AtomicInteger,AtomicLong以及 Lock 相關(guān)類等底層就是用 CAS實(shí)現(xiàn)的,在一定程度上性能比 synchronized 更高。

7.1 什么是CAS

CAS全稱是Compare And Swap,即比較替換,是實(shí)現(xiàn)并發(fā)應(yīng)用到的一種技術(shù)。操作包含三個(gè)操作數(shù) —— 內(nèi)存位置(V)、預(yù)期原值(A)和新值(B)。 如果內(nèi)存位置的值與預(yù)期原值相匹配,那么處理器會(huì)自動(dòng)將該位置值更新為新值 。否則,處理器不做任何操作。

7.2 為什么會(huì)有CAS

如果只是用 synchronized 來保證同步會(huì)存在以下問題
synchronized 是一種悲觀鎖,在使用上會(huì)造成一定的性能問題。在多線程競爭下,加鎖、釋放鎖會(huì)導(dǎo)致比較多的上下文切換和調(diào)度延時(shí),引起性能問題。一個(gè)線程持有鎖會(huì)導(dǎo)致其它所有需要此鎖的線程掛起。

7.3 實(shí)現(xiàn)原理

Java不能直接的訪問操作系統(tǒng)底層,是通過native方法(JNI)來訪問。CAS底層通過Unsafe類實(shí)現(xiàn)原子性操作。

7.4 存在的問題

  • ABA問題
    什么是ABA問題?比如有一個(gè) int 類型的值 N 是 1
    此時(shí)有三個(gè)線程想要去改變它:
    線程A ??:希望給 N 賦值為 2
    線程B: 希望給 N 賦值為 2
    線程C: 希望給 N 賦值為 1??
    此時(shí)線程A和線程B同時(shí)獲取到N的值1,線程A率先得到系統(tǒng)資源,將 N 賦值為 2,線程 B 由于某種原因被阻塞住,線程C在線程A執(zhí)行完后得到 N 的當(dāng)前值2
    此時(shí)的線程狀態(tài)
    線程A成功給 N 賦值為2
    線程B獲取到 N 的當(dāng)前值 1 希望給他賦值為 2,處于阻塞狀態(tài)
    線程C獲取當(dāng)好 N 的當(dāng)前值 2 ?????希望給他賦值為1
    ??
    然后線程C成功給N賦值為1
    ?最后線程B得到了系統(tǒng)資源,又重新恢復(fù)了運(yùn)行狀態(tài),?在阻塞之前線程B獲取到的N的值是1,執(zhí)行compare操作發(fā)現(xiàn)當(dāng)前N的值與獲取到的值相同(均為1),成功將N賦值為了2。
    ?
    在這個(gè)過程中線程B獲取到N的值是一個(gè)舊值??,雖然和當(dāng)前N的值相等,但是實(shí)際上N的值已經(jīng)經(jīng)歷了一次 1到2到1的改變
    上面這個(gè)例子就是典型的ABA問題?
    怎樣去解決ABA問題
    給變量加一個(gè)版本號(hào)即可,在比較的時(shí)候不僅要比較當(dāng)前變量的值 還需要比較當(dāng)前變量的版本號(hào)。Java中AtomicStampedReference 就解決了這個(gè)問題
  • 循環(huán)時(shí)間長開銷大
    在并發(fā)量比較高的情況下,如果許多線程反復(fù)嘗試更新某一個(gè)變量,卻又一直更新不成功,循環(huán)往復(fù),會(huì)給CPU帶來很大的壓力。

CAS只能保證一個(gè)共享變量的原子操作

8. AbstractQueuedSynchronizer(AQS)

AQS抽象的隊(duì)列式同步器,是一種基于狀態(tài)(state)的鏈表管理方式。state 是用CAS去修改的。它是 java.util.concurrent 包中最重要的基石,要學(xué)習(xí)想學(xué)習(xí) java.util.concurrent 包里的內(nèi)容這個(gè)類是關(guān)鍵。 ReentrantLock?、CountDownLatcher、Semaphore 實(shí)現(xiàn)的原理就是基于AQS。想知道他怎么實(shí)現(xiàn)以及實(shí)現(xiàn)原理 可以參看這篇文章https://www.cnblogs.com/waterystone/p/4920797.html

9. Future

在并發(fā)編程我們一般使用Runable去執(zhí)行異步任務(wù),然而這樣做我們是不能拿到異步任務(wù)的返回值的,但是使用Future 就可以。使用Future很簡單,只需把Runable換成FutureTask即可。使用上比較簡單,這里不多做介紹。

10. 線程池

如果我們使用線程的時(shí)候就去創(chuàng)建一個(gè)線程,雖然簡單,但是存在很大的問題。如果并發(fā)的線程數(shù)量很多,并且每個(gè)線程都是執(zhí)行一個(gè)時(shí)間很短的任務(wù)就結(jié)束了,這樣頻繁創(chuàng)建線程就會(huì)大大降低系統(tǒng)的效率,因?yàn)轭l繁創(chuàng)建線程和銷毀線程需要時(shí)間。線程池通過復(fù)用可以大大減少線程頻繁創(chuàng)建與銷毀帶來的性能上的損耗。

Java中線程池的實(shí)現(xiàn)類 ThreadPoolExecutor,其構(gòu)造函數(shù)的每一個(gè)參數(shù)的含義在注釋上已經(jīng)寫得很清楚了,這里幾個(gè)關(guān)鍵參數(shù)可以再簡單說一下

  • corePoolSize :核心線程數(shù)即一直保留在線程池中的線程數(shù)量,即使處于閑置狀態(tài)也不會(huì)被銷毀。要設(shè)置 allowCoreThreadTimeOut 為 true,才會(huì)被銷毀。
  • maximumPoolSize:線程池中允許存在的最大線程數(shù)
  • keepAliveTime :非核心線程允許的最大閑置時(shí)間,超過這個(gè)時(shí)間就會(huì)本地銷毀。
  • workQueue:用來存放任務(wù)的隊(duì)列。
    • SynchronousQueue:這個(gè)隊(duì)列會(huì)讓新添加的任務(wù)立即得到執(zhí)行,如果線程池中所有的線程都在執(zhí)行,那么就會(huì)去創(chuàng)建一個(gè)新的線程去執(zhí)行這個(gè)任務(wù)。當(dāng)使用這個(gè)隊(duì)列的時(shí)候,maximumPoolSizes一般都會(huì)設(shè)置一個(gè)最大值 Integer.MAX_VALUE
    • LinkedBlockingQueue:這個(gè)隊(duì)列是一個(gè)無界隊(duì)列。怎么理解呢,就是有多少任務(wù)來我們就會(huì)執(zhí)行多少任務(wù),如果線程池中的線程小于corePoolSize ,我們就會(huì)創(chuàng)建一個(gè)新的線程去執(zhí)行這個(gè)任務(wù),如果線程池中的線程數(shù)等于corePoolSize,就會(huì)將任務(wù)放入隊(duì)列中等待,由于隊(duì)列大小沒有限制所以也被稱為無界隊(duì)列。當(dāng)使用這個(gè)隊(duì)列的時(shí)候 maximumPoolSizes 不生效(線程池中線程的數(shù)量不會(huì)超過corePoolSize),所以一般都會(huì)設(shè)置為0。
    • ArrayBlockingQueue:這個(gè)隊(duì)列是一個(gè)有界隊(duì)列。可以設(shè)置隊(duì)列的最大容量。當(dāng)線程池中線程數(shù)大于或者等于 maximumPoolSizes 的時(shí)候,就會(huì)把任務(wù)放到這個(gè)隊(duì)列中,當(dāng)當(dāng)前隊(duì)列中的任務(wù)大于隊(duì)列的最大容量就會(huì)丟棄掉該任務(wù)交由 RejectedExecutionHandler 處理。

最后,本文主要對(duì)Java并發(fā)編程開發(fā)需要的知識(shí)點(diǎn)作了簡單的講解,這里每一個(gè)知識(shí)點(diǎn)都可以用一篇文章去講解,由于篇幅原因不能對(duì)每一個(gè)知識(shí)點(diǎn)都詳細(xì)介紹,我相信通過本文你會(huì)對(duì)Java的并發(fā)編程會(huì)有更近一步的了解。如果您發(fā)現(xiàn)還有缺漏或者有錯(cuò)誤的地方,可以在評(píng)論區(qū)補(bǔ)充,謝謝。

相關(guān)鏈接

版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容