愛了,這篇Java并發(fā)編程技術(shù)點(diǎn)總結(jié)的太詳細(xì)了闪水,建議是先收藏再觀看

前言

并發(fā)編程技術(shù)在Java中屬于重要知識點(diǎn)糕非,對于以下內(nèi)容你有了解多少?

進(jìn)程球榆、線程朽肥、協(xié)程關(guān)系概述

進(jìn)程:本質(zhì)上是一個獨(dú)立執(zhí)行的程序,進(jìn)程是操作系統(tǒng)進(jìn)行資源分配和調(diào)度的基本概念持钉,操作系統(tǒng)進(jìn)行資源分配和調(diào)度的一個獨(dú)立單位衡招。
?
線程:操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。它被包含在進(jìn)程之中每强,是進(jìn)程中的實(shí)際運(yùn)作單位始腾。一個進(jìn)程中可以并發(fā)多個線程,每條線程執(zhí)行不同的任務(wù)舀射,切換受系統(tǒng)控制窘茁。
?
協(xié)程:又稱為微線程,是一種用戶態(tài)的輕量級線程脆烟,協(xié)程不像線程和進(jìn)程需要進(jìn)行系統(tǒng)內(nèi)核上的上下文切換山林,協(xié)程的上下文切換是由用戶自己決定的,有自己的上下文邢羔,所以說是輕量級的線程驼抹,也稱之為用戶級別的線程,一個線程可以有多個協(xié)程拜鹤,線程與進(jìn)程都是同步機(jī)制框冀,而協(xié)程則是異步。Java的原生語法中并沒有實(shí)現(xiàn)協(xié)程敏簿,目前python明也、Lua和GO等語言支持宣虾。
?
關(guān)系:一個進(jìn)程可以有多個線程,它允許計算機(jī)同時運(yùn)行兩個或多個程序温数。線程是進(jìn)程的最小執(zhí)行單位绣硝,CPU的調(diào)度切換的是進(jìn)程和線程,進(jìn)程和線程多了之后調(diào)度會消耗大量的CPU撑刺,CPU上真正運(yùn)行的是線程鹉胖,線程可以對應(yīng)多個協(xié)程。

協(xié)程對于多線程的優(yōu)缺點(diǎn)

優(yōu)點(diǎn):

  • 非彻话快速的上下文切換甫菠,不用系統(tǒng)內(nèi)核的上下文切換,減小開銷
  • 單線程即可實(shí)現(xiàn)高并發(fā)冕屯,單核CPU可以支持上萬的協(xié)程
  • 由于只有一個線程寂诱,也不存在同時寫變量的沖突,在協(xié)程中控制共享資源不需要加鎖

缺點(diǎn):

  • 協(xié)程無法利用多核資源安聘,本質(zhì)也是個單線程
  • 協(xié)程需要和進(jìn)程配合才能運(yùn)行在多CPU上
  • 目前Java沒成熟的第三方庫刹衫,存在風(fēng)險
  • 調(diào)試debug存在難度,不利于發(fā)現(xiàn)問題

并發(fā)和并行的區(qū)別

并發(fā) (concurrency):一臺處理器上同時處理多個任務(wù)搞挣,這個同時實(shí)際上是交替處理多個任務(wù),程序中可以同時擁有兩個或者多個線程音羞,當(dāng)有多個線程在操作時囱桨,如果系統(tǒng)只有一個CPU,則它根本不可能真正同時進(jìn)行一個以上的線程嗅绰,它只能把CPU運(yùn)行時間劃分成若干個時間段舍肠,再將時間段分配給各個線程執(zhí)行。
?
并行(parallellism) :多個CPU上同時處理多個任務(wù)窘面,一個CPU執(zhí)行一個進(jìn)程時翠语,另一個CPU可以執(zhí)行另一個進(jìn)程,兩個進(jìn)程互不搶占CPU資源财边,可以同時進(jìn)行肌括。

并發(fā)指在一段時間內(nèi)宏觀上去處理多個任務(wù)。 并行指同一個時刻酣难,多個任務(wù)確實(shí)真的同時運(yùn)行谍夭。

Java里實(shí)現(xiàn)多線程的幾種方式

1.繼承Thread類

繼承Thread類,重寫里面run方法憨募,創(chuàng)建實(shí)例紧索,執(zhí)行start方法。

優(yōu)點(diǎn):代碼編寫最簡單直接操作
缺點(diǎn):無返回值菜谣,繼承一個類后珠漂,沒法繼承其他的類晚缩,拓展性差

