併發編程簡單總結

在這裏插入圖片描述

單核CPU可以多線程麼?

即使是單核處理器也支持多線程執行代碼,CPU通過給每個線程分配CPU時間片來實現
這個機制。時間片是CPU分配給各個線程的時間,因爲時間片非常短,所以CPU通過不停地切
換線程執行,讓我們感覺多個線程是同時執行的,時間片一般是幾十毫秒(ms)。

CPU通過時間片分配算法來循環執行任務,當前任務執行一個時間片後會切換到下一個
任務。但是,在切換前會保存上一個任務的狀態,以便下次切換回這個任務時,可以再加載這
個任務的狀態。所以任務從保存到再加載的過程就是一次上下文切換。

爲什麼併發執行的速度會比串行慢

這是因爲線程有創建和上下文切換的開銷

如何減少上下文切換

減少上下文切換的方法有無鎖併發編程、CAS算法、使用最少線程和使用協程。

  • 無鎖併發編程。多線程競爭鎖時,會引起上下文切換,所以多線程處理數據時,可以用一
    些辦法來避免使用鎖,如將數據的ID按照Hash算法取模分段,不同的線程處理不同段的數據。
  • CAS算法。Java的Atomic包使用CAS算法來更新數據,而不需要加鎖。
  • 使用最少線程。避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這
    樣會造成大量線程都處於等待狀態。
  • 協程:在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。

避免死鎖

  • 避免一個線程同時獲取多個鎖。
  • 避免一個線程在鎖內同時佔用多個資源,儘量保證每個鎖只佔用一個資源。
  • 嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制。
  • 對於數據庫鎖,加鎖和解鎖必須在一個數據庫連接裏,否則會出現解鎖失敗的情況。

java字節碼描述

Java代碼在編譯後會變成Java字節碼,字節碼被類加載器加載到JVM裏,JVM執行字節
碼,最終需要轉化爲彙編指令在CPU上執行。

volatile關鍵字

被volatile關鍵字修飾的變量,再多線程中,一個線程修改他會立即被其他線程看到

有volatile變量修飾的共享變量進行寫操作的時候會多出lock前綴指令:

  • 將當前處理器緩存行的數據寫回到系統內存。
  • 這個寫回內存的操作會使在其他CPU裏緩存了該內存地址的數據無效

處理器爲了提交效率,讀取緩衝,當有寫請求時,如果帶上了volatile關鍵字,會發送一個前綴lock指令,將這個變量所在的緩衝行寫到系統內存,但是有很多處理器,就會有多個緩衝行,他是怎麼保證緩存一致的呢,每個處理器會嗅探總線傳播的數據是不是過期了,如果緩存行的對應的內存地址被修改,那麼就把緩存行設置爲無效,當下次訪問內存的時候,強制寫到緩存行。

在每次volatile寫之前會加上storestore屏障,寫之後會加上storeload屏障。
在每次volatile讀之前會加上loadload屏障,讀之後加上loadstore屏障。
目的是爲了指令重排序。

由於volatile僅僅保證對單個volatile變量的讀/寫具有原子性,而鎖的互斥執行的特性可以
確保對整個臨界區代碼的執行具有原子性。

synchronized的實現原理與應用

具體表現爲以下3種形式:

  • 對於普通同步方法,鎖是當前實例對象。
  • 對於靜態同步方法,鎖是當前類的Class對象。
  • 對於同步方法塊,鎖是Synchonized括號裏配置的對象。

synchronized內部實現

通過 javap -v XXX.class文件可以發現

代碼塊同步是使用monitorenter和monitorexit指令實現的,而方法同步是ACC_SYNCHRONIZED方法獲取到的,無論哪種方式都是通過一個對象的monitor進行獲取

任何線程對同步代碼的訪問,都要先獲取監視器,如果獲取失敗,就會放到SyncchronizedQueue隊列中,當持有線程釋放了鎖後,會釋放阻塞在隊列中的線程。

synchronized用的鎖是存在Java對象頭裏的,HashCode、分代年齡和鎖標記位。

偏向鎖

當一個線程在訪問同步代碼塊的時候,對象頭mark_word會存儲當前線程的Id,在重複獲取鎖的時候不需要cas進行獲取,而是檢查mark_word是否有當前線程的偏行鎖,如果沒有,檢查mark_word是否是1,不是的話,cas競爭,將設置爲1,如果爲1,那麼就將對象頭的偏向鎖指向當前線程。

