Java線程:概念與原理
一、進程與線程
進程是指一個內存中運行的應用程序,每個進程都有自己獨立的一塊內存空間,即進程空間或(虛空間)。進程不依賴於線程而獨立存在,一個進程中可以啓動多個線程。比如在Windows系統中,一個運行的exe就是一個進程。
線程是指進程中的一個執行流程,一個進程中可以運行多個線程。比如java.exe進程中可以運行很多線程。線程總是屬於某個進程,線程沒有自己的虛擬地址空間,與進程內的其他線程一起共享分配給該進程的所有資源。
“同時”執行是人的感覺,在線程之間實際上輪換執行。
進程在執行過程中擁有獨立的內存單元,進程有獨立的地址空間,而多個線程共享內存,從而極大地提高了程序的運行效率。
線程在執行過程中與進程還是有區別的。每個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。
線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。
線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程包含以下內容:
- 一個指向當前被執行指令的指令指針;
- 一個棧;
- 一個寄存器值的集合,定義了一部分描述正在執行線程的處理器狀態的值
- 一個私有的數據區。
我們使用Join()方法掛起當前線程,直到調用Join()方法的線程執行完畢。該方法還存在包含參數的重載版本,其中的參數用於指定等待線程結束的最長時間(即超時)所花費的毫秒數。如果線程中的工作在規定的超時時段內結束,該版本的Join()方法將返回一個布爾量True。
簡而言之:
-
一個程序至少有一個進程,一個進程至少有一個線程。
-
線程的劃分尺度小於進程,使得多進程程序的併發性高。
-
另外,進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率。
-
線程在執行過程中與進程還是有區別的。每個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
-
從邏輯角度來看,多線程的意義在於一個應用程序中,有多個執行部分可以同時執行。但操作系統並沒有將多個線程看做多個獨立的應用,來實現進程的調度和管理以及資源分配。這就是進程和線程的重要區別。
在Java中,每次程序運行至少啓動2個線程:一個是main線程,一個是垃圾收集線程。因爲每當使用java命令執行一個類的時候,實際上都會啓動一個JVM,每一個JVM實際上就是在操作系統中啓動了一個進程。
二、實例化線程
1、如果是擴展java.lang.Thread類的線程,繼承Thread類,則直接new即可。
/**
* 測試擴展Thread類實現的多線程程序
*/
public class TestThread extends Thread {
public TestThread(String name){
super(name);
}
@Override
public void run() {
for(int i=0;i<5;i++){
for(long k=0;k<100000000;k++);
System.out.println(this.getName()+":"+i);
}
}
public static void main(String[] args){
Thread t1=new TestThread("李白");
Thread t2=new TestThread("屈原");
t1.start();
t2.start();
}
}
2、如果是實現了java.lang.Runnable接口的類,則用Thread的構造方法;
/**
* 實現Runnable接口的類
*/
public class RunnableImpl implements Runnable{
private Stringname;
public RunnableImpl(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
for(long k=0;k<100000000;k++);
System.out.println(name+":"+i);
}
}
}
/**
* 測試Runnable類實現的多線程程序
*/
public class TestRunnable {
public static void main(String[] args) {
RunnableImpl ri1=new RunnableImpl("李白");
RunnableImpl ri2=new RunnableImpl("屈原");
Thread t1=new Thread(ri1);
Thread t2=new Thread(ri2);
t1.start();
t2.start();
}
}
三、啓動線程
在線程的Thread對象上調用start()方法,而不是run()或者別的方法。
在調用start()方法之前:線程處於新狀態中,新狀態指有一個Thread對象,但還沒有一個真正的線程。
在調用start()方法之後:發生了一系列複雜的事情——
-
啓動新的執行線程(具有新的調用棧);
-
該線程從新狀態轉移到可運行狀態;
-
當該線程獲得機會執行時,其目標run()方法將運行。
注意:對Java來說,run()方法沒有任何特別之處。像main()方法一樣,它只是新線程知道調用的方法名稱(和簽名)。因此,在Runnable上或者Thread上調用run方法是合法的。但並不啓動新的線程。
四、一些常見問題
1、線程的名字,一個運行中的線程總是有名字的,名字有兩個來源,一個是虛擬機自己給的名字,一個是你自己的定的名字。在沒有指定線程名字的情況下,虛擬機總會爲線程指定名字,並且主線程的名字總是mian,非主線程的名字不確定。
2、線程都可以設置名字,也可以獲取線程的名字,連主線程也不例外。
3、獲取當前線程的對象的方法是:Thread.currentThread();
4、在上面的代碼中,只能保證:每個線程都將啓動,每個線程都將運行直到完成。一系列線程以某種順序啓動並不意味着將按該順序執行。對於任何一組啓動的線程來說,調度程序不能保證其執行次序,持續時間也無法保證。
5、當線程目標run()方法結束時該線程完成。
6、**一旦線程啓動,它就永遠不能再重新啓動。只有一個新的線程可以被啓動,並且只能一次。一個可運行的線程或死線程可以被重新啓動。**
7、線程的調度是JVM的一部分,在一個CPU的機器上上,實際上一次只能運行一個線程。一次只有一個線程棧執行。JVM線程調度程序決定實際運行哪個處於可運行狀態的線程。
衆多可運行線程中的某一個會被選中做爲當前線程。可運行線程被選擇運行的順序是沒有保障的。
8、儘管通常採用隊列形式,但這是沒有保障的。隊列形式是指當一個線程完成“一輪”時,它移到可運行隊列的尾部等待,直到它最終排隊到該隊列的前端爲止,它才能被再次選中。事實上,我們把它稱爲可運行池而不是一個可運行隊列,目的是幫助認識線程並不都是以某種有保障的順序排列而成一個一個隊列的事實。
9、儘管我們沒有無法控制線程調度程序,但可以通過別的方式來影響線程調度的方式。
Java線程:線程狀態的轉換
一、阻止線程執行
對於線程的阻止,考慮一下三個方面:
1、睡眠:Thread.sleep方法
Thread.sleep(longmillis)和Thread.sleep(long millis, int nanos)靜態方法強制當前正在執行的線程休眠(暫停執行),以“減慢線程”。當線程睡眠時,它入睡在某個地方,在甦醒之前不會返回到可運行狀態。當睡眠時間到期,則返回到可運行狀態。
線程睡眠的原因:線程執行太快,或者需要強制進入下一輪,因爲Java規範不保證合理的輪換。
睡眠的實現:調用靜態方法。
2、線程的優先級和線程讓步yield()
線程的讓步是通過Thread.yield()來實現的。yield()方法的作用是:暫停當前正在執行的線程對象,並執行其他線程(同優先級的)。
要理解yield(),必須瞭解線程的優先級的概念。線程總是存在優先級,優先級範圍在1~10之間。
注意:當設計多線程應用程序的時候,一定不要依賴於線程的優先級。因爲線程調度優先級操作是沒有保障的,只能把線程優先級作用作爲一種提高程序效率的方法,但是要保證程序不依賴這種操作。
設置線程的優先級:線程默認的優先級是創建它的執行線程的優先級。可以通過setPriority(int newPriority)更改線程的優先級(1~10)。
Thread t = new MyThread();
t.setPriority(8);
t.start();
yield()應該做的是讓當前運行線程回到可運行狀態,以允許具有相同優先級的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因爲讓步的線程還有可能被線程調度程序再次選中。
結論:yield()從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致線程從運行狀態轉到可運行狀態,但有可能沒有效果。
4、join()方法
Thread的非靜態方法join()讓一個線程B“加入”到另外一個線程A的尾部。在A執行完畢之前,B不能工作。即A.join()
public static void main(String[] args) {
Thread thread1 = new Thread(new JoinTester01("One"));
Thread thread2 = new Thread(new JoinTester01("Two"));
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//*******1與2執行完纔會執行下面***********//
System.out.println("Main thread is finished");
}
小結
到目前位置,介紹了線程離開運行狀態的3種方法:
1、調用Thread.sleep():使當前線程睡眠至少多少毫秒(儘管它可能在指定的時間之前被中斷)。
2、調用Thread.yield():不能保障太多事情,儘管通常它會讓當前運行線程回到可運行性狀態,使得有相同優先級的線程有機會執行。
3、調用join()方法:保證當前線程停止執行,直到該線程所加入的線程完成爲止。然而,如果它加入的線程沒有存活,則當前線程不需要停止。
除了以上三種方式外,還有下面幾種特殊情況可能使線程離開運行狀態:
1、線程的run()方法完成。
2、在對象上調用wait()方法(不是在線程上調用)。
3、線程不能在對象上獲得鎖定,它正試圖運行該對象的方法代碼。
4、線程調度程序可以決定將當前運行狀態移動到可運行狀態,以便讓另一個線程獲得運行機會,而不需要任何理由。
Java線程:線程的同步與鎖
一、同步問題
線程的同步是爲了防止多個線程訪問一個數據對象時,對數據造成的破壞。
二、同步和鎖定
1、鎖的原理
Java中每個對象都有一個內置鎖。
當程序運行到非靜態的synchronized同步方法上時,自動獲得與正在執行代碼類的當前實例(this實例)有關的鎖。獲得一個對象的鎖也稱爲獲取鎖、鎖定對象、在對象上鎖定或在對象上同步。
當程序運行到synchronized同步方法或代碼塊時才該對象鎖才起作用。
一個對象只有一個鎖。所以,如果一個線程獲得該鎖,就沒有其他線程可以獲得鎖,直到第一個線程釋放(或返回)鎖。這也意味着任何其他線程都不能進入該對象上的synchronized方法或代碼塊,直到該鎖被釋放。
釋放鎖是指持鎖線程退出了synchronized同步方法或代碼塊。
public int fix(int y) {
synchronized (this) { //當前對象鎖 this也可爲某個實例對象
x = x - y;
}
return x;
}
線程同步小結
1、線程同步的目的是爲了保護多個線程反問一個資源時對資源的破壞。
2、線程同步方法是通過鎖來實現,每個對象都有切僅有一個鎖,這個鎖與一個特定的對象關聯,線程一旦獲取了對象鎖,其他訪問該對象的線程就無法再訪問該對象的其他同步方法。
3、對於靜態同步方法,鎖是針對這個類的,鎖對象是該類的Class對象。靜態和非靜態方法的鎖互不干預。一個線程獲得鎖,當在一個同步方法中訪問另外對象上的同步方法時,會獲取這兩個對象鎖。
4、對於同步,要時刻清醒在哪個對象上同步,這是關鍵。
5、編寫線程安全的類,需要時刻注意對多個線程競爭訪問資源的邏輯和安全做出正確的判斷,對“原子”操作做出分析,並保證原子操作期間別的線程無法訪問競爭資源。
6、當多個線程等待一個對象鎖時,沒有獲取到鎖的線程將發生阻塞。
7、死鎖是線程間相互等待鎖鎖造成的,在實際中發生的概率非常的小。真讓你寫個死鎖程序,不一定好使,呵呵。但是,一旦程序發生死鎖,程序將死掉。
Java線程:線程的交互
一、線程交互的基礎知識
void notify()——喚醒在此對象監視器上等待的單個線程。
void notifyAll()——喚醒在此對象監視器上等待的所有線程。
void wait()——導致當前的線程等待,直到其他線程調用此對象的 notify()方法或 notifyAll()方法
void wait(longtimeout)——導致當前的線程等待,直到其他線程調用此對象的 notify()方法或 notifyAll()方法,或者超過指定的時間量。
void wait(longtimeout, int nanos)——導致當前的線程等待,直到其他線程調用此對象的 notify()方法或 notifyAll()方法,或者其他某個線程中斷當前線程,或者已超過某個實際時間量。
關於等待/通知,要記住的關鍵點是:
必須從同步環境內調用wait()、notify()、notifyAll()方法。線程不能調用對象上等待或通知的方法,除非它擁有那個對象的鎖。