public class ThreadDemo1 extends Thread {
    @Override
    public void run() {
        System.out.println("繼承Thread實(shí)現(xiàn)多線程、名稱:"+Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
?
      ThreadDemo1 threadDemo1 = new ThreadDemo1();
      threadDemo1.setName("demo1");
      threadDemo1.start();
      System.out.println("主線程名稱:"+Thread.currentThread().getName());
?
}

2.實(shí)現(xiàn)Runnable接口

自定義類實(shí)現(xiàn)Runnable接口媳危,實(shí)現(xiàn)里面run方法荞彼,創(chuàng)建Thread類,使用Runnable接口的實(shí)現(xiàn)對象作為參數(shù)傳遞給Thread對象济舆,調(diào)用start方法卿泽。
?
優(yōu)點(diǎn):線程類可以實(shí)現(xiàn)多個幾接口,可以再繼承一個類
缺點(diǎn):無返回值滋觉,不能直接啟動签夭,需要通過構(gòu)造一個Thread實(shí)例傳遞進(jìn)去啟動

public class ThreadDemo2 implements Runnable {
?
    @Override
    public void run() {
        System.out.println("通過Runnable實(shí)現(xiàn)多線程、名稱:"+Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
        ThreadDemo2 threadDemo2 = new ThreadDemo2();
        Thread thread = new Thread(threadDemo2);
        thread.setName("demo2");
        thread.start();
        System.out.println("主線程名稱:"+Thread.currentThread().getName());
}

JDK8之后采用lambda表達(dá)式

public static void main(String[] args) {
    Thread thread = new Thread(()->{
                System.out.println("通過Runnable實(shí)現(xiàn)多線程椎侠、名稱:"+Thread.currentThread().getName());
            });
    thread.setName("demo2");
    thread.start();
    System.out.println("主線程名稱:"+Thread.currentThread().getName());
}

3.通過Callable和FutureTask方式

創(chuàng)建callable接口的實(shí)現(xiàn)類第租,并實(shí)現(xiàn)call方法,結(jié)合FutureTask類包裝callable對象我纪,實(shí)現(xiàn)多線程慎宾。

優(yōu)點(diǎn):有返回值,拓展性也高
缺點(diǎn):jdk5以后才支持浅悉,需要重寫call方法趟据,結(jié)合多個類比如FutureTask和Thread類

public class MyTask implements Callable<Object> {
    @Override
    public Object call() throws Exception {
?
        System.out.println("通過Callable實(shí)現(xiàn)多線程、名稱:"+Thread.currentThread().getName());
?
        return "這是返回值";
    }
}

 public static void main(String[] args) {
?
        MyTask myTask = new MyTask();
        FutureTask<Object> futureTask = new FutureTask<>(myTask);
?
        //FutureTask繼承了Runnable术健,可以放在Thread中啟動執(zhí)行
        Thread thread = new Thread(futureTask);
        thread.setName("demo3");
        thread.start();
        System.out.println("主線程名稱:"+Thread.currentThread().getName());
?
        try {
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            //阻塞等待中被中斷則拋出
            e.printStackTrace();
        } catch (ExecutionException e) {
            //執(zhí)行過程發(fā)送異常被拋出
            e.printStackTrace();
        }
    }

采用lambda表達(dá)式

 public static void main(String[] args) {
?
        FutureTask<Object> futureTask = new FutureTask<>(()->{
            System.out.println("通過Callable實(shí)現(xiàn)多線程汹碱、名稱:"+Thread.currentThread().getName());
            return "這是返回值";
        });

        //FutureTask繼承了Runnable,可以放在Thread中啟動執(zhí)行
        Thread thread = new Thread(futureTask);
        thread.setName("demo3");
        thread.start();
        System.out.println("主線程名稱:"+Thread.currentThread().getName());
?
        try {
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            //阻塞等待中被中斷則拋出
            e.printStackTrace();
        } catch (ExecutionException e) {
            //執(zhí)行過程發(fā)送異常被拋出
            e.printStackTrace();
        }
?
?
    }

4.通過線程池創(chuàng)建線程

自定義Runnable接口荞估,實(shí)現(xiàn)run方法咳促,創(chuàng)建線程池,調(diào)用執(zhí)行方法并傳入對象勘伺。

優(yōu)點(diǎn):安全高性能跪腹,復(fù)用線程
缺點(diǎn): jdk5后才支持,需要結(jié)合Runnable進(jìn)行使用

public class ThreadDemo4 implements Runnable {
?
    @Override
    public void run() {
        System.out.println("通過線程池+runnable實(shí)現(xiàn)多線程飞醉,名稱:"+Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
?
        for(int i=0;i<10;i++){
            executorService.execute(new ThreadDemo4());
        }
?
        System.out.println("主線程名稱:"+Thread.currentThread().getName());
?
        //關(guān)閉線程池
        executorService.shutdown();
}

  • 一般常用的Runnable 和 第四種線程池+Runnable冲茸,簡單方便擴(kuò)展,和高性能 (池化的思想)

Java線程常見的基本狀態(tài)

JDK的線程狀態(tài)分6種冒掌,JVM里面9種噪裕。

常見的5種狀態(tài)

創(chuàng)建(NEW):生成線程對象,但是并沒有調(diào)用該對象start()股毫。
?
就緒(Runnable):當(dāng)調(diào)用線程對象的start()方法膳音,線程就進(jìn)入就緒狀態(tài),但是此刻線程調(diào)度還沒把該線程設(shè)置為當(dāng)前線程铃诬,就是沒獲得CPU使用權(quán)祭陷。如果線程運(yùn)行后苍凛,從等待或者睡眠中回來之后,也會進(jìn)入就緒狀態(tài)兵志。

運(yùn)行(Running):程序?qū)⑻幱诰途w狀態(tài)的線程設(shè)置為當(dāng)前線程醇蝴,即獲得CPU使用權(quán),這個時候線程進(jìn)入運(yùn)行狀態(tài)想罕,開始運(yùn)行run里面的邏輯悠栓。
?
阻塞(Blocked)
等待阻塞:進(jìn)入該狀態(tài)的線程需要等待其他線程作出一定動作(通知或中斷),這種狀態(tài)的話CPU不會分配過來按价,他們需要被喚醒惭适,可能也會無限等待下去。比如調(diào)用wait(狀態(tài)就會變成WAITING狀態(tài))楼镐,也可能通過調(diào)用sleep(狀態(tài)就會變成TIMED_WAITING)癞志, join或者發(fā)出IO請求,阻塞結(jié)束后線程重新進(jìn)入就緒狀態(tài)框产。

同步阻塞:線程在獲取synchronized同步鎖失敗凄杯,即鎖被其他線程占用,它就會進(jìn)入同步阻塞狀態(tài)秉宿。

備注:相關(guān)資料會用細(xì)分下面的狀態(tài)
    等待(WAITING):進(jìn)入該狀態(tài)的線程需要等待其他線程做出一些特定動作(通知或中斷)戒突。
    超時等待(TIMED_WAITING):該狀態(tài)不同于WAITING,它可以在指定的時間后自行返回描睦。

死亡(TERMINATED):一個線程run方法執(zhí)行結(jié)束妖谴,該線程就死亡了,不能進(jìn)入就緒狀態(tài)酌摇。

多線程開發(fā)常用方法

sleep
屬于線程Thread的方法;
讓線程暫緩執(zhí)行嗡载,等待預(yù)計時間之后再恢復(fù)窑多;
交出CPU使用權(quán),不會釋放鎖洼滚;
進(jìn)入阻塞狀態(tài)TIME_WAITGING埂息,睡眠結(jié)束變?yōu)榫途wRunnable;

yield
屬于線程Thread的方法遥巴;
暫停當(dāng)前線程的對象千康,去執(zhí)行其他線程;
交出CPU使用權(quán)铲掐,不會釋放鎖拾弃,和sleep類似;
作用:讓相同優(yōu)先級的線程輪流執(zhí)行摆霉,但是不保證一定輪流豪椿;
注意:不會讓線程進(jìn)入阻塞狀態(tài)奔坟,直接變?yōu)榫途wRunnable,只需要重新獲得CPU使用權(quán)搭盾;

join
屬于線程Thread的方法咳秉;
在主線程上運(yùn)行調(diào)用該方法,會讓主線程休眠鸯隅,不會釋放已經(jīng)持有的對象鎖澜建;
讓調(diào)用join方法的線程先執(zhí)行完畢,再執(zhí)行其他線程蝌以;

wait
屬于Object的方法炕舵;
當(dāng)前線程調(diào)用對象的wait方法,會釋放鎖饼灿,進(jìn)入線程的等待隊列幕侠;
需要依靠notify或者notifyAll喚醒,或者wait(timeout)時間自動喚醒碍彭;

notify
屬于Object的方法晤硕;
喚醒在對象監(jiān)視器上等待的單個線程,選擇是任意的庇忌;

notifyAll
屬于Object的方法舞箍;
喚醒在對象監(jiān)視器上等待的全部線程;

線程的狀態(tài)轉(zhuǎn)換圖

Java中保證線程安全的方法

  • 加鎖,比如synchronize/ReentrantLock
  • 使用volatile聲明變量皆疹,輕量級同步疏橄,不能保證原子性
  • 使用線程安全類,原子類AtomicXXX略就,并發(fā)容器捎迫,同步CopyOnWriteArrayList/ConcurrentHashMap等
  • ThreadLocal本地私有變量/信號量Semaphore等

解析volatile關(guān)鍵字

volatile是輕量級的synchronized,保證了共享變量的可見性表牢,被volatile關(guān)鍵字修飾的變量窄绒,如果值發(fā)生了變化,其他線程立刻可見崔兴,避免出現(xiàn)臟讀現(xiàn)象彰导。為什么會出現(xiàn)臟讀?JAVA內(nèi)存模型簡稱JMM敲茄,JMM規(guī)定所有的變量存在在主內(nèi)存位谋,每個線程有自己的工作內(nèi)存,線程對變量的操作都在工作內(nèi)存中進(jìn)行堰燎,不能直接對主內(nèi)存就行操作掏父,使用volatile修飾變量,每次讀取前必須從主內(nèi)存屬性獲取最新的值秆剪,每次寫入需要立刻寫到主內(nèi)存中损同。volatile關(guān)鍵字修修飾的變量隨時看到的自己的最新值翩腐,假如線程1對變量v進(jìn)行修改,那么線程2是可以馬上看見的膏燃。
?
volatile:保證可見性茂卦,但是不能保證原子性
synchronized:保證可見性,也保證原子性

?什么是指令重排

指令重排序分兩類:編譯器重排序和運(yùn)行時重排序
?
JVM在編譯Java代碼或者CPU執(zhí)行JVM字節(jié)碼時组哩,對現(xiàn)有的指令進(jìn)行重新排序等龙,主要目的是優(yōu)化運(yùn)行效率(不改變程序結(jié)果的前提)

?舉例:
int a = 3 //第一步 1
int b = 4 //第二步 2
int c =5 //第三步 3
int h = abc //第四步 4
?
定義順序 1,2,3,4
計算順序 1,3,2,4 和 2,1,3,4 結(jié)果都是一樣

什么是happens-before以及為什么需要happens-before

happens-before:A happens-before B就是A先行發(fā)生于B(這種說法不是很準(zhǔn)確),定義為hb(A, B)伶贰。在Java內(nèi)存模型中蛛砰,happens-before的意思是前一個操作的結(jié)果可以被后續(xù)操作獲取。JVM會對代碼進(jìn)行編譯優(yōu)化黍衙,會出現(xiàn)指令重排序情況泥畅,為了避免編譯優(yōu)化對并發(fā)編程安全性的影響,需要happens-before規(guī)則定義一些禁止編譯優(yōu)化的場景琅翻,保證并發(fā)編程的正確性位仁。

happens-before八大規(guī)則

1.程序次序規(guī)則:在一個線程內(nèi)一段代碼的執(zhí)行結(jié)果是有序的。就是還會指令重排方椎,但是隨便它怎么排聂抢,結(jié)果是按照我們代碼的順序生成的不會變。
2.管程鎖定規(guī)則:就是無論是在單線程環(huán)境還是多線程環(huán)境棠众,對于同一個鎖來說琳疏,一個線程對這個鎖解鎖之后,另一個線程獲取了這個鎖都能看到前一個線程的操作結(jié)果闸拿!(管程是一種通用的同步原語空盼,synchronized就是管程的實(shí)現(xiàn))
3.volatile變量規(guī)則:就是如果一個線程先去寫一個volatile變量,然后一個線程去讀這個變量新荤,那么這個寫操作的結(jié)果一定對讀的這個線程可見我注。
4.線程啟動規(guī)則:在主線程A執(zhí)行過程中,啟動子線程B迟隅,那么線程A在啟動子線程B之前對共享變量的修改結(jié)果對線程B可見。
5.線程終止規(guī)則:在主線程A執(zhí)行過程中励七,子線程B終止智袭,那么線程B在終止之前對共享變量的修改結(jié)果在線程A中可見。也稱線程join()規(guī)則掠抬。
6.線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程代碼檢測到中斷事件的發(fā)生吼野,可以通過Thread.interrupted()檢測到是否發(fā)生中斷。
7.傳遞性規(guī)則:這個簡單的两波,就是happens-before原則具有傳遞性瞳步,即hb(A, B) 闷哆, hb(B, C),那么hb(A, C)单起。
8.對象終結(jié)規(guī)則:這個也簡單的抱怔,就是一個對象的初始化的完成,也就是構(gòu)造函數(shù)執(zhí)行的結(jié)束一定 happens-before它的finalize()方法嘀倒。

并發(fā)編程三要素

原子性:一個不可再被分割的顆粒屈留,原子性指的是一個或多個操作要么全部執(zhí)行成功要么全部執(zhí)行失敗,期間不能被中斷测蘑,也不存在上下文切換灌危,線程切換會帶來原子性的問題

int num = 1; // 原子操作
num++; // 非原子操作,從主內(nèi)存讀取num到線程工作內(nèi)存碳胳,進(jìn)行 +1勇蝙,再把num寫到主內(nèi)存, 除非用原子類,即java.util.concurrent.atomic里的原子變量類
?
解決辦法是可以用synchronized 或 Lock(比如ReentrantLock) 來把這個多步操作“變成”原子操作

public class Test {
    private int num = 0;

    //使用lock挨约,每個對象都是有鎖味混,只有獲得這個鎖才可以進(jìn)行對應(yīng)的操作
    Lock lock = new ReentrantLock();
    public  void add1(){
        lock.lock();
        try {
            num++;
        }finally {
            lock.unlock();
        }
    }

    //使用synchronized,和上述是一個操作烫罩,這個是保證方法被鎖住而已惜傲,上述的是代碼塊被鎖住
    public synchronized void add2(){
        num++;
    }
}

解決核心思想:把一個方法或者代碼塊看做一個整體,保證是一個不可分割的整體
?

有序性: 程序執(zhí)行的順序按照代碼的先后順序執(zhí)行贝攒,因?yàn)樘幚砥骺赡軙χ噶钸M(jìn)行重排序

JVM在編譯java代碼或者CPU執(zhí)行JVM字節(jié)碼時盗誊,對現(xiàn)有的指令進(jìn)行重新排序,主要目的是優(yōu)化運(yùn)行效率(不改變程序結(jié)果的前提)

int a = 3 //第一步 1
int b = 4 //第二步 2
int c =5 //第三步 3 
int h = a*b*c //第四步 4

上面的例子 執(zhí)行順序1,2,3,4 和 2,1,3,4 結(jié)果都是一樣隘弊,指令重排序可以提高執(zhí)行效率哈踱,但是多線程上可能會影響結(jié)果
?
假如下面的場景,正常是順序處理

//線程1
before();//處理初始化工作梨熙,處理完成后才可以正式運(yùn)行下面的run方法
flag = true; //標(biāo)記資源處理好了开镣,如果資源沒處理好,此時程序就可能出現(xiàn)問題
//線程2
while(flag){
    run(); //核心業(yè)務(wù)代碼
}

指令重排序后咽扇,導(dǎo)致順序換了邪财,程序出現(xiàn)問題,且難排查

//線程1
flag = true; //標(biāo)記資源處理好了质欲,如果資源沒處理好树埠,此時程序就可能出現(xiàn)問題
//線程2
while(flag){
    run(); //核心業(yè)務(wù)代碼
}
before();//處理初始化工作,處理完成后才可以正式運(yùn)行下面的run方法

可見性: 一個線程A對共享變量的修改,另一個線程B能夠立刻看到
// 線程 A 執(zhí)行
int num = 0;
// 線程 A 執(zhí)行
num++;
// 線程 B 執(zhí)行
System.out.print("num的值:" + num);

線程A執(zhí)行 i++ 后再執(zhí)行線程 B嘶伟,線程 B可能有2個結(jié)果怎憋,可能是0和1。
?
因?yàn)?i++ 在線程A中執(zhí)行運(yùn)算,并沒有立刻更新到主內(nèi)存當(dāng)中绊袋,而線程B就去主內(nèi)存當(dāng)中讀取并打印毕匀,此時打印的就是0;也可能線程A執(zhí)行完成更新到主內(nèi)存了,線程B的值是1癌别。所以需要保證線程的可見性皂岔,synchronized、lock和volatile能夠保證線程可見性规个。

常見的進(jìn)程間調(diào)度算法

先來先服務(wù)調(diào)度算法:
按照作業(yè)/進(jìn)程到達(dá)的先后順序進(jìn)行調(diào)度 凤薛,即:優(yōu)先考慮在系統(tǒng)中等待時間最長的作業(yè),排在長進(jìn)程后的短進(jìn)程的等待時間長诞仓,不利于短作業(yè)/進(jìn)程
?
短作業(yè)優(yōu)先調(diào)度算法:
短進(jìn)程/作業(yè)(要求服務(wù)時間最短)在實(shí)際情況中占有很大比例缤苫,為了使得它們優(yōu)先執(zhí)行,對長作業(yè)不友好
?
高響應(yīng)比優(yōu)先調(diào)度算法:
在每次調(diào)度時墅拭,先計算各個作業(yè)的優(yōu)先權(quán):優(yōu)先權(quán)=響應(yīng)比=(等待時間+要求服務(wù)時間)/要求服務(wù)時間,因?yàn)榈却龝r間與服務(wù)時間之和就是系統(tǒng)對該作業(yè)的響應(yīng)時間活玲,所以 優(yōu)先權(quán)=響應(yīng)比=響應(yīng)時間/要求服務(wù)時間,選擇優(yōu)先權(quán)高的進(jìn)行服務(wù)需要計算優(yōu)先權(quán)信息谍婉,增加了系統(tǒng)的開銷

