概念:
背景
- 程序,進程,線程,多任務,主線程
- 三高應用(高可用,高性能,高併發)
- 學習理論(守破離,斷舍離),
- lambda(JDK8,內部類<靜態,局部,匿名,lambda>,函數式編程),
- 定時器(timer,task),
- 類協作(設計模式),代理模式(靜態代理<婚慶公司>,動態代理,真實角色,代理角色),單例模式,
- CAS(compare and swap),悲觀鎖,樂觀鎖
知識點
- 線程實現(Thread,Runnable,Callable),
- 線程啓動(方式,時機),
- 線程狀態(new新生,runnable<就緒,運行>,terminated死亡,block阻塞,time_wait時間等待<join,sleep,wait>,wait阻塞等待<wait,notify>),
- 同步synchronize(方法,塊,容器),
- 數據可見性和代碼有序性(volatile),
- 守護線程,
- ThreadLocal,JUC
- happen before,CPU指令重排機制
實際問題
- 共享資源問題(併發問題),
- 併發案例(龜兔賽跑,12306,電影院,取錢),
- 共享資源(全局對象、變量、方法),
- 死鎖(哲學家就餐問題),
- 線程協作,生產者消費者模型(緩存法<wait,notify>,信號燈法),
- 單例模式使用了dubble checked。
線程命題
- 線程是爲了解決應用的多任務並行問題。
- 線程是輕量級的進程。
- 線程並行是單CPU模擬並行,所以線程會爭奪CPU的使用時間。
- 應用併發是某一功能被多人同時使用。
- 應用併發造成線程併發問題、服務器資源問題。
- 線程併發問題是線程調度機制和操作公共資源共同造成的。
- 線程調度機制衍生出線程狀態和改變調度邏輯問題。
- 線程併發問題有三個因素,原子性、可見性、有序性。
- synchronize來解決原子性。
- synchronize可能造成死鎖問題。
- synchronize是悲觀鎖。
- ThreadLocal是樂觀鎖。
- volatile解決數據可見性和代碼有序性。
- volatile用來解決CPU指令重排造成的髒數據問題。
- 生產者消費者模型是解決併發問題的異步方案。
- synchronize是解決併發問題的同步方案。
演化過程
線程的來歷
- 計算機簡化模型:程序+計算。
- 計算機使用CPU代替人工計算。
- 程序是算法+數據。
- 操作系統也是一種程序,可以運行應用程序。
- 操作系統管理計算機硬件和程序執行過程。
- 計算機只能一次運行一個程序。
- 操作系統通過進程調度機制實現計算機同時運行多個程序。
- 進程是程序的一次執行過程。
- 每個進程有獨立內存空間。
- 進程調度機制讓多個進程輪流使用CPU。
- 最初一個程序執行一個任務。
- 線程讓程序同時執行多個任務。
- 線程調度機制讓多個線程輪流使用CPU。
- 線程使用進程的內存空間和CPU時間。
創建線程方式
- 繼承Thread類;
- 對象可以直接調用start()啓動。
- 實現Runnable接口,線程不可以返回數據和拋異常;
- 對象需要放入Thead代理對象後,代理對象調用start()啓動。
- 實現Callable接口,線程可以返回數據和拋異常;
- 對象放入Thead代理對象後,代理對象調用start()啓動。
- 對象放入FutureTask代理對象後,使用ExecutorService線程池對象的execute()啓動。
線程狀態
- 創建狀態(NEW):創建一個線程對象。
- 運行狀態(RUNNABLE):線程啓動後,爭取或者獲得CPU使用權。
- 死亡狀態(TERMINATED):線程邏輯(run方法)運行完畢。
- 時間等待狀態(TIME_WAIT):等待指定時間後(sleep/join/yield),才能爭取CPU使用權。
- 等待狀態(WAIT):等待別人喚醒(notify)。
- 阻塞狀態(BLOCK):獲取同步鎖失敗,等待別人釋放鎖。
線程併發問題(原子性:代碼級別)
問題原因:CPU使用權
- 多個線程同時操作公共資源。
- 由於線程調度機制,線程被剝奪了CPU使用權,導致暫停寫入步驟。
- 其他線程可能拿到未修改之前的髒數據。
- 從而出現數據不一致問題。
解決方案:悲觀鎖
- 使用synchronize同步鎖機制。
- 操作公共資源之前,先鎖定資源使用權,阻止其他人使用資源。
- 將資源操作步驟原子化。
- 所有資源操作步驟都完成後,才釋放資源鎖。
- 加鎖的資源類型:
- 同步方法,等同於鎖定this對象(一個線程對象啓動多個線程的情況);
- 同步塊,等同於鎖定數據對象(class對象也可以鎖定=鎖定new操作);
線程併發問題(可見性:指令級別)
問題原因:CPU寄存器緩存
- 線程指令運行時,會從主存中拷貝數據到寄存器;
- 由於寄存器中的數據拷貝,多線程操作同一個主存數據時;
- 會出現拷貝數據不一致;
解決方案:volatile
- volatile申明的變量,在修改後會標識緩存對象爲無效;
- 緩存無效後,CPU會重新加載主存數據;
線程併發問題(有序性:代碼級別)
問題原因:JVM指令重排
- JVM在保證代碼執行效果一直的情況,進行的指令順序重新排列;
- 多線程情況下,變量變化順序,可能造成邏輯判斷混亂;
int i = 0; //語句1
boolean flag = false; //語句2
i = 1; //語句3
flag = true; //語句4
執行順序可能是:1234、1243、2134、2143。
解決方案:volatile
- volatile申明的變量,在編譯時,會保證改變量操作前後代碼不變;
- 比如5條代碼,變量是第3條,那麼12不會到3之後,45不會到3之前;
- 只能是:12345,12354,21345,21354
int i = 0; //語句1
boolean flag = false; //語句2
ticket = 99; //語句3
i = 1; //語句4
flag = true; //語句5
火車票併發案例
/**
* 線程同步機制:synchroneze
* 使用同步塊的方式,創建一個鎖對象lock;
* sleep會繼續佔用lock;
*
* @author jeadong
*
*/
public class SynchronizeTest {
public static void main(String[] args) {
//啓動3個買票線程,模擬併發搶票
new Thread(new Web12306()).start();
new Thread(new Web12306()).start();
new Thread(new Web12306()).start();
}
}
//模擬12306買票操作
class Web12306 implements Runnable{
private static Object lock = new Object();//買票鎖,保證減庫存操作的線程安全
private static int tikects = 99;//總票數
private boolean flag = true;
@Override
public void run() {
while(flag){
//買票
buyTikect();
}//while end
}
private void buyTikect() {
//買票前,先加鎖
synchronized(lock){
if (tikects > 0) {
tikects--;
System.out.println("【"+Thread.currentThread().getName()+"】購票成功!剩餘票量:"+tikects);
}else{
flag=false;//票賣光了,就結束買票
}
//模擬買票操作延遲
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}//sycn end
}
}
單例模式案例,double checked
/**
* 雙重檢測鎖:synchronize+volatile
* 創建單例模式
* 1、保證原子性,用synchronize鎖定class
* 2、保證可見性和有序性,用volatile直接訪問主存對象
* 3、最外層的判空是爲了提升訪問效率
*
* @author jeadong
*
*/
public class DubbleCheckedTest {
public static void main(String[] args) {
//啓動子線程觸發第一次訪問
new Thread(() -> {
System.out.println(Singleton.getInstance());
}).start();
//主線程觸發第二次訪問
System.out.println(Singleton.getInstance());
}
}
class Singleton {
//volatile必須加,爲了防止指令重排,第一次創建時,內存還沒完成創建,就把對象指針暴露出去了
private static volatile Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
//第一層檢測,爲了第二次訪問之後提升效率,不要再次加鎖
if (null == instance) {
//鎖定class,讓其他線程無法創建對象,保證只會執行一次New操作
//如果沒有加鎖,就能模擬出創建兩個對象
synchronized(Singleton.class){
//第二層檢測,保證第一次訪問順利完成對象創建
if(null == instance){
//模擬創建對象比較耗時
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//創建對象
instance = new Singleton();
}
}
}
return instance;
}
}