Java併發編程總結(一)

什麼是線程?

線程是進程的實體,線程本身是不會獨立存在的。進程是代碼在數據集合上的一次運行
活動,是系統進行資源分配和調度的基本單位。線程則是進行的這一個執行
路徑,一個進程中至少有一個線程。進程中的多個線程共享進程的資源。

創建線程

java中創建線程有三種方式:
1、實現Runnable接口的run方法 (無返回值)
2、繼承Thread類並重寫run方法(無返回值)
3、使用FutureTask方式(有返回值)

wait()函數

當一個線程調用一個共享變量的wait()方法時,該調用線程會被阻塞掛起
直到發生如下事情就會返回
1、其他線程調用了該共享對象的notify或者notifyAll方法
2、其他線程調用了該線程的interrupt方法,拋出InterruptedException的異常返回
需要注意的地方
    如果調用wait方法的線程沒有事先獲取該對象的監視器鎖,則
    調用wait方法時調用會拋出IllegalMonitorStateException異常

wait(long timeout)函數

在wait方法多了一個超時時間,和wait不同之處在於如果一個線程調用共享對象的該方法掛起後,
沒有在指定的的timeout ms時間內被其他線程調用該共享變量的notify或者notifyAll方法喚醒
那麼該函數還是會因爲超時而返回。

wait(long timeout,int nano)函數

在其內部調用wait(long timeout)函數,只有當nano>0 時才使參數timaout遞增1

notify()函數

一個線程調用共享對象的notify()方法後,會喚醒一個在該共享變量上調用wait系列方法後被掛起的線程
一個共享變量上可能會有多個線程在等待,具體喚醒哪個等待的線程是隨機的。
被喚醒的線程不能馬上從wait方法返回並繼續執行,他必須在獲取了共享對象的監視器鎖後纔可以返回。
因爲該線程還需要和其他線程一起競爭該鎖。只有該線程競爭到了共享變量的監視器鎖纔可以繼續執行。

notifyAll函數

喚醒所有在該共享變量上由於調用wait系列方法而被掛起的線程。
需要注意的點:
    在共享變量上調用notifyAll方法只會喚醒調用這個方法前調用了wait系列函數而被放入共享變量
    等待集合裏面的線程。如果調用notifyAll方法後一個線程調用了該共享變量的wait方法而被放入
    阻塞集合,該線程不會被喚醒。

如何才能獲取一個共享變量的監視器鎖呢?

1、執行synchronized同步代碼塊時,使用該共享變量作爲參數。
    synchronized(共享變量){
    
    }
2、使用該共享變量的方法,並且該方法使用了synchronized修飾。
    synchronized void add(int a,int b){
    
    }

虛假喚醒

一個線程可以從掛起狀態變爲可以運行狀態,即使該線程沒有被其他線程
調用notify(),notifyAll()方法進行通知或者被中斷,或者等待超時
這就是所謂的虛假喚醒

虛假喚醒的檢查

synchronized(obj){
    while(條件不滿足){
        obj.wait();
    }
}

volatile變量特性

1、可見性:對一個valatile變量的讀,總是能看到任意線程對這個volatile
變量最後的寫入
2.原子性:對任意單個volatile變量的讀/寫具有原子性,但是類型於a++這種
複合操作不具有原子性。
當前線程調用共享對象的wait方法時,當前線程只會釋放當前方向對象的鎖,當前線程持有的其他共享對象的監聽器並不會被釋放

join方法

等待線程執行終止
使用場景:需要等待某幾件事情完成後才能繼續往下執行,比如多個線程加載資源,需要等待多個線程全部加載完畢
再彙總處理。Thread提供了join方法就可以達到這個目的。
join是無參且返回值爲void的方法。
ThreadJoin2Test:
線程A調用線程B的join方法後會阻塞,當其他線程調用了線程A的interrupedException異常而返回。

sleep方法

當一個執行中的線程調用了Thread的sleep方法後,調用線程會暫時讓出指定時間的執行權,也就是在這個期間
不參與CPU的調度,但是該線程所擁有的監視器資源還是持有不讓出的。
指定的睡眠時間到了後該函數會正常返回,線程就處於就緒狀態,然後參與CPU的調度,獲取到CPU的資源後就可以
繼續運行了。
如果在sleep期間,調用了中斷的方法,將拋出InterrupedException異常。