時間片輪轉(zhuǎn)調(diào)度算法:
輪流的為各個進(jìn)程服務(wù)舒憾,讓每個進(jìn)程在一定時間間隔內(nèi)都可以得到響應(yīng),由于高頻率的進(jìn)程切換穗熬,會增加了開銷镀迂,且不區(qū)分任務(wù)的緊急程度
?
優(yōu)先級調(diào)度算法:
根據(jù)任務(wù)的緊急程度進(jìn)行調(diào)度,高優(yōu)先級的先處理唤蔗,低優(yōu)先級的慢處理探遵,如果高優(yōu)先級任務(wù)很多且持續(xù)產(chǎn)生,那低優(yōu)先級的就可能很慢才被處理

常見的線程間調(diào)度算法

線程調(diào)度是指系統(tǒng)為線程分配CPU使用權(quán)的過程妓柜,主要分兩種:
?
協(xié)同式線程調(diào)度(分時調(diào)度模式):線程執(zhí)行時間由線程本身來控制箱季,線程把自己的工作執(zhí)行完之后,要主動通知系統(tǒng)切換到另外一個線程上棍掐。最大好處是實(shí)現(xiàn)簡單藏雏,且切換操作對線程自己是可知的,沒啥線程同步問題作煌。壞處是線程執(zhí)行時間不可控制掘殴,如果一個線程有問題,可能一直阻塞在那里粟誓。
?
搶占式線程調(diào)度:每個線程將由系統(tǒng)來分配執(zhí)行時間奏寨,線程的切換不由線程本身來決定(Java中,Thread.yield()可以讓出執(zhí)行時間努酸,但無法獲取執(zhí)行時間)。線程執(zhí)行時間系統(tǒng)可控杜恰,也不會由一個線程導(dǎo)致整個進(jìn)程阻塞获诈。
?
?Java線程調(diào)度就是搶占式調(diào)度,優(yōu)先讓可運(yùn)行池中優(yōu)先級高的線程占用CPU,如果可運(yùn)行池中的線程優(yōu)先級相同,那就隨機(jī)選擇一個線程仍源。所以我們?nèi)绻M承┚€程多分配一些時間,給一些線程少分配一些時間舔涎,可以通過設(shè)置線程優(yōu)先級來完成笼踩。

