java筆記--java多線程

1、創建線程的第一種方法
class ThreadDemo
{
public static void main(String[] args)
{
Test t1=new Test("t1");
Test t2=new Test("t2");


t1.start();
t2.start();


for(int i=0;i<20;i++)
{
System.out.println("main "+i);
}
}

}


class Test extends Thread
{
private String name;
Test(String name)
{
this.name=name;
}
public void run()
{
for(int i=0;i<10;i++)
System.out.println(name+":"+i);
}
}
2、創建線程的第二種方法
class TickDemo
{
public static void main(String[] args)
{
TickTest tt=new TickTest();
Thread t1=new Thread(tt);
Thread t2=new Thread(tt);
Thread t3=new Thread(tt);
Thread t4=new Thread(tt);


t1.start();
t2.start();
t3.start();
t4.start();


}

}


class TickTest implements Runnable
{
private int tick=100;
public void run()
{
while(true)
{
if(tick<0)break;
System.out.println("sale:"+tick--);
}
}
}


兩種創建線程的區別:
由於java不支持多繼承,而接口支持多實現,在實現接口Runnable的時候還可以繼承其他類。而使用繼承Thread類的方法設計類就會
導致該類無法再繼承其他類。因此在創建線程的方法中方法二使用較爲廣泛。




3、線程運行狀態
(1) 新建狀態(New):新創建了一個線程對象。
(2)就緒狀態(Runnable):線程對象創建後,其他線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。
(3)運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。
(4)阻塞狀態(Blocked):阻塞狀態是線程因爲某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。
阻塞的情況分三種:
(一)、等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。
(二)、同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池中。
(三)、其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
(5) 死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。
3.獲取線程名稱
static Thread currentThread()獲取當前線程對象
String getName()返回線程名稱
可以使用構造方法或者setName()方法設置線程名稱。


4、多線程下的安全問題
看以下例子:


class TickDemo
{
public static void main(String[] args)
{
TickTest tt=new TickTest();
Thread t1=new Thread(tt);
Thread t2=new Thread(tt);
Thread t3=new Thread(tt);
Thread t4=new Thread(tt);


t1.start();
t2.start();
t3.start();
t4.start();


}

}


class TickTest implements Runnable
{
private int tick=100;
public void run()
{
while(true)
{
if(tick>0)
{
try
{
Thread.sleep(10);
}
catch(Exception e)
{
e.printStackTrace();
}
System.out.println("sale:"+tick--);
}
}
}
}
輸出結果會出現負數,顯然多線程出現了安全問題。其原因在於,在多條語句在操作同一個線程的共享數據時,一個線程只執行了多條語句的一部分,還沒有執行完。則當另一個
線程執行時就會導致共享數據發生錯誤。解決方法是,操作共享數據的語句只能讓一個線程執行完,在它執行過程中其他線程不能執行。應該將操作共享數據的語句進行同步。


5、線程同步
(1)同步代碼塊
synchronized(object)
{
if(tick>0)
{
try
{
Thread.sleep(10);
}
catch(Exception e)
{
e.printStackTrace();
}
System.out.println("sale:"+tick--);
}
}
注意:
(1)至少要有多個線程(2個或以上)纔有必要進行同步。
(2)多個線程應該使用同一個鎖。
(3)只對可能出現安全問題的語句(操作共享資源的語句)使用同步。
(4)使用同步的的好處是解決了安全問題,但是會增加系統開銷。


(2)同步函數
例子:
class SynchronizedDemo
{
public static void main(String[] args)
{
SynchronizedTest st=new SynchronizedTest();
Thread t1=new Thread(st);
Thread t2=new Thread(st);
Thread t3=new Thread(st);
Thread t4=new Thread(st);


t1.start();
t2.start();
t3.start();
t4.start();
}

}
class Bank
{
private int sum=0;
//同步函數
public synchronized void add(int n)
{
/*synchronized(this)
{
sum+=n;
System.out.println("sum:"+sum);
}*/
sum+=n;
System.out.println("sum:"+sum);


}
}


class SynchronizedTest implements Runnable
{
private Bank b=new Bank();
public void run()
{
for(int i=1;i<3;i++)
{
b.add(i);
}
}
}
注意同步函數使用的鎖對象:
普通同步函數使用的鎖是this對象。
靜態同步函數使用的鎖是Class對象。




6、死鎖
Java線程死鎖是一個經典的多線程問題,因爲不同的線程都在等待那些根本不可能被釋放的鎖,從而導致所有的工作都無法完成。
假設有兩個線程,分別代表兩個飢餓的人,他們必須共享刀叉並輪流吃飯。他們都需要獲得兩個鎖:共享刀和共享叉的鎖。
假如線程 “A”獲得了刀,而線程“B”獲得了叉。線程“A”就會進入阻塞狀態來等待獲得叉,而線程“B”則阻塞來等待“A”所擁有的刀。
其結果就是兩個線程都會被掛起,導致死鎖現象。
避免死鎖:
(1)讓所有的線程按照同樣的順序獲得一組鎖。這種方法消除了 X 和 Y 的擁有者分別等待對方的資源的問題。
(2)將多個鎖組成一組並放到同一個鎖下。前面Java線程死鎖的例子中,可以創建一個銀器對象的鎖。於是在獲得刀或叉之前都必須獲得這個銀器的鎖。


