之前寫的都亂糟糟的材彪,現(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)額外的開銷
- 上下文切換
- 線程安全
上下文切換
時(shí)間片是CPU分配給各個(gè)線程的時(shí)間,因?yàn)闀r(shí)間非常短惋戏,所以CPU不斷通過切換線程领追,讓我們覺得多個(gè)線程是同時(shí)執(zhí)行的,時(shí)間片一般是幾十毫秒响逢。而每次切換時(shí)绒窑,需要保存當(dāng)前的狀態(tài)起來(lái),以便能夠進(jìn)行恢復(fù)先前狀態(tài)舔亭,而這個(gè)切換時(shí)非常損耗性能些膨,過于頻繁反而無(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ù),使用了樂觀鎖擅威,可以有效的減少一部分不必要的鎖競(jìng)爭(zhēng)帶來(lái)的上下文切換
- 使用最少線程:避免創(chuàng)建不需要的線程壕探,比如任務(wù)很少,但是創(chuàng)建了很多的線程郊丛,這樣會(huì)造成大量的線程都處于等待狀態(tài)
- 協(xié)程:在單線程里實(shí)現(xiàn)多任務(wù)的調(diào)度李请,并在單線程里維持多個(gè)任務(wù)間的切換
線程安全的問題
多線程編程中最難以把握的就是臨界區(qū)線程安全問題瞧筛,稍微不注意就會(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();
}
}
然后通過jps
查看,找個(gè)這個(gè)類的id
然后通過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í)行過程中乍炉,由于競(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)建蜻展,但是還沒有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()
方法開始運(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í)可以通過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()
方法沒有這個(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ì)離開等待池,并且再次獲得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)就沒有對(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í)行,只不過會(huì)當(dāng)做正常的用戶線程執(zhí)行碰镜。
其他的一些概念
同步和異步
同步和異步通常用來(lái)形容一次方法調(diào)用兢卵。
同步方法調(diào)用一開始,調(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)境下不能并行,只能通過切換時(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)沒有一個(gè)線程可以阻塞其他線程伙判,所有的線程都會(huì)嘗試地往前運(yùn)行象对。
臨界區(qū)
臨界區(qū)用來(lái)表示一種公共資源或者說(shuō)是共享數(shù)據(jù),可以被多個(gè)線程使用宴抚。但是每個(gè)線程使用時(shí)企锌,一旦臨界區(qū)資源被一個(gè)線程占有,那么其他線程必須等待来颤。