java并發(fā)編程學(xué)習(xí)

參考資料:
Av86373641:黑馬程序員 - 全面深入學(xué)習(xí)java并發(fā)編程

  • 進(jìn)程和線程



  • 并行并發(fā)
    并發(fā):同一個時間段交叉運(yùn)行多個線程
    并行:同一個時間點(diǎn)運(yùn)行多個線程

  • 線程的創(chuàng)建方式

  1. 覆蓋run方法
  2. 編寫一個Runnable接口對象然后給到Thread對象
  3. FutureTask和Callable接口(Callable比Runnable多一個返回值,并且可以拋出異常),可以返回值,F(xiàn)utureTask創(chuàng)建完也要給到Thread,然后調(diào)用Thread就可以,因為FutureTask也實現(xiàn)了Runnable接口。我們看下FutureTask對runnable的實現(xiàn)就可以知道,它是調(diào)用了Callable對象,處理了異常和返回值。
    public void run() {
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

前兩者推薦推薦用第二種

方法1 是把線程和任務(wù)合并在了一起,方法2 是把線程和任務(wù)分開了
用 Runnable 更容易與線程池等高級 API 配合
用 Runnable 讓任務(wù)類脫離了 Thread 繼承體系,更靈活

// 構(gòu)造方法的參數(shù)是給線程指定名字,推薦
Thread t1 = new Thread("t1") {
@Override
// run 方法內(nèi)實現(xiàn)了要執(zhí)行的任務(wù)
public void run() {
log.debug("hello");
}
};
t1.start();

// 創(chuàng)建任務(wù)對象
Runnable task2 = () -> log.debug("hello");
// 參數(shù)1 是任務(wù)對象; 參數(shù)2 是線程名字,推薦
Thread t2 = new Thread(task2, "t2");
t2.start();
  • Thread和Runnable的關(guān)系
    有了Runnable可以把線程和任務(wù)分開來,Runnable可以更容易與線程池等高級API配合,更加靈活。

  • 棧幀
    棧,每個函數(shù)有一個棧幀

每個棧由多個棧幀(Frame)組成,對應(yīng)著每次方法調(diào)用時所占用的內(nèi)存
每個線程只能有一個活動棧幀,對應(yīng)著當(dāng)前正在執(zhí)行的那個方法

  • Thread方法
    join:調(diào)用的線程等待被調(diào)用的線程運(yùn)行結(jié)束,主要用于同步。
    yield:讓出當(dāng)前線程,從running到runnable
    interrupt:打斷其他線程的運(yùn)行

如果被打斷線程正在 sleep,wait,join 會導(dǎo)致被打斷的線程拋出 InterruptedException,并清除 打斷標(biāo)記 如果打斷的正在運(yùn)行的線程,則會設(shè)置 打斷標(biāo)記 ;park 的線程被打斷,也會設(shè)置 打斷標(biāo)記

  • 守護(hù)線程
    總結(jié):依附其他現(xiàn)成的存在而存在,比如垃圾回收器

默認(rèn)情況下,Java 進(jìn)程需要等待所有線程都運(yùn)行結(jié)束,才會結(jié)束。有一種特殊的線程叫做守護(hù)線程,只要其它非守
護(hù)線程運(yùn)行結(jié)束了,即使守護(hù)線程的代碼沒有執(zhí)行完,也會強(qiáng)制結(jié)束。

  • java的線程狀態(tài)



    waiting是join的時候的狀態(tài)
    blocked是等待鎖的時候的狀態(tài)

  • 臨界區(qū)
    一個程序運(yùn)行多個線程本身是沒有問題的
    問題出在多個線程訪問共享資源
    多個線程讀共享資源其實也沒有問題
    在多個線程對共享資源讀寫操作時發(fā)生指令交錯,就會出現(xiàn)問題
    一段代碼塊內(nèi)如果存在對共享資源的多線程讀寫操作,稱這段代碼塊為臨界區(qū)

  • 臨界區(qū)出問題的解決方案

阻塞式的解決方案:synchronized,Lock
非阻塞式的解決方案:原子變量

  • synchronized的兩種用法

雖然 java 中互斥和同步都可以采用 synchronized 關(guān)鍵字來完成,但它們還是有區(qū)別的:
互斥是保證臨界區(qū)的競態(tài)條件發(fā)生,同一時刻只能有一個線程執(zhí)行臨界區(qū)代碼
同步是由于線程執(zhí)行的先后、順序不同、需要一個線程等待其它線程運(yùn)行到某個點(diǎn)

  • synchronized用于互斥
    要注意兩個都要枷鎖,并且要鎖住同一個對象。

  • 成員變量和靜態(tài)變量是否線程安全?
    如果它們沒有共享,則線程安全
    如果它們被共享了,根據(jù)它們的狀態(tài)是否能夠改變,又分兩種情況
    如果只有讀操作,則線程安全
    如果有讀寫操作,則這段代碼是臨界區(qū),需要考慮線程安全

  • 局部變量是否線程安全?(重點(diǎn)關(guān)注P66)
    局部變量是線程安全的
    但局部變量引用的對象則未必
    如果該對象沒有逃離方法的作用訪問,它是線程安全的
    如果該對象逃離方法的作用范圍,需要考慮線程安全

  • 常見線程安全類
    String
    Integer
    StringBuffer
    Random
    Vector
    Hashtable
    java.util.concurrent 包下的類

  • 線程安全類方法的組合

Hashtable table = new Hashtable();
// 線程1,線程2
if( table.get("key") == null) {
table.put("key", value);
}
  • 開閉原則
    閉原則(final private)可以增加類的安全性,比如String設(shè)置為final就是。(P66)

  • monitor鎖,管程/監(jiān)視器


  • 鎖優(yōu)化(這些措施都是java虛擬機(jī)自動操作的,雖然可能可以配置)
    上面的monitor是重型鎖,還有輕量鎖和偏向鎖。
    偏向鎖:這個鎖的前提是鎖真的主要是其中一個線程在用。
    synchronized最開始加的是輕量級鎖,后面有人來了,進(jìn)行鎖膨脹才加為重量級鎖,鎖膨脹是為了后面的線程有等待隊列。
    鎖膨脹:輕量級鎖變?yōu)橹亓考夋i,鎖膨脹是為了后面的線程有等待隊列。
    自旋優(yōu)化:空轉(zhuǎn)檢查,避免因為進(jìn)入阻塞隊列帶來的上下文切換,多核CPU才有用。自旋失敗的時候才進(jìn)行阻塞。單核的時候其實是利用任務(wù)隊列當(dāng)做隊列,單核其實就很不好,因為自旋優(yōu)化就是為了減少任務(wù)切換。
    偏向鎖:輕量級鎖在沒有競爭的時候,每次重入仍需要執(zhí)行CAS操作。在對象一開始的時候就是使用的偏向鎖,如果后面有任意一次競爭或者偏向的改變,即使解鎖了,重新加鎖,都不是偏向鎖了?;蛘哒{(diào)用wait,notify的時候也會被撤銷,終身禁用。因為wait,notify只有重量級鎖才有。
    批量重偏向:如果發(fā)現(xiàn)在t2線程內(nèi)因為偏向不同而從偏向鎖轉(zhuǎn)向輕量級鎖太多之后(即撤銷偏向鎖),后面把這些對象和鎖都偏向于t2線程。
    批量撤銷:如果撤銷偏向鎖的次數(shù)太多,那么后面對于同一個類的對象再也不用偏向鎖了。
    鎖消除:JIT即時編譯器會優(yōu)化,如果非必要會消除鎖。

  • wait&notify
    進(jìn)入synchronized代碼片段之后(記住wait,notify調(diào)用的這個必要條件),調(diào)用wait的線程會進(jìn)入waitset進(jìn)行等待,并且放棄鎖,這時處于waiting狀態(tài),等待notify/notifyAll后進(jìn)入EntryList等待調(diào)度,這時處于blocked狀態(tài)。這意味著A在wait之后,B進(jìn)入synchronized代碼,B調(diào)用notify,A還是不會馬上執(zhí)行,至少得等待B退出synchronized代碼(或者調(diào)用wait放棄鎖),因為A在EntryList等待鎖,而B沒有退出synchronized代碼就沒有釋放鎖。



    wait,notify都必須在sychronized里面才可以。
    兩者的使用:wait表示要滿足一定的條件,notify表示條件已經(jīng)滿足

  • wait&notify使用搭配

synchronized(lock) {
while(條件不成立) {
lock.wait();
}
// 干活
}
//另一個線程
synchronized(lock) {
lock.notifyAll();
}
  • join的實現(xiàn)
    join是用wait實現(xiàn)的,wait等待線程死掉。
    public final synchronized void join(final long millis)
    throws InterruptedException {
        if (millis > 0) {
            if (isAlive()) {
                final long startTime = System.nanoTime();
                long delay = millis;
                do {
                    wait(delay);
                } while (isAlive() && (delay = millis -
                        TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
            }
        } else if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            throw new IllegalArgumentException("timeout value is negative");
        }
    }
  • park&unpark
    與 Object 的 wait & notify 相比
    wait,notify 和 notifyAll 必須配合 Object Monitor 一起使用,而 park,unpark 不必
    park & unpark 是以線程為單位來【阻塞】和【喚醒】線程,而 notify 只能隨機(jī)喚醒一個等待線程,notifyAll
    是喚醒所有等待線程,就不那么【精確】
    park & unpark 可以先 unpark,而 wait & notify 不能先 notify

  • ReentrantLock可重入鎖



    lock:得不到鎖死等。
    lockInterruptly可打斷:在獲取鎖的時候可以打斷,退出阻塞,得不到鎖而返回。
    tryLock:得不到鎖立即返回,或者可以設(shè)置超時時間。
    synchronized只能是死等,相當(dāng)于只是lock。
    但是synchronized會自動釋放鎖,包括發(fā)生異常的時候。

  • Synchronized和ReentrantLock等價代碼
    Synchronized{臨界代碼段}
    ReentrantLock lock = new ReentrantLock();
    lock.lock ();
    try {
    臨界代碼段
    }finally{
    lock.unlock();
    }

  • Java 內(nèi)存模型
    JMM 即 Java Memory Model,它定義了主存、工作內(nèi)存抽象概念,底層對應(yīng)著 CPU 寄存器、緩存、硬件內(nèi)存、CPU 指令優(yōu)化等。
    JMM 體現(xiàn)在以下幾個方面
    原子性 - 保證指令不會受到線程上下文切換的影響
    可見性 - 保證指令不會受 cpu 緩存的影響
    有序性 - 保證指令不會受 cpu 指令并行優(yōu)化的影響

  • volatile
    保證可見性和有序性,并不能保證原子性。synchronized三者都可以。

  • volatile原理
    volatile 的底層實現(xiàn)原理是內(nèi)存屏障,Memory Barrier(Memory Fence)
    對 volatile 變量的寫指令后會加入寫屏障
    對 volatile 變量的讀指令前會加入讀屏障
    可見性:
    寫屏障(sfence)保證在該屏障之前的,對共享變量的改動,都同步到主存當(dāng)中
    而讀屏障(lfence)保證在該屏障之后,對共享變量的讀取,加載的是主存中最新數(shù)據(jù)
    有序性:
    寫屏障會確保指令重排序時,不會將寫屏障之前的代碼排在寫屏障之后
    讀屏障會確保指令重排序時,不會將讀屏障之后的代碼排在讀屏障之前

  • happens-before
    happens-before 規(guī)定了對共享變量的寫操作對其它線程的讀操作可見,它是可見性與有序性的一套規(guī)則總結(jié),拋開以下 happens-before 規(guī)則,JMM 并不能保證一個線程對共享變量的寫,對于其它線程對該共享變量的讀可見。

  • 無鎖并發(fā)(樂觀鎖)
    CAS:compare and save


  • CAS
    變量必須用volatile修飾,不然不能保證獲得最新的


  • 為什么無鎖效率比較高
    上下文切換的損耗比較高。
    無鎖情況下,即使重試失敗,線程始終在高速運(yùn)行,沒有停歇,而 synchronized 會讓線程在沒有獲得鎖的時候,發(fā)生上下文切換,進(jìn)入阻塞。打個比喻,線程就好像高速跑道上的賽車,高速運(yùn)行時,速度超快,一旦發(fā)生上下文切換,就好比賽車要減速、熄火,等被喚醒又得重新打火、啟動、加速... 恢復(fù)到高速運(yùn)行,代價比較大
    但無鎖情況下,因為線程要保持運(yùn)行,需要額外 CPU 的支持,CPU 在這里就好比高速跑道,沒有額外的跑道,線程想高速運(yùn)行也無從談起,雖然不會進(jìn)入阻塞,但由于沒有分到時間片,仍然會進(jìn)入可運(yùn)行狀態(tài),還是會導(dǎo)致上下文切換。

  • CAS特點(diǎn)
    結(jié)合 CAS 和 volatile 可以實現(xiàn)無鎖并發(fā),適用于線程數(shù)少、多核 CPU 的場景下。
    CAS 是基于樂觀鎖的思想:最樂觀的估計,不怕別的線程來修改共享變量,就算改了也沒關(guān)系,我吃虧點(diǎn)再重試唄。
    synchronized 是基于悲觀鎖的思想:最悲觀的估計,得防著其它線程來修改共享變量,我上了鎖你們都別想改,我改完了解開鎖,你們才有機(jī)會。
    CAS 體現(xiàn)的是無鎖并發(fā)、無阻塞并發(fā),請仔細(xì)體會這兩句話的意思
    因為沒有使用 synchronized,所以線程不會陷入阻塞,這是效率提升的因素之一
    但如果競爭激烈,可以想到重試必然頻繁發(fā)生,反而效率會受影響

  • 不可變類
    不可變類可以解決資源共享的問題

  • 內(nèi)存模型


  • 可見性


  • 有序性
    指令優(yōu)化的時候會進(jìn)行重排,但是有些重排在多線程的情況下會出錯。





  • 如何保證可見性


  • double-check locking


  • happens-before


  • 享元模式flyway
    wikipedia: A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects

  • 線程池繼承體系
    Scheduled修飾的線程池表示這個線程池有定時執(zhí)行等功能。


  • ThreadPoolExecutor參數(shù)

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

corePoolSize 核心線程數(shù)目 (最多保留的線程數(shù))
maximumPoolSize 最大線程數(shù)目
keepAliveTime 生存時間 - 針對救急線程
unit 時間單位 - 針對救急線程
workQueue 阻塞隊列
threadFactory 線程工廠 - 可以為線程創(chuàng)建時起個好名字
handler 拒絕策略

下面幾個是基于ThreadPoolExecutor的各種線程池

  • newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

核心線程數(shù) == 最大線程數(shù)(沒有救急線程被創(chuàng)建),阻塞隊列是無界的,可以放任意數(shù)量的任務(wù),適用于任務(wù)量已知,相對耗時的任務(wù)。

  • newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

核心線程數(shù)是0, 最大線程數(shù)是 Integer.MAX_VALUE,救急線程的空閑生存時間是 60s,意味著1. 全部都是救急線程(60s 后可以回收)2. 救急線程可以無限創(chuàng)建。
隊列采用了 SynchronousQueue 實現(xiàn)特點(diǎn)是,它沒有容量,沒有線程來取是放不進(jìn)去的(一手交錢、一手交貨)。但是在不超過最大線程數(shù)的情況下,每次都會新建新的線程。
這種對比newFixedThreadPool就是另外一種極端,沒有固定線程,每次需要多少就創(chuàng)建多少,不需要有一定容量的隊列來存儲任務(wù)。

  • newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

使用場景:希望多個任務(wù)排隊執(zhí)行。線程數(shù)固定為 1,任務(wù)數(shù)多于 1 時,會放入無界隊列排隊。任務(wù)執(zhí)行完畢,這唯一的線程也不會被釋放。
區(qū)別:
自己創(chuàng)建一個單線程串行執(zhí)行任務(wù),如果任務(wù)執(zhí)行失敗而終止那么沒有任何補(bǔ)救措施,而線程池還會新建一個線程,保證池的正常工作
Executors.newSingleThreadExecutor() 線程個數(shù)始終為1,不能修改
FinalizableDelegatedExecutorService 應(yīng)用的是裝飾器模式,只對外暴露了 ExecutorService 接口,因此不能調(diào)用 ThreadPoolExecutor 中特有的方法。Executors.newFixedThreadPool(1) 初始時為1,以后還可以修改對外暴露的是 ThreadPoolExecutor 對象,可以強(qiáng)轉(zhuǎn)后調(diào)用 setCorePoolSize 等方法進(jìn)行修改。

  • invoke和execute的區(qū)別
// 執(zhí)行任務(wù)
void execute(Runnable command);
// 提交任務(wù) task,用返回值 Future 獲得任務(wù)執(zhí)行結(jié)果
<T> Future<T> submit(Callable<T> task);
  • 創(chuàng)建線程數(shù)量多少
    簡單來說,CPU密集型要創(chuàng)建cpu 核數(shù) + 1個線程差不多,IO密集型要創(chuàng)建多一點(diǎn),因為IO密集型的線程經(jīng)常在IO,CPU占有率并不高,多一點(diǎn)CPU占有率才高。

  • 線程池任務(wù)異常

  1. 主動捕獲
  2. Future
  • SynchronousQueue VS AbstractQueuedSynchronizer(AQS)
    SynchronousQueue 同步隊列,放進(jìn)去的時候如果沒有人來取會進(jìn)行阻塞,如果放進(jìn)去已經(jīng)有人來取了,那就不會阻塞。
    AbstractQueuedSynchronizer 簡單說就是同步工具的隊列
  • 任務(wù)放棄策略


  • 讀寫鎖的示例


  • synchronize和aqs的區(qū)別


  • CountDownLatch


  • join跟CountdownLatch的區(qū)別
  1. join比較底層,CountdownLatch比較高層
  2. join必須等到線程結(jié)束的時候才可以,而CountdownLatch只需調(diào)用。
  • CopyOnWriteArrayList
    CopyOnWriteArrayList 可以實現(xiàn)讀寫并發(fā),但是具有弱一致性,其他的并發(fā)容器一般只做到讀讀并發(fā)。并發(fā)高和一致性是矛盾的。

  • 線程安全類合集



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

相關(guān)閱讀更多精彩內(nèi)容

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