yield方法

讓出CPU執行權,線程處於就緒狀態

線程中斷

線程中斷是一種線程間的協作模式,通過設置線程的中斷標誌並不能直接終止該線程的執行,而是被中斷的線程根據
中斷狀態自行處理
interrupt函數:中斷線程,當線程A運行時,線程B可以調用線程A的interrupt函數方法來設置線程A的中斷
標誌爲true,設置標記僅僅爲設置標記,線程A實際並沒有中斷,他會繼續往下執行。如果線程A應爲調用了wait、join
或者sleep方法的時候,線程B調用線程A的interrupt函數將會拋出異常。
boolean isInterrupted()方法:檢查當前線程是否被中斷。
boolean interrupted()方法,檢查當前線程是否被中斷。該方法如果發現線程被中斷,則會清除中斷標誌,並且
該方法是static方法,可以通過Thread類直接調用

線程切換

在多線程編程中,線程個數一般是大於CPU的個數,CPU資源分配採用了時間輪轉的策略,也就是給每個線程分配一個
時間片,線程在時間片內佔用CPU執行任務。當前線程使用完時間片後,就會處於就緒狀態並讓出CPU讓其他線程佔用
,這就是線程上線切換
    線程上下文切換時機:
        1.當前線程的CPU時間片使用完處於就緒狀態時,當前線程被其他線程中斷時。

線程死鎖

死鎖是指兩個或兩個以上的線程在執行過程中,因爭奪資源而造成的互相等待的現象,在無外力的作用下,這些線程
會一直相互等待而無法繼續運行下去。
產生死鎖的四個條件:
    1、互斥條件:值線程對已經獲取到的資源進行排它性使用,即該資源同時只能有一個線程佔用。如果此時還有
    其他線程請求獲取該資源,則請求則只能等待,直到佔有資源的線程釋放該資源。
    2、請求並持有條件:指一個線程已經持有了至少一個資源,但又提出了新的資源請求,而新資源已經被其他線程
    佔有,所有當前線程會被阻塞,但阻塞的同時並不釋放自己已經獲取的資源。
    3、不可剝奪條件:指線程獲取到的資源在自己使用完之前不能被其他線程搶佔,只有在自己使用完畢後纔有自己
    釋放該資源。
    4、環路等待條件:指在發生死鎖時,必然存在一個線程-資源的環型鏈,及線程集合中等待各自線程的佔用的資源

如何避免線程死鎖

只需要破壞掉至少一個構造死鎖的必要條件即可,但是隻有請求並持有和環路等待條件時可以被破壞的。造成死鎖的原因
其實和申請資源的順序有很大關係,使用資源申請的有序性原則就可以避免死鎖。

守護進程與用戶進程

Java中的線程分爲兩類,分別爲守護進程和用戶線程。
   用戶線程:在JVM啓動時會調用main函數,main函數所在的線程就是一個用戶線程
   其實在JVM內部同時還啓動了好多守護進程,比如垃圾回收。
   區別:是當最後一個非守護進程結束時,JVM會正常退出,而不管當前是否有守護線程。也就是守護經常是否結束
   並不影響JVM的退出。

ThreadLocal

多線程訪問同一個共享變量時特別容易出現併發問題,特別是在多個線程需要對一個共享變量進行寫入時。爲了保證線程
安全,一般使用者在訪問共享變量時需要進行適當的同步。
    同步的措施一般是加鎖。
    ThreadLocal提供了線程本地變量,也就是如果你創建了一個ThreadLocal變量,那麼訪問這個變量的每個線程
    都會有這個變量的一個本地副本。當多個線程操作這個變量時,實際操作的是自己本地內存裏面的變量,從而避免了
    線程安全問題。

ThreadLocal不支持繼承性

同一個ThreadLocal變量在父線程中被設置後,在子線程中是獲取不到的。

InheritableThreadLocal繼承了ThreadLocal,實現了一個特性
讓子線程能訪問在父類進程中設置的本地變量

併發和並行

併發:指同意時間段內多個任務同時都在執行,並且沒有執行結束
並行:指在單位時間內多個任務同時在執行。併發是強調在一個時間段內同時執行
而一個時間段有多個單位時間累積而成,所以說併發的多個任務在單位時間內不一定
同時執行。

線程安全

