四、多線程的同步
1、同步問題的引出
在前面分析售票系統的時候,有如下代碼
if(count>0){
System.out.println(Thread.currentThread().getName()+" count = "+count--);
}
這種情況下會碰到一些意外,同一張票被打印兩次獲多次,票號爲0或者負數的情況。
假設tickets值爲1的時候,線程1剛執行完if(count>0)這段代碼,此時cpu切換到了線程2上,tickets值仍爲1,線程2執行完上面兩行代碼,tickets值變爲0後,cpu又切回到線程1上,其不會在執行if(count>0),繼續執行下一段代碼,此刻tickets的值變爲0,屏幕上打印出來的將是0.
我們可以使用Thread.sleep()方法來可以造成這種意外,迫使線程執行到該處後暫停執行,讓出cpu給別的線程,在指定的時間後,cpu回到剛纔暫停的線程上執行。
範例1:
public class Test {
public static void main(String args[]){
TestThread t = new TestThread();
Thread mTestThread1 = new Thread(t,"A");
Thread mTestThread2 = new Thread(t,"B");
Thread mTestThread3 = new Thread(t,"C");
mTestThread1.start();
mTestThread2.start();
mTestThread3.start();
}
}
class TestThread implements Runnable{
private int tickets = 5;
public void run(){
String ThreadName = Thread.currentThread().getName();
while(true){
if(tickets>0){
try{
Thread.sleep(100);
}catch(Exception e){}
System.out.println(ThreadName+" sales tickets "+tickets--);
}
}
}
}
運行結果:
B sales tickets 5
A sales tickets 3
C sales tickets 4
B sales tickets 2
A sales tickets 1
C sales tickets 0
B sales tickets -1
從運行的結果可以看到,票號被打印出來了負數,說明同一張票被賣了3次的意外發生。造成這種意外的根本原因就是因爲資源數據反問不同步引起的。下面引入同步的概念。
2、同步代碼塊
如何避免上述意外?如何保證開發出的程序是線程安全的?這就要涉及到線程間的同步問題。
再看下面這段代碼
if(count>0){
System.out.println(Thread.currentThread().getName()+" count = "+count--);
}
即當一個線程運行到if(count>0)後,cpu不去執行其他線程中的可能影響當前線程中的下一句代碼的執行結果的代碼塊,必須等到下一句執行完後才能執行其他線程中的有關代碼塊。這段代碼好比一座獨木橋,任何時刻都只能有一個人在橋上行走,即程序中不能有多個線程同時這兩句代碼之間執行,這就是線程同步。
語法如下
......
synchronized(對象){
需要同步的代碼;
}
.....
範例2:
public class Test {
public static void main(String args[]){
TestThread t = new TestThread();
Thread mTestThread1 = new Thread(t,"A");
Thread mTestThread2 = new Thread(t,"B");
Thread mTestThread3 = new Thread(t,"C");
mTestThread1.start();
mTestThread2.start();
mTestThread3.start();
}
}
class TestThread implements Runnable{
private int tickets = 20;
public void run(){
String ThreadName = Thread.currentThread().getName();
while(true){
synchronized(this){
if(tickets>0){
try{
Thread.sleep(100);
}catch(Exception e){}
System.out.println(ThreadName+" sales tickets "+tickets--);
}
}
}
}
}
運行結果如下:
B sales tickets 20
B sales tickets 19
B sales tickets 18
B sales tickets 17
B sales tickets 16
B sales tickets 15
B sales tickets 14
B sales tickets 13
B sales tickets 12
B sales tickets 11
B sales tickets 10
B sales tickets 9
B sales tickets 8
B sales tickets 7
B sales tickets 6
B sales tickets 5
B sales tickets 4
B sales tickets 3
B sales tickets 2
B sales tickets 1
由於數據較小,只有線程B,使用大量數據會有多個線程交替
3、同步方法
除了可以對代碼塊同步,也可以對方法實現同步,只要在需要同步的方法定義前面加上synchronized關鍵字即可,語法如下
訪問控制符 synchronized 返回值類型 方法名稱(參數){
.........;
}
範例3:
public class Test {
public static void main(String args[]){
TestThread t = new TestThread();
Thread mTestThread1 = new Thread(t,"A");
Thread mTestThread2 = new Thread(t,"B");
Thread mTestThread3 = new Thread(t,"C");
mTestThread1.start();
mTestThread2.start();
mTestThread3.start();
}
}
class TestThread implements Runnable{
private int tickets = 20;
public void run(){
while(true){
sale();
}
}
public synchronized void sale(){
if(tickets>0){
try{
Thread.sleep(100);
}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+" sales tickets "+tickets--);
}
}
}
編譯運行結果與上述同步同步代碼塊一致,所以說使用同步方法也可以實現線程間的同步
在同一類中,使用synchronized關鍵字定義的若干方法,可以在多個線程之間同步,當有一個線程進入有synchronized修飾的方法時,其他線程就不能進入同一個對象使用synchronized來修飾所以方法,直到第一個線程執行完它所進入的synchronized修飾的方法爲止。
五、死鎖
一旦有多個進程,且他們都要徵用對多個鎖的獨佔訪問,那麼就有可能發生死鎖。如果有一組進程或線程,其中每個都在等待一個只有其他進程或線程纔可以進行的操作,那麼就稱他們被死鎖了。
最常見的死鎖形式是:線程1持有對象A上的鎖,而且正在等待對象B上的鎖;而線程2持有對象B上的鎖,而且正在等待對象A上的鎖。這兩個線程擁有不會獲得第二個鎖,或是釋放第1個鎖,所以它們只會永遠等待下去。
範例1:
class A{
synchronized void funA(B b){
String name = Thread.currentThread().getName();
System.out.println(name+" enter A function");
try{
Thread.sleep(1000);
}catch(Exception e){}
System.out.println(name+" get class B last()");
b.last();
}
synchronized void last(){
System.out.println("class A last()");
}
}
class B{
synchronized void funB(A a){
String name = Thread.currentThread().getName();
System.out.println(name+" enter B function");
try{
Thread.sleep(1000);
}catch(Exception e){}
System.out.println(name+" get class A last()");
a.last();
}
synchronized void last(){
System.out.println("class B last()");
}
}
class Test implements Runnable{
A a = new A();
B b = new B();
Test(){
Thread.currentThread().setName("Main Thread");
Thread t = new Thread(this);
t.start();
a.funA(b);
System.out.println("main thread run finish");
}
public void run(){
Thread.currentThread().setName("Test Thread");
b.funB(a);
System.out.println("other thread run finish");
}
public static void main(String args[]){
new Test();
}
}
運行結果:
Main Thread enter A function
Test Thread enter B function
Main Thread get class B last()
Test Thread get class A last()
從結果來看,main thread進入了a的監視器,並等待b的監視器;同時test thread進入了b的監視器,然後又在等待a的監視器。這個程序永遠不會完成。
要避免死鎖,應該確保在獲取多個鎖時,在所有的線程中都以相同的順序獲取鎖,所以我們使用同步時一定要注意。
六、線程的狀態
線程在一定條件下,狀態會發生變化。線程變化的狀態轉換圖如下:
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()方法,該線程結束生命週期。