JAVA的線程的優(yōu)先級,以1到10的整數(shù)指定亡嫌。當(dāng)多個線程可以運(yùn)行時嚎于,JVM一般會運(yùn)行最高優(yōu)先級的線程(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY)。在兩線程同時處于就緒runnable狀態(tài)時挟冠,優(yōu)先級越高的線程越容易被系統(tǒng)選擇執(zhí)行于购,但是優(yōu)先級并不是100%可以獲得,只不過是機(jī)會更大而已知染。

Java多線程里面常用的鎖

悲觀鎖:當(dāng)線程去操作數(shù)據(jù)的時候肋僧,總認(rèn)為別的線程會去修改數(shù)據(jù),所以它每次拿數(shù)據(jù)的時候都會上鎖控淡,別的線程去拿數(shù)據(jù)的時候就會阻塞嫌吠,比如synchronized
樂觀鎖:每次去拿數(shù)據(jù)的時候都認(rèn)為別人不會修改,更新的時候會判斷是別人是否回去更新數(shù)據(jù)掺炭,通過版本來判斷辫诅,如果數(shù)據(jù)被修改了就拒絕更新,比如CAS是樂觀鎖涧狮,但嚴(yán)格來說并不是鎖炕矮,是通過原子性來保證數(shù)據(jù)的同步,比如說數(shù)據(jù)庫的樂觀鎖勋篓,通過版本控制來實(shí)現(xiàn)吧享,CAS不會保證線程同步,樂觀的認(rèn)為在數(shù)據(jù)更新期間沒有其他線程影響
小結(jié):悲觀鎖適合寫操作多的場景譬嚣,樂觀鎖適合讀操作多的場景钢颂,樂觀鎖的吞吐量會比悲觀鎖多。
?
公平鎖:指多個線程按照申請鎖的順序來獲取鎖拜银,簡單來說殊鞭,一個線程組里,能保證每個線程都能拿到鎖尼桶,比如ReentrantLock(底層是同步隊列FIFO:First Input First Output來實(shí)現(xiàn))
非公平鎖:獲取鎖的方式是隨機(jī)獲取的操灿,保證不了每個線程都能拿到鎖,也就是存在有線程餓死,一直拿不到鎖泵督,比如synchronized趾盐、ReentrantLock
小結(jié):非公平鎖性能高于公平鎖,更能重復(fù)利用CPU的時間。
?
可重入鎖:也叫遞歸鎖救鲤,在外層使用鎖之后久窟,在內(nèi)層仍然可以使用,并且不發(fā)生死鎖
不可重入鎖:若當(dāng)前線程執(zhí)行某個方法已經(jīng)獲取了該鎖本缠,那么在方法中嘗試再次獲取鎖時斥扛,就會獲取不到被阻塞
小結(jié):可重入鎖能一定程度的避免死鎖 ,synchronized丹锹、ReentrantLock 是重入鎖且叁。
?
自旋鎖:一個線程在獲取鎖的時候,如果鎖已經(jīng)被其它線程獲取,那么該線程將循環(huán)等待缎患,然后不斷的判斷鎖是否能夠被成功獲取境输,直到獲取到鎖才會退出循環(huán),任何時刻最多只能有一個執(zhí)行單元獲得鎖
小結(jié):自旋鎖不會發(fā)生線程狀態(tài)的切換杉女,一直處于用戶態(tài)壹哺,減少了線程上下文切換的消耗,缺點(diǎn)是循環(huán)會消耗CPU窜锯。常見的自旋鎖:TicketLock,CLHLock,MSCLock张肾。