線程安全是指當多個線程同時讀寫一個共享資源並且沒有任何同步措施時,導致出現髒數據或者其他不可預見的結果的問題。

synchronized關鍵字

synchronized塊是Java提供的一種原子性內置鎖,Java中的每個對象都可以把它當做一個同步鎖來使用,Java內置的
的使用者看不到的鎖叫內部鎖。
線程執行代碼在進入synchronized代碼塊前會自動獲取內部鎖,這時候其他線程訪問該同步代碼塊時會被阻塞掛起。

valatile關鍵字

該關鍵字可以確保對一個變量的更新對其他線程馬上可見。當一個變量被聲明爲valatile時,線程在寫入變量時不會把值
緩存在寄存器或者其他地方,而是會把值刷新回主內存。當其他線程讀取該共享變量時,會從主內存重新獲取最新值,而不
是使用當前線程的工作內存中的值。
    volatile的操作並不能保證原子性。
    使用場景:1、寫入變量值不依賴變量的當前值時。因爲如果依賴當前值,將是獲取-計算-寫入三步操作,這三步不是
    原子性的,而volatile並不保證原子性。
    2.讀寫變量值時沒有加鎖。因爲加鎖本身已經保證了內存可見性,這時候不需要把變量聲明爲volatile的

unSafe類

Unsafe類提供了硬件級別的原子性操作,Unsafe類中的方法都是native方法,使用JNI的方式訪問本地的C++實現庫。
Unsafe類的初始化運行的時候會檢查是使用哪一種類加載器:
@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}
//判斷是不是bootstrap類加載器加載的localClass,如果使用AppClassLoader加載的,會拋出異常
//爲什麼會拋出異常?
//unsafe類是在rt.jar包中,這個包中的裏面的類是使用bootstrap類加載器加載的,而我們啓動main函數
//是使用AppClassLoader加載的,如果不做這個限制的話,就可以隨意使用unsafe了,unsafe是可以直接
//操作內存的,這是不安全的。
public static boolean isSystemDomainLoader(ClassLoader var0) {
    return var0 == null;
}

Java指令重排序

java內存模型允許編譯器和處理器對指令重排序已提高運行性能,並且只會對不存在數據依賴性的指令重排序
在單線程下重排序可以保證最終執行的結果和程序順序執行的結果一致。但是在多線程下就會存在問題。
reordering:
重排序在多線程下會導致非預期的程序執行結果,而使用volatile修飾ready就可以避免重排序問題和內存可見
性問題。
寫volatile變量時,可以確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後。讀volatile
變量時,可以確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。

Java中的僞共享

爲了解決計算機系統中主存與CPU之間運行速度差問題,會在CPU和主內存之間添加一級或者多級高速緩衝存儲器,
這個Cache一般是被集成到CPU內部,所以叫CPU Cache。
在Cache內部是換行存儲的,其中每一行稱爲Cache行。Cache行是Cache與主內存進行數據交換的單位,Cache
行的大小一般爲2的冪次數字節。
當CPU訪問某個變量時,首先會去看CPU Cache內是否有這個變量,如果有則直接從中獲取,否則就去主內存中
裏面獲取該變量,然後把該變量所在內存區域的一個Cache行大小的內存複製到Cache中。由於存放到Cache行的
是內存塊而不是單個變量,所以可能會把多個變量存放到一個Cache行中,當多個線程同時修改一個緩存行裏面
的多個變量時,由於同時只能有一個線程操作緩存行,所以相比將每個變量放到一個緩存行,性能會有所下降,這
就是僞共享。
在單線程下訪問時將數組元素放入一個或者多個緩存行對代碼執行是有利的,因爲數據都在緩存中,代碼執行會更
快。
在單個線程下順序修改一個緩存行中的多個變量,會充分利用程序運行的局部性原則。從而加速了程序運行。而再
多線程下併發修改一個緩存行中的多個變量時就會競爭緩存行,從而降低程序運行性能。

如何避免僞共享?

在java 8之前一般是通過字節填充的方式來避免該問題,也就是創建一個變量時使用填充字段填充該變量所在的
緩存行,這樣就避免 將多個變量存放在同一個緩存行中。
java 8中提供了一個Contended註解,用來解決僞共享問題。
需要注意的是在默認情況下,@Contended註解只用於java核心類,如果用戶路徑下的類需要使用這個註解,則
需要添加JVM參數-XX:-RestrictContended,填充的寬度默認爲128,自定義的話則可以設置
-XX:ContendedPaddingWidth參數。

