Java基礎複習之旅(3)-執行緒篇
1. 基本概念
1.1 執行緒、程式、協程是什麼?
首先要知道,一個程式可以有多個執行緒,一個執行緒可以有多個協程。
先說說執行緒跟程式:
- 程式是資源分配的最小單元,執行緒是CPU排程的最小單位。所有與程式相關的資源,均被記錄在PCB(印刷電路板)中。
- 執行緒隸屬於某一個程式,共享程式的資源。執行緒只由堆疊暫存器,程式計數器和棧堆指標組成。
- 程式有獨立的地址空間,彼此之間相互不影響,可以看做一個獨立的應用。而執行緒只是程式的執行路徑。
- 總結成一句話就是:對作業系統來說,程式是最小的資源管理單元,執行緒的最小的執行單元。
協程平時聽的比較少,所以這裡引用維基百科的解釋:
- 協程是計算機程式的一類元件,推廣了協作式多工的子程式,允許執行被掛起與被恢復。
- 協程是一種比執行緒更加輕量級的存在,並且協程與執行緒最大的區別就在於。執行緒是搶佔式多工的,協程是協作式多工的。這就意味著協程提供併發性而非並行性,所以不需要加鎖的操作。
1.2 run()方法與start()方法的區別?
start()方法是執行多執行緒的主方法,在start()方法內部,通過呼叫run()方法啟動這個執行緒。run()方法用以啟動一個執行緒,然後主執行緒立刻返回。該啟動的執行緒不會馬上執行,而是在等待佇列中等待CPU的排程。
1.3 程式的狀態與執行緒的狀態
我們知道在作業系統中,程式有五種狀態: 新建、執行、就緒、阻塞、終止。注意這只是比較廣泛的說法,不同的作業系統對程式狀態定義也有不同。
- 1.初始(NEW):新建立了一個執行緒物件,但是還沒有呼叫start()方法。
- 2.可執行(RUNNABLE):分為就緒狀態與執行中狀態。
在呼叫start()方法之後執行緒就進入就緒狀態,等待CPU的排程。注意:執行緒在睡眠和掛起中恢復的時候也會進入就緒狀態。
就緒狀態的執行緒得到了CPU的時間片,執行緒被設定為當前執行緒,開始執行run()方法,執行緒進入執行中狀態。 - 3.阻塞(BLOCKED):表示執行緒由於鎖的原因造成了阻塞。
- 4.等待(WAITING)比如使用Thread.sleep()方法,CPU不會給執行緒分配時間,使得執行緒處於等待狀態。處於該狀態的執行緒需要被期待物件所喚醒,否則會處於無限期的等待狀態。
- 5.超時等待(TIME_WAITTING):與等待轉態的區別就在於,他不會無限期的等待,當達到一定的時間之後,他們會被自動的喚醒。
-
6.終止狀態(TERMINATED):當執行緒的run()方法執行完畢時,或者主執行緒的main()執行完畢時,我們就認為這個執行緒終止了。注意:執行緒一旦終止了,就不能呼叫start()方法。
補充: 進入Thread.getState()方法可以發現在JVM中也定義了六種執行緒的狀態:
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
複製程式碼
sun.misc包中的VM類中的執行緒狀態:
private static final int JVMTI_THREAD_STATE_ALIVE = 1;
private static final int JVMTI_THREAD_STATE_TERMINATED = 2;
private static final int JVMTI_THREAD_STATE_RUNNABLE = 4;
private static final int JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER = 1024;
private static final int JVMTI_THREAD_STATE_WAITING_INDEFINITELY = 16;
private static final int JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT = 32;
複製程式碼
1.4 Synchronized與ReentrantLock的相同點與不同點
- 相同點在於,二者都是採用加鎖的方式實現同步,而且都是阻塞式的同步。
- 區別在於,Synchronized是Java的關鍵字,需要JVM去實現。而ReentrantLock是JDK1.5之後提供的API層面的互斥鎖,需要lock(),unlock()方法以及try/catch去實現。
1.5 新建執行緒的幾種方法
- 通過繼承Thread類,重寫run()方法。
- 通過實現Callable介面。
- 通過實現Runnable介面。
1.6 實現執行緒安全的方法
- 使用不可變的類:Integer/String
- 使用synchronized同步塊同步物件
synchronized({Object})可以用以鎖住一個物件,同樣synchronized還可以用在方法簽名上。那麼問題來了,對於非靜態方法,synchronized很明顯是鎖住呼叫這個方法的實體,那麼對於靜態方法,鎖住的是什麼?答案是鎖住的是類在JVM中所儲存的Class物件。
- 使用ReentrantLock,配合lock(),unLock()使用。相比於synchronized,ReentrantLock更靈活,可以在這個方法中加鎖,在其他方法中將鎖釋放。
- 使用java.util.concurrent包下面的方法去替換在多執行緒情況下不安全的實現,比如HashMap可以替換為ConcurrentHashMap。
- 使用Collections.synchronized{Colleation}
private static List<Integer> list = Collections.synchronizedList(new ArrayList<>());
複製程式碼
- 使用原子操作:AtomicInteger/AtomicLong/AtomicBoolean
1.7 wait(),notify(),notifyAll()方法
首先,他們都是Object類中的方法,接下來看下面這個例子:
public static void main(String[] args) {
new Thread(new Thread1()).start();
new Thread(new Thread2()).start();
new Thread(new Thread3()).start();
}
static class Thread1 extends Thread {
@Override
public void run() {
System.out.println("Thread1");
}
}
static class Thread2 extends Thread {
@Override
public void run() {
System.out.println("Thread2");
}
}
static class Thread3 extends Thread {
@Override
public void run() {
System.out.println("Thread3");
}
}
複製程式碼
這裡我開闢了三個執行緒,分別去實現自己的方法。結果很明顯,就是按順序列印結果:
Thread1
Thread2
Thread3
複製程式碼
但是當我對Thread1使用wait()方法時,
static class Thread1 extends Thread {
@Override
public void run() {
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread1");
}
}
複製程式碼
Thread會交出自己所持有的鎖,讓其他程式去爭奪這個鎖,而自己就一直等待著其他執行緒的喚醒。
他就一直在這等啊,直到我們在其他執行緒中使用notify()方法將其喚醒:static class Thread3 extends Thread {
@Override
public void run() {
synchronized (lock){
lock.notify();
}
System.out.println("Thread3");
}
}
複製程式碼
這時候,執行的順序也改變了:
Thread2
Thread3
Thread1
複製程式碼
那麼notifyAll()就是喚醒其他正在等待的執行緒,然後讓他們之間重新去爭奪鎖:
static class Thread1 extends Thread {
@Override
public void run() {
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread1");
}
}
static class Thread2 extends Thread {
@Override
public void run() {
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread2");
}
}
static class Thread3 extends Thread {
@Override
public void run() {
synchronized (lock){
lock.notifyAll();
}
System.out.println("Thread3");
}
}
複製程式碼
結果:
Thread1
Thread2
Thread3
複製程式碼
注意:使用Object的wait(),notify(),notifyAll()方法都需要持有這個物件的監視器,就是程式碼中的synchronized()。java doc中的原話:
* This method should only be called by a thread that is the owner
* of this object's monitor.
複製程式碼
否則會報出IllegalMonitorStateException
異常。
8.什麼是執行緒池?
由於Java的執行緒排程完全依賴於作業系統,所以每個執行緒都會佔用一定的資源,進而我們不可能隨心所欲的去開闢執行緒。所以,這時候就需要執行緒池了,執行緒池就是預先在記憶體中開闢一塊資源,專門用於執行緒的排程。當需要執行緒執行任務時,就通過執行緒池管理器來實現執行緒的分配。
9.有哪些建立執行緒池的方法,他們之間的區別在哪?
java.util.concurrent包中的Executors類中,封裝了四種開闢執行緒池的方法。
- newFixedThreadPool—數量固定的執行緒池
規定最大的數量,超過這個數量後進來的執行緒任務放入等待數列中。
- newSingleThreadExecutor—只有一個執行緒的執行緒池
一次只能執行一個任務。
- newCachedThreadPool-快取型執行緒池
快取型即在核心執行緒達到最大值之前,有新的任務就線上程池中加入新的執行緒,即使有空閒的執行緒也不復用。達到最大執行緒後,再複用空閒的執行緒。沒有空餘的執行緒則新建臨時執行緒,用於處理大量短時間工作的執行緒池。
- new ScheduledThreadPool—計劃型執行緒池
可以設定指定的延時或定期執行任務。
10.Runnable()與Callable()的區別?
- Runnable()是沒有返回值的,而Callable()允許有返回值。
- Runnable()不能宣告異常,Callable()可以。 程式碼演示:
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 1;
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(" ");
}
});
System.out.println(submit.get());
//執行緒池使用完畢後,必須關閉
executorService.shutdown();
複製程式碼
11. 守護執行緒與非守護執行緒的區別?
程式允許完畢,jvm會等待非守護執行緒完成後關閉,但是jvm不會等待守護執行緒關閉,守護執行緒最典型的例子就是GC程式。
12. 什麼是樂觀鎖?什麼是悲觀鎖?
- 樂觀鎖:樂觀鎖認為競爭不總是會發生,因此它不需要持有鎖,將比較-替換這兩個動作作為一個原子操作嘗試去修改記憶體中的變數,如果失敗則表示發生衝突,那麼就應該有相應的重試邏輯。
- 悲觀鎖:悲觀鎖認為競爭總是會發生,因此每次對某資源進行操作時,都會持有一個獨佔的鎖,就像synchronized,不管三七二十一,直接上了鎖就操作資源了。
2. 死鎖問題
2.1 如何產生死鎖?
像下面這種情況鎖與鎖之間相互競爭的情況:
public class MultiplyThreadTest {
private static final Object lock = new Object();
private static final Object lock1 = new Object();
public static void main(String[] args) {
new ThreadClass1().start();
new ThreadClass2().start();
}
static class ThreadClass1 extends Thread {
@Override
public void run() {
synchronized (lock) {
try {
Thread.sleep(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread1");
}
}
}
}
static class ThreadClass2 extends Thread {
@Override
public void run() {
synchronized (lock1) {
synchronized (lock) {
System.out.println("Thread2");
}
}
}
}
}
複製程式碼
其實我剛開始寫出的來的時候,我自己都有疑問:
- 1.不加Thread.sleep(0)這兩個執行緒是不會產生死鎖的,為什麼?
- 2.就算加了Thread.sleep(0),那也只是讓執行緒睡0毫秒,加了跟沒加不是一樣嘛,為什麼加上了就能產生死鎖呢?
其實這地方,涉及到一個冷門的知識點,可以參考stackoverflow的回答,那就是絕大部分的作業系統的時間精度都在10ms,所以看上去是sleep(0),但由於精度問題,實際上不是睡0ms。另外一個,不加Thread.sleep(0)
就不會產生死鎖的原因在於,執行緒執行這兩個簡單的方法的過程其實是非常短暫的,所以鎖之間根本來不及相互競爭。
2.2 如何排查及預防死鎖?
- 使用
jsp
命令檢視哪些程式正在執行: - 找到執行的類對應的程式編號,顯示詳細的呼叫棧資訊,匯出到記事本中:
jstack 17424 > ~/Desktop/1.txt
複製程式碼
可以看到日誌中,有詳細的死鎖資訊。
- 預防死鎖的原則就是:保證每個執行緒都以相同的順序拿到資源的鎖。像這個例子中兩個執行緒拿到鎖的順序就相互矛盾,所以容易產生死鎖。
3.三種方式實現生產者,消費者模型
3.1 使用wait(),notify()實現
public class ProducerConsumer1 {
private static final Object lock = new Object();
private static Optional<Integer> optional = Optional.empty();
public static void main(String[] args) throws InterruptedException {
Container container = new Container(optional);
Producer producer = new Producer(container);
Consumer consumer = new Consumer(container);
producer.start();
consumer.start();
producer.join();
producer.join();
}
public static class Producer extends Thread {
Container container;
public Producer(Container container) {
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (lock) {
while (container.getOptional().isPresent()) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Integer integer = new Random().nextInt();
System.out.println("Product:" + integer);
container.setOptional(Optional.of(integer));
lock.notify();
}
}
}
}
public static class Consumer extends Thread {
Container container;
public Consumer(Container container) {
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (lock) {
while (!container.getOptional().isPresent()) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Consume:" + container.getOptional().get());
container.setOptional(Optional.empty());
lock.notify();
}
}
}
}
}
複製程式碼
雖然看起來程式碼挺多的,但其實就是一個思想,就是生產者在生產之前,先檢查容器中是否為空,如果不為空就等待,為空才生產。同樣,消費者也需要在容器中有值的情況下才會消費,否則就等待。
3.2 使用lock,condition實現
借鑑Condition介面中的提示:
* public void put(Object x) throws InterruptedException {
* <b>lock.lock();
* try {</b>
* while (count == items.length)
* <b>notFull.await();</b>
* items[putptr] = x;
* if (++putptr == items.length) putptr = 0;
* ++count;
* <b>notEmpty.signal();</b>
* <b>} finally {
* lock.unlock();
* }</b>
* }
複製程式碼
程式碼如下:
public class ProducerConsumer2 {
private static final Lock lock = new ReentrantLock();
private static final Condition notConsumer = lock.newCondition();
private static final Condition notProduct = lock.newCondition();
private static Optional<Integer> optional = Optional.empty();
public static void main(String[] args) throws InterruptedException {
Container container = new Container(optional);
Producer producer = new Producer(container);
Consumer consumer = new Consumer(container);
producer.start();
consumer.start();
producer.join();
producer.join();
}
public static class Producer extends Thread {
private Container container;
public Producer(Container container) {
this.container = container;
}
@Override
public void run() {
lock.lock();
try {
while (container.getOptional().isPresent()) {
try {
//注意:是await()而不是wait()
notConsumer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int random = new Random().nextInt();
System.out.println("Product" + random);
container.setOptional(Optional.of(random));
notProduct.signal();
} finally {
lock.unlock();
}
}
}
public static class Consumer extends Thread {
private Container container;
public Consumer(Container container) {
this.container = container;
}
@Override
public void run() {
try {
lock.lock();
while (!container.getOptional().isPresent()) {
try {
notProduct.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(container.getOptional().get());
container.setOptional(Optional.empty());
notConsumer.notify();
} finally {
lock.unlock();
}
}
}
}
複製程式碼
與第一種方法類似,這裡增加了Condition條件判斷,並用到了ReentrantLock。
3.3 使用BlockingQueue.take()/put()方法實現
public class ProducerConsumer3 {
private static final BlockingQueue<Integer> queue = new LinkedBlockingDeque<>(1);
private static final BlockingQueue<Integer> signal = new LinkedBlockingDeque<>(1);
public static void main(String[] args) throws InterruptedException {
Producer producer = new Producer(queue,signal);
Consumer consumer = new Consumer(queue,signal);
producer.start();
consumer.start();
producer.join();
producer.join();
}
public static class Producer extends Thread {
BlockingQueue<Integer> queue;
BlockingQueue<Integer> signal;
public Producer(BlockingQueue<Integer> queue,BlockingQueue<Integer> signal) {
this.queue = queue;
this.signal = signal;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
int random = new Random().nextInt();
System.out.println("Product:" + random);
try {
queue.put(random);
signal.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static class Consumer extends Thread {
BlockingQueue<Integer> queue;
BlockingQueue<Integer> signal;
public Consumer(BlockingQueue<Integer> queue,BlockingQueue<Integer> signal) {
this.queue = queue;
this.signal = signal;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
System.out.println("Consumer:" + queue.take());
signal.put(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
複製程式碼
這種方法就比較簡單了,不用判斷容器是否為空,因為BlockingQueue在內部為我們實現了判斷,直接拿來用就可以了。這裡需要注意一點,由於take()與put()方法幾乎是同時執行的,所以需要加一個signal標誌資訊,避免亂序。
Container容器:
public class Container {
Optional<Integer> optional;
public Container(Optional<Integer> optional) {
this.optional = optional;
}
public Optional<Integer> getOptional() {
return optional;
}
public void setOptional(Optional<Integer> optional) {
this.optional = optional;
}
}
複製程式碼