1 多執行緒的優缺點
之前寫的都亂糟糟的,現在也需要重新記憶一遍。所以重新整理一下JUC包。
多執行緒及其優缺點
什麼是執行緒
是作業系統能夠進行運算排程的最小單位。它被包含在程式之中,是程式中的實際運作單位。(wiki百科)
建立執行緒的三種方式
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
//1、繼承Thread方式
Thread thread1 = new Thread(){
@Override
public void run() {
System.out.println("thread1 start");
}
};
thread1.start();
//2、實現Runnable介面
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread2 start");
}
});
thread2.start();
//3、實現Callable介面
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "future start";
}
});
try {
String result = future.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
在jdk8之後用lambda表示式轉換一下
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
//1、繼承Thread方式
Thread thread1 = new Thread(() -> System.out.println("thread1 start"));
thread1.start();
//2、實現Runnable介面
Thread thread2 = new Thread(() -> System.out.println("thread2 start"));
thread2.start();
//3、實現Callable介面
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(() -> "future start");
try {
String result = future.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
簡化了一點,但是更多是有點懵,lambda為什麼會簡化方法,->
是怎麼找到對應的方法,下次在研究。
為什麼要用多執行緒
早期的CPU是單核的,為了提升計算能力,將多個計算單元整合到一起。形成了多核CPU。多執行緒就是為了將多核CPU發揮到極致,一邊提高效能。
多執行緒缺點呢
上面說了多執行緒的有點是:為了提高計算效能。那麼一定會提高?
答案是不一定的。有時候多執行緒不一定比單執行緒計算快。引入《java併發程式設計的藝術》上第一個例子
public class ConcurrencyTest {
/** 執行次數 */
private static final long count = 10000l;
public static void main(String[] args) throws InterruptedException {
//併發計算
concurrency();
//單執行緒計算
serial();
}
private static void concurrency() throws InterruptedException {
long start = System.currentTimeMillis();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
System.out.println(a);
}
});
thread.start();
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
thread.join();
long time = System.currentTimeMillis() - start;
System.out.println("concurrency :" + time + "ms,b=" + b);
}
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
System.out.println("serial:" + time + "ms,b=" + b + ",a=" + a);
}
}
結果為
50000
concurrency :22ms,b=-10000
serial:0ms,b=-10000,a=50000
而且多執行緒會帶來額外的開銷
- 上下文切換
- 執行緒安全
上下文切換
時間片是CPU分配給各個執行緒的時間,因為時間非常短,所以CPU不斷通過切換執行緒,讓我們覺得多個執行緒是同時執行的,時間片一般是幾十毫秒。而每次切換時,需要儲存當前的狀態起來,以便能夠進行恢復先前狀態,而這個切換時非常損耗效能,過於頻繁反而無法發揮出多執行緒程式設計的優勢。
減少上下文切換可以採用無鎖併發程式設計,CAS演算法,使用最少的執行緒和使用協程。
- 無鎖併發程式設計:可以參照concurrentHashMap鎖分段的思想,不同的執行緒處理不同段的資料,這樣在多執行緒競爭的條件下,可以減少上下文切換的時間。
- CAS演算法,利用Atomic下使用CAS演算法來更新資料,使用了樂觀鎖,可以有效的減少一部分不必要的鎖競爭帶來的上下文切換
- 使用最少執行緒:避免建立不需要的執行緒,比如任務很少,但是建立了很多的執行緒,這樣會造成大量的執行緒都處於等待狀態
- 協程:在單執行緒裡實現多工的排程,並在單執行緒裡維持多個任務間的切換
執行緒安全的問題
多執行緒程式設計中最難以把握的就是臨界區執行緒安全問題,稍微不注意就會出現死鎖的情況
同樣引入《java併發程式設計的藝術》的一個例子
public class DeadLockDemo {
private static String resource_a = "A";
private static String resource_b = "B";
public static void main(String[] args) {
deadLock();
}
public static void deadLock() {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resource_a) {
System.out.println("get resource a");
try {
Thread.sleep(3000);
synchronized (resource_b) {
System.out.println("get resource b");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resource_b) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("get resource b");
synchronized (resource_a) {
System.out.println("get resource a");
}
}
}
});
threadA.start();
threadB.start();
}
}
然後通過jps
檢視,找個這個類的id
然後通過jstack id
來檢視
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000000016074808 (object 0x00000000e0b89280, a java.lang.String),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0000000016075ca8 (object 0x00000000e0b892b0, a java.lang.String),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1" #11 prio=5 os_prio=0 tid=0x00000000175ba800 nid=0x232c waiting for monitor entry [0x000000001889f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at DeadLockDemo$2.run(DeadLockDemo.java:37)
- waiting to lock <0x00000000e0b89280> (a java.lang.String)
- locked <0x00000000e0b892b0> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"Thread-0" #10 prio=5 os_prio=0 tid=0x00000000175b7800 nid=0x234c waiting for monitor entry [0x000000001861f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at DeadLockDemo$1.run(DeadLockDemo.java:18)
- waiting to lock <0x00000000e0b892b0> (a java.lang.String)
- locked <0x00000000e0b89280> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
兩個執行緒相互等待,仔細看上面的waiting to lock 和locked兩個物件。是相互的。造成死鎖。
造成死鎖的原因和解決方案
死鎖:指兩個或兩個以上的程式在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。
造成死鎖的原因是:
- 因為系統資源不足。
- 程式執行推進的順序不合適。
- 資源分配不當等。
如果系統資源充足,程式的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則
就會因爭奪有限的資源而陷入死鎖。其次,程式執行推進順序與速度不同,也可能產生死鎖。
那麼死鎖的必要條件是:
- 互斥條件:一個資源每次只能被一個程式使用。
- 請求與保持條件:一個程式因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:程式已獲得的資源,在末使用完之前,不能強行剝奪。
- 迴圈等待條件:若干程式之間形成一種頭尾相接的迴圈等待資源關係。
這四個條件是 死鎖的必要條件 ,只要系統發生死鎖,這些條件必然成立,而只要上述條件之
一不滿足,就不會發生死鎖。
執行緒的狀態
執行緒有6種狀態
- NEW:新建,執行緒被構建,但是還沒有start()
- RUNNABLE:執行,java中將就緒和執行統稱為執行中
- BLOCKED:阻塞,執行緒阻塞於鎖
- WAITING:等待,表示執行緒進入等待狀態,需要其他執行緒的特定動作(通知或中斷)
- TIMED_WAITING:帶超時的等待,可以在指定的時間內自動返還
- TERMINATED:終止,表示執行緒已經執行完畢
執行緒建立之後呼叫
start()
方法開始執行。當呼叫
wait(),join()
,LockSupport.lock()
方法執行緒會進入到WAITING
狀態,而同樣的wait(long timeout)
,sleep(long), join(long)
, LockSupport.parkNanos()
, LockSupport.parkUtil()
增加了超時等待的功能,也就是呼叫這些方法後執行緒會進入TIMED_WAITING
狀態,當超時等待時間到達後,執行緒會切換到Runable
的狀態,另外當WAITING
和TIMED _WAITING
狀態時可以通過Object.notify()
,Object.notifyAll()
方法使執行緒轉換到Runable
狀態。當執行緒出現資源競爭時,即等待獲取鎖的時候,執行緒會進入到BLOCKED
阻塞狀態,當執行緒獲取鎖時,執行緒進入到Runable
狀態。執行緒執行結束後,執行緒進入到TERMINATED
狀態,狀態轉換可以說是執行緒的生命週期。注意
當執行緒進入到
synchronized方法
或者synchronized程式碼塊
時,執行緒切換到的是BLOCKED
狀態.而使用
java.util.concurrent.locks
下lock
進行加鎖的時候執行緒切換的是WAITING
或者TIMED_WAITING
狀態,因為lock會呼叫LockSupport的方法。
執行緒狀態的操作
interrupted()
中斷可以理解為執行緒的一個標誌位,它表示了一個執行中的執行緒是否被其他執行緒進行了中斷操作。中斷好比其他執行緒對該執行緒打了一個招呼。
其他執行緒可以呼叫該執行緒的interrupt()方法對其進行中斷操作,同時該執行緒可以呼叫 isInterrupted()來感知其他執行緒對其自身的中斷操作,從而做出響應。
另外,同樣可以呼叫Thread的靜態方法 interrupted()對當前執行緒進行中斷操作,該方法會清除中斷標誌位。
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
//sleepThread睡眠1000ms
final Thread sleepThread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
super.run();
}
};
//busyThread一直執行死迴圈
Thread busyThread = new Thread() {
@Override
public void run() {
while (true) ;
}
};
sleepThread.start();
busyThread.start();
sleepThread.interrupt();
busyThread.interrupt();
while (sleepThread.isInterrupted()) ;
System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
}
}
執行結果是:
對著兩個執行緒進行中斷操作,可以看出sleepThread丟擲InterruptedException後清除標誌位,而busyThread就不會清除標誌位。
join()
join方法可以看做是執行緒間協作的一種方式。
如果一個執行緒例項A執行了threadB.join(),其含義是:當前執行緒A會等待threadB執行緒終止後threadA才會繼續執行。
public class JoinDemo {
public static void main(String[] args) {
Thread previousThread = Thread.currentThread();
for (int i = 1; i <= 5; i++) {
Thread curThread = new JoinThread(previousThread);
curThread.start();
previousThread = curThread;
}
}
static class JoinThread extends Thread {
private Thread thread;
public JoinThread(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
//join
thread.join();
System.out.println(thread.getName() + " terminated.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
如果註釋了上面的thread.join();
每個執行緒都會等待前一個執行緒結束才會繼續執行。
sleep() VS wait()
兩者主要的區別:
- sleep()方法是Thread的靜態方法,而wait是Object例項方法
-
wait()
方法必須要在同步方法或者同步塊中呼叫,也就是必須已經獲得物件鎖。而sleep()
方法沒有這個限制可以在任何地方種使用。另外,wait()
方法會釋放佔有的物件鎖,使得該執行緒進入等待池中,等待下一次獲取資源。而sleep()
方法只是會讓出CPU並不會釋放掉物件鎖; -
sleep()
方法在休眠時間達到後如果再次獲得CPU時間片就會繼續執行,而wait()
方法必須等待Object.notift/Object.notifyAll
通知後,才會離開等待池,並且再次獲得CPU時間片才會繼續執行。
守護執行緒Daemon
守護執行緒是一種特殊的執行緒,就和它的名字一樣,它是系統的守護者,在後臺默默地守護一些系統服務,比如垃圾回收執行緒,JIT執行緒就可以理解守護執行緒。與之對應的就是使用者執行緒,使用者執行緒就可以認為是系統的工作執行緒,它會完成整個系統的業務操作。使用者執行緒完全結束後就意味著整個系統的業務任務全部結束了,因此係統就沒有物件需要守護的了,守護執行緒自然而然就會退。當一個Java應用,只有守護執行緒的時候,虛擬機器就會自然退出。下面以一個簡單的例子來表述Daemon執行緒的使用。
public class DaemonDemo {
public static void main(String[] args) {
Thread daemonThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
System.out.println("i am alive");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("finally block");
}
}
}
});
//設定為守護執行緒
daemonThread.setDaemon(true);
daemonThread.start();
//確保main執行緒結束前能給daemonThread能夠分到時間片
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
執行結果:
守護執行緒應該先於
start()
方法之前。如果在之後,但是該執行緒還是會執行,只不過會當做正常的使用者執行緒執行。
其他的一些概念
同步和非同步
同步和非同步通常用來形容一次方法呼叫。
同步方法呼叫一開始,呼叫者必須等待被呼叫的方法結束後,呼叫者後面的程式碼才能執行。
而非同步呼叫,指的是,呼叫者不用管被呼叫方法是否完成,都會繼續執行後面的程式碼,當被呼叫的方法完成後會通知呼叫者。
併發與並行
併發指的是多個任務交替進行,而並行則是指真正意義上的“同時進行”。
實際上,如果系統內只有一個CPU,而使用多執行緒時,那麼真實系統環境下不能並行,只能通過切換時間片的方式交替進行,而成為併發執行任務。真正的並行也只能出現在擁有多個CPU的系統中。
阻塞和非阻塞
阻塞和非阻塞通常用來形容多執行緒間的相互影響。
比如一個執行緒佔有了臨界區資源,那麼其他執行緒需要這個資源就必須進行等待該資源的釋放,會導致等待的執行緒掛起,這種情況就是阻塞。
而非阻塞就恰好相反,它強調沒有一個執行緒可以阻塞其他執行緒,所有的執行緒都會嘗試地往前執行。
臨界區
臨界區用來表示一種公共資源或者說是共享資料,可以被多個執行緒使用。但是每個執行緒使用時,一旦臨界區資源被一個執行緒佔有,那麼其他執行緒必須等待。
相關文章
- python多執行緒的優缺點總結Python執行緒
- 執行緒和程式的優缺點執行緒
- Linux系統中多程式和多執行緒的優缺點及聯絡!Linux執行緒
- C#執行緒使用的20種方式和優缺點C#執行緒
- 多執行緒系列(1),多執行緒基礎執行緒
- 多執行緒考點執行緒
- 多執行緒核心技術(1)-執行緒的基本方法執行緒
- 【java】【多執行緒】程式、執行緒的基本概念(1)Java執行緒
- 執行緒和程式有什麼優缺點?Linux運維學習執行緒Linux運維
- Java多執行緒學習(1)建立執行緒與執行緒的生命週期Java執行緒
- Java多執行緒-1(3)Java執行緒
- 什麼是多執行緒?Python多執行緒有什麼優勢?執行緒Python
- C#多執行緒下的調優C#執行緒
- 多執行緒和多執行緒同步執行緒
- “多執行緒”重點概念整理執行緒
- JAVA重點類 多執行緒Java執行緒
- 多執行緒面試題1執行緒面試題
- 執行緒以及多執行緒,多程式的選擇執行緒
- 多執行緒--執行緒管理執行緒
- 執行緒與多執行緒執行緒
- 多執行緒【執行緒池】執行緒
- Java 多執行緒基礎(十一)執行緒優先順序和守護執行緒Java執行緒
- 多執行緒基礎必要知識點!看了學習多執行緒事半功倍執行緒
- 多執行緒(三)、執行緒池 ThreadPoolExecutor 知識點總結執行緒thread
- 1、多執行緒同步——CPU、core核、執行緒、記憶體執行緒記憶體
- Java併發 之 執行緒池系列 (1) 讓多執行緒不再坑爹的執行緒池Java執行緒
- MySQL多執行緒併發調優MySql執行緒
- 多執行緒(五)---執行緒的Yield方法執行緒
- 【Java多執行緒】執行緒安全的集合Java執行緒
- Java多執行緒-執行緒池的使用Java執行緒
- 【java】【多執行緒】睡眠/守護/加入/禮讓執行緒,執行緒優先順序(4)Java執行緒
- Java多執行緒-執行緒中止Java執行緒
- 多執行緒之初識執行緒執行緒
- java多執行緒那點事兒Java執行緒
- 多執行緒斷點下載原理執行緒斷點
- 多執行緒那點事—Parallel.for執行緒Parallel
- 多執行緒------執行緒與程式/執行緒排程/建立執行緒執行緒
- Android中的多執行緒斷點續傳Android執行緒斷點