鎖的概念 樂觀鎖和悲觀鎖

樂觀鎖:他認爲數據在一般情況下不會造成衝突,所以在訪問記錄前不會加排他鎖,而是在進行數據提交更新時,
纔會對數據衝突與否進行校驗。根據update返回的行數讓用戶決定如何去做。
樂觀鎖並不會使用數據庫提供的鎖機制,一般在表中添加version字段或者業務狀態來實現,樂觀鎖知道提交時
才鎖定,所以不會產生任何死鎖。
悲觀鎖:悲觀鎖指對數據被外界修改持保守態度。認爲數據很容易就會被其他線程修改,所以在數據被處理前先對
數據進行加鎖,並在整個數據處理過程中,是數據處於鎖定狀態。悲觀鎖的實現往往依靠數據庫提供的鎖機制,即
在數據庫中,在對數據記錄操作前給記錄加排它鎖。如果獲取鎖失敗,則說明數據正在被其他線程修改,當前線程
則等待或者拋出異常。如果獲取鎖成功,則對記錄進行操作,然後提交事務後釋放排它鎖。

公平鎖和非公平鎖

根據線程獲取鎖的搶佔機制,鎖可以分爲公平鎖和非公平鎖。
公平鎖:表示線程獲取鎖的順序是按照線程請求鎖的時間早晚來決定的,也就是最早請求鎖的線程將最早獲取到鎖
非公平鎖:則是在運行時闖入。
ReentrantLock提供了公平和非公平鎖的實現。

獨佔鎖和共享鎖

獨佔鎖保證任何時候都只有一個線程能得到鎖,ReentrantLock就是以獨佔方式實現的。
共享鎖則可以同時由多個線程持有。
獨佔鎖是一種悲觀鎖,由於每次訪問資源都先加上互斥鎖,這個限制了併發性,因爲讀操作並不會影響數據的一致性
而獨佔鎖只允許同一時間由一個線程讀取數據,其他線程必須等待當前線程釋放鎖才能進行讀取操作。
共享鎖則是一種樂觀鎖,它放寬了加鎖的條件,允許多個線程同時進行讀操作。

可重入鎖

當一個線程要獲取一個唄其他線程持有的獨佔鎖時,該線程會被阻塞,那麼當一個線程再次獲取它自己已經獲取的鎖
時是否會被阻塞呢?如果不阻塞,那麼我們所該鎖是可重入的。也就是隻要該線程獲取看該鎖,那麼可以無限次地進
入被該鎖鎖住的代碼
實際上,synchronized內部鎖是可重入鎖。可重入鎖的原理是在鎖內部維護一個線程標識,用來標識該鎖目前被
哪個線程佔用,然後關聯一個計數器,一開始計數器值爲0,說明該鎖沒有任何線程佔用,當一個線程獲取了該鎖時
計數器的值會變成1,這是其他線程再來獲取該鎖時會發現鎖的所有者不是自己而被阻塞掛起。當獲取了該鎖的線程
再次獲取鎖時,會將計數器加1,當釋放鎖後計數器值-1,當計數器值爲0時,鎖裏面的線程標識被重置爲null,
這時候被阻塞的線程會被喚醒來競爭獲取該鎖。

自旋鎖

由於java中的線程是域操作系統中的線程一一對應,所以當一個線程在獲取鎖失敗後,或被切換到內核狀態而被掛
起。當該線程獲取到鎖時又需要將其切換到內核狀態而喚醒該線程。而從用戶狀態切換到內核狀態的開銷是比較大
的,在一定程度影響併發性能,自旋鎖則是當前線程在獲取鎖時,如果發現鎖已經被其他線程佔有,它不會馬上阻塞
自己,在不放棄CPU使用權的情況下,多次嘗試,默認爲10次,可以使用--XX:PreBlockSpinsh參數設置該值
很有可能在幾次嘗試其他線程釋放了鎖,如果嘗試指定的次數後仍然沒有獲取到鎖,則當前線程纔會被阻塞掛起,
由此來看,自旋鎖是使用CPU的時間換取線程阻塞域調度的開銷,可能CPU時間白白浪費了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章