共享鎖:也叫S鎖/讀鎖,能查看但無法修改和刪除的一種數(shù)據(jù)鎖锚扎,加鎖后其它用戶可以并發(fā)讀取吞瞪、查詢數(shù)據(jù),但不能修改驾孔,增加芍秆,刪除數(shù)據(jù),該鎖可被多個線程所持有翠勉,用于資源數(shù)據(jù)共享妖啥。
?
互斥鎖:也叫X鎖/排它鎖/寫鎖/獨(dú)占鎖/獨(dú)享鎖/ 該鎖每一次只能被一個線程所持有,加鎖后任何試圖再次加鎖的線程會被阻塞对碌,直到當(dāng)前線程解鎖荆虱。例子:如果 線程A 對 data1 加上排他鎖后,則其他線程不能再對 data1 加任何類型的鎖朽们,獲得互斥鎖的線程即能讀數(shù)據(jù)又能修改數(shù)據(jù)怀读。
?
死鎖:兩個或兩個以上的線程在執(zhí)行過程中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象骑脱,若無外力作用菜枷,它們都將無法讓程序進(jìn)行下去。
?
下面三種是JVM為了提高鎖的獲取與釋放效率而做的優(yōu)化叁丧,針對Synchronized的鎖升級啤誊,鎖的狀態(tài)是通過對象監(jiān)視器在對象頭中的字段來表明岳瞭,是不可逆的過程。

