理解程序啦租、進程、線程的概念
程序可以理解為靜態(tài)的代碼
進程可以理解為執(zhí)行中的程序
線程可以理解為進程的進一步細分日丹,程序的一條執(zhí)行路徑
使用多線程的優(yōu)點:
提高應用程序的響應鲤桥。對圖形化界面更有意義,可增強用戶體驗拢操。
提高計算機系統(tǒng)CPU的利用率
改善程序結(jié)構(gòu)枉圃。將既長又復雜的進程分為多個線程,獨立運行庐冯,利于理解和修改
在java中要想實現(xiàn)多線程孽亲,有兩種手段,一種是繼續(xù)Thread類展父,另外一種是實現(xiàn)Runable接口
繼承java.lang.Thread類
下面來看一個簡單的實例:
class Thread1 extends Thread{
private String name;
public Thread1(String name) {
this.name=name;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "運行 : " + i);
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();
}
}
輸出
輸出:
A運行 : 0
B運行 : 0
A運行 : 1
A運行 : 2
A運行 : 3
A運行 : 4
B運行 : 1
B運行 : 2
B運行 : 3
B運行 : 4
再運行一下:
A運行 : 0
B運行 : 0
B運行 : 1
B運行 : 2
B運行 : 3
B運行 : 4
A運行 : 1
A運行 : 2
A運行 : 3
A運行 : 4
說明:
程序啟動運行main時候返劲,java虛擬機啟動一個進程,主線程main在main()調(diào)用時候被創(chuàng)建栖茉。隨著調(diào)用Thread1的兩個對象的start方法篮绿,另外兩個線程也啟動了,這樣吕漂,整個應用就在多線程下運行亲配。
以下是關系到線程運行狀態(tài)的幾個方法:
1)start方法
start()用來啟動一個線程,當調(diào)用start方法后惶凝,系統(tǒng)才會開啟一個新的線程來執(zhí)行用戶定義的子任務吼虎,在這個過程中,會為相應的線程分配需要的資源苍鲜。
2)run方法
run()方法是不需要用戶來調(diào)用的思灰,當通過start方法啟動一個線程之后,當線程獲得了CPU執(zhí)行時間混滔,便進入run方法體去執(zhí)行具體的任務洒疚。注意歹颓,繼承Thread類必須重寫run方法,在run方法中定義具體要執(zhí)行的任務油湖。
3)sleep方法
sleep相當于讓線程睡眠巍扛,交出CPU,讓CPU去執(zhí)行其他的任務乏德。
實現(xiàn)java.lang.Runnable接口
用Runnable也是非常常見的一種电湘,我們只需要重寫run方法即可。下面也來看個實例:
class Thread2 implements Runnable{
private String name;
public Thread2(String name) {
this.name=name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "運行 : " + i);
try {
Thread.sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
new Thread(new Thread2("C")).start();
new Thread(new Thread2("D")).start();
}
}
輸出
輸出:
C運行 : 0
D運行 : 0
D運行 : 1
C運行 : 1
D運行 : 2
C運行 : 2
D運行 : 3
C運行 : 3
D運行 : 4
C運行 : 4
說明:
Thread2類通過實現(xiàn)Runnable接口鹅经,使得該類有了多線程類的特征。run()方法是多線程程序的一個約定怎诫。所有的多線程代碼都在run方法里面瘾晃。Thread類實際上也是實現(xiàn)了Runnable接口的類。
在啟動的多線程的時候幻妓,需要先通過Thread類的構(gòu)造方法Thread(Runnable target) 構(gòu)造出對象蹦误,然后調(diào)用Thread對象的start()方法來運行多線程代碼。
實際上所有的多線程代碼都是通過運行Thread的start()方法來運行的肉津。因此强胰,不管是擴展Thread類還是實現(xiàn)Runnable接口來實現(xiàn)多線程,最終還是通過Thread的對象的API來控制線程的妹沙,熟悉Thread類的API是進行多線程編程的基礎偶洋。
Thread和Runnable的區(qū)別
對比一下繼承的方式 vs 實現(xiàn)的方式
1.聯(lián)系:public class Thread implements Runnable(繼承的方式的Thread也實現(xiàn)了Runnable接口)
2.哪個方式好?
實現(xiàn)的方式優(yōu)于繼承的方式 why?
① 避免了java單繼承的局限性
② 如果多個線程要操作同一份資源(或數(shù)據(jù))距糖,更適合使用實現(xiàn)的方式
看一個例子:
//模擬火車站售票窗口玄窝,開啟三個窗口售票,總票數(shù)為100張
//存在線程的安全問題
class Window extends Thread {
int ticket = 100;
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "售票悍引,票號為:"+ ticket--);
} else {
break;
}
}
}
}
public class TestWindow {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
class Window implements Runnable {
int ticket = 100;//要將全局變量聲明為靜態(tài)恩脂,不然每個對象都有這個屬性,會賣出300張票
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售票趣斤,票號為:"+ ticket--);
} else {
break;
}
}
}
}
public class Main {
//模擬火車站售票窗口俩块,開啟三個窗口售票,總票數(shù)為100張
//存在線程的安全問題
public static void main(String[] args) {
Window w1 = new Window();
Thread t1 = new Thread(w1, "t1");
Thread t2 = new Thread(w1, "t2");
Thread t3 = new Thread(w1, "t3");
t1.start();
t2.start();
t3.start();
}
}
問題原因:
某個線程執(zhí)行完輸出ticket后浓领,還沒有來得及ticket--玉凯,CPU時間片被分配給了另外一個線程,導致同一個票號被輸出2次联贩。
另外一種情況壮啊,打印到ticket=1時,有2個線程同時進入到了條件里撑蒜,導致-1的票號被輸出毅否。
解決辦法旁涤,為關鍵代碼段笨鸡,加鎖,參見后文藤树。
中斷線程
當線程的run方法執(zhí)行方法體中最后一條語句后,并經(jīng)由return語句返回時拓萌,或者出現(xiàn)了在方法中過沒有捕獲的異常時岁钓,線程將被終止。
線程同步
根據(jù)各線程訪問數(shù)據(jù)的次序微王,可能會產(chǎn)生訛誤的對象屡限。這樣的一個情況稱為競爭條件(race condition)。
1)競爭條件的一個例子
銀行例程:多線程操作時炕倘,本應恒等的余額總值發(fā)生了變化钧大。
public class SynchBankTest
{
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static void main(String[] args)
{
Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
int i;
for (i = 0; i < NACCOUNTS; i++)
{
TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
Thread t = new Thread(r);
t.start();
}
}
}
public class Bank
{
private final double[] accounts;
public Bank(int n, double initialBalance)
{
accounts = new double[n];
for (int i = 0; i < accounts.length; i++)
accounts[i] = initialBalance;
}
public void transfer(int from, int to, double amount) throws InterruptedException
{
if (accounts[from] >= amount) {
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
}
}
public double getTotalBalance()
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
public int size()
{
return accounts.length;
}
}
public class TransferRunnable implements Runnable
{
private Bank bank;
private int fromAccount;
private double maxAmount;
private int DELAY = 10;
public TransferRunnable(Bank b, int from, double max)
{
bank = b;
fromAccount = from;
maxAmount = max;
}
public void run()
{
try
{
while (true)
{
int toAccount = (int) (bank.size() * Math.random());
double amount = maxAmount * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
}
catch (InterruptedException e)
{
}
}
}
2)詳解競爭條件
假定兩個線程同時執(zhí)行指令
accounts[to] += amount;
這不是原子操作。該指令可能被處理如下:
1)將accounts[to]加載到寄存器
2)增加amount
3)將結(jié)果寫回accounts[to]
假定第一個線程執(zhí)行步驟1和2罩旋,然后啊央,它被剝奪了運行權。假定第二個線程被喚醒并修改了accounts數(shù)組中的同一項涨醋。然后瓜饥,第一個線程被喚醒并完成其第三步。這一動作擦去了第二個線程所做的更新浴骂。
線程同步
當使用多個線程來訪問同一個數(shù)據(jù)時乓土,非常容易出現(xiàn)線程安全問題(比如多個線程都在操作同一數(shù)據(jù)導致數(shù)據(jù)不一致),所以我們用同步機制來解決這些問題。
鎖和條件的關鍵之處:
鎖用來保護代碼片段溯警,任意時刻只能有一個線程執(zhí)行被保護的代碼帐我。
鎖可以管理試圖進入保護代碼片段的線程
鎖可以擁有一個或者多個相關的條件對象
每個條件對象管理那些已經(jīng)進入被保護的代碼段但還不能運行的線程。
鎖對象
在Java SE5.0引入ReentrantLock類愧膀。Lock是Java.util.concurrent.locks包下的接口拦键,Lock 實現(xiàn)提供了比使用synchronized 方法和語句可獲得的更廣泛的鎖定操作。
用ReentrantLock保護代碼塊的基本結(jié)構(gòu)如下:
myLock.lock(); //a ReentrantLock object
try
{
critical section
}
finally
{
myLock.unlock();//確保代碼拋出異常鎖必須被釋放
}
這一結(jié)構(gòu)確保任何時刻只有一個線程進入臨界區(qū)檩淋。一旦一個線程封鎖了鎖對象芬为,其他任何線程都無法通過lock語句。當其他線程調(diào)用lock時蟀悦,他們被阻塞媚朦,直到第一個線程釋放鎖對象。把解鎖操作放在finally子句之內(nèi)是至關重要的日戈。如果在臨界區(qū)的代碼拋出異常询张,鎖必須釋放。否則浙炼,其他線程將永遠阻塞份氧。
public class Bank
{
private Lock bankLock= new ReentrantLock();
public void transfer(int from, int to, double amount)
{
bankLock.lock();
try
{
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
}
finally
{
bankLock.unlock();
}
}
public double getTotalBalance()
{
bankLock.lock();
try
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
finally
{
bankLock.unlock();
}
}
}
假定一個線程調(diào)用transfer唯袄,在執(zhí)行結(jié)束前被剝奪運行權。假定第二個線程也調(diào)用了transfer蜗帜,由于第二個線程并不能獲得鎖恋拷,將在調(diào)用lock方法時被阻塞。他必須等待第一個線程完成transfer方法之后才能再度被激活厅缺。當?shù)谝粋€線程釋放鎖時蔬顾,第二個線程才能開始運行。這樣余額就不會出錯了湘捎。
每個Bank對象有自己的ReentrantLock對象诀豁,如果兩個線程試圖訪問同一個Bank對象,那么鎖以串行方式提供服務窥妇。但是舷胜,如果兩個線程訪問不同的Bank對象,每個線程得到不同的鎖對象秩伞,兩個線程都不會發(fā)生阻塞。兩個線程在操作不同的Bank實例的時候欺矫,線程之間不會相互影響纱新。
鎖是可重入的,因為線程可以重復的獲得已經(jīng)持有的鎖穆趴。鎖保持一個持有計數(shù)來跟蹤對Lock方法的嵌套調(diào)用脸爱。線程每一次調(diào)用都用unlock來釋放鎖。由于這一特性未妹,被一個鎖保護的代碼可以調(diào)用另一個使用相同鎖的方法簿废。例如,transfer方法調(diào)用getTotalBalance方法络它,這也會封鎖bankLock對象族檬,此時bankLock對象的持有計數(shù)為2,當getTotalBalance方法退出時化戳,持有計數(shù)變?yōu)?单料,當transfer方法退出時,持有計數(shù)變?yōu)?点楼。線程釋放鎖扫尖。通常,可能想要保護需若干個操作來更新或者檢查共享對象的代碼塊掠廓。要確保這些操作完成后换怖,另一個線程才能使用相同的對象。
要留心臨界區(qū)的代碼蟀瞧,不要因為異常的拋出二跳出了臨界區(qū)沉颂。如果在臨界區(qū)代碼結(jié)束之前拋出了異常条摸,finally字句釋放鎖,但會使對象可能出于一種受損狀態(tài)兆览。
條件對象
通常屈溉,線程進入臨界區(qū),卻發(fā)現(xiàn)在某一條件滿足后才能執(zhí)行抬探。要使用一個條件對象來管理那些已經(jīng)獲得了一個鎖子巾,但是不能做有用工作的線程。(條件對象經(jīng)常被稱為條件變量)
分析上文中的銀行模擬程序小压,
if(bank.getBalance(from)>=amount)
transfer(from, to, amount);
如果當前程序通過if條件判斷线梗,且在調(diào)用transfer之前被中斷,在線程再次運行前怠益,賬戶余額可能已經(jīng)低于提款金額仪搔。必須確保沒有其他線程在本檢查余額與轉(zhuǎn)賬活動之間修改余額。通過使用鎖來保護檢查與轉(zhuǎn)賬動作來做到這一點:
public void transfer(int from, int to, double amount)
{
bankLock.lock();
try
{
while (accounts[from] < amount)
{
//wait()
}
//transfer funds
........
finally
{
bankLock.unlock();
}
}
現(xiàn)在蜻牢,當賬戶中沒有足夠的余額時烤咧,等待直到另一個線程向賬戶中注入資金。但是抢呆,這一線程剛剛獲得了bankLock的排他性訪問煮嫌,因此別的線程沒有進行存款操作的機會,這就是為什么需要用條件對象的原因抱虐。
一個鎖對象可以有一個或者多個相關的條件對象昌阿。可以用newCondition方法獲得一個條件對象恳邀。習慣的給每一個條件對象命名為可以反應它所表達條件的名字懦冰。如sufficientFunds = bankLock.newCondition();
如果transfer方法發(fā)現(xiàn)余額不足,它調(diào)用sufficientFunds.await();當前線程被他阻塞了谣沸,并放棄鎖刷钢。我們希望這樣可以等待另一個線程進行增加賬戶余額的操作。
等待獲得鎖的線程和調(diào)用await方法的線程在本質(zhì)上存在不同乳附。一旦一個線程調(diào)用await方法闯捎,他進入該條件的等待集。當該鎖可用時许溅,該線程不能馬上解除阻塞瓤鼻。相反,它處于阻塞狀態(tài)贤重,直到另一個線程調(diào)用同一條件上的signalAll方法時為止茬祷。
當另一個線程轉(zhuǎn)賬時,它應該調(diào)用sufficientFunds.signalAll();這一調(diào)用重新激活因為這一條件等待的所有線程并蝗。當這些線程從等待集當中移除時祭犯,他們再次成為可運行的秸妥,調(diào)度器將再次激活它們。同時沃粗,他們試圖重新進入該對象粥惧。一旦鎖成為可用,他們將從await調(diào)用返回最盅,獲得該鎖并從被阻塞的地方繼續(xù)執(zhí)行突雪。
此時,線程應該再次測試該條件涡贱。由于無法確保該條件被滿足咏删,signalAll方法僅僅通知正在等待的線程:此時有可能已經(jīng)滿足條件,值得再次去檢測條件问词。
最關重要的是最終需要某個其他線程調(diào)用signalAll方法督函。當一個線程調(diào)用await時,他沒有辦法自己激活自身激挪,它寄希望于其他線程辰狡。如果沒有其他線程重新來激活等待的線程,他就永遠不再運行垄分。導致死鎖宛篇。如果所有其他線程被阻塞,最后一個線程再解除其他阻塞線程之前就調(diào)用await锋喜,那么它也被阻塞些己。沒有線程解除其他阻塞線程豌鸡,那么該程序就掛起嘿般。
應該何時調(diào)用signalAll呢,在本例中涯冠,當一個賬戶的余額發(fā)生改變時炉奴,等待的線程就有機會檢查余額。調(diào)用signalAll不會立即激活一個等待線程蛇更。它僅僅解除等待線程的阻塞瞻赶,以便這些線程可以在當前線程同步推出后,通過競爭實現(xiàn)對對象的訪問派任。當一個線程擁有某個條件的鎖時砸逊,它僅僅可以在該條件上調(diào)用await,signalAll和signal方法掌逛。
以下為完整的例子:
synch/Bank.java
package synch;
import java.util.concurrent.locks.*;
public class Bank
{
private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;
public Bank(int n, double initialBalance)
{
accounts = new double[n];
for (int i = 0; i < accounts.length; i++)
accounts[i] = initialBalance;
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}
public void transfer(int from, int to, double amount) throws InterruptedException
{
bankLock.lock();
try
{
while (accounts[from] < amount)
sufficientFunds.await();
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll();
}
finally
{
bankLock.unlock();
}
}
public double getTotalBalance()
{
bankLock.lock();
try
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
finally
{
bankLock.unlock();
}
}
public int size()
{
return accounts.length;
}
}
[java] view plain copy
synch/TransferRunnable.java
package synch;
public class TransferRunnable implements Runnable
{
private Bank bank;
private int fromAccount;
private double maxAmount;
private int DELAY = 10;
public TransferRunnable(Bank b, int from, double max)
{
bank = b;
fromAccount = from;
maxAmount = max;
}
public void run()
{
try
{
while (true)
{
int toAccount = (int) (bank.size() * Math.random());
double amount = maxAmount * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
}
catch (InterruptedException e)
{
}
}
}
synch/SynchBankTest
package synch;
public class SynchBankTest
{
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static void main(String[] args)
{
Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
int i;
for (i = 0; i < NACCOUNTS; i++)
{
TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
Thread t = new Thread(r);
t.start();
}
}
}
生產(chǎn)者消費者問題
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Buffer {
private final Lock lock;
private final Condition notFull;
private final Condition notEmpty;
private int maxSize;
private List<Date> storage;
Buffer(int size){
//使用鎖lock师逸,并且創(chuàng)建兩個condition,相當于兩個阻塞隊列
lock=new ReentrantLock();
notFull=lock.newCondition();
notEmpty=lock.newCondition();
maxSize=size;
storage=new LinkedList<>();
}
public void put() {
lock.lock();
try {
while (storage.size() ==maxSize ){//如果隊列滿了
System.out.print(Thread.currentThread().getName()+": wait \n");;
notFull.await();//阻塞生產(chǎn)線程
}
storage.add(new Date());
System.out.print(Thread.currentThread().getName()+": put:"+storage.size()+ "\n");
Thread.sleep(1000);
notEmpty.signalAll();//喚醒消費線程
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
lock.unlock();
}
}
public void take() {
lock.lock();
try {
while (storage.size() ==0 ){//如果隊列滿了
System.out.print(Thread.currentThread().getName()+": wait \n");;
notEmpty.await();//阻塞消費線程
}
Date d=((LinkedList<Date>)storage).poll();
System.out.print(Thread.currentThread().getName()+": take:"+storage.size()+ "\n");
Thread.sleep(1000);
notFull.signalAll();//喚醒生產(chǎn)線程
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
class Producer implements Runnable{
private Buffer buffer;
Producer(Buffer b){
buffer=b;
}
@Override
public void run() {
while(true){
buffer.put();
}
}
}
class Consumer implements Runnable{
private Buffer buffer;
Consumer(Buffer b){
buffer=b;
}
@Override
public void run() {
while(true){
buffer.take();
}
}
}
public class Main{
public static void main(String[] arg){
Buffer buffer=new Buffer(10);
Producer producer=new Producer(buffer);
Consumer consumer=new Consumer(buffer);
for(int i=0;i<3;i++){
new Thread(producer,"producer-"+i).start();
}
for(int i=0;i<3;i++){
new Thread(consumer,"consumer-"+i).start();
}
}
}
synchronized關鍵字
Lock和condition接口為程序設計人員提供了高度的鎖定控制豆混。大多數(shù)情況下并不需要那樣的控制篓像,并且可以使用一種嵌入到Java語言內(nèi)部的機制动知。從Java1.0版本開始,Java中的每個對象都有一個內(nèi)部鎖员辩。如果一個方法用synchronized關鍵字聲明盒粮,那么對象的鎖將保護整個方法。也就是說奠滑,要調(diào)用該方法丹皱,線程必須獲得內(nèi)部的對象鎖。使用synchronized修飾的方法或者代碼塊可以看成是一個原子操作养叛。
synchronized方法是一種粗粒度的并發(fā)控制种呐,某一時刻,只能有一個線程執(zhí)行該synchronized方法;
synchronized塊則是一種細粒度的并發(fā)控制弃甥,只會將塊中的代碼同步爽室,位于方法內(nèi)、synchronized塊之外的代碼是可以被多個線程同時訪問到的淆攻。
用同步方法實現(xiàn)的銀行例子:
package synch2;
public class Bank
{
private final double[] accounts;
public Bank(int n, double initialBalance)
{
accounts = new double[n];
for (int i = 0; i < accounts.length; i++)
accounts[i] = initialBalance;
}
public synchronized void transfer(int from, int to, double amount) throws InterruptedException
{
while (accounts[from] < amount)
wait();
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
notifyAll();
}
public synchronized double getTotalBalance()
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
public int size()
{
return accounts.length;
}
}
public class TransferRunnable implements Runnable
{
private Bank bank;
private int fromAccount;
private double maxAmount;
private int DELAY = 10;
public TransferRunnable(Bank b, int from, double max)
{
bank = b;
fromAccount = from;
maxAmount = max;
}
public void run()
{
try
{
while (true)
{
int toAccount = (int) (bank.size() * Math.random());
double amount = maxAmount * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
}
catch (InterruptedException e)
{
}
}
}
public class SynchBankTest2
{
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static void main(String[] args)
{
Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
int i;
for (i = 0; i < NACCOUNTS; i++)
{
TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
Thread t = new Thread(r);
t.start();
}
}
}
線程狀態(tài)
線程可以有以下6中狀態(tài)
- New(新創(chuàng)建)
- Runnable(可運行)
- Blocked(被阻塞)
- Waiting(等待)
- Timed waiting(計時等待)
- Terminated(被終止)
要想確定一個線程當前的狀態(tài)阔墩,可調(diào)用getState方法。
由上圖可以看出瓶珊,一個線程由出生到死亡分為五個階段:
1).創(chuàng)建狀態(tài)
?當用new操作符創(chuàng)建一個新的線程對象時啸箫,該線程處于創(chuàng)建狀態(tài)。
?處于創(chuàng)建狀態(tài)的線程只是一個空的線程對象伞芹,系統(tǒng)不為它分配資源
2). 可運行狀態(tài)
? 執(zhí)行線程的start()方法將為線程分配必須的系統(tǒng)資源忘苛,安排其運行,并調(diào)用線程體—run()方法唱较,這樣就使得該線程處于可運行( Runnable )狀態(tài)扎唾。
? 這一狀態(tài)并不是運行中狀態(tài)(Running ),因為線程也許實際上并未真正運行南缓。這取決于操作系統(tǒng)給線程提供的運行時間胸遇。
一旦一個線程開始運行,它不必始終保持運行汉形。事實上纸镊,運行中的線程被中斷,目的是為了讓其他線程獲得運行機會概疆。線程調(diào)度的細節(jié)依賴于操作系統(tǒng)提供的服務逗威。搶占式調(diào)度系統(tǒng)給每一個可運行線程一個時間片來執(zhí)行任務。當時間片用完岔冀,操作系統(tǒng)剝奪該線程的運行權凯旭,并給另一個線程機會運行。
3).被阻塞的線程或等待線程
當一個線程試圖獲取一個內(nèi)部的對象鎖,而該鎖被其他的線程持有尽纽,則該線程進入阻塞狀態(tài)咐蚯。當所有其他線程釋放該鎖,并且線程調(diào)度器允許本線程持有它的時候弄贿,該線程將變?yōu)榉亲枞麪顟B(tài)春锋。
當線程等待另一個線程通知調(diào)度器一個條件時,它自己 進入等待狀態(tài)差凹。
方法有一個超時參數(shù)時期奔。調(diào)用他們導致線程進入計時等待狀態(tài)。這一狀態(tài)將一直保持到超時期滿或者收到適當?shù)耐ㄖD颉в谐瑫r參數(shù)的方法有:Thread.sleep
4). 被終止的線程
兩個原因之一而被終止:
當線程的run方法執(zhí)行結(jié)束后呐萌,該線程自然消亡。
因為一個沒有捕獲的異常終止了run方法而意外死亡谊娇。
阻塞隊列
對于實際貶稱過來說肺孤,應該盡可能原理底層結(jié)構(gòu)。使用由并發(fā)處理的專業(yè)人士實現(xiàn)的較高層次的結(jié)構(gòu)要方便和安全的多济欢。
許多線程問題可以通過使用一個或多個隊列以優(yōu)雅且安全的方式將其形式化赠堵。生產(chǎn)者線程向隊列插入元素,消費者線程則取出它們法褥。使用隊列茫叭,可以安全地從一個線程向另一個線程傳遞數(shù)據(jù)。例如半等,轉(zhuǎn)賬程序中揍愁,轉(zhuǎn)賬線程將轉(zhuǎn)賬指令對象插入一個隊列中,而不是直接訪問銀行對象杀饵。另一個線程從隊列中取出指令執(zhí)行轉(zhuǎn)賬莽囤。只有該線程可以訪問該銀行對象的內(nèi)部。因此不需要同步凹髓。(當然烁登,線程安全的隊列類的實現(xiàn)者不能不考慮鎖和條件怯屉。)
當試圖向隊列添加元素而隊列已滿蔚舀,或是想從隊列移出元素而隊列為空的時候,阻塞隊列(BlockingQueue)導致線程阻塞锨络。隊列會自動地平衡負載赌躺。
LinkedBlockingQueue的容量在默認下是沒有上邊界的,也可設置之羡儿。
阻塞隊列方法:
方法 | 正常動作 | 特殊情況下的動作 |
---|---|---|
add | 添加一個元素 | 隊列滿時拋出IllegalStateException異常 |
element | 返回隊列的頭元素 | 隊列空時拋出NoSuchElementException異常 |
offer | 添加一個元素并返回true | 如果隊列滿礼患,則返回false |
peek | 返回隊列的頭元素 | 如果隊列空,則返回null |
poll | 移出并返回隊列的頭元素 | 如果隊列空,則返回null |
put | 添加一個元素 | 如果隊列滿缅叠,則阻塞 |
remove | 移出并返回頭元素 | 隊列空時拋出NoSuchElementException異常 |
take | 移出并返回頭元素 | 如果隊列空悄泥,則阻塞 |
poll和peek方法返回空來指示失敗,因此肤粱,向這些隊列中插入null值是非法的弹囚。
還有帶有超時的offer方法和poll方法的變體。例如
boolean success = q.offer(x, 100, TimeUnit.MILLISECONDS);
嘗試在100ms內(nèi)在隊列的尾部插入一個元素领曼。如果成功返回true鸥鹉;否則,達到超時時返回false庶骄。類似地毁渗,下面的調(diào)用:
Object head = q.poll(100, TimeUnit.MILLISECONDS);
嘗試在100ms內(nèi)移除隊列的頭元素;如果成功返回頭元素单刁,否則灸异,達到超時時返回false。
阻塞隊列練習羔飞,開啟一個線程讀取目錄結(jié)構(gòu)绎狭,把文件添加到阻塞隊列中,另外數(shù)個線程從隊列中讀取文件褥傍,并把文件中包含已設定關鍵字的行打印出來:
package learn.test.object;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class BlockingQueueTest
{
public static void main(String[] args)
{
BlockingQueue<File> q = new LinkedBlockingQueue<File>(Q_SIZE);
FilePicker aFilePicker = new FilePicker(q, ROOT_DIR);
new Thread(aFilePicker).start();
for(int i=0; i< THREAD_COUNT; i++)
{
FileAnalyzer analyzer = new FileAnalyzer(q, KEY_WORD);
new Thread(analyzer).start();
}
}
public static final String ROOT_DIR = "/home/joseph/Documents/WorkSpace/Source/Java";
public static final String KEY_WORD = "public";
public static final int THREAD_COUNT = 10;
public static final int Q_SIZE = 10;
}
class FilePicker implements Runnable
{
public FilePicker(BlockingQueue<File> q, String rootDir)
{
this.q = q;
this.rootDir = rootDir;
}
public void run()
{
try
{
pickFile();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
public void pickFile() throws InterruptedException
{
File root = new File(rootDir);
if(!root.exists())
System.out.println("the selected file/dir do not exists");
pickFile(root);
q.put(END_FLAG);
}
public void pickFile(File file) throws InterruptedException
{
if(file.isDirectory())
{
for(File subFile : file.listFiles())
pickFile(subFile);
}
else
{
q.put(file);
}
}
private BlockingQueue<File> q;
private String rootDir;
public static final File END_FLAG = new File("");
}
class FileAnalyzer implements Runnable
{
public FileAnalyzer(BlockingQueue<File> q, String key)
{
this.key = key;
this.q = q;
}
public void run()
{
try
{
while((curFile = q.take()) != FilePicker.END_FLAG)
{
Scanner in = new Scanner(new FileInputStream(curFile));
int lineCount = 1;
while(in.hasNext())
{
String str = in.nextLine();
if(str.contains(key))
System.out.printf("thread [%s] line [%d] in file [%s] : %s%n",Thread.currentThread().getName(), lineCount++, curFile.getName(), str);
}
}
q.put(FilePicker.END_FLAG);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
}
private File curFile;
private BlockingQueue<File> q;
private String key;
}
線程安全的集合
[ArrayList線程不安全分析]
一個 ArrayList 儡嘶,在添加一個元素的時候,它可能會有兩步來完成:
1. 在 Items[Size] 的位置存放此元素恍风;
2. 增大 Size 的值蹦狂。
在單線程運行的情況下,如果 Size = 0朋贬,添加一個元素后凯楔,此元素在位置 0,而且 Size=1锦募;
而 如果是在多線程情況下摆屯,比如有兩個線程,線程 A 先將元素存放在位置 0糠亩。但是此時 CPU 調(diào)度線程A暫停虐骑,線程 B 得到運行的機會。線程B也向此 ArrayList 添加元素赎线,因為此時 Size 仍然等于 0 (注意哦廷没,我們假設的是添加一個元素是要兩個步驟哦,而線程A僅僅完成了步驟1)垂寥,所以線程B也將元素存放在位置0颠黎。
線程不安全的例子:
public class ArrayListInThread implements Runnable {
List<String> list1 = new ArrayList<String>(1);// not thread safe
// List<String> list1 = Collections.synchronizedList(new ArrayList<String>());// thread safe
public void run() {
try {
Thread.sleep((int)(Math.random() * 2));
}
catch (InterruptedException e) {
e.printStackTrace();
}
list1.add(Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
ThreadGroup group = new ThreadGroup("testgroup");
ArrayListInThread t = new ArrayListInThread();
for (int i = 0; i < 10000; i++) {
Thread th = new Thread(group, t, String.valueOf(i));
th.start();
}
while (group.activeCount() > 0) {
Thread.sleep(10);
}
System.out.println();
System.out.println(t.list1.size()); // it should be 10000 if thread safe collection is used.
}
}
作業(yè)
使用同步機制修正窗口賣票問題另锋。