Java多線程
從本篇開始丙者,筆者開始了一個新的專題复斥,來說說Java多線程。
在講解Java多線程之前械媒,我們來了解下進程和線程的概念D慷А!@哪侣集!
進程
進程的概念,是60年代初首先由麻省理工學院的MULTICS系統(tǒng)和IBM公司的CTSS/360系統(tǒng)引入的兰绣。
對于操作系統(tǒng)來說世分,進程是最核心的概念,操作系統(tǒng)實現(xiàn)并發(fā)的基礎缀辩。進程是一個動態(tài)的過程臭埋,存在生命周期踪央,可以申請和擁有系統(tǒng)資源,是一個程序的執(zhí)行過程瓢阴,是一個活動的實體畅蹂。
程序作為一種軟件資料長期存在,指令和數(shù)據(jù)的有序集合荣恐,其本身沒有任何運行的含義液斜,是一個靜態(tài)的概念。而進程是有一定生命期的叠穆,程序在處理機上的一次執(zhí)行過程少漆,它是一個動態(tài)的概念,也就是說:程序是永久的硼被,進程是暫時的示损。
簡單的理解:進程是正在運行程序的實例。
我們知道嚷硫,進程是一個實體检访,它擁有自己的內(nèi)存空間,包含了文本區(qū)域(代碼)仔掸、數(shù)據(jù)區(qū)域(變量信息)和堆棧信息(調(diào)用指令)脆贵。此外,程序是沒有生命周期的起暮,只有當處理器賦予程序生命時丹禀,它便成為了一個活動的實體,即成為了一個進程鞋怀。
在不同操作系統(tǒng)下,進程的圖形化展示:
線程
線程持搜,也被稱為輕量級進程(Lightweight Process密似,LWP),是程序執(zhí)行流的最小單元葫盼。一個標準的線程由線程ID残腌,當前指令指針(PC),寄存器集合和堆棧組成贫导。
當我們運行一個程序時抛猫,系統(tǒng)會為我們創(chuàng)建一個進程,在實際運行過程中孩灯,進程會創(chuàng)建一個個線程闺金,以來實現(xiàn)程序不同的功能。
通常在一個進程中會包含若干個線程峰档,它們可以利用進程所擁有的資源败匹,但是其本身并不擁有系統(tǒng)資源寨昙。
在我們常見的操作系統(tǒng)中,進程是資源分配的基本單位掀亩,而把線程是獨立運行和獨立調(diào)度的基本單位舔哪。直白點,就是說操作系統(tǒng)給進程分配系統(tǒng)內(nèi)存槽棍、CPU等核心資源捉蚤,而進程來實現(xiàn)程序種的功能。
由于線程比進程更小炼七,不占用系統(tǒng)資源缆巧,對線程的調(diào)度所付出的開銷要小得多,所以能更高效的提高系統(tǒng)中多個程序間并發(fā)程度特石,從而顯著提高系統(tǒng)資源的利用率和吞吐量盅蝗。
多線程
在一個進程中,同時運行多個線程來完成不同的工作姆蘸,就稱為多線程墩莫。
多線程的存在,是為了同時完成多項任務逞敷,提高資源使用效率狂秦。
在Java中,一個Java程序的啟動推捐,意味著虛擬機這個進程的啟動裂问,當我們執(zhí)行一個main()方法時,實際上啟動了一個叫做main-thread的線程牛柒,這個線程就來實現(xiàn)我們所需要的功能堪簿、邏輯。
接下來皮壁,我們就來介紹下在Java中椭更,多線程的實現(xiàn)。
線程創(chuàng)建
在Java中蛾魄,創(chuàng)建創(chuàng)建有兩種方法:
繼承 Thread 類創(chuàng)建線程虑瀑;
實現(xiàn) Runnable 接口類創(chuàng)建線程;
(1)繼承Thread類
public class ThreadTest1 extends Thread{
@Override
public void run() {
System.out.println("新啟線程為:"+Thread.currentThread().getName());
}
public static void main(String[] agrs){
ThreadTest1 threadTest11 = new ThreadTest1();
ThreadTest1 threadTest12 = new ThreadTest1();
threadTest11.start();
threadTest12.start();
System.out.println("main線程為:"+Thread.currentThread().getName());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
測試結(jié)果如下:
main線程為:main
新啟線程為:Thread-0
新啟線程為:Thread-1
繼承Thread類滴须,需要重寫Thread中的run()方法舌狗,在run()方法中實現(xiàn)具體邏輯。在main()方法中扔水,創(chuàng)建線程對象痛侍,調(diào)用start()方法來啟動線程,之后會執(zhí)行run()方法中的邏輯魔市。此外恋日,我們還可以通過調(diào)用Thread的getName()方法膀篮,來獲取到線程的名稱。
(2)實現(xiàn)Runnable接口
public class ThreadTest2 implements Runnable{
@Override
public void run() {
System.out.println("新啟線程為:"+Thread.currentThread().toString());
}
public static void main(String[] agrs){
for(int x=0;x<10;x++){
new Thread(new ThreadTest2()).start();
}
System.out.println("main線程為:"+Thread.currentThread().toString());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
測試結(jié)果如下:
main線程為:Thread[main,5,main]
新啟線程為:Thread[Thread-0,5,main]
新啟線程為:Thread[Thread-1,5,main]
新啟線程為:Thread[Thread-2,5,main]
新啟線程為:Thread[Thread-3,5,main]
新啟線程為:Thread[Thread-4,5,main]
新啟線程為:Thread[Thread-6,5,main]
新啟線程為:Thread[Thread-9,5,main]
新啟線程為:Thread[Thread-5,5,main]
新啟線程為:Thread[Thread-8,5,main]
新啟線程為:Thread[Thread-7,5,main]
實現(xiàn)Runnable接口岂膳,需要實現(xiàn)接口中的run()方法誓竿。與繼承Thread不同的是,在創(chuàng)建線程對象時需要借助Thread的構(gòu)造方法谈截,再調(diào)用start()方法來完成啟動線程筷屡。
在run()方法中,我們調(diào)用了Thread的toString()方法簸喂,該方法返回結(jié)果包括:線程的名稱毙死,線程的優(yōu)先級,線程組的名稱喻鳄;
通常扼倘,我們都是使用實現(xiàn)Runnable接口的方式來完成線程的創(chuàng)建和啟動。對于繼承Thread來說除呵,該方式實現(xiàn)起來編碼更簡單再菊,在run()方法內(nèi)部即可調(diào)用Thread類的方法;而實現(xiàn)Runnable接口方式颜曾,則極大避免了Java單繼承的局限纠拔。
從上面的兩個例子中可以看出,無論是繼承Thread泛豪、還是實現(xiàn)Runnable接口的方式稠诲,本質(zhì)上來說都離不開Thread類。
下面诡曙,我們來具體看下Thread類中有哪些主要方法:
線程方法
方法 | 方法描述 |
---|---|
public void start() | 使該線程開始執(zhí)行臀叙;Java 虛擬機調(diào)用該線程的 run 方法 |
public void run() | 虛擬機執(zhí)行線程調(diào)用的方法 |
public final void setName(String name) | 改變線程名稱 |
public final void setPriority(int priority) | 更改線程的優(yōu)先級 |
public final void setDaemon(boolean on) | 將該線程標記為守護線程 |
public final void join() | 當我們調(diào)用某個線程的這個方法時,這個方法會掛起調(diào)用線程价卤,直到被調(diào)用線程結(jié)束執(zhí)行匹耕,調(diào)用線程才會繼續(xù)執(zhí)行 |
public void interrupt() | 中斷線程,給在執(zhí)行的線程一個中斷信號荠雕,并不是停止線程的運行 |
public final boolean isAlive() | 測試線程是否處于活動狀態(tài) |
public static void yield() | 暫停當前正在執(zhí)行的線程對象,并執(zhí)行其他線程(也可能是自己) |
public static void sleep(long millisec) | 在指定的毫秒數(shù)內(nèi)讓當前正在執(zhí)行的線程休眠(暫停執(zhí)行 |
public static Thread currentThread() | 返回對當前正在執(zhí)行的線程對象的引用 |
(1)調(diào)整線程優(yōu)先級--setPriority(int priority)
public class ThreadTest3 implements Runnable{
@Override
public void run() {
System.out.println("新啟線程的優(yōu)先級為:"+Thread.currentThread().getPriority());
}
public static void main(String[] agrs){
//設置main的線程優(yōu)先級:
Thread.currentThread().setPriority(10);
System.out.println("設置main線程的優(yōu)先級為:"+Thread.currentThread().getPriority());
for(int x=0;x<10;x++){
Thread thread = new Thread(new ThreadTest3());
if(x%2==0){
thread.setPriority(7);
}
thread.start();
}
System.out.println("main線程為:"+Thread.currentThread().toString());
}
}
在上面的例子中驶赏,我們通過setPriority(int priority)來設置線程的優(yōu)先級炸卑,優(yōu)先級高的線程優(yōu)先執(zhí)行。
Java線程的優(yōu)先級取值范圍是1~~10煤傍,Thread類中有下面三個靜態(tài)常量:
static int MAX_PRIORITY:線程可以具有的最高優(yōu)先級盖文,取值為10
static int MIN_PRIORITY:線程可以具有的最低優(yōu)先級,取值為1
static int NORM_PRIORITY:分配給線程的默認優(yōu)先級蚯姆,取值為5
在Java線程中五续,每個線程都有默認的優(yōu)先級洒敏,默認為5.
此外,Java線程的優(yōu)先級還有繼承關系疙驾,例如上面的例子中凶伙,我們首先設置了main線程的優(yōu)先級,當我們在main中啟動別的線程時它碎,如果沒有對新啟動的線程指定優(yōu)先級函荣,那么新啟動的線程繼承main線程的優(yōu)先級。
(2)線程睡眠--sleep(long millisec)
public class ThreadTest4 implements Runnable{
@Override
public void run() {
System.out.println("新啟線程:"+Thread.currentThread().toString());
try {
Thread.sleep(100);
System.out.println("新啟線程停止500毫秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] agrs){
for(int x=0;x<10;x++){
Thread thread = new Thread(new ThreadTest4());
thread.start();
}
System.out.println("main線程為:"+Thread.currentThread().toString());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使正在運行的Java線程轉(zhuǎn)到阻塞狀態(tài)扳肛,millisec參數(shù)設定的是線程的睡眠時間傻挂,以毫秒為單位,sleep(long millisec)是靜態(tài)方法挖息,只能控制當前正在運行的線程金拒。
當Java線程在睡眠結(jié)束后,便會轉(zhuǎn)為就緒(Runnable)狀態(tài)套腹。
(關于線程的狀態(tài)绪抛,在下一小節(jié)介紹)
值得注意的是,一個線程執(zhí)行了sleep操作沉迹,如果這個線程獲取到鎖睦疫,那么sleep并不會讓出鎖。
前面說了鞭呕,一個線程在睡眠結(jié)束后蛤育,便會轉(zhuǎn)為就緒狀態(tài),并不會立刻執(zhí)行葫松,需要等待CPU的調(diào)度瓦糕,那么sleep中指定的時間就是線程休眠的最短時間。
(3)父線程等待子線程結(jié)束之后再運行--join()
public class ThreadTest5 implements Runnable{
@Override
public void run() {
System.out.println("新啟線程:"+Thread.currentThread().toString());
}
public static void main(String[] agrs){
List<Thread> list = new ArrayList<Thread>();
for(int x=0;x<10;x++){
Thread thread = new Thread(new ThreadTest5());
list.add(thread);
thread.start();
}
for(Thread thread:list){
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main繼續(xù)做事腋么,main線程為:"+Thread.currentThread().toString());
}
}
等待線程執(zhí)行完成后咕娄,再繼續(xù)執(zhí)行,這就是join存在的意義珊擂。當我們在線程A中圣勒,調(diào)用了線程B的join()方法,那么線程A會停下摧扇,被阻塞圣贸,但是不會釋放鎖(這一點跟sleep一樣),等待線程B執(zhí)行完成扛稽,此時線程A又恢復到了就緒狀態(tài)(不是立即執(zhí)行吁峻,這一點跟sleep也一樣)。
我們鎖了,join()會阻塞線程的執(zhí)行用含,那么為什么呢矮慕?我們來看看源碼:
//無參數(shù)的 join():
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
//如果等于0,則一直執(zhí)行while循環(huán)啄骇,循環(huán)體中調(diào)用wait()方法
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
//如果不為0痴鳄,則計算阻塞的時間:
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
通過源碼,我們發(fā)現(xiàn)肠缔,join內(nèi)部其實是由于wait()方法實現(xiàn)夏跷。當我們調(diào)用無參數(shù)的join()方法時,線程會一直執(zhí)行while循環(huán)明未,探測實現(xiàn)是否還存活槽华,存活就wait(0),就這樣一直在while循環(huán)中做判斷趟妥,當線程執(zhí)行結(jié)束后猫态,isAlive()返回false,while循環(huán)結(jié)束披摄。
(4)暫停當前正在執(zhí)行的線程亲雪,并執(zhí)行其他線程--yield()
public class ThreadTest8 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("新啟動線程:" + i);
Thread.yield();
}
}
public static void main(String[] agrs){
new Thread(new ThreadTest8()).start();
System.out.println("新線程啟動了");
for (int i = 0; i < 10; i++) {
System.out.println("main線程:" + i);
Thread.yield();
}
}
}
暫停當前正在執(zhí)行的線程,執(zhí)行其他線程疚膊。對于其他線程义辕,這里面包含了兩種含義。
Thread.yield()執(zhí)行后寓盗,允許其他線程獲得運行機會灌砖。因此,使用yield()讓多個線程之間能適當?shù)妮嗈D(zhuǎn)執(zhí)行傀蚌。
但是基显,測試結(jié)果來看無法完全保證Thread.yield()的目的,執(zhí)行Thread.yield()的線程有可能被線程調(diào)度程序再次選中善炫,也就是說自己被認為了其他線程撩幽。
值得注意的是,Thread.yield()是將線程從運行狀態(tài)轉(zhuǎn)為了就緒狀態(tài)箩艺,并沒有阻塞線程窜醉。
(5)中斷線程--interrupt()
public class ThreadTest6 implements Runnable{
@Override
public void run() {
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println("我被中斷了");
}else{
System.out.println("我一直在運行");
}
}
}
public static void main(String[] agrs){
Thread thread = new Thread(new ThreadTest6());
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
interrupt()方法通過修改了被調(diào)用線程的中斷狀態(tài)來告知那個線程, 告訴它自己已經(jīng)被中斷了,這里指的中斷是一個中斷信號艺谆,不是說將被調(diào)用的線程給干掉了榨惰,不要理解錯了。
我們可以通過調(diào)用線程的isInterrupted()方法擂涛,來獲取線程的中斷狀態(tài),通過此狀態(tài)來判斷線程中的執(zhí)行邏輯。
對于非阻塞線程來說(例如上面的例子)撒妈,調(diào)用interrupt()方法后恢暖,只是修改了線程的中斷狀態(tài),isInterrupted()返回true狰右。
但是對于阻塞線程來說杰捂,就不同了。
回想下棋蚌,當我們在程序中調(diào)用Thread.sleep()嫁佳、Object.wait()、Thread.join()時谷暮,會拋出一個叫InterruptedException的異常蒿往,看這個異常的命名,是不是跟現(xiàn)在我們所講的interrupt()方法類似湿弦。
沒錯瓤漏,對于阻塞線程來說,當我們執(zhí)行interrupt()方法后颊埃,被阻塞的線程會拋出InterruptedException異常蔬充,并且將線程中斷狀態(tài)置為true。至于班利,對異常的處理就因業(yè)務需求而已了饥漫。
public class ThreadTest6 implements Runnable{
@Override
public void run() {
while(true){
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("終于明白sleep會拋出異常了捕獲異常了");
}
}
}
public static void main(String[] agrs){
Thread thread = new Thread(new ThreadTest6());
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
(6)設置守護線程--setDaemon()
public class ThreadTest7 implements Runnable{
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("守護線程中,我會執(zhí)行嗎罗标?");
}
}
public static void main(String[] agrs){
Thread thread = new Thread(new ThreadTest7());
thread.setDaemon(true);
thread.start();
System.out.println("守護線程啟動");
}
}
在Java中庸队,線程分為兩大類型:用戶線程和守護線程。
通過Thread.setDaemon(false)設置為用戶線程馒稍,默認不調(diào)用此方法而創(chuàng)建的線程也是用戶線程皿哨;
通過Thread.setDaemon(true)設置為守護線程。
setDaemon()方法必須在start()方法之前設置纽谒,否則會拋出IllegalThreadStateException異常证膨。
守護線程和用戶線程有何不同?
當我們將一個線程設置為守護線程鼓黔,如果主線程執(zhí)行結(jié)束央勒,那么守護線程也會跟隨主線程一起結(jié)束。
而用戶線程卻不同澳化,如果主線程執(zhí)行結(jié)束崔步,但是用戶線程還在執(zhí)行,那么程序就不會停止缎谷。
最典型的守護線程井濒,就是JVM虛擬機中的垃圾回收器。
上面的例子中,新啟動的線程被設置成了守護線程瑞你,當main線程結(jié)束時酪惭,守護線程也隨之結(jié)束。但是者甲,守護線程中的finally代碼塊并不會執(zhí)行春感。
線程生命周期
線程是一個動態(tài)執(zhí)行的過程,它有一個從出生到死亡的過程虏缸。在Thread類中鲫懒,Java提供了線程的一生中會經(jīng)過哪些狀態(tài)。
(1)NEW:出生刽辙,new Thread()
(2)RUNNABLE:運行窥岩,start()、run()
(3)BLOCKED:阻塞扫倡,等待鎖synchronized block
(4)WAITING:無限等待谦秧,join()、wait()
(5)TIMED_WAITING:定時等待撵溃,sleep(x)巾兆、wait(x)州弟、join(x)
(6)TERMINATED:終結(jié),線程執(zhí)行完畢
列個表格,具體說下量瓜。需要注意的是跪呈,上面6種狀態(tài)與網(wǎng)上搜出來的很多文章并不一致(網(wǎng)上多一些狀態(tài)進行了歸類)妇汗,請以此為準孩革,因為這些狀態(tài)是Thread明確列出的。
方法 | 簡要說明 |
---|---|
NEW | 線程的初始狀態(tài)惶翻,也就是我們在代碼中new Thread()后的狀態(tài)姑蓝,還沒有調(diào)用start()方法 |
RUNNABLE | 運行狀態(tài),這一點與網(wǎng)上的很多文章不一樣吕粗,在Thread類中纺荧,運行狀態(tài)包含了就緒和運行,也就是調(diào)用了start()和實際執(zhí)行run() |
BLOCKED | 阻塞狀態(tài)颅筋,線程在進入同步代碼塊之前宙暇,發(fā)現(xiàn)已經(jīng)有線程獲取到了鎖,所以本線程阻塞等待鎖的釋放 |
WAITING | 等待狀態(tài)议泵,等待其他線程做一事情占贫,例如當我們的一個線程被執(zhí)行了Object.wait(),那么該線程實際在等待其他線程觸發(fā)Object.notify先口、Object.notifyAll() |
TIMED_WAITING | 定時等待型奥,與WAITING不同的是瞳收,此種狀態(tài)在達到一定時間便可返回運行狀態(tài),而不需要依賴其他線程處理厢汹,例如:Thread.sleep(x)缎讼、Object.wait(x) |
TERMINATED | 終結(jié)狀態(tài),也就是說線程執(zhí)行完畢了 |
下面坑匠,我們通過圖片再具體了解下,狀態(tài)之間的流轉(zhuǎn):
需要注意的是卧惜,圖中從其他狀態(tài)變?yōu)檫\行狀態(tài)時厘灼,其實是恢復成了就緒狀態(tài),還需要等待CPU的調(diào)度咽瓷,才能真正的執(zhí)行设凹。