Java併發之執行緒與執行緒池
我們經常聽到一些大佬說一些概念,比如執行緒不安全,那到底什麼是執行緒不安全呢?
執行緒不安全指的是,我們在多執行緒的環境下,操作一些共享資料的時候可能會讓我們無法得到期望的結果
執行緒
為什麼會出現執行緒呢?
執行緒就好比一個人,俗話說的好,眾人拾柴火焰高,而多執行緒也是這個道理!
我們先來看一下執行緒6種狀態的狀態
執行緒狀態
-
New(新生態)
-
Runnable(可執行態)
在可執行態中又可以分為,Running(執行態)和Ready(就緒態)
-
Running(執行態)
執行態指的是,該執行緒已經獲取了CPU的時間片,簡單來說,就是該執行緒正在執行 * Ready(就緒態)
而就緒態指的是,該執行緒萬事俱備只欠“東風”,就差CPU給他分配時間片了 * Blocking(阻塞態) 執行緒在獲取鎖失敗之後,就會進入該狀態,當該執行緒獲取了鎖就會結束該狀態,所有阻塞狀態的執行緒都會放在阻塞佇列中。
-
-
Waiting(等待態)
當處於執行態的執行緒呼叫wait(),park(),join()方法後,就會進入該狀態,處於等待態的執行緒,會釋放CPU時間片,並且會釋放資源(例如鎖),這個狀態下的執行緒只能等待其他執行緒來喚醒它。 * Timed Waiting(超時等待態)
超時等待態和等待態類似,不過這個狀態的執行緒不需要顯式地去喚醒,這個狀態的執行緒在超過一定時間後,將由系統自動喚醒
- Terminated(結束態) 執行緒結束後的狀態
執行緒的三種使用方式
執行緒的使用方式有三種,分別是實現Runnable介面、實現Callable介面、繼承Thread類 * 實現Runnable介面
建立一個自定義類然後實現該介面,實現該介面的run()方法
然後在我們需要使用該類的地方直接去例項化即可
-
實現Callable介面
建立一個自定義類然後實現該介面,實現該介面的call()方法 然後通過開啟執行緒池服務來建立該類 * 繼承Thread類
建立一個自定義類然後繼承該類,需要重寫該類的run()方法 這種方式其實和實現Runnable介面的方式類似,因為Thread類也實現了Runnable介面 我們在實現的時候直接去實現即可
三種方式的對比
其實我們更加推薦實現Runable介面的這種方式,因為相較於其他兩種,實現Callable介面這種方式使用起來更加繁瑣,而繼承Thread類的這種方式是繼承,我們都知道在Java中是不支援多重繼承的,但是支援多重實現,並且用起來實現Runnable介面這種方式也更加簡單
執行緒之間的協作工作
join()方法
我們在很多情況下,會在一個執行緒中呼叫另一個執行緒的方法,這個時候我們就會使用到join方法了
在這裡我們先定義一個執行緒類,為了方便我們直接去繼承Thread類 ```java public class YlOneThread extends Thread {
@Override
public void run() {
System.out.println("我是亞雷1執行緒");
}
}
在這裡我們又定義了一個執行緒類,不過在這個執行緒類中需要使用前面那個執行緒
java
public class YlTwoThread extends Thread{
private YlOneThread thread;
public YlTwoThread(YlOneThread thread){
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是亞雷2執行緒");
}
} ``` 最後我們再建立一個測試類來測試
java
public class test {
public static void main(String[] args) {
YlOneThread Thread1 = new YlOneThread();
YlTwoThread Thread2 = new YlTwoThread(Thread1);
Thread2.start();
Thread1.start();
}
}
我們可以很明顯的看到再執行緒2中呼叫了執行緒1不出意外我們得到了這樣的結果
我是亞雷1執行緒
我是亞雷2執行緒
在這裡雖然是執行緒2先啟動,不過線上程2中呼叫了執行緒1,執行緒2會先等待執行緒1完成然後繼續執行
wait()、notify()、notifyAll()方法.
在這裡我就簡略的說一下這些方法、因為這些方法並不是來自於JUC包下的而是來自於Object類下面的
-
wait()方法
這個方法在前面我們也見過了,讓執行緒進入等待態並且也會釋放資源(鎖) * notify()、notifyAll()方法
這兩個方法用於喚醒執行緒、不同的是一個是喚醒所有執行緒而另一個不是
wait()和sleep()
我們經常拿這兩個方法進行比較、因為它們在我們淺層的認識中都是使執行緒進行等待
其實它們大相徑庭
wait()方法會釋放資源(鎖),這在前面我們已經知道了, 而sleep()方法並不會釋放資源(鎖)
wait()方法是基於Object的方法,而sleep()是基於Thread類的方法
await()、signal()、signalAll()方法
而在JUC中我們使用這些方法來實現執行緒間的排程
```java public class AWaitTest { private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void methodOne(){
lock.lock();
try {
System.out.println("方法一執行......");
condition.signalAll();
} catch (Exception e) {
}finally{
lock.unlock();
}
}
public void methodTwo(){
lock.lock();
try {
condition.await();
System.out.println("方法二執行.......");
} catch (Exception e) {
//TODO: handle exception
}finally{
lock.unlock();
}
}
} ```
```java public static void main(String[] args) {
ExecutorService ThreadPool = Executors.newCachedThreadPool();
AWaitTest aWaitTest = new AWaitTest();
ThreadPool.execute(() -> aWaitTest.methodTwo());
ThreadPool.execute(() -> aWaitTest.methodOne());
}
```
方法一執行......
方法二執行.......
這種方法很明顯wait()那一套更加靈活
執行緒池
說到執行緒池,我們不得不提一下執行緒池的七大引數了。
執行緒池七大引數
java
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1. corePoolSize 核心執行緒數
當提交一個任務時,執行緒池建立一個新的執行緒來執行任務,直到執行緒數量達到corePoolSize的時候
這個時候會將執行緒放入到工作隊列當中阻塞
-
maximumPoolSize 最大執行緒數
這個引數只在,工作佇列是有邊界的時候生效,如果工作佇列沒有邊界,這個引數將不生效 因為會將新的執行緒一直新增到工作隊列當中 3. keepAliceTime 執行緒空閒的存活時間
因為執行緒執行任務執行完畢之後不會立即死亡,會繼續存活下來,而這個引數就是在限制執行緒的存活時間 還有一點需要注意,這個引數預設只在當前執行緒數大於核心執行緒數的情況下生效 4. unit 執行緒存活時間單位
見名知意,這個引數作為keepAliceTime的單位 5. workQueue 工作佇列
用於儲存等待需要執行任務的新執行緒 6. threadFactory 執行緒工廠
用於建立執行緒的執行緒工廠
-
handler 飽和策略
當阻塞佇列滿了,並且沒有空閒的的工作執行緒,如果此時還不斷提交任務,執行緒池必須進行處理 執行緒池提供了四種飽和策略 1. AbortPolicy: 直接丟擲異常,預設策略 2. CallerRunsPolicy: 用呼叫者所在的執行緒來執行任務 3. DiscardOldestPolicy: 丟棄阻塞佇列中靠最前的任務,並執行當前任務 4. DiscardPolicy: 直接丟棄任務
執行緒池的執行流程
執行緒池到底是怎麼來建立執行緒的呢?
我們在提交任務後,
執行緒池會先判斷當前執行緒數是否大於核心執行緒數,
如果不大於則直接建立工作執行緒,
否則則建立執行緒新增到阻塞隊列當中,
然後,如果執行緒池會再次進行判斷阻塞佇列是否滿
如果不滿,則直接新增到阻塞隊列當中
否則,會再次判斷當前執行緒數是否大於最大執行緒數
如果大於則執行拒絕策略,否則就建立執行緒