1、多線程安全問題分析
多線程安全問題原因是在cpu執行多線程時,在執行的過程中可能隨時切換到其他的線程上執行。
在以上紅色選中的三個部分,線程都有可能進行切換。只要cpu在這個三個地中的任何地方切換了,都可能導致錯誤數據出現,線程的不安全因素就有了。
造成錯誤數據的原因是多個線程可能出現同時訪問num的情況。而任何一個線程在訪問num的過程中都可以切換到其他的線程上。而其他線程一旦把num的數據改變了,再切換回來時,錯誤數據就有了。
2、多線程安全問題解決
要解決上述的線程安全問題,錯誤數據問題。在一個線程進入到if中之后,當cpu切換到其他線程上時,不讓其他的線程進入if語句,那么就算線程繼續執行當前其他的線程,也無法進入到if中,這樣就不會造成錯誤數據的出現。
Java中給我們提供是同步代碼塊技術來解決這個線程的安全問題:
格式:
synchronized(任意的對象(鎖) ) { 寫要被同步的代碼 }
同步代碼塊就可以認為成衛生間的門和鎖。
多線程安全問題,是由於多個線程在訪問共享的數據(共享的資源),並且操作共享數據的語句不止一條。那么這樣在多條操作共享數據的之間線程就可能發生切換。只要切換就有安全問題,那么我們就可以把同步加在這些操作共享數據的代碼上。
ThreadDemo.java源代碼
1 //書寫售票的示例 2 class Demo implements Runnable{ 3 //定義變量記錄剩余的票數 4 int num = 100; 5 6 //創建一個對象,用作同步中的鎖對象 7 Object obj = new Object(); 8 9 //實現run方法 10 public void run() { 11 12 //實現售票的過程 13 while( true ) { 14 // t1 t2 t3 15 //判斷當前有沒有線程正在if中操作num,如果有當前線程就在這里臨時等待 16 //這種思想稱為線程的同步 17 //當票數小等於0的時候,就不再售票了 18 //使用同步代碼塊把線程要執行的任務代碼可以同步起來 19 synchronized( obj ) //t1 在進入同步之前線程要先獲取鎖 20 /* 21 當某個線程執行到synchronized關鍵字之后,這時JVM會判斷當前 22 同步上的這個對象有沒有已經被其他線程獲取走了,如果這時沒有其他 23 線程獲取這個對象,這時就會把當前同步上的這個對象交給當前正要進入 24 同步的這個線程。 25 */ 26 { 27 if( num > 0 ) 28 { 29 //t0 30 try{Thread.sleep(2);}catch( InterruptedException e ){} 31 System.out.println(Thread.currentThread().getName()+"....."+num); 32 num--; 33 } 34 }//線程執行完同步之后,那么這時當前這個線程就會把鎖釋放掉 35 } 36 } 37 } 38 class ThreadDemo { 39 public static void main(String[] args) { 40 //創建線程任務 41 Demo d = new Demo(); 42 //創建線程對象 43 Thread t = new Thread( d ); 44 Thread t2 = new Thread(d); 45 Thread t3 = new Thread(d); 46 Thread t4 = new Thread(d); 47 //開啟線程 48 t.start(); 49 t2.start(); 50 t3.start(); 51 t4.start(); 52 } 53 }
3、多線程安全問題細節
3.1、同步的好處和弊端
好處:可以保證多線程操作共享數據時的安全問題
弊端:降低了程序的執行效率。
3.2、同步的前提
要同步,必須有多個線程,多線程在操作共享的數據,同時操作共享數據的語句不止一條。
3.3、加入了同步安全依然存在
首先查看同步代碼塊的位置是否加在了需要被同步的代碼上。如果同步代碼的位置沒有錯誤,這時就再看同步代碼塊上使用的鎖對象是否是同一個。多個線程是否在共享同一把鎖。
4、同步鎖的問題
4.1、同步是否可以加在run方法上
方法上是可以加同步的,但是不建議把同步加在run方法,如果把同步加在了run方法上,導致任何一個線程在調用start方法開啟之后,JVM去調用run方法的時候,首先都要先獲取同步的鎖對象,只有獲取到了同步的鎖對象之后,才能去執行run方法。而我們在run中書寫的被多線程操作的代碼,永遠只會有一個線程在里面執行。只有這個線程把這個run執行完,出去之后,把鎖釋放了,其他某個線程才能進入到這個run執行。
4.2、同步方法上的鎖
同步代碼塊使用的鎖是任意對象(由使用者自己來手動的指定)。
非靜態的方法上加的同步使用的鎖是當前對象(當前調用這個方法的那個對象)。
靜態方法上使用的鎖是當前的class文件(當前這個方法所屬的class文件)。
演示同步代碼塊的鎖、同步方法的鎖 、靜態同步方法的鎖源代碼:
1 class Demo implements Runnable 2 { 3 static int num = 100; 4 boolean flag = true; 5 6 Object obj = new Object(); 7 8 public void run() 9 { 10 if( flag ) //使用判斷進行線程執行任務的代碼切換 11 { 12 while(true) 13 { //thread-0 14 synchronized( Demo.class ) 15 { 16 if( num > 0 ) 17 { 18 System.out.println(Thread.currentThread().getName()+"......"+num); 19 num--; 20 } 21 } 22 } 23 } 24 else 25 { 26 while(true) 27 { 28 //thread-1 29 show(); 30 } 31 } 32 } 33 /* 34 show方法中的所有代碼全部是需要被同步的代碼。這時就可以把這個同步加載show方法 35 在方法上加同步的格式:直接在方法上書寫同步關鍵字即可 36 37 38 非靜態的方法在執行的時候需要被對象調用。 39 這個show方法是被當前的Demo對象調用的,這時在show方法上加的同步使用的鎖就是當前的 40 那個Demo對象。就是this 41 42 43 靜態方法上使用的鎖不是this,靜態方法執行時不需要對象。而靜態方法是被類名直接調用。 44 靜態方法上使用的鎖是當前的class文件 45 */ 46 public static synchronized void show() 47 { 48 if( num > 0 ) 49 { 50 System.out.println(Thread.currentThread().getName()+"================="+num); 51 num--; 52 } 53 } 54 } 55 56 class ThreadDemo3 57 { 58 public static void main(String[] args) 59 { 60 //創建任務 61 Demo d = new Demo(); 62 63 //創建線程 64 Thread t = new Thread(d); 65 Thread t2 = new Thread(d); 66 67 t.start(); 68 //程序是從主線程開始運行,在運行時,雖然開啟了thread-0線程, 69 //但是cpu可能不會立刻切換到thread-0線程上。 70 //為了保證thread-0一定能夠進入到if中執行,thread-1進入else執行 71 //讓主線程在開啟thread-0線程之后,讓主線程休眠 72 try{Thread.sleep(1);}catch(InterruptedException e){} 73 //把標記改為false,讓下一個線程進入的else中執行 74 d.flag = false; 75 t2.start(); 76 } 77 }
售票的例子使用Runnable接口實現並加同步:
1 //書寫售票的示例 2 class Demo implements Runnable{ 3 //定義變量記錄剩余的票數 4 int num = 100; 5 6 //創建一個對象,用作同步中的鎖對象 7 Object obj = new Object(); 8 9 //實現run方法 10 public void run() { 11 12 //實現售票的過程 13 while( true ) { 14 // t1 t2 t3 15 //判斷當前有沒有線程正在if中操作num,如果有當前線程就在這里臨時等待 16 //這種思想稱為線程的同步 17 //當票數小等於0的時候,就不再售票了 18 //使用同步代碼塊把線程要執行的任務代碼可以同步起來 19 synchronized( obj ) //t1 在進入同步之前線程要先獲取鎖 20 /* 21 當某個線程執行到synchronized關鍵字之后,這時JVM會判斷當前 22 同步上的這個對象有沒有已經被其他線程獲取走了,如果這時沒有其他 23 線程獲取這個對象,這時就會把當前同步上的這個對象交給當前正要進入 24 同步的這個線程。 25 */ 26 { 27 if( num > 0 ) 28 { 29 //t0 30 try{Thread.sleep(2);}catch( InterruptedException e ){} 31 System.out.println(Thread.currentThread().getName()+"....."+num); 32 num--; 33 } 34 }//線程執行完同步之后,那么這時當前這個線程就會把鎖釋放掉 35 36 37 } 38 } 39 } 40 class ThreadDemo { 41 public static void main(String[] args) { 42 //創建線程任務 43 Demo d = new Demo(); 44 //創建線程對象 45 Thread t = new Thread( d ); 46 Thread t2 = new Thread(d); 47 Thread t3 = new Thread(d); 48 Thread t4 = new Thread(d); 49 //開啟線程 50 t.start(); 51 t2.start(); 52 t3.start(); 53 t4.start(); 54 } 55 }
售票的例子使用Thread實現並加同步:
1 class Ticket extends Thread 2 { 3 static int num = 10000; 4 static Object obj = new Object(); 5 public void run() 6 { 7 while( num > 0 ) 8 { 9 synchronized(obj) 10 { 11 if( num > 0 ) 12 { 13 System.out.println(Thread.currentThread().getName()+"..."+num); 14 15 num--; 16 } 17 } 18 } 19 20 } 21 22 } 23 class ThreadDemo2 24 { 25 public static void main(String[] args) 26 { 27 Ticket t = new Ticket(); 28 Ticket t2 = new Ticket(); 29 Ticket t3 = new Ticket(); 30 Ticket t4 = new Ticket(); 31 32 t.start(); 33 t2.start(); 34 t3.start(); 35 t4.start(); 36 } 37 }
5、單例懶漢式線程並發問題
單例設計模式:保證當前程序中的這個類對象唯一。
單例設計模式代碼書寫步驟:
1、私有本類的構造方法
2、創建本類對象
3、對外提供訪問本類對象的方法
常用的模版代碼:
1 //餓漢式 2 class Single 3 { 4 private Single(){} 5 6 private static final Single s = new Single(); 7 8 public static Single getInstance() 9 { 10 return s; 11 } 12 13 } 14 15 16 //懶漢式 17 class Single 18 { 19 private Single(){} 20 21 private static Single s = null; 22 23 public static Single getInstance() 24 { 25 if( s == null ) 26 { 27 s = new Single(); 28 } 29 return s; 30 } 31 }
懶漢式的多線程並發訪問的安全問題:
1 //書寫單例類 2 class Single 3 { 4 private static Object obj = new Object(); 5 //私有構造方法 6 private Single() 7 { 8 } 9 //定義成員變量記錄當前本類的對象 10 private static Single s = null; 11 12 //對外提供方法訪問本類的對象 13 public static Single getInstance() 14 { 15 /* 16 這里有安全問題,當多個線程進來之后,在執行判斷和創建對象之間可能出現 17 cpu在線程之間的切換,一旦cpu切換了線程,就會導致每個線程都創建 18 一個當前單例類的對象,這樣就會導致當前的單例類不再保證對象唯一了 19 */ 20 //t1 21 if( s == null )// 這里加判斷的目的是保證后續來的線程不用在進入同步代碼塊中,這個可以提高后續程序效率 22 { 23 synchronized( obj ) 24 { 25 //先判斷有沒有對象 26 if( s == null ) //判斷最后線程進入同步之后到底有沒有對象,只有在沒有對象的情況下才能創建對象 27 { 28 s = new Single(); 29 } 30 } 31 //t0 32 } 33 //返回對象 34 return s; 35 } 36 } 37 //多線程並發訪問單例類獲取對象 38 class Demo implements Runnable 39 { 40 public void run() 41 { 42 Single s = Single.getInstance(); 43 System.out.println("s="+s); 44 } 45 } 46 class SingleThreadDemo 47 { 48 public static void main(String[] args) 49 { 50 //創建線程的任務 51 Demo d = new Demo(); 52 53 //創建線程對象 54 Thread t = new Thread(d); 55 Thread t2 = new Thread(d); 56 57 t.start(); 58 t2.start(); 59 } 60 }
6、死鎖示例
死鎖:
在多線程執行任務的時候,可能出現需要獲取到多把鎖才能去完成某個任務。
Thread-0 、Thread-1
它們要完成線程的任務,而它們都需要獲取不同的鎖才能完成。
Thread-0 需要先獲取A鎖 再獲取B才能去完成任務
而Thread-1線程需要先獲取B鎖,在獲取A鎖才能完成任務。
這時Thread-0 獲取到了A的時候,CPU切換到Thread-1上,這時Thread-1就獲取到了B鎖。
這時就出現了2個線程要執行任務都需要獲取對方線程上的那個鎖。
死鎖示例源代碼:
1 //死鎖程序演示 2 class Demo implements Runnable 3 { 4 int num = 100; 5 //定義一個鎖 6 Object obj = new Object(); 7 8 //定義標記,讓兩個線程去不同的代碼塊中實現售票 9 boolean flag = true; 10 11 public void run() 12 { 13 if( flag ) 14 { 15 while( true ) 16 { 17 synchronized( obj ) 18 { 19 show(); 20 } 21 } 22 } 23 else 24 { 25 while(true) 26 { 27 show(); 28 } 29 } 30 } 31 public synchronized void show() 32 { 33 34 synchronized( obj ) 35 { 36 if( num > 0 ) 37 { 38 System.out.println(Thread.currentThread().getName()+"............"+num ); 39 num--; 40 } 41 } 42 } 43 } 44 class DeadLockDemo 45 { 46 public static void main(String[] args) 47 { 48 Demo d = new Demo(); 49 50 Thread t = new Thread(d); 51 Thread t2 = new Thread(d); 52 53 t.start(); 54 try{Thread.sleep(1);}catch(Exception e){} 55 d.flag = false; 56 t2.start(); 57 58 } 59 }
面試的死鎖程序源代碼:
1 //死鎖程序書寫 2 class Demo implements Runnable 3 { 4 private Object objA = new Object(); 5 private Object objB = new Object(); 6 //標記實現線程可以切換 7 boolean flag = true; 8 //實現run方法 9 public void run() 10 { 11 //通過判斷flag標記目標是讓不同的線程在if和else中執行 12 if( flag ) 13 { 14 while(true) 15 { 16 synchronized( objA ) 17 { 18 System.out.println(Thread.currentThread().getName()+"....objA"); 19 //t0 20 synchronized( objB ) 21 { 22 System.out.println(Thread.currentThread().getName()+"....objB"); 23 } 24 } 25 } 26 } 27 else 28 { 29 while(true) 30 { 31 synchronized( objB ) 32 { 33 System.out.println(Thread.currentThread().getName()+"....objB"); 34 //t1 35 synchronized( objA ) 36 { 37 System.out.println(Thread.currentThread().getName()+"....objA"); 38 } 39 } 40 } 41 } 42 } 43 } 44 45 class DeadLock 46 { 47 public static void main(String[] args) 48 { 49 Demo d = new Demo(); 50 51 Thread t = new Thread(d); 52 Thread t2 = new Thread(d); 53 54 t.start(); 55 try{Thread.sleep(1);}catch(Exception e){} 56 d.flag = false; 57 t2.start(); 58 } 59 }
7、生產者消費者
生產者和消費者程序:
生產者主要負責生產商品,消費者主要負責消費商品。假如生產者把生產者的商品要存放的容器中,而消費者要從容器中取出商品進行消費。
當生產者正在生產的時候,消費者不能來消費。或者是消費者正在消費的時候生產者不要來生產。
當生產者把容器存放滿了,這時生產者就不能在繼續給容器中存放商品,消費者如果把容器中的商品消費完了,消費者就不能在繼續消費。
分析生產者線程和消費者線程:
生產者線程主要負責給容器中存放商品,而消費者線程主要負責從容器中取出商品進行消費。生產者線程和消費者線程它們的線程任務不一樣。
由於生產者和消費者線程任務不同,就無法寫同一個run方法中,就需要書寫兩個run方法來封裝生產者的現場那個任務和封裝消費者的線程任務,
那么就需要定義2個類。這個2個類分別負責生產者和消費者線程任務。
分析容器:
消費者線程任務對象一旦創建就必須明確自己消費的是那個容器中的商品,而生產者線程任務對象一創建,就必須明確自己生產的商品存放到哪個容器中。
這時我們可以單獨定義一個類,這個類專門負責封裝容器。並且這個類需要對外提供給容器中存放商品的功能,和從容器中取出商品的功能。
單生產單消費代碼簡單實現:
1 /* 2 生產者和消費者 3 為了程序簡單化,先研究單生產和單消費。 4 */ 5 6 //一個用來封裝容器的類 7 class Resource 8 { 9 //這里定義一個容器,主要負責用來存放商品和取出商品 10 //定義了一個數組容器,這個容器中只能存放一個商品 11 private Object[] objs = new Object[1]; 12 13 private Object obj = new Object(); 14 15 //對外提供給容器中存放商品的方法 16 public void put( Object obj ) 17 { 18 objs[0] = obj; 19 try{Thread.sleep(1);}catch( InterruptedException e ){} 20 System.out.println(Thread.currentThread().getName()+"..存入的商品是..."+objs[0]); 21 } 22 //對外提供給容器中取出商品的方法 23 public void get() 24 { 25 System.out.println(Thread.currentThread().getName()+"----取出的商品是--------"+objs[0]); 26 27 objs[0] = null; 28 } 29 } 30 31 //一個類用來描述生產者線程的任務 32 class Producer implements Runnable 33 { 34 //定義一個成員變量記錄當前傳遞進來的資源對象 35 private Resource r; 36 //在一創建生產者任務對象的時候,就必須明確當前的商品要存放到那個容器資源中 37 Producer( Resource r ) 38 { 39 this.r = r; 40 } 41 //實現run方法,在run方法中完成生產線程的存放商品的動作 42 public void run() 43 { 44 //定義變量記錄當前生產的商品編號 45 int num = 1; 46 //生產者線程一旦進入run方法之后就不斷的來生產 47 while( true ) 48 { 49 r.put( "面包"+num ); 50 num++; 51 } 52 } 53 } 54 55 //一個類用來描述消費者線程的任務 56 class Consumer implements Runnable 57 { 58 private Resource r; 59 Consumer( Resource r ) 60 { 61 this.r = r; 62 } 63 //run方法中封裝的消費者消費商品的任務代碼 64 public void run() 65 { 66 while( true ) 67 { 68 r.get(); 69 } 70 } 71 } 72 73 class ThreadDemo4 74 { 75 public static void main(String[] args) 76 { 77 //創建生產者或者消費者要操作的資源對象 78 Resource r = new Resource(); 79 80 //創建生產者任務對象 81 Producer pro = new Producer( r ); 82 //創建消費者任務對象 83 Consumer con = new Consumer( r ); 84 85 86 //創建線程對象 87 Thread t = new Thread( pro ); 88 Thread t2 = new Thread( con ); 89 90 t.start(); 91 t2.start(); 92 93 } 94 }
單生產單消費 中的同步實現:
/* 加入同步解決的是在存入的時候不能消費 在消費的時候不能存入。 */ //一個用來封裝容器的類 class Resource { //這里定義一個容器,主要負責用來存放商品和取出商品 //定義了一個數組容器,這個容器中只能存放一個商品 private Object[] objs = new Object[1]; private Object obj = new Object(); //對外提供給容器中存放商品的方法 public void put( Object obj ) { //加入同步目的是保證生產者在生產的時候,消費者不能消費 synchronized( obj ) { objs[0] = obj; System.out.println(Thread.currentThread().getName()+"..存入的商品是..."+objs[0]); } } //對外提供給容器中取出商品的方法 public void get() { //加入同步目的是在消費者消費的時候生產者不要來生產 synchronized( obj ) { System.out.println(Thread.currentThread().getName()+"----取出的商品是--------"+objs[0]); objs[0] = null; } } } //一個類用來描述生產者線程的任務 class Producer implements Runnable { //定義一個成員變量記錄當前傳遞進來的資源對象 private Resource r; //在一創建生產者任務對象的時候,就必須明確當前的商品要存放到那個容器資源中 Producer( Resource r ) { this.r = r; } //實現run方法,在run方法中完成生產線程的存放商品的動作 public void run() { //定義變量記錄當前生產的商品編號 int num = 1; //生產者線程一旦進入run方法之后就不斷的來生產 while( true ) { r.put( "面包"+num ); num++; } } } //一個類用來描述消費者線程的任務 class Consumer implements Runnable { private Resource r; Consumer( Resource r ) { this.r = r; } //run方法中封裝的消費者消費商品的任務代碼 public void run() { while( true ) { r.get(); } } } class ThreadDemo5 { public static void main(String[] args) { //創建生產者或者消費者要操作的資源對象 Resource r = new Resource(); //創建生產者任務對象 Producer pro = new Producer( r ); //創建消費者任務對象 Consumer con = new Consumer( r ); //創建線程對象 Thread t = new Thread( pro ); Thread t2 = new Thread( con ); t.start(); t2.start(); } }
單生產單消費 中的等待喚醒機制:
/* 解決問題: 由於程序中給的容易僅僅只能存放一個商品, 當生產者正好把商品存放到容器中之后,這時cpu依然在執行生產者線程,很多有可能出現繼續執行生產者往 容器中存放商品的現象。這時一旦生產者第二次或者多次存放商品前面已經存放進去的商品就沒有被消費 而被后續的商品給覆蓋了。 當消費者正好把容器中的商品消費完了,cpu還在執行消費者線程時,就可能發生消費者再次來消費, 但是容器中並沒有商品,這時就會出現消費null 解決思路: 在生產者生產商品的時候,首先不應該立刻給容器中就存放商品,而應該先判斷當前容器中 有沒有商品,如果有就不能存放,應該等待消費者來消費這個商品,當消費者消費完這個商品之后 生產者就可以來把商品存放到容器中。 消費者來消費商品的時候,也不能直接去消費,也需要判斷當前的容器中有沒有商品,如果有 才能消費,如果沒有就需要等待。等待生產者給容器中存放商品。 在解決生產者和消費者問題的時候,我們需要線程進行等待。如何讓線程等待呢? 在查閱api的時候在Thread類中沒有找到等待的方法,而在Object類中找到等待的方法。 在Java中我們要讓那個線程等待,這個線程必須位於同步代碼塊中,只要線程位於同步中 那么這個線程肯定會持有當前同步的鎖對象。 我們如果要讓某個線程從執行狀態改變為等待的狀態,這時需要使用當前同步上的鎖對象, 這時這個鎖對象就知道當前是那個線程持有自己。這時這個鎖對象就有能力讓線程處於等待狀態。 一個鎖可以監視多個線程。就可以使用這個鎖讓當前線程等待。 鎖讓線程等待,那么這個等待的方法應該屬於鎖的。而在同步代碼塊中,鎖又是任意的。 任何一個對象都可以作為鎖,那么任何一個對象都應該具備讓線程處於等待的功能。 任何對象都具備的功能,就需要抽取到Object類中。 */ //一個用來封裝容器的類 class Resource { //這里定義一個容器,主要負責用來存放商品和取出商品 //定義了一個數組容器,這個容器中只能存放一個商品 private Object[] objs = new Object[1]; private Object obj_lock = new Object(); //對外提供給容器中存放商品的方法 public void put( Object obj ) { //加入同步目的是保證生產者在生產的時候,消費者不能消費 synchronized( obj_lock ) { //判斷當前容器中是否有商品 if( objs[0] != null ) { //讓生產者線程等待 try{obj_lock.wait();}catch(InterruptedException e){} } objs[0] = obj; System.out.println(Thread.currentThread().getName()+"..存入的商品是..."+objs[0]); //生產者在生產完商品之后,要通知消費來消費 obj_lock.notify(); } } //對外提供給容器中取出商品的方法 public void get() { //加入同步目的是在消費者消費的時候生產者不要來生產 synchronized( obj_lock ) { //判斷當前是否有商品,如果沒有消費者線程要等待 if( objs[0] == null ) { //讓消費者線程等待 try{obj_lock.wait();}catch(InterruptedException e){} } System.out.println(Thread.currentThread().getName()+"----取出的商品是--------"+objs[0]); objs[0] = null; //消費者在消費完商品之后,要通知生產來生產 obj_lock.notify(); } } } //一個類用來描述生產者線程的任務 class Producer implements Runnable { //定義一個成員變量記錄當前傳遞進來的資源對象 private Resource r; //在一創建生產者任務對象的時候,就必須明確當前的商品要存放到那個容器資源中 Producer( Resource r ) { this.r = r; } //實現run方法,在run方法中完成生產線程的存放商品的動作 public void run() { //定義變量記錄當前生產的商品編號 int num = 1; //生產者線程一旦進入run方法之后就不斷的來生產 while( true ) { r.put( "面包"+num ); num++; } } } //一個類用來描述消費者線程的任務 class Consumer implements Runnable { private Resource r; Consumer( Resource r ) { this.r = r; } //run方法中封裝的消費者消費商品的任務代碼 public void run() { while( true ) { r.get(); } } } class ThreadDemo6 { public static void main(String[] args) { //創建生產者或者消費者要操作的資源對象 Resource r = new Resource(); //創建生產者任務對象 Producer pro = new Producer( r ); //創建消費者任務對象 Consumer con = new Consumer( r ); //創建線程對象 Thread t = new Thread( pro ); Thread t2 = new Thread( con ); t.start(); t2.start(); } }
多生產多消費 中的等待喚醒機制 需要把if改成while 需要把notify改為notifyAll:
1 /* 2 程序修改成多個生產者和多個消費之后,有出現了商品的覆蓋現象, 3 或者消費者不斷的消費null現象 4 導致這個原因的問題,是生產者在喚醒的時候把另外的生產者喚醒了,而被喚醒的 5 生產者沒有去判斷還有沒有商品,直接就開始生產了。這樣就會出現商品的覆蓋。 6 7 消費者消費null的原因是在消費者喚醒其他的消費者,被喚醒的消費者也不去判斷有沒有 8 商品,就直接開始消費 9 10 解決方案: 11 在喚醒之后,重新去判斷有沒有商品即可。 12 這時只需要把判斷有沒有商品的if結構改為while這樣就可以在每次喚醒之后,繼續進行 13 while的條件進行判斷 14 15 把if改為while之后死鎖了,原因是生產者喚醒時喚醒了生產者或者消費者消費時喚醒了消費者。 16 這時就在喚醒之后,又回到while中判斷,當前被喚醒的繼續等待。就會出現死鎖現象。 17 解決方案:在喚醒的時候,不僅喚醒本方,也需要喚醒對象。我們這時可以使用notifyAll方法喚醒當前 18 鎖對象上等待的所有線程。 19 20 */ 21 22 //一個用來封裝容器的類 23 class Resource 24 { 25 //這里定義一個容器,主要負責用來存放商品和取出商品 26 //定義了一個數組容器,這個容器中只能存放一個商品 27 private Object[] objs = new Object[1]; 28 29 private Object obj_lock = new Object(); 30 31 int num = 1; 32 33 //對外提供給容器中存放商品的方法 34 public void put( Object obj ) 35 { 36 //加入同步目的是保證生產者在生產的時候,消費者不能消費 37 synchronized( obj_lock ) 38 { 39 //判斷當前容器中是否有商品 40 while( objs[0] != null ) 41 { 42 //讓生產者線程等待 43 try{obj_lock.wait();}catch(InterruptedException e){} // t1 //t0 44 } 45 46 objs[0] = obj+""+num; 47 num++; 48 49 System.out.println(Thread.currentThread().getName()+"..存入的商品是..."+objs[0]); 50 51 //生產者在生產完商品之后,要通知消費來消費 52 obj_lock.notifyAll(); 53 54 } 55 } 56 57 58 //對外提供給容器中取出商品的方法 59 public void get() 60 { 61 //加入同步目的是在消費者消費的時候生產者不要來生產 62 synchronized( obj_lock ) 63 { 64 //判斷當前是否有商品,如果沒有消費者線程要等待 65 while( objs[0] == null ) 66 { 67 //讓消費者線程等待 68 try{obj_lock.wait();}catch(InterruptedException e){} // t3 69 } 70 System.out.println(Thread.currentThread().getName()+"----取出的商品是--------"+objs[0]); 71 72 objs[0] = null; 73 74 //消費者在消費完商品之后,要通知生產來生產 75 76 obj_lock.notifyAll(); //t2 77 } 78 } 79 } 80 81 //一個類用來描述生產者線程的任務 82 class Producer implements Runnable 83 { 84 //定義一個成員變量記錄當前傳遞進來的資源對象 85 private Resource r; 86 //在一創建生產者任務對象的時候,就必須明確當前的商品要存放到那個容器資源中 87 Producer( Resource r ) 88 { 89 this.r = r; 90 } 91 //實現run方法,在run方法中完成生產線程的存放商品的動作 92 public void run() 93 { 94 //生產者線程一旦進入run方法之后就不斷的來生產 95 while( true ) 96 { 97 r.put( "面包" ); 98 } 99 } 100 } 101 102 //一個類用來描述消費者線程的任務 103 class Consumer implements Runnable 104 { 105 private Resource r; 106 Consumer( Resource r ) 107 { 108 this.r = r; 109 } 110 //run方法中封裝的消費者消費商品的任務代碼 111 public void run() 112 { 113 while( true ) 114 { 115 r.get(); 116 } 117 } 118 } 119 120 class ThreadDemo7 121 { 122 public static void main(String[] args) 123 { 124 //創建生產者或者消費者要操作的資源對象 125 Resource r = new Resource(); 126 127 //創建生產者任務對象 128 Producer pro = new Producer( r ); 129 //創建消費者任務對象 130 Consumer con = new Consumer( r ); 131 132 133 //創建線程對象 134 Thread t = new Thread( pro ); 135 Thread t2 = new Thread( pro ); 136 137 Thread t3 = new Thread( con ); 138 Thread t4 = new Thread( con ); 139 140 t.start(); 141 t2.start(); 142 t3.start(); 143 t4.start(); 144 145 } 146 }
8、JDK5對鎖的升級
在學習生產者和消費者的時候在使用等待和喚醒機制的時候,發生了本方喚醒本方的。這樣導致程序的效率降低。
在jdk5中對等待喚醒以及鎖的機制進行了升級。
鎖的升級:
在jdk5之前同步中獲取鎖和釋放鎖都是隱式的。把隱式的鎖變成了顯示由程序員自己手動的獲取和釋放。
jdk5對鎖的描述:采用的接口來描述。在java.util.concurrent.locks包下有接口Lock它是專門放負責描述鎖這個事物。
Lock鎖代替了同步代碼塊。在多線程中能夠使用同步代碼塊的地方都可以使用Lock接口來代替。
當我們使用Lock接口代替同步代碼塊的時候,就需要程序員手動的來獲取鎖和釋放鎖,如果在程序中出現了獲取鎖之后,沒有釋放鎖,導致其他程序無法進入這個同步語句中。需要使用try-finally語句在finally中書寫釋放鎖的代碼。
在Lock接口中lock方法和unLock方法分別是獲取鎖和釋放鎖。
1 //使用JDK5的鎖完成售票的例子 2 import java.util.concurrent.locks.*; 3 class Ticket implements Runnable 4 { 5 int num = 100; 6 7 //創建鎖對象 8 Lock loc = new ReentrantLock(); //多態了 9 10 public void run() 11 { 12 while( true ) 13 { 14 //獲取鎖 15 loc.lock(); 16 try 17 { 18 if( num > 0 ) 19 { 20 System.out.println(Thread.currentThread().getName()+"............."+num); 21 num--; 22 } 23 } 24 finally 25 { 26 //釋放鎖 27 loc.unlock(); 28 } 29 } 30 } 31 } 32 33 class LockDemo 34 { 35 public static void main(String[] args) 36 { 37 //創建線程任務 38 Ticket t = new Ticket(); 39 40 //創建線程對象 41 Thread tt = new Thread(t); 42 Thread tt2 = new Thread(t); 43 Thread tt3 = new Thread(t); 44 Thread tt4 = new Thread(t); 45 46 //開啟線程 47 tt.start(); 48 tt2.start(); 49 tt3.start(); 50 tt4.start(); 51 } 52 }
9、JDK5對監視器方法的升級
JDK5對同步的鎖進行了升級,同時還對等待喚醒機制也進行了升級。
在JDK5之前等待喚醒機制的方法和當前鎖綁定在一起。在編寫生產者和消費者任務的時候,由於消費者和生產者使用的同一個鎖,那么就會導致使用當前這個鎖來喚醒在這個鎖上等待的線程的時,就可能發生生產者喚醒生產者或者消費者喚醒消費者。
到了JDK5之后,鎖被Lock接口來描述,等待喚醒的方法也被重新封裝成一個類。專門使用一個類負責等待喚醒。JDK5中使用Condtion接口來專門負責描述等待喚醒機制。Condition代替了jdk5之前的Object類中的wait、notify、notifyAll方法。await方法代替了Object中wait方法、signal代替的notify、signalAll代替notifyAll方法。
當我們需要使用等待喚醒的時候,就需要獲取到Condition對象,然后把這個Condition對象和Lock接口綁定在一起即可。這樣就可以使用一個Lock下面綁定多個Condition。在Lock接口有個方法newCondition這個方法就可以讓一個Condtion對象和Lock接口對象綁定在一起了。
10、使用JDK完成生產者消費者案例
1 //使用Lock代替同步,使用Condition接口代替等待和喚醒 2 import java.util.concurrent.locks.*; 3 class Resource 4 { 5 Object[] objs = new Object[1]; 6 //記錄存入的商品編號 7 int num = 1; 8 9 //獲取Lock鎖對象 10 Lock loc = new ReentrantLock(); 11 //定義生產者的等待喚醒對象(監視器對象 Condition) 12 Condition pro_con = loc.newCondition(); 13 //定義消費者者的等待喚醒對象(監視器對象 Condition) 14 Condition con_con = loc.newCondition(); 15 16 public void put( Object obj ) 17 { 18 //獲取鎖 19 loc.lock(); 20 try 21 { 22 //判斷有沒有商品 23 if( objs[0] !=null ) 24 { 25 //讓生產者等待 26 try{pro_con.await();}catch(Exception e){} 27 } 28 objs[0] = obj +"" + num; 29 num++; 30 System.out.println(Thread.currentThread().getName()+"........"+objs[0]); 31 //喚醒消費者 32 con_con.signal(); 33 34 } 35 finally 36 { 37 loc.unlock(); 38 } 39 40 } 41 public void get() 42 { 43 //獲取鎖 44 loc.lock(); 45 try 46 { 47 //判斷有沒有商品 48 if( objs[0] == null ) 49 { 50 //讓消費者等待 51 try{con_con.await();}catch(Exception e){} 52 } 53 54 System.out.println(Thread.currentThread().getName()+"================="+objs[0]); 55 objs[0]=null; 56 //喚醒生產者 57 pro_con.signal(); 58 } 59 finally 60 { 61 loc.unlock(); 62 } 63 } 64 65 } 66 class Producer implements Runnable 67 { 68 private Resource r; 69 public Producer( Resource r ) 70 { 71 this.r = r; 72 } 73 public void run() 74 { 75 while( true ) 76 { 77 r.put("方便面"); 78 } 79 } 80 } 81 82 class Consumer implements Runnable 83 { 84 private Resource r; 85 public Consumer( Resource r ) 86 { 87 this.r = r; 88 } 89 public void run() 90 { 91 while( true ) 92 { 93 r.get(); 94 } 95 } 96 } 97 98 99 class LockConditionTest 100 { 101 public static void main(String[] args) 102 { 103 Resource r = new Resource(); 104 105 Producer pro = new Producer(r); 106 Consumer con = new Consumer(r); 107 108 Thread t = new Thread(pro); 109 Thread t2 = new Thread(pro); 110 Thread t3 = new Thread(con); 111 Thread t4 = new Thread(con); 112 113 t.start(); 114 t2.start(); 115 t3.start(); 116 t4.start(); 117 } 118 }