偏向鎖:一段同步代碼一直被一個線程所訪問蚊锹,那么該線程會自動獲取鎖寝优,獲取鎖的代價更低。

輕量級鎖:當(dāng)鎖是偏向鎖的時候枫耳,被其他線程訪問,偏向鎖就會升級為輕量級鎖孟抗,其他線程會通過自旋的形式嘗試獲取鎖迁杨,但不會阻塞,且性能會高點(diǎn)凄硼。

重量級鎖:當(dāng)鎖為輕量級鎖的時候铅协,其他線程雖然是自旋,但自旋不會一直循環(huán)下去摊沉,當(dāng)自旋一定次數(shù)的時候且還沒有獲取到鎖狐史,就會進(jìn)入阻塞,該鎖升級為重量級鎖说墨,重量級鎖會讓其他申請的線程進(jìn)入阻塞骏全,性能也會降低。
?
分段鎖尼斧、行鎖姜贡、表鎖

編寫多線程死鎖的例子

死鎖:線程在獲得了鎖A并且沒有釋放的情況下去申請鎖B,這時另一個線程已經(jīng)獲得了鎖B棺棵,在釋放鎖B之前又要先獲得鎖A楼咳,因此閉環(huán)發(fā)生,陷入死鎖循環(huán)烛恤。

