之前寫(xiě)的都亂糟糟的,現(xiàn)在也需要重新記憶一遍。所以重新整理一下JUC包。
多線程及其優(yōu)缺點(diǎn)
什么是線程
是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。它被包含在進(jìn)程之中,是進(jìn)程中的實(shí)際運(yùn)作單位。(wiki百科)
創(chuàng)建線程的三種方式
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、實(shí)現(xiàn)Runnable接口
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread2 start");
}
});
thread2.start();
//3、實(shí)現(xiàn)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表達(dá)式轉(zhuǎn)換一下
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、實(shí)現(xiàn)Runnable接口
Thread thread2 = new Thread(() -> System.out.println("thread2 start"));
thread2.start();
//3、實(shí)現(xiàn)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();
}
}
}
簡(jiǎn)化了一點(diǎn),但是更多是有點(diǎn)懵,lambda為什么會(huì)簡(jiǎn)化方法,->是怎么找到對(duì)應(yīng)的方法,下次在研究。
為什么要用多線程
早期的CPU是單核的,為了提升計(jì)算能力,將多個(gè)計(jì)算單元整合到一起。形成了多核CPU。多線程就是為了將多核CPU發(fā)揮到極致,一邊提高性能。
多線程缺點(diǎn)呢
上面說(shuō)了多線程的有點(diǎn)是:為了提高計(jì)算性能。那么一定會(huì)提高?
答案是不一定的。有時(shí)候多線程不一定比單線程計(jì)算快。引入《java并發(fā)編程的藝術(shù)》上第一個(gè)例子
public class ConcurrencyTest {
/** 執(zhí)行次數(shù) */
private static final long count = 10000l;
public static void main(String[] args) throws InterruptedException {
//并發(fā)計(jì)算
concurrency();
//單線程計(jì)算
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);
}
}
結(jié)果為
50000
concurrency :22ms,b=-10000
serial:0ms,b=-10000,a=50000
而且多線程會(huì)帶來(lái)額外的開(kāi)銷
- 上下文切換
- 線程安全
上下文切換
時(shí)間片是CPU分配給各個(gè)線程的時(shí)間,因?yàn)闀r(shí)間非常短,所以CPU不斷通過(guò)切換線程,讓我們覺(jué)得多個(gè)線程是同時(shí)執(zhí)行的,時(shí)間片一般是幾十毫秒。而每次切換時(shí),需要保存當(dāng)前的狀態(tài)起來(lái),以便能夠進(jìn)行恢復(fù)先前狀態(tài),而這個(gè)切換時(shí)非常損耗性能,過(guò)于頻繁反而無(wú)法發(fā)揮出多線程編程的優(yōu)勢(shì)。
減少上下文切換可以采用無(wú)鎖并發(fā)編程,CAS算法,使用最少的線程和使用協(xié)程。
- 無(wú)鎖并發(fā)編程:可以參照concurrentHashMap鎖分段的思想,不同的線程處理不同段的數(shù)據(jù),這樣在多線程競(jìng)爭(zhēng)的條件下,可以減少上下文切換的時(shí)間。
- CAS算法,利用Atomic下使用CAS算法來(lái)更新數(shù)據(jù),使用了樂(lè)觀鎖,可以有效的減少一部分不必要的鎖競(jìng)爭(zhēng)帶來(lái)的上下文切換
- 使用最少線程:避免創(chuàng)建不需要的線程,比如任務(wù)很少,但是創(chuàng)建了很多的線程,這樣會(huì)造成大量的線程都處于等待狀態(tài)
- 協(xié)程:在單線程里實(shí)現(xiàn)多任務(wù)的調(diào)度,并在單線程里維持多個(gè)任務(wù)間的切換
線程安全的問(wèn)題
多線程編程中最難以把握的就是臨界區(qū)線程安全問(wèn)題,稍微不注意就會(huì)出現(xiàn)死鎖的情況
同樣引入《java并發(fā)編程的藝術(shù)》的一個(gè)例子
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();
}
}
然后通過(guò)jps查看,找個(gè)這個(gè)類的id
然后通過(guò)jstack id來(lái)查看
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)
兩個(gè)線程相互等待,仔細(xì)看上面的waiting to lock 和locked兩個(gè)對(duì)象。是相互的。造成死鎖。
造成死鎖的原因和解決方案
死鎖:指兩個(gè)或兩個(gè)以上的進(jìn)程在執(zhí)行過(guò)程中,由于競(jìng)爭(zhēng)資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象,若無(wú)外力作用,它們都將無(wú)法推進(jìn)下去。
造成死鎖的原因是:
- 因?yàn)橄到y(tǒng)資源不足。
- 進(jìn)程運(yùn)行推進(jìn)的順序不合適。
- 資源分配不當(dāng)?shù)取?/li>
如果系統(tǒng)資源充足,進(jìn)程的資源請(qǐng)求都能夠得到滿足,死鎖出現(xiàn)的可能性就很低,否則
就會(huì)因爭(zhēng)奪有限的資源而陷入死鎖。其次,進(jìn)程運(yùn)行推進(jìn)順序與速度不同,也可能產(chǎn)生死鎖。
那么死鎖的必要條件是:
- 互斥條件:一個(gè)資源每次只能被一個(gè)進(jìn)程使用。
- 請(qǐng)求與保持條件:一個(gè)進(jìn)程因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放。
- 不剝奪條件:進(jìn)程已獲得的資源,在末使用完之前,不能強(qiáng)行剝奪。
- 循環(huán)等待條件:若干進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。
這四個(gè)條件是 死鎖的必要條件 ,只要系統(tǒng)發(fā)生死鎖,這些條件必然成立,而只要上述條件之
一不滿足,就不會(huì)發(fā)生死鎖。
線程的狀態(tài)
線程有6種狀態(tài)
- NEW:新建,線程被構(gòu)建,但是還沒(méi)有start()
- RUNNABLE:運(yùn)行,java中將就緒和運(yùn)行統(tǒng)稱為運(yùn)行中
- BLOCKED:阻塞,線程阻塞于鎖
- WAITING:等待,表示線程進(jìn)入等待狀態(tài),需要其他線程的特定動(dòng)作(通知或中斷)
- TIMED_WAITING:帶超時(shí)的等待,可以在指定的時(shí)間內(nèi)自動(dòng)返還
- TERMINATED:終止,表示線程已經(jīng)執(zhí)行完畢

