1、內存模型
我們以一個最簡單的例子開始
int i = 5;
i = i + 1;
i=i+1這條語句,雖然看起來只有一步,但是從微觀的角度可以將它分解爲以下幾步
(1)從內存中讀取i=5,並複製到cpu緩存中
(2)將cpu緩存中i的值+1,現在cpu緩存中i=6,而內存中i=5
(3)將cpu緩存中的i刷新到內存中,此時i=6
簡單一句話就是,賦值會先刷到緩存再刷到內存,取值會直接從內存取。
2、導致併發問題的三個原因,以及JVM提供的解決方案
2.1、可見性問題
2.1.1、問題描述
由上面內存模型的例子,我們可以引出一個問題,如果我們兩個線程執行i=i+1這個操作,期望的最終結果i=7,但是如果兩個線程同時執行了(1)呢?我們知道多核CPU中的緩存是獨立的、不共享的。兩個線程會同時將i=5刷到自己的緩存中,並分別執行i=i+1,再刷回內存,結果是i=6,這就是緩存一致性問題,也就是線程間緩存不可見導致的可見性問題
2.1.2、解決方案------volatile+Happens-Before原則
volatile是“易變的”的意思,修飾在屬性(常量/成員變量)上表示通知JVM該屬性可能不太穩定,需要立即將值刷入主存,並禁用cpu緩存,禁用了緩存,自然就不存在緩存一致性問題了。
接下來介紹一下Happens-Before原則:
Happens-Before的意思是,如果A Happens-Before B,則A的操作結果對B可見,它一共有8個原則:
(1)程序次序規則:
一個線程內一段代碼的執行結果是有序的。即兩行代碼先後執行,先執行的代碼產生的結果對後執行的代碼可見。
(2)管程鎖定規則:
對一個鎖的解鎖Happens-Before於後續對這個鎖的加鎖。即上一輪的加鎖解鎖產生的結果對下一輪加鎖解鎖中的操作可見。
(3)volatile變量規則:
對一個volatile變量的寫操作Happens-Before於後續對這個變量的讀操作。
(4)線程啓動規則:
主線程啓動的操作Happens-Before於子線程。即主線程在啓動子線程前的操作對子線程可見。
(5)線程終止規則:
子線程終止前的操作Happens-Before於主線程。即子線程的操作對主線程可見。
(6)線程中斷規則:
調用interrupt方法Happens-Before於檢測到中斷事件。即對一個線程執行interrupt方法的結果,對被中斷線程檢測到中斷狀態Thread.isInterrupted之前可見
(7)傳遞規則:
A Happens-Before B,B Happens-Before C,則 A Happens-Before C.
(8)對象終結規則:
一個對象的初始化操作Happens-Before於銷燬操作。
2.2、有序性問題
2.2.1、問題描述
雙重鎖實現單例模式:
public class Singleton {
private Singleton() { }
private static Singleton instance;
public static Singleton getInstance(){
if(instance==null){
synchronized (Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
這段代碼中new Singleton()的執行順序本來應該是這樣的:
JVM開一塊內存空間--->在這塊內存空間上初始化Singleton對象--->把內存空間地址賦值給instance
但其實編譯器在編譯過程中會對指令進行重排序,執行順序有可能是這樣的:
JVM開一塊內存空間--->把內存空間地址賦值給instance--->在這塊內存空間上初始化Singleton對象
雖然兩種執行順序在單線程中結果是正常的,但是後者在多線程中會出現問題:
假如A線程執行到了new Singleton(),A把空間地址賦值給instance,這時候B線程進來了,instance==null就是false,但是實際上對象還沒初始化,就會造成調用方法空指針
2.2.2、解決方案------volatile+Happens-Before原則
volatile還有禁用編譯優化的功能
2.3、原子性問題
2.3.1、問題描述
最經典的取錢問題,假如一個賬戶只有500元,A和B兩個人同時對這個賬戶進行取500元的操作,分別對應一個進程裏兩個不同的線程,而賬戶裏的錢對應共享資源
A取錢:檢測賬戶裏有500元---->取出500元
B取錢,檢測賬戶裏有500元---->取出500元
如果A和B同時執行檢測賬戶餘額的操作,就會同時執行取錢的操作,此時賬戶餘額就會出現-500的情況
原子性:我們把一個或多個操作在CPU中執行的過程中不被中斷的特性叫原子性,可以認爲原子性問題就是由多個線程(A、B兩個人)同時對共享資源(賬戶)進行操作(取錢)導致的。
取錢過程分爲檢測餘額和取錢兩步,這兩步是不可分割的,但是由於A和B是兩個不同的線程,就有可能在檢測餘額的時候出現“線程切換”的操作,破壞了取錢這個過程應具有的原子性
2.3.2、解決方案------synchronized關鍵字
首先synchronized是一種互斥鎖,即同一時刻只能有一個線程訪問臨界資源
synchronized是同步的意思,用來告訴JVM多個線程必須同步執行該段代碼。
synchronized可以修飾成員方法和靜態方法,分別加對象鎖和類鎖;還可以修飾方法內的方法塊,需要自己指定加鎖類型,注意對同一個變量的讀寫操作一定要加同一種鎖
package com.lcy.thread.part41;
/**
* 功能描述:
*
* @author liuchaoyong
* @version 1.0
* @date 2019-08-04 14:55
*/
public class Test {
//修飾類方法
private static synchronized void test1(){
//修飾代碼塊,加類鎖
synchronized (Test.class){
}
}
//修飾成員方法
private synchronized void test2(){
//修飾代碼塊,加對象鎖
synchronized (this){
}
}
}
2.3.2.1、粗粒度鎖
粗粒度鎖,即用同一把鎖保護多個不同的臨界資源,優點就是實現容易,缺點就是所有操作串行,性能低:
package com.lcy.thread.part41;
/**
* 功能描述:
*
* @author liuchaoyong
* @version 1.0
* @date 2019/9/3 09:44
*/
public class Account {
//保護鎖
private final Object lock = new Object();
//賬戶餘額
private Integer balance;
//賬戶信息
private String userInfo;
//存款
private void addBalance(Integer amt) {
synchronized (lock) {
balance += amt;
}
}
//取款
private void subBalance(Integer amt){
synchronized (lock){
if(balance > amt){
balance -= amt;
}
}
}
//設置賬戶信息
private void setUserInfo(String newUserInfo){
synchronized (lock){
userInfo = newUserInfo;
}
}
//查看賬戶信息
private String getUserInfo(){
synchronized (lock){
return userInfo;
}
}
}
2.3.2.2、細粒度鎖
使用不同的鎖保護不同的臨界資源,叫細粒度鎖,優點就是對不同臨界資源的操作並行化,性能高,缺點就是容易產生死鎖:
package com.lcy.thread.part41;
/**
* 功能描述:
*
* @author liuchaoyong
* @version 1.0
* @date 2019/9/3 09:44
*/
public class Account {
//賬戶鎖
private final Object balLock = new Object();
//用戶信息鎖
private final Object infoLock = new Object();
//賬戶餘額
private Integer balance;
//賬戶信息
private String userInfo;
//存款
private void addBalance(Integer amt) {
synchronized (balLock) {
balance += amt;
}
}
//取款
private void subBalance(Integer amt){
synchronized (balLock){
if(balance > amt){
balance -= amt;
}
}
}
//設置賬戶信息
private void setUserInfo(String newUserInfo){
synchronized (infoLock){
userInfo = newUserInfo;
}
}
//查看賬戶信息
private String getUserInfo(){
synchronized (infoLock){
return userInfo;
}
}
}
3、併發編程中應該注意的三個問題
3.1、安全性問題
就是保證程序運行結果的正確性,上面導致併發問題的三個原因,就都是安全性問題。
導致安全性問題的原因就是數據競爭,也就是存在多個線程對同一數據進行讀寫的情況,這時你就要注意安全性問題。
3.2、活躍性問題
3.2.1、死鎖
3.2.1.1、問題描述
假如現實生活中有這樣一個場景,現在有兩個人都要炒菜,卻只有一口鍋和一把鏟子,A先拿了鍋,B先拿了鏟子,A在等B用完鏟子才能炒菜,而B也在等A用完鍋才能炒菜,A和B會一直等待下去,就發生了死鎖。
一組相互競爭資源的線程,由於相互等待對方釋放自己執行下一步所需的臨界資源,導致永久阻塞的現象稱爲死鎖。
3.2.1.2、解決方案------預防死鎖,破壞死鎖條件
3.2.1.2.1、佔用且等待條件
線程已經取得了一個共享資源,在對下一個被佔用的共享資源發出申請時被阻塞,此時該線程並不釋放已經取得的共享資源
破壞佔用且等待條件很簡單,就是一次性申請所有資源。對應到做飯的情景就是,鍋和鏟子都放在廚房,只有一個人能進廚房
package com.lcy.thread.part41;
import java.util.ArrayList;
import java.util.List;
/**
* 功能描述:
*
* @author liuchaoyong
* @version 1.0
* @date 2019/9/3 17:21
*/
public class Kitchen {
private List<Object> kitchen = new ArrayList<>();
private Object pot = new Object();
private Object spatula = new Object();
synchronized List<Object> getTool(){
if(kitchen.contains(pot) || kitchen.contains(spatula)){
return null;
}
kitchen.add(pot);
kitchen.add(spatula);
return kitchen;
}
synchronized void backTool(Object pot,Object spatula){
kitchen.remove(pot);
kitchen.remove(spatula);
}
}
3.2.1.2.2、不可搶佔條件
線程取得的共享資源不能被其他線程釋放
破壞不可搶佔條件需要用到java併發包裏的相關類,我們到時候再說。對應到做飯的情景就是,一個人拿了鍋再去拿鏟子,如果鏟子拿不到鍋也不要了。
3.2.1.2.3、循環等待條件
當前線程會佔用下一個線程的至少一種資源
破壞循環等待條件可以把資源排序,規定線程從小到大申請資源。對應到做飯的情景就是,規定做飯必須先拿鏟子再拿鍋。
3.2.2、活鎖
3.2.2.1、問題描述
現實生活中可能會有這樣的情況,兩個人面對面走,快要撞上的時候,同時互相謙讓走到另一條道上,結果還是過不去,如此反覆。。。
線程之間並沒有阻塞,但就是無法滿足繼續執行下去的條件,一直循環嘗試->失敗->嘗試->失敗的操作,這種情況稱爲活鎖。
3.2.2.2、解決方案
失敗之後設置一個隨機等待時間再嘗試
3.2.3、飢餓
3.2.3.1、問題描述
大家都知道,過馬路要等紅綠燈,如果有一天有個路口的紅綠燈突然壞了,一邊一直是綠燈,另一邊一直是紅燈,等紅燈的人和車就要一直等一直等(如果都遵紀守法的話)。
線程因執行優先級較低或永久等待,無法得到cpu的運行時間塊,而無法繼續執行下去的狀態稱爲飢餓
3.2.3.2、解決方案
保證資源分配的公平性,使用基於先來後到原則的公平鎖,在java併發包裏有相關類,我們後面會講到
4、線程的生命週期
4.1、線程的生命週期
(1)初始化狀態(NEW)
new Thread();JVM僅僅爲其分配內存
(2)可運行狀態(RUNNABLE)
Thread.start();表示線程已經可以運行了,但是什麼時候運行取決於JVM線程調度器的調度
(3)運行狀態(RUNNING)
線程獲得CPU時間塊,執行方法體
(4)阻塞狀態(BLOCKED)
線程在等待獲取共享資源的鎖的時候
(5)無時間限制等待狀態(WAITING)
在線程獲取鎖的時候,主動調用wait()等方法,會進入等待被喚醒的狀態,並釋放對應的鎖
(6)有時間限制的等待狀態(TIMED_WAITING)
在線程獲取鎖的時候,主動調用wait(long millis)等方法,會進入等待被喚醒的狀態,並釋放對應的鎖,如果一定時間內沒有被喚醒,則自己主動甦醒。
(7)終止狀態(TERMINATED)
線程執行完畢或異常終止
4.2、wait()、notify()、notifyAll()的使用方法
我們以最簡單的生產者消費者模型爲背景來介紹,下面的例子主要就是兩個線程對一個數的增減
生產者Producer,搶到鎖後,數小於等於0就執行+5然後喚醒等待線程,sychronized結束纔會釋放鎖;大於0就不執行:
package com.lcy.thread.part08;
/**
* 功能描述:
*
* @author liuchaoyong
* @version 1.0
* @date 2019/9/10 09:29
*/
public class Producer implements Runnable {
private Test test;
public Producer(Test test) {
this.test = test;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
while (true) {
synchronized (test) {
if (test.i <= 0) {
test.i += 5;
System.out.println(threadName +"生產...剩餘" + test.i);
System.out.println(threadName +"去喚醒...");
test.notify();
}
}
}
}
}
消費者Consumer,搶到鎖後,小於等於0就釋放鎖等待被喚醒;大於0就執行-1:
package com.lcy.thread.part08;
/**
* 功能描述:
*
* @author liuchaoyong
* @version 1.0
* @date 2019/9/10 09:29
*/
public class Consumer implements Runnable {
private Test test;
public Consumer(Test test) {
this.test = test;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
while (true) {
synchronized (test) {
if (test.i <= 0) {
System.out.println(threadName +"等待...");
try {
test.wait();
System.out.println(threadName +"醒了...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
test.i--;
System.out.println(threadName +"消費...剩餘" + test.i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
測試類:
package com.lcy.thread.part08;
/**
* 功能描述:
*
* @author liuchaoyong
* @version 1.0
* @date 2019/9/10 09:35
*/
public class Test {
public int i = 5;
public static void main(String[] args) {
Test test = new Test();
Thread thread = new Thread(new Consumer(test));
Thread thread1 = new Thread(new Producer(test));
thread.start();
thread1.start();
}
}
其實要注意的只有一點,就是調用某個對象的wait()、notifyAll()的線程,一定要獲取到該對象的鎖纔可以,否則會報java.lang.IllegalMonitorStateException異常