public class DeadLockDemo {
?
    private static String locka = "locka";
?
    private static String lockb = "lockb";
?
    public void methodA(){
?
        synchronized (locka){
            System.out.println("我是A方法中獲得了鎖A "+Thread.currentThread().getName() );
?
            //讓出CPU執(zhí)行權(quán)母怜,不釋放鎖
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
?
            synchronized(lockb){
                System.out.println("我是A方法中獲得了鎖B "+Thread.currentThread().getName() );
            }
        }

    }
?
    public void methodB(){
        synchronized (lockb){
            System.out.println("我是B方法中獲得了鎖B "+Thread.currentThread().getName() );
?
            //讓出CPU執(zhí)行權(quán),不釋放鎖
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
?
            synchronized(locka){
                System.out.println("我是B方法中獲得了鎖A "+Thread.currentThread().getName() );
            }
        }
?
    }
?
    public static void main(String [] args){
?
        System.out.println("主線程運(yùn)行開始運(yùn)行:"+Thread.currentThread().getName());
?
        DeadLockDemo deadLockDemo = new DeadLockDemo();
?
        new Thread(()->{
            deadLockDemo.methodA();
        }).start();
?
        new Thread(()->{
            deadLockDemo.methodB();
        }).start();
?
        System.out.println("主線程運(yùn)行結(jié)束:"+Thread.currentThread().getName());
?
    }
?
}

對于上面的例子如何解決死鎖缚柏,常見的解決辦法有兩種:

  • 調(diào)整申請鎖的范圍
  • 調(diào)整申請鎖的順序
public class FixDeadLockDemo {
?
    private static String locka = "locka";
?
    private static String lockb = "lockb";
?
    public void methodA(){
?
        synchronized (locka){
            System.out.println("我是A方法中獲得了鎖A "+Thread.currentThread().getName() );
?
            //讓出CPU執(zhí)行權(quán)苹熏,不釋放鎖
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
?
        }
?
        synchronized(lockb){
            System.out.println("我是A方法中獲得了鎖B "+Thread.currentThread().getName() );
        }
    }
?
?
    public void methodB(){
        synchronized (lockb){
            System.out.println("我是B方法中獲得了鎖B "+Thread.currentThread().getName() );
?
            //讓出CPU執(zhí)行權(quán),不釋放鎖
            try {
                Thread.sleep(2000);
?
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
?
        }
?
        synchronized(locka){
            System.out.println("我是B方法中獲得了鎖A "+Thread.currentThread().getName() );
        }
    }
?
?
    public static void main(String [] args){
?
        System.out.println("主線程運(yùn)行開始運(yùn)行:"+Thread.currentThread().getName());
?
        FixDeadLockDemo deadLockDemo = new FixDeadLockDemo();
?
        for(int i=0; i<10;i++){
            new Thread(()->{
                deadLockDemo.methodA();
            }).start();
?
            new Thread(()->{
                deadLockDemo.methodB();
            }).start();
        }
?
        System.out.println("主線程運(yùn)行結(jié)束:"+Thread.currentThread().getName());
?
    }
?
}

死鎖的4個必要條件:

  • 互斥條件:進(jìn)程對所分配到的資源不允許其他進(jìn)程進(jìn)行訪問船惨,若其他進(jìn)程訪問該資源柜裸,只能等待,直至占有該資源的進(jìn)程使用完成后釋放該資源
  • 請求和保持條件:進(jìn)程獲得一定的資源之后粱锐,又對其他資源發(fā)出請求疙挺,但是該資源可能被其他進(jìn)程占有,此事請求阻塞怜浅,但又對自己獲得的資源保持不放
  • 不可剝奪條件:是指進(jìn)程已獲得的資源铐然,在未完成使用之前蔬崩,不可被剝奪,只能在使用完后自己釋放
  • 環(huán)路等待條件:是指進(jìn)程發(fā)生死鎖后搀暑,若干進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系

這四個條件是死鎖的必要條件沥阳,只要系統(tǒng)發(fā)生死鎖,這些條件必然成立自点,而只要上述條件之 一不滿足桐罕,就不會發(fā)生死鎖。

設(shè)計一個簡單的不可重入鎖例子

不可重入鎖:若當(dāng)前線程執(zhí)行某個方法已經(jīng)獲取了該鎖桂敛,那么在其他方法中嘗試再次獲取鎖時功炮,就會獲取不到被阻塞。