線程創(chuàng)建之后調(diào)用
start()方法開(kāi)始運(yùn)行。當(dāng)調(diào)用
wait(),join(),LockSupport.lock()方法線程會(huì)進(jìn)入到WAITING狀態(tài),而同樣的wait(long timeout),sleep(long), join(long), LockSupport.parkNanos(), LockSupport.parkUtil()增加了超時(shí)等待的功能,也就是調(diào)用這些方法后線程會(huì)進(jìn)入TIMED_WAITING狀態(tài),當(dāng)超時(shí)等待時(shí)間到達(dá)后,線程會(huì)切換到Runable的狀態(tài),另外當(dāng)WAITING和TIMED _WAITING狀態(tài)時(shí)可以通過(guò)Object.notify(),Object.notifyAll()方法使線程轉(zhuǎn)換到Runable狀態(tài)。當(dāng)線程出現(xiàn)資源競(jìng)爭(zhēng)時(shí),即等待獲取鎖的時(shí)候,線程會(huì)進(jìn)入到BLOCKED阻塞狀態(tài),當(dāng)線程獲取鎖時(shí),線程進(jìn)入到Runable狀態(tài)。線程運(yùn)行結(jié)束后,線程進(jìn)入到TERMINATED狀態(tài),狀態(tài)轉(zhuǎn)換可以說(shuō)是線程的生命周期。注意
當(dāng)線程進(jìn)入到
synchronized方法或者synchronized代碼塊時(shí),線程切換到的是BLOCKED狀態(tài).而使用
java.util.concurrent.locks下lock進(jìn)行加鎖的時(shí)候線程切換的是WAITING或者TIMED_WAITING狀態(tài),因?yàn)閘ock會(huì)調(diào)用LockSupport的方法。
線程狀態(tài)的操作
interrupted()
中斷可以理解為線程的一個(gè)標(biāo)志位,它表示了一個(gè)運(yùn)行中的線程是否被其他線程進(jìn)行了中斷操作。中斷好比其他線程對(duì)該線程打了一個(gè)招呼。
其他線程可以調(diào)用該線程的interrupt()方法對(duì)其進(jìn)行中斷操作,同時(shí)該線程可以調(diào)用 isInterrupted()來(lái)感知其他線程對(duì)其自身的中斷操作,從而做出響應(yīng)。
另外,同樣可以調(diào)用Thread的靜態(tài)方法 interrupted()對(duì)當(dāng)前線程進(jìn)行中斷操作,該方法會(huì)清除中斷標(biāo)志位。
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一直執(zhí)行死循環(huán)
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());
}
}
運(yùn)行結(jié)果是:

對(duì)著兩個(gè)線程進(jìn)行中斷操作,可以看出sleepThread拋出InterruptedException后清除標(biāo)志位,而busyThread就不會(huì)清除標(biāo)志位。
join()
join方法可以看做是線程間協(xié)作的一種方式。
如果一個(gè)線程實(shí)例A執(zhí)行了threadB.join(),其含義是:當(dāng)前線程A會(huì)等待threadB線程終止后threadA才會(huì)繼續(xù)執(zhí)行。
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();

