多線程編程——基礎篇 (一)
時間:2006-08-08
作者:
axman
[寫在前面]
隨著計算機技術的發展,編程模型也越來越復雜多樣化。但多線程編程模型是目前計算機系統架構的最終模型。隨著CPU主頻的不斷攀升,X86架構的硬件已經成為瓶,在這種架構的CPU主頻最高為4G。事實上目前3.6G主頻的CPU已經接近了頂峰。
如果不能從根本上更新當前CPU的架構(在很長一段時間內還不太可能),那么繼續提高CPU性能的方法就是超線程CPU模式。那么,作業系統、應用程序要發揮CPU的最大性能,就是要改變到以多線程編程模型為主的并行處理系統和并發式應用程序。
所以,掌握多線程編程模型,不僅是目前提高應用性能的手段,更是下一代編程模型的核心思想。多線程編程的目的,就是"最大限度地利用CPU資源",當某一線程的處理不需要占用CPU而只和I/O,OEMBIOS等資源打交道時,讓需要占用CPU資源的其它線程有機會獲得CPU資源。從根本上說,這就是多線程編程的最終目的。
[第一需要弄清的問題]
如同程序和進程的區別,要掌握多線程編程,第一要弄清的問題是:線程對象和線程的區別。
線程對象是可以產生線程的對象。比如在java平臺中Thread對象,Runnable對象。線程,是指正在執行的一個指點令序列。在java平臺上是指從一個線程對象的start()開始,運行run方法體中的那一段相對獨立的過程。
鑒于作者的水平,無法用更確切的詞匯來描述它們的定義。但這兩個有本質區別的概念請初學者細細體會,隨著介紹的深入和例程分析的增加,就會慢慢明白它們所代表的真實含義。
天下難事必始于易,天下大事必始于細。
讓我們先從最簡單的"單線程"來入手:(1)帶引號說明只是相對而言的單線程,(2)基于java。
class BeginClass{
public static void main(String[] args){
for(int i=0;i<100;i++)
System.out.println("Hello,World!");
}
}
如果我們成功編譯了該java文件,然后在命令行上敲入:
java BeginClass
現在發生了什么呢?每一個java程序員,從他開始學習java的第一分鐘里都會接觸到這個問
題,但是,你知道它到底發生發什么?
JVM進程被啟動,在同一個JVM進程中,有且只有一個進程,就是它自己。然后在這個JVM環境中,所有程序的運行都是以線程來運行。JVM最先會產生一個主線程,由它來運行指定程序的入口點。在這個程序中,就是主線程從main方法開始運行。當main方法結束后,主線程運行完成。JVM進程也隨之退出。
我們看到的是一個主線程在運行main方法,這樣的只有一個線程執行程序邏輯的流程我們稱
之為單線程。這是JVM提供給我們的單線程環境,事實上,JVM底層還至少有垃圾回收這樣的后臺線程以及其它非java線程,但這些線程對我們而言不可訪問,我們只認為它是單線程的。
主線程是JVM自己啟動的,在這里它不是從線程對象產生的。在這個線程中,它運行了main方法這個指令序列。理解它,但它沒有更多可以研究的內容。
[接觸多線程]
class MyThread extends Thread{
public void run(){
System.out.println("Thread say:Hello,World!");
}
}
public class MoreThreads{
public static void main(String[] args){
new MyThread();
new MyThread().start();
System.out.println("Main say:Hello,World");
}
}
執行這個程序,main方法第一行產生了一個線程對象,但并沒有線程啟動。
main方法第二行產生了一個線程對象,并啟動了一個線程。
main方法第三行,產生并啟動一個線程后,主線程自己也繼續執行其它語句。
我們先不研究Thread對象的具體內容,稍微來回想一下上面的兩個概念,線程對象和線程。在JAVA中,線程對象是JVM產生的一個普通的Object子類。而線程是CPU分配給這個對象的一個運行過程。我們說的這個線程在干什么,不是說一個線程對象在干什么,而是這個運行過程在干什么。如果一時想不明白,不要急,但你要記得它們不是一回事就行了。
累了吧?為不么不繼續了?
基于這種風格來介紹多線程,并不是每個人都喜歡和接受的,如果你不喜歡,正好不浪費你的時間了,而如果你接受的話,那就看下一節吧。
========================================================================
多線程編程——基礎篇 (二)
時間:2006-08-16
作者:
axman
在進入java平臺的線程對象之前,基于
基礎篇(一)的一些問題,我先插入兩個基本概念。
[線程的并發與并行]
在單CPU系統中,系統調度在某一時刻只能讓一個線程運行,雖然這種調試機制有多種形式(大多數是時間片輪巡為主),但無論如何,要通過不斷切換需要運行的線程讓其運行的方式就叫并發(concurrent)。而在多CPU系統中,可以讓兩個以上的線程同時運行,這種可以同時讓兩個以上線程同時運行的方式叫做并行(parallel)。
在上面包括以后的所有論述中,請各位朋友諒解,我無法用最準確的詞語來定義儲如并發和并行這類術語,但我以我的經驗能通俗地告訴大家它是怎么一回事,如果您看到我說的一些"標準"文檔上說的不一樣,只要意思一致,那您就不要挑刺了。
[JAVA線程對象]
現在我們來開始考察JAVA中線程對象。
在JAVA中,要開始一個線程,有兩種方式。一是直接調用Thread實例的start()方法,二是
將Runable實例傳給一個Thread實例然后調用它的start()方法。
在前面已經說過,線程對象和線程是兩個完全不同的概念。這里我們再次深入一下,生成一個線程的實例,并不代表啟動了線程。而啟動線程是說在某個線程對象上啟動了該實例對應的線程,當該線程結束后,并不會就立即消失。
對于從很多書籍上可以看到的基礎知識我就不用多說了。既然是基礎知識,我也著重于從普通文檔上讀不到的內容。所以本節我重點要說的是兩種線程對象產生線程方式的區別。
class MyThread extends Thread{
public int x = 0;
public void run(){
for(int i=0;i<100;i++){
try{
Thread.sleep(10);
}catch(Exception e){}
System.out.println(x++);
}
}
}
如果我們生成MyThread的一個實例,然后調用它的start()方法,那么就產生了這個實例對應的線程:
public class Test {
public static void main(String[] args) throws Exception{
MyThread mt = new MyThread();
mt.start();
}
}
不用說,最終會打印出0到99,現在我們稍微玩一點花樣:
public class Test {
public static void main(String[] args) throws Exception{
MyThread mt = new MyThread();
mt.start();
System.out.println(101);
}
}
也不用說,在基礎篇(一)中我們知道由于單CPU的原因,一般會先打印101,然后打印0到99。不過我們可以控制線程讓它按我們的意思來運行:
public class Test {
public static void main(String[] args) throws Exception{
MyThread mt = new MyThread();
mt.start();
mt.join();
System.out.println(101);
}
}
好了,我們終于看到,mt實例對應的線程(假如我有時說mt線程請你不要怪我,不過我盡量不這么說)。在運行完成后,主線程才打印101。因為我們讓當前線程(這里是主線程)等待mt線程的運行結束。"在線程對象a上調用join()方法,就是讓當前正在執行的線程等待線程對象a對應的線程運行完成后才繼續運行。" 請大家一定要深刻理解并熟記這句話,而我這里引出這個知識點的目的是為了讓你繼續看下面的例子:
public class Test {
public static void main(String[] args) throws Exception{
MyThread mt = new MyThread();
mt.start();
mt.join();
Thread.sleep(3000);
mt.start();
}
}
當線程對象mt運行完成后,我們讓主線程休息一下,然后我們再次在這個線程對象上啟動線程。結果我們看到:
Exception in thread "main" java.lang.IllegalThreadStateException
也就是這種線程對象一時運行一次完成后,它就再也不能運行第二次了。我們可以看一下它有具體實現:
public synchronized void start() {
if (started)
throw new IllegalThreadStateException();
started = true;
group.add(this);
start0();
}
一個Thread的實例一旦調用start()方法,這個實例的started標記就標記為true,事實中不管這個線程后來有沒有執行到底,只要調用了一次start()就再也沒有機會運行了,這意味著:
[通過Thread實例的start(),一個Thread的實例只能產生一個線程]
那么如果要在一個實例上產生多個線程(也就是我們常說的線程池),我們應該如何做呢?這就是Runnable接口給我們帶來的偉大的功能。
class R implements Runnable{
private int x = 0;
public void run(){
for(int i=0;i<100;i++){
try{
Thread.sleep(10);
}catch(Exception e){}
System.out.println(x++);
}
}
}
正如它的名字一樣,Runnable的實例是可運行的,但它自己并不能直接運行,它需要被Thread對象來包裝才行運行:
public class Test {
public static void main(String[] args) throws Exception{
new Thread(new R()).start();
}
}
當然這個結果和mt.start()沒有什么區別。但如果我們把一個Runnable實例給Thread對象多次包裝,我們就可以看到它們實際是在同一實例上啟動線程:
public class Test {
public static void main(String[] args) throws Exception{
R r = new R();
for(int i=0;i<10;i++)
new Thread(r).start();
}
}
x是實例對象,但結果是x被加到了999,說明這10個線程是在同一個r對象上運行的。請大家注意,因為這個例子是在單CPU上運行的,所以沒有對多個線程同時操作共同的對象進行同步。這里是為了說明的方便而簡化了同步,而真正的環境中你無法預知程序會在什么環境下運行,所以一定要考慮同步。
到這里我們做一個完整的例子來說明線程產生的方式不同而生成的線程的區別:
package debug;
import java.io.*;
import java.lang.Thread;
class MyThread extends Thread{
public int x = 0;
public void run(){
System.out.println(++x);
}
}
class R implements Runnable{
private int x = 0;
public void run(){
System.out.println(++x);
}
}
public class Test {
public static void main(String[] args) throws Exception{
for(int i=0;i<10;i++){
Thread t = new MyThread();
t.start();
}
Thread.sleep(10000);//讓上面的線程運行完成
R r = new R();
for(int i=0;i<10;i++){
Thread t = new Thread(r);
t.start();
}
}
}
上面10個線程對象產生的10個線程運行時打印了10次1。下面10個線程對象產生的10個線程運行時打印了1到10。我們把下面的10個線程稱為同一實例(Runnable實例)的多個線程。
下節我們將研究線程對象方法,還是那句話,一般文檔中可以讀到的內容我不會介紹太多
請大家自己了解。
========================================================================
多線程編程——基礎篇 (三)
時間:2006-08-22
作者:
axman
線程對象的幾個重要的方法
盡管線程對象的常用方法可以通過API文檔來了解,但是有很多方法僅僅從API說明是無法詳細了解的。
本來打算用一節的篇幅來把線程方法中一些重要的知識說完,但這樣下來估計要很常的篇幅,可能要用好幾節才能說把和線程方法相關的一些重要的知識說完。
首先我們接基礎篇(二)來說明start()方法。
一個線程對象生成后,如果要產生一個執行的線程,就一定要調用它的start()方法.在介紹這個方法時不得不同時說明run方法.其實線程對象的run方法完全是一個接口回調方法,它是你這個線程對象要完成的具體邏輯.簡單說你要做什么就你在run中完成,而如何做,什么時候做就不需要你控制了,你只要調用start()方法,JVM就會管理這個線程對象讓它產生一個線程并注冊到線程處理系統中。
從表面上看,start()方法調用了run()方法,事實上,start()方法并沒有直接調用run方法.在JDK1.5以前start()方法是本地方法,它如何最終調用run方法已經不是JAVA程序員所能了解的.而在JDK1.5中,原來的那個本地start()方法被start0()代替,另個一個純JAVA的start()中調用本地方法start0(),而在start()方法中做了一個驗證,就是對一個全局變量(對象變量)started做檢驗,如果為true,則start()拋出異常,不會調用本地方法start0(),否則,先將該變量設有true,然后調用start0()。
從中我們可以看到這個為了控制一個線程對象只能運行成功一次start()方法.這是因為線程的運行要獲取當前環境,包括安全,父線程的權限,優先級等條件,如果一個線程對象可以運行多次,那么定義一個static 的線程在一個環境中獲取相應權限和優先級,運行完成后它在另一個環境中利用原來的權限和優先級等屬性在當前環境中運行,這樣就造成無法預知的結果.簡單說來,讓一個線程對象只能成功運行一次,是基于對線程管理的需要。
start()方法最本質的功能是從CPU中申請另一個線程空間來執行run()方法中的代碼,它和當前的線程是兩條線,在相對獨立的線程空間運行,也就是說,如果你直接調用線程對象的run()方法,當然也會執行,但那是在當前線程中執行,run()方法執行完成后繼續執行下面的代碼.而調用start()方法后,run()方法的代碼會和當前線程并發(單CPU)或并行(多CPU)執行。
所以請記住一句話[調用線程對象的run方法不會產生一個新的線程],雖然可以達到相同的執行結果,但執行過程和執行效率不同。
[線程的interrupt()方法,interrupted()和isInterrupted()]
這三個方法是關系非常密切而且又比較復雜的,雖然它們各自的功能很清楚,但它們之間的關系有大多數人不是真正的了解。
先說interrupt()方法,它是實例方法,而它也是最奇怪的方法,在java語言中,線程最初被設計為"隱晦難懂"的東西,直到現在它的語義不沒有象它的名字那樣準確。大多數人以為,一個線程象調用了interrupt()方法,那它對應的線程就應該被中斷而拋出異常,事實中,當一個線程對象調用interrupt()方法,它對應的線程并沒有被中斷,只是改變了它的中斷狀態。
使當前線程的狀態變以中斷狀態,如果沒有其它影響,線程還會自己繼續執行。
只有當線程執行到sleep,wait,join等方法時,或者自己檢查中斷狀態而拋出異常的情況下,線程才會拋出異常。
如果線程對象調用interrupt()后它對應的線程就立即中斷,那么interrupted()方法就不可能執行。
因為interrupted()方法是一個static方法,就是說只能在當前線程上調用,而如果一個線程interrupt()后它已經中斷了,那它又如何讓自己interrupted()?
正因為一個線程調用interrupt()后只是改變了中斷狀態,它可以繼續執行下去,在沒有調用sleep,wait,join等法或自己拋出異常之前,它就可以調用interrupted()來清除中斷狀態(還會原狀)interrupted()方法會檢查當前線程的中斷狀態,如果為 "被中斷狀態"則改變當前線程為"非中斷狀態"并返回true,如果為"非中斷狀態"則返回false,它不僅檢查當前線程是否為中斷狀態,而且在保證當前線程回來非中斷狀態,所以它叫"interrupted",是說中斷的狀態已經結束(到非中斷狀態了)isInterrupted()方法則僅僅檢查線程對象對應的線程是否是中斷狀態,并不改變它的狀態。
目前大家只能先記住這三個方法的功能,只有真正深入到多線程編程實踐中,才會體會到它們為什么是對象方法,為什么是類方法。
線程到底什么時候才被中斷拋出InterruptedException異常,我們將在提高篇中詳細討論。
[sleep(),join(),yield()方法]
在現在的環節中,我只能先說明這些方法的作用和調用原則,至于為什么,在基礎篇中無法深入,只能在提高篇中詳細說明。
sleep()方法中是類方法,也就是對當前線程而言的,程序員不能指定某個線程去sleep,只能是當前線程執行到sleep()方法時,睡眠指定的時間(讓其它線程運行).事實上也只能是類方法,在當前線程上調用.試想如果你調用一個線程對象的sleep()方法,那么這個對象對應的線程如果不是正在運行,它如何sleep()?所以只有當前線程,因為它正在執行,你才能保證它可以調用sleep()方法。
原則:[在同步方法中盡量不要調用線程的sleep()方法],或者簡單說,對于一般水平的程序員你基本不應該調用sleep()方法。
join()方法,正如第一節所言,在一個線程對象上調用join方法,是當前線程等待這個線程對象對應的線程結束,比如有兩個工作,工作A要耗時10秒鐘,工作B要耗時10秒或更多。我們在程序中先生成一個線程去做工作B,然后做工作A。
new?B().start();//做工作B
A();//做工作A
工作A完成后,下面要等待工作B的結果來進行處理.如果工作B還沒有完成我就不能進行下面的工作C,所以
B?b?=?new?B();
b.start();//做工作B
A();//做工作A
b.join();//等工作B完成。
C();//繼續工作C。
原則:[join是測試其它工作狀態的唯一正確方法],我見過很多人,甚至有的是博士生,在處理一項工作時如果另一項工作沒有完成,說讓當前工作線程sleep(x),我問他,你這個x是如何指定的,你怎么知道是100毫秒而不是99毫秒或是101毫秒?其實這就是OnXXX事件的實質,我們不是要等多長時間才去做什么事,而是當等待的工作正好完成的時候去做。
yield()方法也是類方法,只在當前線程上調用,理由同上,它主是讓當前線程放棄本次分配到的時間片原則:[不是非常必要的情況下,沒有理由調用它].調用這個方法不會提高任何效率,只是降低了CPU的總周期上面介紹的線程一些方法,基于(基礎篇)而言只能簡單提及.以后具體應用中我會結合實例詳細論述。
線程本身的其它方法請參看API文檔.下一節介紹非線程的方法,但和線程密切相關的兩[三]個對象方法:
[wait(),notify()/notifyAll()]
這是在多線程中非常重要的方法。
=========================================================================
多線程編程 —— 基礎篇 (四)
時間:2006-08-31
作者:
axman
[wait(),notify()/notityAll()方法]
關于這兩個方法,有很多的內容需要說明.在下面的說明中可能會有很多地方不能一下子明白,但在看完本節后,即使不能完全明白,你也一定要回過頭來記住下面的兩句話:
[wait(),notify()/notityAll()方法是普通對象的方法(Object超類中實現),而不是線程對象的方法]
[wait(),notify()/notityAll()方法只能在同步方法中調用]
[線程的互斥控制]
多個線程同時操作某一對象時,一個線程對該對象的操作可能會改變其狀態,而該狀態會影響另一線程對該對象的真正結果.
這個例子我們在太多的文檔中可以看到,就象兩個操售票員同時售出同一張票一樣.
線程A |
線程B |
1.線程A在數據庫中查詢存票,發現票C可以賣出 |
|
class="left"2.線程A接受用戶訂票請求,準備出票. |
|
|
3.這時切換到了線程B執行 |
|
4.線程B在數據庫中查詢存票,發現票C可以賣出 |
|
5.線程B將票賣了出去 |
6.切換到線程A執行,線程A賣了一張已經賣出的票 |
|
所以需要一種機制來管理這類問題的發生,當某個線程正在執行一個不可分割的部分時,其它線程不能不能同時執行這一部分.
象這種控制某一時刻只能有一個線程執行某個執行單元的機制就叫互斥控制或共享互斥(mutual exclusion)
在JAVA中,用synchornized關鍵字來實現互斥控制(暫時這樣認為,JDK1.5已經發展了新的機制)
[synchornized關鍵字]
把一個單元聲明為synchornized,就可以讓在同一時間只有一個線程操作該方法.
有人說synchornized就是一把鎖,事實上它確實存在鎖,但是是誰的鎖,鎖誰,這是一個非常復雜的問題.
每個對象只有一把監視鎖(monitor lock),一次只能被一個線程獲取.當一個線程獲取了這一個鎖后,其它線程就只能等待這個線程釋放鎖才能再獲取.
那么synchornized關鍵字到底鎖什么?得到了誰的鎖?
對于同步塊,synchornized獲取的是參數中的對象鎖:
synchornized(obj){
//...............
}
線程執行到這里時,首先要獲取obj這個實例的鎖,如果沒有獲取到線程只能等待.如果多個線程執行到這里,只能有一個線程獲取obj的鎖,然后執行{}中的語句,所以,obj對象的作用范圍不同,控制程序不同.
假如:
public void test(){
Object o = new Object();
synchornized(obj){
//...............
}
}
這段程序控制不了任何,多個線程之間執行到Object o = new Object();時會各自產生一個對象然后獲取這個對象有監視鎖,各自皆大歡喜地執行.
而如果是類的屬性:
class Test{
Object o = new Object();
public void test(){
synchornized(o){
//...............
}
}
}
所有執行到Test實例的synchornized(o)的線程,只有一個線程可以獲取到監視鎖.
有時我們會這樣:
public void test(){
synchornized(this){
//...............
}
}
那么所有執行Test實例的線程只能有一個線程執行.而synchornized(o)和synchornized(this)的范圍是不同的,因為執行到Test實例的synchornized(o)的線程等待時,其它線程可以執行Test實例的synchornized(o1)部分,但多個線程同時只有一個可以執行Test實例的synchornized(this).]
而對于
synchornized(Test.class){
//...............
}
這樣的同步塊而言,所有調用Test多個實例的線程賜教只能有一個線程可以執行.
[synchornized方法]
如果一個方法聲明為synchornized的,則等同于把在為個方法上調用synchornized(this).
如果一個靜態方法被聲明為synchornized,則等同于把在為個方法上調用synchornized(類.class).
現在進入wait方法和notify/notifyAll方法.這兩個(或叫三個)方法都是Object對象的方法,而不是線程對象的方法.如同鎖一樣,它們是在線程中調用某一對象上執行的.
class Test{
public synchornized void test(){
//獲取條件,int x 要求大于100;
if(x < 100)
wait();
}
}
這里為了說明方法沒有加在try{}catch(){}中,如果沒有明確在哪個對象上調用wait()方法,則為this.wait();
假如:
Test t = new Test();
現在有兩個線程都執行到t.test();方法.其中線程A獲取了t的對象鎖,進入test()方法內.
這時x小于100,所以線程A進入等待.
當一個線程調用了wait方法后,這個線程就進入了這個對象的休息室(waitset),這是一個虛擬的對象,但JVM中一定存在這樣的一個數據結構用來記錄當前對象中有哪些程線程在等待.
當一個線程進入等待時,它就會釋放鎖,讓其它線程來獲取這個鎖.
所以線程B有機會獲得了線程A釋放的鎖,進入test()方法,如果這時x還是小于100,線程B也進入了t的休息室.
這兩個線程只能等待其它線程調用notity[All]來喚醒.
但是如果調用的是有參數的wait(time)方法,則線程A,B都會在休息室中等待這個時間后自動喚醒.
[為什么真正的應用都是用while(條件)而不用if(條件)]
在實際的編程中我們看到大量的例子都是用?
while(x < 100)
wait();go();而不是用if,為什么呢?
在多個線程同時執行時,if(x <100)是不安全的.因為如果線程A和線程B都在t的休息室中等待,這時另一個線程使x==100了,并調用notifyAll方法,線程A繼續執行下面的go().而它執行完成后,x有可能又小于100,比如下面的程序中調用了--x,這時切換到線程B,線程B沒有繼續判斷,直接執行go();就產生一個錯誤的條件,只有while才能保證線程B又繼續檢查一次.
[notify/notifyAll方法]
這兩個方法都是把某個對象上休息區內的線程喚醒,notify只能喚醒一個,但究竟是哪一個不能確定,而notifyAll則喚醒這個對象上的休息室中所有的線程.
一般有為了安全性,我們在絕對多數時候應該使用notifiAll(),除非你明確知道只喚醒其中的一個線程.
那么是否是只要調用一個對象的wait()方法,當前線程就進入了這個對象的休息室呢?事實中,要調用一個對象的wait()方法,只有當前線程獲取了這個對象的鎖,換句話說一定要在這個對象的同步方法或以這個對象為參數的同步塊中.
class MyThread extends Thread{
Test t = new Test();
public void run(){
t.test();
System.out.println("Thread say:Hello,World!");
}
}
public class Test {
int x = 0;
public void test(){
if(x==0)
try{
wait();
}catch(Exception e){}
}
public static void main(String[] args) throws Exception{
new MyThread().start();
}
}
這個線程就不會進入t的wait方法而直接打印出Thread say:Hello,World!.
而如果改成:
public class Test {
int x = 0;
public synchornized void test(){
if(x==0)
try{
wait();
}catch(Exception e){}
}
public static void main(String[] args) throws Exception{
new MyThread().start();
}
}
我們就可以看到線程一直等待,注意這個線程進入等待后沒有其它線程喚醒,除非強行退出JVM環境,否則它一直等待.
所以請記住:
[線程要想調用一個對象的wait()方法就要先獲得該對象的監視鎖,而一旦調用wait()后又立即釋放該鎖]
以上是對線程基礎知識的簡單介紹,不進入實例,我們無法真正了解它的真實意義.下節我們就會以實例來進入多線程編程的 實戰篇
轉載自dev2dev網友axman的go deep into java專欄。
posted on 2007-11-09 13:37
lk 閱讀(318)
評論(0) 編輯 收藏 所屬分類:
j2se