偏向鎖的撤銷只有在競爭的時候纔會釋放,只有等到安全點的時候(這個時間沒有執行的字節碼的時候),首先會暫停擁有偏向鎖的線程,檢查擁有偏向鎖的棧,擁有的將會被執行,最後會喚醒競爭的線程。

java保證原子性有哪些方式

簡單分爲兩種:加鎖和使用自旋cas

JVM鎖除了偏向鎖,都是使用cas實現的加鎖

cas

比較預期值和更新之後的值是否相等,如果相等的話,就更形

cas 的缺點

  1. ABA問題,他是比較當前值和預期的值是否相等,如果一個值原來是A,變成了B,又變成了A,那麼cas檢測時,會發現這個值是沒有變化的,但是實際上卻變化了,ABA問題的解決的方法時加上版本號,每次更新的時候版本號會加一,JDK1.5出現的AtomicStampedReference來解決ABA問題,比較了當前標誌是否等於預期的標誌。
  2. 循環時間開銷大。自旋時間太長會出現較大的CPU開銷
  3. 只能保證一個共享變量的原子性。從JDK1.5出現了AtomicReference保證引用對象的原子性。可以把多個變量放到一個對象裏面,進行cas操作。

java內存模型的基礎

線程之間的通信有兩種方式,分別爲共享內存和消息傳遞。

在共享內存的併發模型中,通過共享程序的公共狀態,隱式通信。

在消息傳遞的併發模型中,線程之間必須通知傳遞消息通信。

java內存模型的抽象結構

JMM定義了抽象關係,線程之間的共享變量存儲在主內存中,每個線程都有自己的本地內存,本地內存存儲了該線程的讀寫副本。

如果線程A與線程B之間要通信的話:

  1. 線程A把本地內存A中更新過的共享變量刷新到主內存中去。
  2. 線程B到主內存中去讀取線程A之前已更新過的共享變量。

happens-before原則

從JDK 5開始,Java使用新的JSR-133內存模型(除非特別說明,本文針對的都是JSR-133內
存模型)。JSR-133使用happens-before的概念來闡述操作之間的內存可見性。在JMM中,如果一
個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關
系。這裏提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間。

與程序員密切相關的happens-before規則如下:

  • 程序順序規則:一個線程中的每個操作,happens-before於該線程中的任意後續操作。
  • 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
  • volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的
    讀。
  • 傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。

兩個操作之間具有happens-before關係,並不意味着前一個操作必須要在後一個
操作之前執行!happens-before僅僅要求前一個操作(執行的結果)對後一個操作可見

一個happens-before規則對應於一個或多個編譯器和處理器重排序規則

獲取鎖和釋放鎖的內存語義

釋放鎖的時候,會把共享變量刷到主內存裏面。獲取鎖的時候,臨界區的本地變量無效,從主存中刷到本地內存中。

aqs

Java隊列同步器框架AbstractQueuedSynchronizer,AQS使用一個整型的volatile變量(命名爲state)來維護同步狀態。

主要包括:

  1. 同步隊列
  2. 獨佔式同步狀態獲取與釋放
  3. 共享式同步狀態獲取與釋放
  4. 以及超時獲取同步狀態等同步器的核心數據結構與模板方法

同步隊列

同步器依靠的是一個FIFO的雙向隊列來維持一個同步狀態,當線程阻塞時,會把節點信息放到隊尾(這個時候會調用compareAndSetTail,多個線程競爭,保證線程安全),當有線程釋放的時候,會喚醒隊頭的線程(這個時候不用cas,只是頭節點斷開就行了,只會有一個成功)。

獨佔式同步狀態獲取與釋放

在獲取同步狀態時,同步器維護一個同步隊列,獲取狀態失敗的線程都會被加入到隊列中並在隊列中進行自旋,移出隊列(或停止自旋)的條件是前驅節點爲頭節點且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRelease(int arg)方法釋放同步狀態,然後喚醒頭節點的後繼節點。

ReentrantLock

ReentrantLock分爲公平鎖和非公平鎖。

  • 公平鎖和非公平鎖釋放時,最後都要寫一個volatile變量state。
  • 公平鎖獲取時,首先會去讀volatile變量。
  • 非公平鎖獲取時,首先會用CAS更新volatile變量,這個操作同時具有volatile讀和volatile
    寫的內存語義

鎖釋放-獲取的內存語義的實現至少有下面兩種方式。

  1. 利用volatile變量的寫-讀所具有的內存語義。
  2. 利用CAS所附帶的volatile讀和volatile寫的內存語義。

ReadWriteLock保證HashMap解決併發安全問題

