哈嘍,大家好,線程是Java中很重要的一個知識點,我相信大家都知道如何運用多線程來處理任務(wù),但是其中有很多細節(jié)可能不是特別的明白,我打算做一系列有關(guān)線程的文章,就當是個記錄,順便和大家分享一下有關(guān)線程的知識。
這篇文章我們先來講一講線程的基礎(chǔ)知識,那么下面直接開始。
進程
一說到線程,那就不得不提進程。這兩個概念很多人最開始容易混淆,而且面試的時候,有的面試官也會問到。那么什么是進程呢,進程是程序關(guān)于某數(shù)據(jù)集合上的一次運行活動,是系統(tǒng)進行資源分配和調(diào)度的基本單位,是操作系統(tǒng)結(jié)構(gòu)的基礎(chǔ)。
這樣說可能還是有點懵逼,舉個簡單的栗子,你在手機上啟動一個軟件,那么這個軟件就是一個進程。或者說你在電腦上打開QQ,那么這個QQ就是一個進程。
線程
進程說完了,來說說線程,線程是操作系統(tǒng)能夠進行運算調(diào)度的最小單位。它被包含在進程之中,是進程中的實際運作單位。線程可以在進程中獨立運行子任務(wù),并且一個進程至少有一個線程。
來舉個栗子,假如你在手機上啟動了QQ,在QQ中你可以和好友聊天,下載文件,傳輸數(shù)據(jù),其中每一項工作我們都可以理解為一個線程在執(zhí)行。這些工作也可以同時執(zhí)行,當它們同時進行的時候我們可以理解為多個線程同時執(zhí)行,這也是線程的好處之一,同時處理多個任務(wù),以節(jié)約時間。
多線程同時工作的時候其實是CPU在各個線程之間快速切換,速度很快,使我們感覺是在同時進行。
線程運用
線程的調(diào)用大家肯定都很熟悉了,有兩種方法來調(diào)用線程執(zhí)行任務(wù),下面我們來分別講一講。
新建一個類并繼承Thread類
下面我們看下代碼
public class TestMain {
public static void main(String[] args) {
TestThread testThread = new TestThread();
testThread.start();
}
public static class TestThread extends Thread {
@Override
public void run() {
System.out.println("TestThread is run");
}
}
}
代碼大家肯定都很熟悉,需要注意的是start方法重復(fù)調(diào)用會報錯。
當我們繼承Thread類的時候有一個不好的地方是Java并不能多繼承,這樣可能會影響代碼的靈活性,所以一般來說實現(xiàn)Runnable接口是一個更好的選擇。
新建一個類實現(xiàn)Runnable接口
我們在Thread源碼中看到,Thread的構(gòu)造函數(shù)可以傳入Runnable。
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
所以我們也可以新建一個類并實現(xiàn)Runnable接口傳入Thread中,并執(zhí)行該線程。
下面我們先看看代碼
public class TestMain {
public static void main(String[] args) {
Thread thread = new Thread(new TestThread());
thread.start();
}
public static class TestThread implements Runnable{
@Override
public void run() {
System.out.println("test is run");
}
}
}
這個相信大家也是寫了很多遍了,沒什么好說的。
線程執(zhí)行不確定性
線程在執(zhí)行的過程中有不確定性,這里我們先來看個例子。
public class TestMain {
public static void main(String[] args) {
Thread thread = new Thread(new TestThread());
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println("main");
}
}
public static class TestThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("thread");
}
}
}
}
運行結(jié)果
thread
thread
thread
main
main
main
main
main
thread
thread
thread
我們可以看到在運行的結(jié)果中thread和main是交叉打印出來的,并不是先執(zhí)行完thread或者main。當我們調(diào)用start方法的時候,會告訴"線程規(guī)劃器"這個線程已經(jīng)準備好了,等待調(diào)用線程對象的run方法。這個過程就是讓系統(tǒng)安排一個時間來調(diào)用該線程中的run方法,使線程得到運行,具有異步執(zhí)行的效果。所以我們會看到thread和main會交叉打印出來。
線程安全
線程安全是線程知識里面一個重要的知識點,簡單來說就是當多個線程同時訪問同一個變量時,可能會造成變量的不同步。我們先來舉例,加入有5張門票,5個售票員,每個售票員賣出一張門票,門票數(shù)量就少1。下面先看看代碼。
public class TestMain {
private static int count = 5;
public static void main(String[] args) {
TestThread testThread = new TestThread();
Thread a = new Thread(testThread, "A");
Thread b = new Thread(testThread, "B");
Thread c = new Thread(testThread, "C");
Thread d = new Thread(testThread, "D");
Thread e = new Thread(testThread, "E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
public static class TestThread extends Thread {
@Override
public void run() {
count--;
System.out.println(currentThread().getName() + "賣出一張票,還剩余:" + count);
}
}
}
代碼很簡單,就是依照上面的栗子寫的,那么我們來看看運行結(jié)果
A賣出一張票,還剩余:3
B賣出一張票,還剩余:3
C賣出一張票,還剩余:2
D賣出一張票,還剩余:1
E賣出一張票,還剩余:0
我們可以看到結(jié)果中出現(xiàn)了兩個3,這個是因為A,B同時訪問了這個變量造成的。這就是線程安全問題,那么我們?nèi)绾谓鉀Q這個問題呢。Java給我們提供了synchronized字符,我們先來修改一下代碼。
public static class TestThread extends Thread {
@Override
synchronized public void run() {
count--;
System.out.println(currentThread().getName() + "賣出一張票,還剩余:" + count);
}
}
我們在run方法前面加入synchronized。下面我們來看看運行結(jié)果。
B賣出一張票,還剩余:4
C賣出一張票,還剩余:3
A賣出一張票,還剩余:2
D賣出一張票,還剩余:1
E賣出一張票,還剩余:0
結(jié)果中并沒有重復(fù)的數(shù)字出現(xiàn)。當在run方法前面加入synchronized的時,運行到run方法,會先去判斷run方法是否有加鎖,如果加鎖了,證明別的線程在調(diào)用這個方法,就先等待其他線程調(diào)用完畢后再執(zhí)行這個方法。這樣run方法就是排隊執(zhí)行完成的,所以結(jié)果正常,沒有同時訪問同一個變量。當運行run方法的時候,如果沒有加鎖,那么線程會去拿這個鎖,注意這里是所有線程同時搶這把鎖,誰搶到了就先執(zhí)行誰的run方法。
isAlive
isAlive方法是判斷線程是否處于激活狀態(tài)。我們先來看看代碼
public class TestMain {
public static void main(String[] args) {
TestThread testThread = new TestThread();
System.out.println("start testThread isAlive = " + testThread.isAlive());
testThread.start();
System.out.println("end testThread isAlive = " + testThread.isAlive());
}
public static class TestThread extends Thread {
@Override
public void run() {
System.out.println("testThread isAlive = " + currentThread().isAlive());
}
}
}
運行結(jié)果為
start testThread isAlive = false
end testThread isAlive = true
testThread isAlive = true
我們可以發(fā)現(xiàn)當調(diào)用start方法過后,線程就處于激活狀態(tài)了。因為這里end在線程執(zhí)行完成之前就打印了,所以也是true,如果我們修改下代碼,那么end就可能為false了。
public static void main(String[] args) {
try {
TestThread testThread = new TestThread();
System.out.println("start testThread isAlive = " + testThread.isAlive());
testThread.start();
Thread.sleep(1000);
System.out.println("end testThread isAlive = " + testThread.isAlive());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
結(jié)果為:
start testThread isAlive = false
testThread isAlive = true
end testThread isAlive = false
下面我們再看一個有趣的栗子:
public class TestMain {
public static void main(String[] args) {
TestThread testThread = new TestThread();
Thread thread = new Thread(testThread);
thread.start();
}
public static class TestThread extends Thread {
@Override
public void run() {
System.out.println("currentThread isAlive = " + currentThread().isAlive());
System.out.println("this isAlive = " + this.isAlive());
}
}
}
這個運行結(jié)果為:
currentThread isAlive = true
this isAlive = false
這里第一個為true我相信大家都可以理解,那么為什么第二個為false呢。這個就要說說currentThread這個方法了,這個方法獲取的是在哪個線程中運行,而this獲取的是當前線程。因為testThread 是以參數(shù)傳入到了Thread中,在Thread中并不是像線程調(diào)用start方法那樣來運行run方法的。而是直接調(diào)用run方法,所以this.isAlive()獲取的當前線程并沒有調(diào)用start方法,所以為false。而currentThread獲取的是運行的線程,所以結(jié)果為true。
線程停止
線程的停止我們主要來講一講interrupt方法。我們先來看一段代碼:
public class TestMain {
public static void main(String[] args) {
TestThread testThread = new TestThread();
testThread.start();
testThread.interrupt();
}
public static class TestThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
System.out.println(i);
}
}
}
}
我們在線程調(diào)用start方法過后馬上又調(diào)用了interrupt方法,按理來說線程應(yīng)該立馬停止,那么我們看看結(jié)果:
.......
49997
49998
49999
最后我們可以看到線程是完完整整執(zhí)行完成了的。難道interrupt方法沒有作用嗎?我們先來看看另外兩個方法
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
這兩個方法都是判斷線程是否已經(jīng)中斷。第一個方法是一個靜態(tài)方法,并且在方法中調(diào)用了currentThread方法,所以它判斷的是當前運行的線程是否已經(jīng)中斷。第二個方法是判斷線程對象是否已經(jīng)中斷。我們發(fā)現(xiàn)他們最終都是調(diào)用了同一個方法,我們先來看看這個方法:
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
這是一個native方法,傳入的參數(shù)是指是否清除線程的中斷狀態(tài)。true為清除,false為不清除。我們在代碼中加入判斷試試
public static void main(String[] args) {
TestThread testThread = new TestThread();
testThread.start();
testThread.interrupt();
System.out.println("線程是否中斷:" + testThread.isInterrupted());
}
結(jié)果為:
線程是否中斷:true
我們可以發(fā)現(xiàn)在調(diào)用interrupt方法過后其實是給線程加了一個中斷的標識,我們調(diào)用isInterrupted方法就可以看出。那么我們就可以運用這個特性,讓線程實現(xiàn)真正的中斷。下面來看看修改的代碼:
public class TestMain {
public static void main(String[] args) {
try {
TestThread testThread = new TestThread();
testThread.start();
Thread.sleep(200);
testThread.interrupt();
} catch (Exception e) {
e.printStackTrace();
}
}
public static class TestThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
if (isInterrupted()) {
System.out.println("線程已經(jīng)終止");
break;
}
System.out.println(i);
}
}
}
}
結(jié)果為:
......
35289
35290
線程已經(jīng)終止
我們可以發(fā)現(xiàn)線程進入了中斷判斷并跳出了for循環(huán)。這樣雖然可以終止for循環(huán),但是for循環(huán)以下的代碼依然會執(zhí)行,有的人肯定會想到用return,這樣也是可以的,并且不會執(zhí)行for循環(huán)下面的代碼,但是return太多會造成代碼污染,這里我們推薦另一個方法。先來看看代碼:
public static class TestThread extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 50000; i++) {
if (isInterrupted()) {
System.out.println("線程已經(jīng)終止");
throw new InterruptedException();
}
System.out.println(i);
}
System.out.println("for循環(huán)后面的代碼");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("進入catch:" + e.toString());
}
}
}
我們利用try catch來停止線程,并可以在catch中做一些釋放等操作。
結(jié)果為:
......
39160
線程已經(jīng)終止
java.lang.InterruptedException
at test.TestMain$TestThread.run(TestMain.java:24)
進入catch:java.lang.InterruptedException
yield()
yield()方法的作用是先放棄當前的CPU資源,讓其他線程去占用CPU執(zhí)行時間。但是放棄的時間不確定,可能剛剛放棄馬上又占有CPU資源了。下面我們舉個栗子:
public class TestMain {
public static void main(String[] args) {
TestThread1 testThread1 = new TestThread1();
testThread1.start();
TestThread2 testThread2 = new TestThread2();
testThread2.start();
}
public static class TestThread1 extends Thread {
@Override
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
int a = i;
}
long end = System.currentTimeMillis();
System.out.println("1使用時間為:" + (end - start));
}
}
public static class TestThread2 extends Thread {
@Override
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
yield();
int a = i;
}
long end = System.currentTimeMillis();
System.out.println("2使用時間為:" + (end - start));
}
}
}
結(jié)果為:
1使用時間為:1
2使用時間為:8
我們可以明顯的看到2使用的時間長于1使用的時間。
線程優(yōu)先級
在線程中有一個方法可以設(shè)置線程的優(yōu)先級
public final void setPriority(int newPriority)
Java線程中線程分為1-10個等級,等級越高,線程被執(zhí)行的幾率也就越大,這里要注意是執(zhí)行的幾率,而不是優(yōu)先級高的就比優(yōu)先級低的先執(zhí)行。
另外線程的優(yōu)先級是有傳遞效果的,舉個栗子,A線程啟動B線程,如果A線程優(yōu)先級為5,那么B線程的優(yōu)先級也為5。
守護線程
守護線程可能大家平時都沒有怎么用,我們平時經(jīng)常使用的是用戶線程,守護線程是一個特殊的線程,當我們進程中沒有用戶線程的時候,守護線程就會自動銷毀。Java中典型的守護線程就是垃圾回收線程。下面我們來舉個栗子:
public class TestMain {
public static void main(String[] args) {
try {
TestThread1 testThread1 = new TestThread1();
testThread1.setDaemon(true);
testThread1.start();
Thread.sleep(1000);
System.out.println("end");
} catch (Exception e) {
e.printStackTrace();
}
}
public static class TestThread1 extends Thread {
@Override
public void run() {
try {
int i = 0;
while (true) {
i++;
System.out.println(i);
Thread.sleep(200);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
結(jié)果為:
1
2
3
4
5
end
而當我們?nèi)サ魌estThread1.setDaemon(true);這句代碼,結(jié)果為:
......
3
4
5
end
6
7
......
這樣我們就發(fā)現(xiàn)當線程為守護線程的時候,main結(jié)束了,守護線程也就結(jié)束了,如果不是守護線程,則會一直執(zhí)行。
到這里線程的基礎(chǔ)就講完了,上文中有錯誤的地方歡迎大家指出。