Thread類的核心方法較多,讀者應該着重掌握如下關鍵技術點:
□線程的啓動
□如果使線程暫停
□如何使線程停止
□線程的優先級
□線程安全相關的問題
1.1 進程與多線程的概念及線程的優點
本節主要介紹在Java語言中使用多線程技術。但是講到多線程不得不提到進程這個概念:
進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,
是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。在早
期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代
面向線程設計的計算機結構中,進程是線程的容器。程序是指令、數據
及其組織形式的描述,進程是程序的實體。
將一個正在操作系統中運行的exe程序理解成一個進程!通過查看任務管理器中的列表,完全可以將運行在內存中的exe文件理解成進程,進程是受操作系統管理的基本運行單元。
那什麼是線程呢?線程可以理解成是進程中獨立運行的子任務。比如,QQ.exe運行時就有很多的子任務在同時運行。再如,好友視頻線程,下載文件線程,傳輸數據線程等等,這些不同的任務或者說功能都可以同時運行,其中每一項任務完全可以理解成是線程在工作。使用多任務操作系統後,可以最大限度地利用CPU的空閒時間來處理其它的任務,比如一邊打印,一邊使用word編輯。而CPU在這些任務之間不停地切換,由於切換速度非常快,給使用者的感覺就是這些任務似乎在同時運行。所以在使用多線程技術後,可以在同一時間內運行更多不同種類的任務。
1.2 使用多線程
一個進程正在運行時至少會有一個線程在運行,這種情況在java中也是存在的。這些線程在後臺默默地執行:
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());// main
}
在控制檯輸出的main其實就是一個名稱叫做main的線程在執行main方法中的代碼。另外需要說明一下,在控制檯輸出的main和main方法沒有任何的關係,僅僅是名字相同而已。
1.2.1 繼承Thread類
實現多線程編程的方式有兩種,一種是繼承Thread類,另一種是實現Runnable接口。先來看看Thread類的結構:
public class Thread implements Runnable
從源代碼可以發現,Thread類實現了Runnable接口,它們之間具有多態關係。其實,使用繼承Thread類的方式創建線程時,最大的侷限就是不支持多繼承,因爲Java語言的特點就是單根繼承,所以爲了支持多繼承,完全可以實現Runnable接口的方式,一邊實現一邊繼承。
public class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("My Thread!");
}
public static void main(String[] args) {
MyThread my = new MyThread();
my.start();
System.out.println("運行結束");// 運行結束/n My Thread!
}
}
以上代碼的打印順序可以看出來,run方法執行的時間比較晚,這也說明在使用多線程技術時,代碼的運行結果與代碼執行順序或調用順序無關。
上面代碼介紹了線程的調用的隨機性,下面將演示線程的隨機性。
public class MyThread extends Thread {
@Override
public void run() {
try {
for(int i=0; i<10; i++) {
int time = (int)(Math.random()*1000);
Thread.sleep(time);
System.out.println("run="+Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MyThread my = new MyThread();
my.setName("myThread");
my.start();
try {
for(int i=0; i<10; i++) {
int time = (int)(Math.random()*1000);
Thread.sleep(time);
System.out.println("main="+Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在代碼中,爲了展現出線程具有隨機特性,所以使用隨機數的形式來使線程得到掛起的效果,從而表現出CPU執行哪個線程具有不確定性。Thread.java類中的start方法通過‘線程規劃器‘此線程已經準備就緒,等待調用線程對象的run方法。這個過程其實就是讓系統安排一個時間來調用Thread中的run方法,也就是線程得到運行,啓動線程,具有異步執行的效果。如果直接調用代碼thread.run就不是異步執行。 而是同步,那麼此線程對象並不會交給"線程規劃器"來進行處理,而是由main主線程來調用run方法,也就是必須等run方法中的代碼執行完成後才能執行後面的代碼。(看下面代碼中的註釋)
public static void main(String[] args) {
MyThread my = new MyThread();
my.setName("myThread");
my.run();// 由start替換成run,運行結果發生變化,先執行完線程裏面的,然後再執行main方法裏面的
try {
for(int i=0; i<10; i++) {
int time = (int)(Math.random()*1000);
Thread.sleep(time);
System.out.println("main="+Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
另外還需要注意一點,執行start()方法的順序不代表線程啓動的順序.
1.2.2 實現Runnable接口
如果欲創建的線程類已經有一個父類了,這時就不能再繼承自Thread類了,因爲Java不支持多繼承,所以就需要實現Runnable接口來應對這樣的情況.
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("運行中!");
}
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
System.out.println("運行結束");// 運行結束/n 運行中!
}
}
使用繼承Thread類的方式來開發多線程應用程序在設計上是有侷限性的,因爲Java是單根繼承,不支持多繼承.另外需要說明的是,Thread類也實現了Runnable接口,那也就意味着構造函數Thread(Runnable target)不光可以傳入Runnable接口的對象,還可以傳入一個Thread類的對象,這樣做完全可以將一個Thread對象中的run()方法交由其他的線程進行調用.
1.2.3 實例變量與線程安全
自定義線程類中的實例變量針對其他線程可以有共享與不共享之分,這在多個線程之間進行交互時是很重要的一個技術點.
(1)不共享數據的情況
public class MyThread extends Thread {
private int count = 5;
public MyThread(String name) {
super();
this.setName(name);
}
@Override
public void run() {
super.run();
while(count > 0) {
count --;
System.out.println("由"+this.currentThread().getName()+"計算,count=" + count);
}
}
public static void main(String[] args) {
Thread t1 = new MyThread("A");
Thread t2 = new MyThread("B");
Thread t3 = new MyThread("C");
t1.start();
t2.start();
t3.start();
}
}
打印結果爲:
由B計算,count=4
由B計算,count=3
由B計算,count=2
由B計算,count=1
由B計算,count=0
由C計算,count=4
由A計算,count=4
由A計算,count=3
由C計算,count=3
由A計算,count=2
由A計算,count=1
由A計算,count=0
由C計算,count=2
由C計算,count=1
由C計算,count=0
(1)共享數據的情況,意思就是多個線程訪問同一個變量
public class MyThread extends Thread {
private int count = 5;
public MyThread() {
super();
}
public MyThread(String name) {
super();
this.setName(name);
}
@Override
public void run() {
super.run();
count--;
System.out.println("由" + this.currentThread().getName() + "計算,count=" + count);
}
public static void main(String[] args) {
Thread thread = new MyThread();
Thread t1 = new Thread(thread, "A");
Thread t2 = new Thread(thread, "B");
Thread t3 = new Thread(thread, "C");
Thread t4 = new Thread(thread, "D");
Thread t5 = new Thread(thread, "E");
t1.start();
t2.start();
t3.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t4.start();
t5.start();
}
}
結果如下:由此可以得知產生了"非線程安全"問題.
由B計算,count=3
由C計算,count=2
由A計算,count=3
由E計算,count=1
由D計算,count=0
如果要結果正確,代碼改動如下(新添加一個synchronize關鍵字)
@Override
public synchronized void run() {
super.run();
count--;
System.out.println("由" + this.currentThread().getName() + "計算,count=" + count);
}
通過在run方法前加入synchronize關鍵字,使多個線程在執行run方法時,以排隊的方式進行處理.當一個線程調用run前,先判斷run方法有沒有上鎖,如果上鎖,說明有其它線程正在調用run方法,必須等待其它線程對run方法調用結束後纔可以執行run方法.這樣也就實現了排隊調用run方法的目的,synchronize可以在任意對象及方法上加鎖,而加鎖的這段代碼稱爲"互斥區"或"臨界區".
當一個線程想要執行同步方法裏面的代碼時,線程首先嚐試去拿這把鎖,如果能拿到這把鎖,那麼這個線程就可以執行synchronize裏面的代碼.如果不能拿到這把鎖,那麼這個線程就會不斷地去長度拿這把鎖,知道能拿到爲止.而且是多線程同時去爭搶這把鎖.
1.3 currentThread()方法
currentThread()方法可返回代碼正在被那個線程調用的信息.
public class CurrentThreadTest extends Thread{
public CurrentThreadTest() {
System.out.println("constructor is :" + currentThread().getName());
}
@Override
public void run() {
super.run();
System.out.println("run is :" + currentThread().getName());
}
public static void main(String[] args) {
Thread thread = new CurrentThreadTest();
thread.start();// run is :thread-0
// thread.run(); // run is :main
}
}
稍微複雜一點的:
public class CountOperate extends Thread {
public CountOperate() {
System.out.println("CountOperate--begin");
System.out.println("Thread.currentThread().getName()="+Thread.currentThread().getName());
System.out.println("this.getName()="+this.getName());
System.out.println("CountOperate--end");
}
@Override
public void run() {
System.out.println("run--begin");
System.out.println("Thread.currentThread().getName()="+Thread.currentThread().getName());
System.out.println("this.getName()="+this.getName());
System.out.println("run--end");
}
public static void main(String[] args) {
Thread thread = new Thread(new CountOperate());
thread.setName("Demo");
thread.start();
}
}
打印結果如下:
CountOperate--begin
Thread.currentThread().getName()=main
this.getName()=Thread-0
CountOperate--end
run--begin
Thread.currentThread().getName()=Demo
this.getName()=Thread-0
run--end
1.4 isAlive()
isAlive()的功能是判斷該線程是否處於活動狀態.活動狀態就是線程未停止.當線程處於正在運行或者準備開始運行的狀態,就認爲線程是存活的.
public class IsAliveTest extends Thread{
@Override
public void run() {
System.out.println("run: isAlive=" + this.isAlive());// true
}
public static void main(String[] args) {
Thread thread = new IsAliveTest();
System.out.println("begin: isAlive=" + thread.isAlive());// false
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("end: isAlive=" + thread.isAlive());// false
}
}
1.5 sleep()方法
sleep()的作用是在指定的毫秒數內讓當前"正在執行的線程"休眠(暫停執行).這個"正在執行的線程"是指this.currentThread()返回的線程.
public class MyThread1 extends Thread{
@Override
public void run() {
try {
System.out.println("run threadName = " + this.currentThread().getName() + " begin");// 1
Thread.sleep(2000);
System.out.println("run threadName = " + this.currentThread().getName() + " end");// 2
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
MyThread1 myThread = new MyThread1();
System.out.println("begin = " + System.currentTimeMillis());// 3
// myThread.start();// 順序 3,4,1,2
myThread.run();// 順序 3,1,2,4
System.out.println("end = " + System.currentTimeMillis());// 4
}
}
1.6 getId()方法
getId()方法的作用是取得線程的唯一標識.