每個(gè)線程都會(huì)等待前一個(gè)線程結(jié)束才會(huì)繼續(xù)運(yùn)行。
sleep() VS wait()
兩者主要的區(qū)別:
- sleep()方法是Thread的靜態(tài)方法,而wait是Object實(shí)例方法
-
wait()方法必須要在同步方法或者同步塊中調(diào)用,也就是必須已經(jīng)獲得對(duì)象鎖。而sleep()方法沒(méi)有這個(gè)限制可以在任何地方種使用。另外,wait()方法會(huì)釋放占有的對(duì)象鎖,使得該線程進(jìn)入等待池中,等待下一次獲取資源。而sleep()方法只是會(huì)讓出CPU并不會(huì)釋放掉對(duì)象鎖; -
sleep()方法在休眠時(shí)間達(dá)到后如果再次獲得CPU時(shí)間片就會(huì)繼續(xù)執(zhí)行,而wait()方法必須等待Object.notift/Object.notifyAll通知后,才會(huì)離開(kāi)等待池,并且再次獲得CPU時(shí)間片才會(huì)繼續(xù)執(zhí)行。
守護(hù)線程Daemon
守護(hù)線程是一種特殊的線程,就和它的名字一樣,它是系統(tǒng)的守護(hù)者,在后臺(tái)默默地守護(hù)一些系統(tǒng)服務(wù),比如垃圾回收線程,JIT線程就可以理解守護(hù)線程。與之對(duì)應(yīng)的就是用戶線程,用戶線程就可以認(rèn)為是系統(tǒng)的工作線程,它會(huì)完成整個(gè)系統(tǒng)的業(yè)務(wù)操作。用戶線程完全結(jié)束后就意味著整個(gè)系統(tǒng)的業(yè)務(wù)任務(wù)全部結(jié)束了,因此系統(tǒng)就沒(méi)有對(duì)象需要守護(hù)的了,守護(hù)線程自然而然就會(huì)退。當(dāng)一個(gè)Java應(yīng)用,只有守護(hù)線程的時(shí)候,虛擬機(jī)就會(huì)自然退出。下面以一個(gè)簡(jiǎn)單的例子來(lái)表述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");
}
}
}
});
//設(shè)置為守護(hù)線程
daemonThread.setDaemon(true);
daemonThread.start();
//確保main線程結(jié)束前能給daemonThread能夠分到時(shí)間片
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
運(yùn)行結(jié)果:

守護(hù)線程應(yīng)該先于
start()方法之前。如果在之后,但是該線程還是會(huì)執(zhí)行,只不過(guò)會(huì)當(dāng)做正常的用戶線程執(zhí)行。
其他的一些概念
同步和異步
同步和異步通常用來(lái)形容一次方法調(diào)用。
同步方法調(diào)用一開(kāi)始,調(diào)用者必須等待被調(diào)用的方法結(jié)束后,調(diào)用者后面的代碼才能執(zhí)行。
而異步調(diào)用,指的是,調(diào)用者不用管被調(diào)用方法是否完成,都會(huì)繼續(xù)執(zhí)行后面的代碼,當(dāng)被調(diào)用的方法完成后會(huì)通知調(diào)用者。
并發(fā)與并行
并發(fā)指的是多個(gè)任務(wù)交替進(jìn)行,而并行則是指真正意義上的“同時(shí)進(jìn)行”。
實(shí)際上,如果系統(tǒng)內(nèi)只有一個(gè)CPU,而使用多線程時(shí),那么真實(shí)系統(tǒng)環(huán)境下不能并行,只能通過(guò)切換時(shí)間片的方式交替進(jìn)行,而成為并發(fā)執(zhí)行任務(wù)。真正的并行也只能出現(xiàn)在擁有多個(gè)CPU的系統(tǒng)中。
阻塞和非阻塞
阻塞和非阻塞通常用來(lái)形容多線程間的相互影響。
比如一個(gè)線程占有了臨界區(qū)資源,那么其他線程需要這個(gè)資源就必須進(jìn)行等待該資源的釋放,會(huì)導(dǎo)致等待的線程掛起,這種情況就是阻塞。
而非阻塞就恰好相反,它強(qiáng)調(diào)沒(méi)有一個(gè)線程可以阻塞其他線程,所有的線程都會(huì)嘗試地往前運(yùn)行。
臨界區(qū)
臨界區(qū)用來(lái)表示一種公共資源或者說(shuō)是共享數(shù)據(jù),可以被多個(gè)線程使用。但是每個(gè)線程使用時(shí),一旦臨界區(qū)資源被一個(gè)線程占有,那么其他線程必須等待。