多線程概念
創(chuàng)新互聯(lián)公司是一家專業(yè)提供南關(guān)企業(yè)網(wǎng)站建設(shè),專注與網(wǎng)站設(shè)計(jì)、成都網(wǎng)站設(shè)計(jì)、H5場(chǎng)景定制、小程序制作等業(yè)務(wù)。10年已為南關(guān)眾多企業(yè)、政府機(jī)構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專業(yè)網(wǎng)站制作公司優(yōu)惠進(jìn)行中。
在我們的程序?qū)用鎭碚f,多線程
通常是在每個(gè)進(jìn)程
中執(zhí)行的,相應(yīng)的附和我們常說的線程與進(jìn)程
之間的關(guān)系。線程與進(jìn)程的關(guān)系:線程可以說是進(jìn)程的兒子,一個(gè)進(jìn)程可以有多個(gè)線程
。但是對(duì)于線程來說,只屬于一個(gè)進(jìn)程。再說說進(jìn)程,每個(gè)進(jìn)程的有一個(gè)主線程
作為入口,也有自己的唯一標(biāo)識(shí)PID
,它的PID也就是這個(gè)主線程的線程ID
。
對(duì)于我們的計(jì)算機(jī)硬件來說,線程是進(jìn)程
中的一部分,也是進(jìn)程的的實(shí)際運(yùn)作單位,它也是操作系統(tǒng)中的最小運(yùn)算調(diào)度單位。多線程可以提高CPU的處理速度。當(dāng)然除了單核CPU,因?yàn)閱魏诵腃PU同一時(shí)間只能處理一個(gè)線程。在多線程環(huán)境下,對(duì)于單核CP來說,并不能提高響應(yīng)速度,而且還會(huì)因?yàn)轭l繁切換線程上下文導(dǎo)致性能降低。多核心CPU具有同時(shí)并行執(zhí)行線程的能力,因此我們需要注意使用環(huán)境。線程數(shù)超出核心數(shù)時(shí)也會(huì)引起線程切換,并且操作系統(tǒng)對(duì)我們線程切換是隨機(jī)的。
引入
共享資源
,從而實(shí)現(xiàn)一些特殊任務(wù)。上面說了,多線程在進(jìn)行切換時(shí)CPU隨機(jī)調(diào)度
的,假如我們直接運(yùn)行多個(gè)線程操作共享資源的話,勢(shì)必會(huì)引起一些不可控錯(cuò)誤因素。為了解決多線程對(duì)同一共享變量的爭(zhēng)奪
。Java 線程通信的方式
主要介紹wait/notify,也有ReentrantLock的Condition條件變量的await/signal,LockSupport的park/unpark方法,也能實(shí)現(xiàn)線程之間的通信。主要是阻塞/喚醒通信模式。
首先說明這種方法一般都是作用于調(diào)用方法的所在線程。比如在主線程執(zhí)行wait方法,就是將主線程阻塞了。
wait/notify機(jī)制
await/signal
park/unpark
如果一個(gè)線程先于被通知線程調(diào)用wait()前調(diào)用了notify(),等待的線程將錯(cuò)過這個(gè)信號(hào)。
flag=FALSE
表示還沒wait,在wait之前將設(shè)置flag=TRUE
,在notify之后設(shè)置flag=FALSE
。每次notify喚醒之前都判斷flag=true
是否已經(jīng)wait,在wait中判斷flag=false
是否已經(jīng)notify。核心代碼演示
// 線程一使用LOCK1對(duì)象調(diào)用wait方法阻塞自己
executor.execute(new ThreadTest("線程一",LOCK1,LOCK2));
synchronized (LOCK1) {
System.out.println("main執(zhí)行notify方法讓線程一醒過來");
LOCK1.notify();
}
但是他很有可能醒不來,因?yàn)橹骶€程調(diào)用LOCK1對(duì)象的notify方法,可能主線程已經(jīng)執(zhí)行完了,上面線程還沒創(chuàng)建完成,也就是沒有進(jìn)入wait狀態(tài)。就醒不來了。
解決方式:使用信號(hào)量標(biāo)志進(jìn)行判斷是否已經(jīng)進(jìn)入wait
synchronized (LOCK1) {
while (true) {
if (FLAG.getFlag()) {
System.out.println("main馬上執(zhí)行notify方法讓線程一醒過來" + "flag = " + FLAG.getFlag());
LOCK1.notify();
// 將標(biāo)志位變?yōu)镕ALSE
FLAG.setFlag(Constants.WaitOrNoWait.NO_WAIT.getFlag());
System.out.println("main執(zhí)行notify方法完畢" + "flag = " + FLAG.getFlag());
break;
}
}
}
由于莫名其妙的原因,線程有可能在沒有調(diào)用過notify()和notifyAll()的情況下醒來。
synchronized (waitName) {
while (!flag.getFlag()) {
try {
// 將標(biāo)志位設(shè)置為TRUE
flag.setFlag(Constants.WaitOrNoWait.WAIT.getFlag());
System.out.println("name;"+name+" 我睡著了進(jìn)入阻塞狀態(tài)" + "flag = " + flag.getFlag());
waitName.wait();
System.out.println("name;"+name+" 我醒來了" + "flag = " + flag.getFlag());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private final static Object LOCK1 = new Object();
private final static Object LOCK2 = new Object();
private final static Constants.WaitStatus FLAG = new Constants.WaitStatus(false);
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 1, TimeUnit.DAYS, new ArrayBlockingQueue<>(4), new ThreadPoolExecutor.AbortPolicy());
executor.execute(new ThreadTest("線程一",LOCK1,LOCK2, FLAG));
// ···喚醒
}
class ThreadTest implements Runnable { //阻塞··· }
完整代碼可以看這[Gitee倉(cāng)庫(kù)完整代碼][https://gitee.com/malongfeistudy/javabase/tree/master/Java多線程_Study/src/main/java/com/mlf/thread/demo_wait_notify]
首先復(fù)習(xí)一下創(chuàng)建線程的幾種方式和其的優(yōu)缺點(diǎn):
使用線程池的步驟
在主線程自定義線程池使用實(shí)例,這里需要根據(jù)實(shí)際情況定義鎖對(duì)象,因?yàn)槲覀冃枰褂眠@些鎖對(duì)象控制多線程之間的運(yùn)行順序以及線程之間的通信。在Java中每個(gè)對(duì)象都會(huì)在初始化的時(shí)候擁有一個(gè)監(jiān)視器,我們需要利用好他進(jìn)行并發(fā)編程。這種創(chuàng)建線程池的方法也是阿里巴巴推薦的方式,想想以阿里的體量多年總結(jié)出來的總沒有錯(cuò),大家還是提前約束自己的編碼習(xí)慣等。安裝一個(gè)阿里代碼規(guī)范的插件對(duì)自己的程序員道路是比較nice的。
/**
* 每個(gè)使用對(duì)應(yīng)唯一的對(duì)象作為監(jiān)視器對(duì)象鎖。
*/
public static final Object A_O = new Object();
public static final Object B_O = new Object();
/** 參數(shù):
* int corePoolSize, 核心線程數(shù)
* int maximumPoolSize, 最大線程數(shù)
* long keepAliveTime, 救急存活時(shí)間
* TimeUnit unit, 單時(shí)間位
* BlockingQueue workQueue, 阻塞隊(duì)列
* RejectedExecutionHandler handler 拒絕策略
**/
// 使用阿里巴巴推薦的創(chuàng)建線程池的方式
// 通過ThreadPoolExecutor構(gòu)造函數(shù)自定義參數(shù)創(chuàng)建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
3,
5,
1,
TimeUnit.DAYS,
new ArrayBlockingQueue<>(2),
new ThreadPoolExecutor.AbortPolicy());
class ThreadDiy implements Runnable {
private final String name;
/**
* 阻塞鎖對(duì)象 等待標(biāo)記
**/
private final Object waitFor;
/**
* 執(zhí)行鎖對(duì)象 下一個(gè)標(biāo)記
**/
private final Object next;
public AlternateThread(String name, Object waitFor, Object next) {
}
@Override
public void run() {
// 線程的代碼邏輯···
}
}
題目:現(xiàn)在有兩個(gè)線程,不論線程的啟動(dòng)順序,我需要指定線程一先執(zhí)行,然后線程二再執(zhí)行。
初始化兩個(gè)對(duì)象鎖作為線程監(jiān)視器。
private final static Object ONE_LOCK = new Object();
private final static Object TWO_LOCK = new Object();
接下來初始化線程池,上面有具體的介紹,在這就不多說了
使用線程池去執(zhí)行我們的兩個(gè)線程,在這里我們需要分析的是
// 使用線程池創(chuàng)建線程
executor.execute(new DiyThread(1, ONE_LOCK, TWO_LOCK));
executor.execute(new DiyThread(2, TWO_LOCK, ONE_LOCK));
synchronized (ONE_LOCK) {
ONE_LOCK.notify();
}
創(chuàng)建線程類
我們使用繼承Runnable的方式去創(chuàng)建線程對(duì)象,需要在這個(gè)類中實(shí)現(xiàn)每個(gè)線程執(zhí)行的邏輯,我們根據(jù)題目可以得出,我們要控制每個(gè)線程的執(zhí)行順序,怎么辦?那么就要實(shí)現(xiàn)所有線程之間的通信,通信方式采用wait-notify的方式即可。我們使用wait-notify的時(shí)候必須結(jié)合synchronized,那么就需要控制兩個(gè)對(duì)象鎖。因?yàn)槲覀儾还馐强刂谱约?,還有另一個(gè)線程。
我們?cè)俜治鲆幌骂}意,首先需要指定先后執(zhí)行的順序,那么就需要實(shí)現(xiàn)兩個(gè)線程之間的通信。其次呢,我們得控制兩個(gè)線程,那么就需要兩個(gè)監(jiān)視器去監(jiān)視這兩個(gè)線程。
我們定義這兩個(gè)監(jiān)視器對(duì)象為own和other。然后再新增一個(gè)屬性threadId來標(biāo)識(shí)自己。
private final int threadId;
private final Object own;
private final Object other;
接下來就是編寫Run方法了
每個(gè)線程首先需要阻塞自己,等待喚醒。然后喚醒之后,再去喚醒另外一個(gè)線程。這樣就實(shí)現(xiàn)了自定義順序。至于先喚醒哪個(gè)線程,交給我們的主線程去完成。
這里需要注意的是,如果我們只是單純地執(zhí)行了多個(gè)線程對(duì)象,但是主線程沒有主動(dòng)去喚醒其中一個(gè),這樣就會(huì)形成類似于死鎖的循環(huán)等待。你需要我喚醒,我需要你喚醒。這個(gè)時(shí)候需要主線程去插手喚醒其中的任意一個(gè)線程。
第一步阻塞自己own
synchronized (own) {
try {
own.wait();
System.out.println(num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
第二步喚醒other
synchronized (other) {
other.notify();
}
題目需求:現(xiàn)在需要使用三個(gè)線程輪流打印輸出。說白了也就是多線程輪流執(zhí)行罷了,和問題一控制兩個(gè)線程打印順序沒什么區(qū)別
/**
* 阻塞鎖對(duì)象 等待標(biāo)記
**/
private final Object waitFor;
/**
* 喚醒鎖對(duì)象 下一個(gè)標(biāo)記
**/
private final Object next;
run方法的邏輯和上面的基本一樣。 一個(gè)線程一旦調(diào)用了任意對(duì)象的wait()方法,它就釋放了所持有的監(jiān)視器對(duì)象上的鎖,并轉(zhuǎn)為非運(yùn)行狀態(tài)。
每個(gè)線程首先會(huì)調(diào)用 waitFor對(duì)象的 wait()方法,隨后該線程進(jìn)入阻塞狀態(tài),等待其他線程執(zhí)行自己引用的該 waitFor對(duì)象的 notify()方法即可。
while (true) {
synchronized (waitFor) {
try {
waitFor.wait();
System.out.println(name + " 開始執(zhí)行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (next) {
next.notify();
}
}
主線程需要初始化線程池、執(zhí)行三個(gè)線程,并且最后需要打破僵局,因?yàn)榇藭r(shí)每個(gè)線程都是阻塞狀態(tài),他們沒法阻塞/喚醒循環(huán)下去。
synchronized (A_O) {
A_O.notify();
}
模擬執(zhí)行流程
/**
* 模擬執(zhí)行流程
* 打印名(name) 等待標(biāo)記(waitFor) 下一個(gè)標(biāo)記(next)
* 1 A B
* 2 B C
* 3 C A
*
* 像不像Spring的循環(huán)依賴:確實(shí)很像,Spring中的循環(huán)依賴就是 BeanA 依賴 BeanB,BeanB 依賴 BeanA;
* 他們實(shí)例化過程中都需要先屬性注入對(duì)方的實(shí)例,倘若剛開始的時(shí)候都沒有實(shí)例化,初始化就會(huì)死等。類似于死鎖。
**/
使用多線程輪流打印 01234····
具體代碼請(qǐng)移步到Gitee倉(cāng)庫(kù):[順序打印自增變量][https://gitee.com/malongfeistudy/javabase/blob/master/Java多線程_Study/src/main/java/com/mlf/thread/print/AddNumberPrint2.java]
條件變量Condition的使用
如有問題,請(qǐng)留言評(píng)論。