每次修改操作之前加上寫鎖,讀之前加上讀鎖。

concurrent包的實現

首先,聲明共享變量爲volatile。

然後,使用CAS的原子條件更新來實現線程之間的同步。

同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現線程之間的通信

AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent
包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的

final域的內存語義

線程優先級

java將線程的優先級分爲1-10,默認爲5,線程的優先級越高,代表獲取的時間片數量越高。

線程的狀態

同一時刻,線程只能擁有一個狀態

  • new 線程被構建,還沒有調用start方法
  • Runnable 運行狀態,java將就緒和運行籠統稱作運行中
  • blocked 阻塞狀態,表示線程阻塞於鎖
  • waiting 等待狀態,表示線程需要等待其他線程發出一些動作(通知或者中斷)
  • time_waiting 超時等待狀態,指定時間自行返回
  • terminated 終止狀態

daemon線程

後臺線程,只能在運行前設置爲daemon

等待通知機制

注意

  1. 使用wait()、notify()和notifyAll()時需要先對調用對象加鎖。
  2. 調用wait()方法後,線程狀態由RUNNING變爲WAITING,並將當前線程放置到對象的
    等待隊列
  3. notify()或notifyAll()方法調用後,等待線程依舊不會從wait()返回,需要調用notify()或
    notifAll()的線程釋放鎖之後,等待線程纔有機會從wait()返回
  4. notify()方法將等待隊列中的一個等待線程從等待隊列中移到同步隊列中,而notifyAll()
    方法則是將等待隊列中所有的線程全部移到同步隊列,被移動的線程狀態由WAITING變爲
    BLOCKED。
  5. 從wait()方法返回的前提是獲得了調用對象的鎖。

WaitThread首先獲取了對象的鎖,然後調用對象的wait()方法,從而放棄了鎖並進入了對象的等待隊列WaitQueue中,進入等待狀態。由於WaitThread釋放了對象的鎖,NotifyThread隨後獲取了對象的鎖,並調用對象的notify()方法,將WaitThread從WaitQueue移到SynchronizedQueue中,此時WaitThread的狀態變爲阻塞狀態。NotifyThread釋放了鎖之後,WaitThread再次獲取到鎖並從wait()方法返回繼續執行。

Thread.join()的使用

如果一個線程A執行了thread.join()語句,其含義是:當前線程A等待thread線程終止之後才
從thread.join()返回,誰調用誰等待。

java併發框架

爲什麼要使用ConcurrentHashMap

  1. hashMap併發put導致死循環,因爲會形成一個環形鏈表。
  2. hashTable使用synchronized,效率低
  3. 分段鎖提升效率
  • get操作

get 操作不用加鎖,經過兩次hash運算,第一次找到對應的segment,第二次找到對應的entry,並且都將變量聲明成volatile類型,保證segment的count字段和entry的value保證內容可見性,能夠被多個線程同時讀,即使兩個線程同時修改和讀取同一變量,由於happen before原則,對volatile的寫優先於讀。

  • put操作

需要對共享變量進行加鎖,需要進行兩個過程,第一,定位到segment,判斷segment中的hashEntry是否需要擴容,第二,定位到對應的hashEntry

值得一提的是,Segment的擴容判斷比HashMap更恰當,因爲HashMap是在插入元素後判斷元素是否已經到達容量的,如果到達了就進行擴容,但是很有可能擴容之後沒有新元素插入,這時HashMap就進行了一次無效的擴容。

如何擴容,創建一個是原來兩倍的數組,將原來數組的元素放到新的數組裏面,爲了高效,只是對segment進行擴容。

  • size 操作

先嚐試通過不鎖柱segment的方式統計兩次各segment的大小,如果在統計的過程中,count發生了變化,則加鎖統計所有segment的大小

如何判斷容器是否發生變化了呢,有一個變量modCount,當有remove、clean、add操作時,modCont會加1,前後比較modCount的值即可判斷

ConcurrentLinkedQueue

非阻塞的無界隊列,cas實現。

阻塞隊列

支持兩個操作:

  1. 支持添加時隊列如果滿了,阻塞當前線程,直到隊列不滿。
  2. 支持從隊列中移除元素,如果隊列爲空,獲取元素的線程會等待到隊列非空
拋出異常 返回特殊值 一直阻塞 超時退出
add offer put offer(e,time)
remove poll take poll(e,time)

線程池

好處

  • 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
  • 提高線程的可管理性。線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源,
    還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。但是,要做到合理利用
    線程池,必須對其實現原理瞭如指掌。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章