    private void methodA(){
            //獲取鎖 TODO
        methodB();
    }
?
    private void methodB(){
            //獲取鎖 TODO
            //其他操作
    }

/**
 * 不可重入鎖 簡單例子
 *
 *  不可重入鎖:若當(dāng)前線程執(zhí)行某個方法已經(jīng)獲取了該鎖术唬,那么在其他方法中嘗試再次獲取鎖時薪伏,就會獲取不到被阻塞
 */
public class UnreentrantLock {
?
    private boolean isLocked = false;
?
    public synchronized void lock() throws InterruptedException {
?
        System.out.println("進(jìn)入lock加鎖 "+Thread.currentThread().getName());
?
        //判斷是否已經(jīng)被鎖,如果被鎖則當(dāng)前請求的線程進(jìn)行等待
        while (isLocked){
            System.out.println("進(jìn)入wait等待 "+Thread.currentThread().getName());
            wait();
        }
        //進(jìn)行加鎖
        isLocked = true;
    }
    public synchronized void unlock(){
        System.out.println("進(jìn)入unlock解鎖 "+Thread.currentThread().getName());
        isLocked = false;
        //喚醒對象鎖池里面的一個線程
        notify();
    }
}
?
?
?
public class Main {
    private UnreentrantLock unreentrantLock = new UnreentrantLock();
    //加鎖建議在try里面粗仓,解鎖建議在finally
    public void  methodA(){
        try {
            unreentrantLock.lock();
            System.out.println("methodA方法被調(diào)用");
            methodB();
        }catch (InterruptedException e){
            e.fillInStackTrace();
        } finally {
            unreentrantLock.unlock();
        }
    }
?
    public void methodB(){
        try {
            unreentrantLock.lock();
            System.out.println("methodB方法被調(diào)用");
        }catch (InterruptedException e){
            e.fillInStackTrace();
        } finally {
            unreentrantLock.unlock();
        }
    }
    public static void main(String [] args){
        //演示的是同個線程
        new Main().methodA();
    }
}
?
//同一個線程嫁怀,重復(fù)獲取鎖失敗,形成死鎖借浊,這個就是不可重入鎖

設(shè)計一個簡單的可重入鎖例子

可重入鎖:也叫遞歸鎖塘淑,在外層使用鎖之后,在內(nèi)層仍然可以使用蚂斤,并且不發(fā)生死鎖

/**
 * 可重入鎖 簡單例子
 *
 *  可重入鎖:也叫遞歸鎖朴爬,在外層使用鎖之后,在內(nèi)層仍然可以使用橡淆,并且不發(fā)生死鎖
 */
public class ReentrantLock {
?
    private boolean isLocked = false;
?
    //用于記錄是不是重入的線程
    private Thread lockedOwner = null;
?
    //累計加鎖次數(shù)召噩,加鎖一次累加1,解鎖一次減少1
    private int lockedCount = 0;
?
    public synchronized void lock() throws InterruptedException {
?
        System.out.println("進(jìn)入lock加鎖 "+Thread.currentThread().getName());
?
        Thread thread = Thread.currentThread();
?
        //判斷是否是同個線程獲取鎖, 引用地址的比較
        while (isLocked && lockedOwner != thread ){
            System.out.println("進(jìn)入wait等待 "+Thread.currentThread().getName());
            System.out.println("當(dāng)前鎖狀態(tài) isLocked = "+isLocked);
            System.out.println("當(dāng)前count數(shù)量 lockedCount =  "+lockedCount);
            wait();
        }
?
        //進(jìn)行加鎖
        isLocked = true;
        lockedOwner = thread;
        lockedCount++;
    }
    public synchronized void unlock(){
        System.out.println("進(jìn)入unlock解鎖 "+Thread.currentThread().getName());
?
        Thread thread = Thread.currentThread();
?
        //線程A加的鎖逸爵,只能由線程A解鎖具滴,其他線程B不能解鎖
        if(thread == this.lockedOwner){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                lockedOwner = null;
                //喚醒對象鎖池里面的一個線程
                notify();
            }
        }
    }
}

public class Main {
    //private UnreentrantLock unreentrantLock = new UnreentrantLock();
    private ReentrantLock reentrantLock = new ReentrantLock();
?
    //加鎖建議在try里面,解鎖建議在finally
    public void  methodA(){
?
        try {
            reentrantLock.lock();
            System.out.println("methodA方法被調(diào)用");
            methodB();
?
        }catch (InterruptedException e){
            e.fillInStackTrace();
?
        } finally {
            reentrantLock.unlock();
        }
?
    }
?
    public void methodB(){
?
        try {
            reentrantLock.lock();
            System.out.println("methodB方法被調(diào)用");
?
        }catch (InterruptedException e){
            e.fillInStackTrace();
?
        } finally {
            reentrantLock.unlock();
        }
    }
?
    public static void main(String [] args){
        for(int i=0 ;i<10;i++){
            //演示的是同個線程
            new Main().methodA();
        }
    }
}

歡迎補(bǔ)充J蟆9乖稀!

最后

感謝你看到這里趋艘,看完有什么的不懂的可以在評論區(qū)問我疲恢,覺得文章對你有幫助的話記得給我點(diǎn)個贊,每天都會分享java相關(guān)技術(shù)文章或行業(yè)資訊瓷胧,歡迎大家關(guān)注和轉(zhuǎn)發(fā)文章显拳!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市搓萧,隨后出現(xiàn)的幾起案子杂数,更是在濱河造成了極大的恐慌宛畦,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件揍移,死亡現(xiàn)場離奇詭異次和,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)那伐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門踏施,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人罕邀,你說我怎么就攤上這事读规。” “怎么了燃少?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長铃在。 經(jīng)常有香客問我阵具,道長,這世上最難降的妖魔是什么定铜? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任阳液,我火速辦了婚禮,結(jié)果婚禮上揣炕,老公的妹妹穿的比我還像新娘帘皿。我一直安慰自己,他們只是感情好畸陡,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布鹰溜。 她就那樣靜靜地躺著,像睡著了一般丁恭。 火紅的嫁衣襯著肌膚如雪曹动。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天牲览,我揣著相機(jī)與錄音墓陈,去河邊找鬼。 笑死第献,一個胖子當(dāng)著我的面吹牛贡必,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播庸毫,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼仔拟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了飒赃?” 一聲冷哼從身側(cè)響起理逊,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤橡伞,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后晋被,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兑徘,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年羡洛,在試婚紗的時候發(fā)現(xiàn)自己被綠了挂脑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡欲侮,死狀恐怖崭闲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情威蕉,我是刑警寧澤刁俭,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站韧涨,受9級特大地震影響牍戚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜虑粥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一如孝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧娩贷,春花似錦第晰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至储笑,卻和暖如春腹躁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背南蓬。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工纺非, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赘方。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓烧颖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親窄陡。 傳聞我的和親對象是個殘疾皇子炕淮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評論 2 359