7、線程間通信
線程間的相互作用:線程之間需要一些協調通信,來共同完成一件任務。Object類中相關的方法有兩個notify方法和三個wait方法:因爲wait和notify方法定義在Object類中,
因此會被所有的類所繼承。但由於這些方法都是final的,因此它們都是不能被重寫的,不能通過子類覆寫去改變它們的行爲。
(1)wait方法
wait()方法使得當前線程必須要等待,等到另外一個線程調用notify()或者notifyAll()方法。而當前的線程必須擁有當前對象的monitor,也即lock,就是鎖。
線程調用wait()方法,會釋放它對鎖的擁有權,然後等待另外的線程來通知它(通知的方式是notify()或者notifyAll()方法),這樣它才能重新獲得鎖的擁有權和恢復執行。
注意:要確保調用wait()方法的時候擁有鎖,即wait()方法的調用必須放在synchronized方法或synchronized塊中。
當線程調用了wait()方法時,它會釋放掉對象的鎖。而Thread.sleep()方法的調用會導致線程睡眠指定的毫秒數,但線程在睡眠的過程中是不會釋放掉對象的鎖的。這和wait方法有區別。


(2)notify方法和notifyAll方法
notify()方法就是對對象鎖的喚醒操作。但有一點需要注意的是notify()調用後,並不是馬上就釋放對象鎖的,而是在相應的synchronized(){}語句塊執行結束,自動釋放鎖後,
JVM會在wait()對象鎖的線程中隨機選取一線程,賦予其對象鎖,喚醒線程,繼續執行。這樣就提供了在線程間同步、喚醒的操作。
notifyAll方法與notify類似,它會喚醒所有在該對象上調用wait方法的線程。
例子:以下是一個生產消費者多線程模型
import java.util.*;


class Consumer implements Runnable
{
private int count=0;
private List<Object> container=null;
Consumer(List<Object> container)
{
this.container=container;
}
public void run()
{
while(true)
{
synchronized(container)
{
while(container.size()==0)
{
try{
container.wait();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
try{
Thread.sleep(100);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
container.remove(0);
container.notify();
System.out.println(Thread.currentThread().getName()+"consum:"+count++);
}
}
}
}




class Product implements Runnable
{
private static final int MaxNum=10;
private int count=0;
private List<Object> container=null;
Product(List<Object> container)
{
this.container=container;
}
public void run()
{
while(true)
{
synchronized(container)
{
while(container.size()>=MaxNum)
{
try{
container.wait();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
try{
Thread.sleep(100);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
container.add(new Object());
container.notify();
System.out.println(Thread.currentThread().getName()+" produce:"+count++);

}
}
}
}




class ProductAndConsumer
{
public static void main(String[] args)
{
List<Object> list=new ArrayList<Object>();
Consumer c=new Consumer(list);
Product p=new Product(list);


Thread t1=new Thread(c);
Thread t2=new Thread(c);
Thread t3=new Thread(p);
Thread t4=new Thread(p);


t1.start();
t2.start();
t3.start();
t4.start();
}
}


8、幾個重要的方法
(1)停止線程
interrupt方法向線程發送中斷請求。線程的中斷狀態將被設置爲true。如果目前該線程被一個sleep調用阻塞,
那麼InterruptException異常將會拋出。可以使用靜態方法static boolean interrupted()測試當前線程是否
處於被中斷狀態,該方法的調用會使得當前線程的中斷狀態重新置爲false。也可以使用boolean isInterrupted()方法
測試線程是否被中斷,但是該方法不像interrupted方法,它不會改變線程的中斷狀態。
(2)守護線程
Daemon的作用是爲其他線程的運行提供服務,比如說GC線程。其實User Thread線程和Daemon Thread守護線程本質上來說去沒啥區別的,唯一的區別之處就在虛擬機的離開:如果User Thread全部撤離,那麼Daemon Thread也就沒啥線程好服務的了,所以虛擬機也就退出了。
守護線程並非虛擬機內部可以提供,用戶也可以自行的設定守護線程,方法:public final void setDaemon(boolean on) ;但是有幾點需要注意:
1)、thread.setDaemon(true)必須在thread.start()之前設置,否則會跑出一個IllegalThreadStateException異常。你不能把正在運行的常規線程設置爲守護線程。  (備註:這點與守護進程有着明顯的區別,守護進程是創建後,讓進程擺脫原會話的控制+讓進程擺脫原進程組的控制+讓進程擺脫原控制終端的控制;所以說寄託於虛擬機的語言機制跟系統級語言有着本質上面的區別)
2)、 在Daemon線程中產生的新線程也是Daemon的。  (這一點又是有着本質的區別了:守護進程fork()出來的子進程不再是守護進程,儘管它把父進程的進程相關信息複製過去了,但是子進程的進程的父進程不是init進程,所謂的守護進程本質上說就是“父進程掛掉,init收養,然後文件0,1,2都是/dev/null,當前目錄到/”)
3)、不是所有的應用都可以分配給Daemon線程來進行服務,比如讀寫操作或者計算邏輯。因爲在Daemon Thread還沒來的及進行操作時,虛擬機可能已經退出了。


(3)join方法
用於主線程等待子線程終止。
(4)yield方法
導致當前執行線程處於讓步狀態。如果有其他可執行線程具有至少與此線程同樣高度的優先級,那麼
這些線程接下來會被調度。注意,這是Thread類中的一個靜態方法。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章