一、簡介
1
、什么是線程
要說線程,就必須先說說進程,進程就是程序的運行時的一個實例。線程呢可以看作單獨地占有
CPU
時間來執行相應的代碼的。對早期的計算機(如
DOS
)而言,線程既是進程,進程既是進程,因為她是單線程的。當然一個程序可以是多線程的,多線程的各個線程看上去像是并行地獨自完成各自的工作,就像一臺一臺計算機上運行著多個處理機一樣。在多處理機計算機上實現多線程時,它們確實可以并行工作,而且采用適當的分時策略可以大大提高程序運行的效率。但是二者還是有較大的不同的,線程是共享地址空間的,也就是說多線程可以同時讀取相同的地址空間,并且利用這個空間進行交換數據。
2
、為什么要使用線程
為什么要使用多線程呢?學過《計算機體系結構》的人都知道。將順序執行程序和采用多線程并行執行程序相比,效率是可以大大地提高的。比如,有五個線程
thread1, thread2, thread3, thread4, thread5
,所耗的
CPU
時間分別為
4
,
5
,
1
,
2
,
7
。(假設
CPU
輪換周期為
4
個
CPU
時間,而且線程之間是彼此獨立的)順序執行需要花費
19
個
CPU
時間,而并行需要的時間肯定少于
19
個
CPU
時間,至于具體多少時間要看那些線程是可以同時執行的。這是在非常小規模的情況下,要是面對大規模的進程之間的交互的話,效率可以表現得更高。
3
、
java
中是如何實現多線程的
與其他語言不一樣的是,線程的觀念在
java
是語言中是重要的,根深蒂固的,因為在
java
語言中的線程系統是
java
語言自建的,
java
中有專門的支持多線程的
API
庫,所以你可以以最快的速度寫一個支持線程的程序。在使用
java
創建線程的時候,你可以生成一個
Thread
類或者他的子類對象,并給這個對象發送
start()
消息(程序可以向任何一個派生自
Runnable
接口的類對象發送
start()
消息的),這樣一來程序會一直執行,直到
run
返回為止,此時該線程就死掉了。
在
java
語言中,線程有如下特點:
§
在一個程序中而言,主線程的執行位置就是
main
。而其他線程執行的位置,程序員是可以自定義的。值得注意的是對
Applet
也是一樣。
§
每個線程執行其代碼的方式都是一次順序執行的。
§
一個線程執行其代碼是與其他線程獨立開來的。如果諸線程之間又相互協作的話,就必須采用一定的交互機制。
§
前面已經說過,線程是共享地址空間的,如果控制不當,這里很有可能出現死鎖。
各線程之間是相互獨立的,那么本地變量對一個線程而言就是完全獨立,私有的。所以呢,線程執行時,每個線程都有各自的本地變量拷貝。對象變量
(instance variable)
在線程之間是可以共享的,這也就是為什么在
java
中共享數據對象是如此的好用,但是
java
線程不能夠武斷地訪問對象變量:他們是需要訪問數據對象的權限的。
二、準備知識
在分析這個例子之前,然我們先看看關于線程的幾個概念,上鎖,信號量,和
java
所提供的
API
。
上鎖
對于大多數的程序而言,他們都需要線程之間相互的通訊來完成整個線程的生命周期,二實現線程之間同步的最簡單的辦法就是上鎖。為了防止相互關聯的兩個線程之間錯誤地訪問共享資源,線程需要在訪問資源的時候上鎖和解鎖,對于鎖而言,有讀鎖,寫鎖和讀寫鎖等不同的同步策略。在
java
中,所有的對象都有鎖;線程只需要使用
synchronized
關鍵字就可以獲得鎖。在任一時刻對于給定的類的實例,方法或同步的代碼塊只能被一個線程執行。這是因為代碼在執行之前要求獲得對象的鎖。
信號量
通常情況下,多個線程所訪問為數不多的資源,那怎么控制呢?一個比較非常經典而起非常簡單的辦法就是采用信號量機制。信號量機制的含義就是定義一個信號量,也就是說能夠提供的連接數;當有一個線程占用了一個連接時,信號量就減一;當一個線程是放了連接時,信號量就加一。采用這種方法就可以簡單有效地控制線程的同步問題,而且實現起來也特別方便。看下面的代碼:
class Semaphore {
private int count;
public Semaphore(int count) {
this.count = count;
}
public synchronized void acquire() {
while(count == 0) {
try {
wait();
} catch (InterruptedException e) {
//keep trying
}
}
count--;
}
public synchronized void release() {
count++;
notify(); //alert a thread that′
s blocking on this semaphore
}
}
java
中提供了哪些
api
以編寫多線程程序
這里只列出幾個常用的方法和屬性值。
屬性值,有三個
MAX_PRIORITY
,
MIN_PRIORITY
,
NORM_PRIORITY
方法:
Thread(); //
建立一個線程
void run(); //
對于一個繼承了
Runnable
接口的
class
而言,
//
他運行一個線程
,
否著他什么都不做
void setPriority(int newPriority); //
設置優先級
void start(); //
運行一個程序
void sleep(long millis); //
線程睡眠
millis
毫秒
static void yield(); //
臨時
pause
一個程序以便起他線程運行
三、程序示例
例一、
讓我們看看下面的例子。取錢的流程是輸入密碼,然后確定要取得金額,如果所取的金額小于或等于可以取出的金額,
WITHDRAW
則返回
TRUE
,然后
ATM
機出錢,然后打印清單;否則返回
FALSE
,然后打印清單。如下圖:
public class AutomatedTellerMachine extends Teller {
public void withdraw(float amount) {
Account a = getAccount();
if (a.deduct(amount))
dispense(amount);
printReceipt();
}
}
public class Account {
private float total;
public boolean deduct(float t) {
if (t <= total) {
total -= t;
return true;
}
return false;
}
}
就這個例子而言,假設有這種情況,對同一個賬號可以在不同的地方取錢,在同一時間,不同地點,妻子和丈夫取錢,妻子輸入了賬號上的最大金額,丈夫也是一樣,假如妻子輸入后已經得到
true
的返回值,但是丈夫的線程所得到的值還沒有更新,這樣丈夫也能夠得到
true
的返回值,這樣就出現了問題!這個問題怎么解決呢?在
java
里面提供了控制機制以保證
deduct
操作時的原子性,那就是關鍵字
synchronized
。
在
Account
的
deduct
方法加入
synchronized
就可以解決這個問題。
例二、
在這里我們用多線程中最典型的例子,生產者與消費者問題。在這個例子里面我們定義了生產者
Producer
,消費者
Consumer
和倉庫
Warehouse
三個類,在整個程序的生命周期里,生產者隨機地制造出產品放到倉庫中,消費者也是隨即地從倉庫中取出產品。
import exception.ProducerConsumerException;
/**
* Consumer.java
* Consumer
* By: Jiabo
* Date: Mar 21, 2004
* Time: 2:47:58 PM
*/
public class Consumer extends Thread {
private Warehouse warehouse;
private String id;
public Consumer(Warehouse warehouse, String id) {
this.warehouse = warehouse;
this.id = id;
}
public void run() {
int tmp = (int) Math.random() * 10;
try {
warehouse.get(tmp);
System.out.println("Consumer # " + this.id + " get " + tmp);
} catch (ProducerConsumerException e) {
e.printStackTrace();
}
try {
sleep((int) (Math.random() * 100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在這個類中,值得注意的一點是
run
方法中必須使用
try-catch
,因為,消費者從倉庫中取東西時有可能諸如倉庫中的儲量不夠得異常,在消費者里面也是一樣,只不過異常變為倉庫已滿。
import exception.*;
/**
* Producer.java
* Producer
* By: Jiabo
* Date: Mar 21, 2004
* Time: 2:47:45 PM
*/
public class Producer extends Thread {
private Warehouse warehouse;
private String id;
public Producer(Warehouse warehouse, String id) {
this.warehouse = warehouse;
this.id = id;
}
public void run() {
int tmp = (int) Math.random() * 10;
if (tmp != 0) {
try {
warehouse.put(tmp);
System.out.println("Consumer # " + this.id + " put " + tmp);
} catch (ProducerConsumerException e) {
e.printStackTrace();
}
}
try {
sleep((int) (Math.random() * 100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
最重要的一部分在
Warehouse
類,如上所說為了保證
get
何
set
的原子性,在這里使用了
synchronized
關鍵字,并且在操作時拋出了可能跑出的異常。
import exception.*;
/**
* Warehouse
* By: Jiabo
* Date: Mar 21, 2004
* Time: 2:48:10 PM
*/
public class Warehouse {
// max capability of the warehouse
private int MAX;
private int contents;
// init with max capacity
public Warehouse(int max) {
this.MAX = max;
this.contents = 0;
}
public synchronized void get(int amount) throws ProducerConsumerException {
// the amount you want to get is bigger than the contends that the warehouse stores
if (amount > this.contents) {
throw new NotEnoughGoodsException();
}
amount -= contents;
}
public synchronized void put(int amount) throws ProducerConsumerException {
// the amount you want to put is out of the capability of the warehouse
if (amount > (this.MAX - this.contents)) {
throw new WarehouseFullException();
} else if (this.contents == 0) {
// warehouse is empty
throw new WarehouseEmptyException();
}
amount += contents;
}
}
?