一、多線程編程
1.多線程的概念
一般來說,程序只能循序單獨運行一個程序塊,不能同時運行多個程序塊。但Java提供了內置的多線程支持。
多線程是在單個進程中運行多個不同的線程,執行不同的任務,
它允許不同的程序塊在同一個程序中幾乎同時運行,可以提高處理效率、達到多任務的目的。
2.進程與線程
學習多線程需要區分兩個概念:進程與線程。
進程:
- 每一個進程有獨立的一塊內存空間和一組系統資源。進程間,數據和狀態是完全獨立的。
- 創建和執行一個線程的系統開銷相對較大。
- 進程表示程序的一次執行過程,它是系統運行程序的基本單位。
線程:
- 線程不能獨立存在,它是進程的一部分。
- 一條線程表示程序中單個按順序的程序流控制。
- 同一個進程的線程間共享內存空間和系統資源;線程的創建和切換需要的開銷比進程小得多。因此線程也稱輕負荷進程。
二、多線程的實現方法
Java默認擁有一個主線程,它是執行main方法的線程。
Java提供了多種實現多線程的方式。其中比較常用且簡單的是繼承Thread類和實現Runnable接口。
1.Thread類
繼承Thread類是最簡單的多線程實現方法。其步驟如下:
- 新建一個類繼承自Thread類;
- 在run方法中覆寫想要同時運行的代碼塊;
- 在需要運行線程的地方創建一個自建類的實例,即可同時運行寫入的代碼塊。
public class Demo {
public static void main(String[] args) {
new TestThread().start();
int t = 10;
while(t-- > 0) {
Thread.sleep(50); //爲了清晰表示運行結果,使用sleep暫停線程,該方法需要顯示異常處理,此處省略
System.out.println('1');
}
}
}
class TestThread extends Thread{
@Override
public void run() {
int t = 10;
Thread.sleep(25);
while(t-- > 0) {
Thread.sleep(50);
System.out.println('2');
}
}
}
//Output:12121212121212121212
2.Runnable接口
Java只允許單繼承,如果一個類已經是子類,還想使用多線程技術,就不能再使用Thread類,而要實現Runnable接口。
- 新建一個實現Runnbale接口的類;
- 覆寫run方法;
- 在需要運行線程的地方創建一個以Thread對象包裝的自建類實例;
- 調用包裝對象的start方法即可運行寫入的代碼塊。
public class Demo {
public static void main(String[] args) {
Thread thread = new Thread(new TestThread()); //Thread類允許使用一個實現了runnable接口的實例構造Thread實例
thread.start();
int t = 10;
while(t-- > 0) {
Thread.sleep(50);
System.out.println('1');
}
}
}
class TestThread implements Runnable{
@Override
public void run() {
int t = 10;
Thread.sleep(25);
while(t-- > 0) {
Thread.sleep(50);
System.out.println('2');
}
}
}
//Output:12121212121212121212
3.兩者的區別
Thread類實際上也是一個實現了Runnable接口的類。但是使用這兩種方式實現多個線程在資源共享上有一些區別:
//使用Thread類創建兩個線程
public class Demo {
public static void main(String[] args) throws Exception {
TestThread t1 = new TestThread();
TestThread t2 = new TestThread();
t1.start();
t2.start();
}
}
class TestThread extends Thread{
private int v = 0;
@Override
public void run() {
int t = 10;
while(t-- > 0)
System.out.print(v++);
}
}
//Output:01234567801234567899 兩個線程同時運行,擁有獨立的數據v
//使用Runnable接口創建兩個線程
public class Demo {
public static void main(String[] args) throws Exception {
TestRunnable tr = new TestRunnable();
t1 = new Thread(tr);
t2 = new Thread(tr);
t1.start();
Thread.sleep(250);
t2.start();
}
}
class TestRunnable implements Runnable{
private int v = 0;
@Override
public void run() {
int t = 10;
while(t-- > 0){
//Thread.sleep(500);
System.out.print(v++);
}
}
}
//Output1:0012345687991010111112131414 兩個線程可能同時訪問到數據v,因此兩個線程讀取的數據相同
//Output2:012345678910111213141516171819 加入註釋掉的兩條語句,將兩個線程的運行錯開,最終可以輸出19,也就是執行了20次v++
由此可以發現,使用同一個Runnable對象構造的兩個線程,互相之間可以共享對象的數據;
而繼承Thread類的TestThread類不能共享成員數據,只能創建兩個不同的實例,因此它們之間的實例成員也是獨立的。
並且如果試圖用同一個TestThread對象執行兩次start,會引發異常,原因就是線程的狀態。
綜上,Runnable接口相較於Thread類有幾個明顯的優勢:
● 可以使用多個相同代碼的線程處理同一個資源。
● 避免單線程特性帶來的侷限。
● 令代碼與數據相互獨立,增強程序的健壯性。
三、線程的狀態與優先級
1.線程的狀態
一個線程在其生命週期內,有五種可能的狀態,它們的關係如下:
對一個Thread類實例調用getState方法能夠獲取線程的當前狀態,可能得到的結果如下:
● NEW:尚未啓動的線程處於的狀態,對應圖中的創建;
● RUNNABLE:正在JVM中執行的線程處於的狀態,對應就緒和運行,兩種狀態由CPU調度切換;
● BLOCKED:受阻塞並等待某個監視器鎖的線程處於的狀態,屬於阻塞的一種;
● WAITING:無限期地等待另一個線程來執行某一個特定操作的線程處於的狀態,屬於阻塞的一種;
● TIMED_WAITING:等待另一個線程來執行,取決於指定等待時間的線程處於的狀態,屬於阻塞的一種;
● TERMINATED:已經結束/退出的線程處於的狀態,對應圖中的阻止;
任意一個確定的時刻,線程只能處於上面的一個狀態。
線程在不同的狀態,只能調用對應的狀態轉換方法,如圖中所示;例如在程序運行前調用join、sleep等進入阻塞狀態的方法;在運行狀態下調用start方法;在線程結束後調用阻塞方法或start方法等,都會引發特定的illegalThreadStateException異常。
2.線程的優先級
每一個Java線程都有一個優先級,便於操作系統確定線程的調度順序。
線程的優先級是一個整數,取值範圍爲1(Thread.MIN_PRIORITY) ~ 10(Thread.MAX_PRIORITY)。
默認情況下,線程會自動分配一個普通優先級 5 (NORM_PRIORITY)。
一般來說,具有較高優先級的線程對程序而言更爲重要,並應先於較低優先級的線程分配CPU資源;但線程的優先級並不保證線程的執行順序,並且資源的分配依賴於平臺完成。
四、線程操作的方法
1.線程名稱
Thread類的getName方法可以獲取線程的名稱,setName方法可以設置線程的名稱;在創建實例時也可使用對應的構造函數指定線程的名稱;若沒有爲線程指定名稱,系統會爲線程自動分配名稱。
此外,setName和getName方法沒有限制調用時的狀態。可以在線程啓動前設置名稱,也可以啓動後修改名稱。
Thread test = new Thread(new TestRunnable());
System.out.println(test.getName()); //自動分配的名稱:Thread-0
test.setName("test");
System.out.println(test.getName()); //修改後的名稱:test
2.線程啓動
Thread類的start方法,可以啓動線程;isAlive方法可以判斷線程是否已經啓動,或是否尚未終止;
class TestRunnable implements Runnable{
@Override
public void run() {
for(int i = 0;i < 10;i++) {
try {
Thread.sleep(100); //每次停留100ms,1s後線程結束
}
catch (Exception e) {
e.printStackTrace();
}
}
}
}
public class Demo {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new TestRunnable());
System.out.println(t.isAlive()); //線程啓動前:false
t.start();
System.out.println(t.isAlive()); //線程剛啓動:true
Thread.sleep(500);
System.out.println(t.isAlive()); //線程啓動後0.5s:true
System.out.println(t.getState()); //線程狀態:TIMED_WAITING
Thread.sleep(600);
System.out.println(t.isAlive()); //線程啓動後1.1s:false
}
}
在這裏例子中可以看出,處於阻塞狀態的線程,也被認爲是活線程(isAlive返回true)。也就是說isAlive方法只有在線程開始前和線程結束後,才返回false。
3.後臺線程
Java程序中,線程分爲前臺線程和後臺線程。程序結束的標誌是所有前臺線程結束。也就是說,即使還有後臺線程正在運行,在前臺線程結束時,整個進程就結束了。
默認情況下,新建的線程都是前臺線程。若需要將線程設置爲後臺線程,則需要在線程運行前調用setDaemon方法。
class TestRunnable implements Runnable{
private int v = 0;
@Override
public void run() {
while(true) { //無限循環
try {
Thread.sleep(100);
System.out.print(v++);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
}
public class Demo {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new TestRunnable());
t.setDaemon(true); //若沒有該語句,程序將無限打印自然數
t.start();
Thread.sleep(1000);
}
}
//Output:012345678 由於線程運行start語句的時間消耗,不能打印到9
4.線程插入
Thread類的join方法可以將指定線程插入到當前線程的前面,以合併線程,達到線程的順序執行。
調用join方法的當前線程(執行程序塊的線程,而不是調用join的實例線程)將進入WAITING狀態。
class TestRunnable implements Runnable{
@Override
public void run() {
for(int i = 0;i < 10;i++) {
try {
Thread.sleep(100);
System.out.print(i);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
}
public class Demo {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new TestRunnable());
t.start();
t.join();
for(int i = 0;i < 10;i++) {
Thread.sleep(100);
System.out.print(i);
}
}
}
//Output:01234567890123456789 在t線程執行完後,再執行main線程
join類有多個重載,可以指定兩個線程合併的時間,在時間到後,兩個線程由再次分離,同時運行。
調用指定時間的join方法的線程將進入TIMED_WAITING狀態。
class TestRunnable implements Runnable{
@Override
public void run() {
for(int i = 0;i < 10;i++) {
try {
Thread.sleep(100);
System.out.print(i);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
}
public class Demo {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new TestRunnable());
t.start();
t.join(500);
for(int i = 0;i < 10;i++) {
Thread.sleep(100);
System.out.print(i);
}
}
}
//Output:01234051627384956789 在500ms後,t線程和main線程同時執行
5.線程休眠
Thread類的sleep靜態方法可以讓當前執行的線程休眠一段時間。
調用了sleep方法的線程將進入TIMED_WAITING狀態。
該方法會可能會拋出一個檢查性異常InteruptedException,需要顯示處理。
package learning_test;
class TestRunnable implements Runnable{
@Override
public void run() {
try {
Thread.sleep(10000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Demo {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new TestRunnable());
t.start();
System.out.println(t.getState()); //RUNNABLE 線程start方法調用和run方法調用需要時間,因此立即檢查狀態可能得到RUNNABlE
Thread.sleep(100);
System.out.println(t.getState()); //TIMED_WAITTING 留給t線程進入run方法的時間,此時t線程由於sleep休眠,線程將進入TIMED_WAITING狀態
}
}
6.線程中斷
Java中線程中斷有三種方式:
● 對於非無限執行的run方法,在run方法結束後,線程自動中斷;
對於無限執行的run方法:
● 在while循環中使用標識符,當需要線程結束時,修改標識符。
class TestRunnable implements Runnable{
public boolean exit = false;
@Override
public void run() {
while(!exit){
<Statement>
}
}
}
● 使用stop方法。該方法由於線程不安全,已經被棄用;
● 使用interrupt方法。該方法不同於stop方法那樣立即停止run方法的執行,它僅僅是給線程發送停止標記,通知線程終止;但收到通知後,run方法並不會強制終止。
class TestRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.print(i);
}
}
}
public class Demo {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new TestRunnable());
t.start();
t.interrupt();
}
}
//Output:0123456789
可以看到,在主線程中給t線程發送interrupt信息後,t線程並沒有強制結束。這雖然避免了stop方法帶來的安全問題,也需要我們單獨處理程序的結束。
class TestRunnable implements Runnable {
@Override
public void run() {
int v = 0;
while(true) {
if(Thread.currentThread().isInterrupted())
break;
System.out.println(v++);
}
}
}
public class Demo {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new TestRunnable());
t.start();
Thread.sleep(1); //給t線程執行的時間
t.interrupt(); //發送停止信息
}
}
//Output:本次輸出0 ~ 180,具體數字每次運行不定
除了使用break停止線程,interrupt方法還會引發sleep方法的異常。利用異常處理機制可以中斷線程。
package learning_test;
class TestRunnable implements Runnable {
@Override
public void run() {
int v = 0;
try {
while (true) {
Thread.sleep(100);
System.out.print(v++);
}
} catch (InterruptedException e) {
System.out.print('\t');
System.out.println(e.getMessage());
}
}
}
public class Demo {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new TestRunnable());
t.start();
Thread.sleep(1000);
t.interrupt();
}
}
//Output:012345678 sleep interrupted