昨天寫了關於多線程引發的並發問題及解決方案—鎖,然後咱們今天繼續…
一、並發分析
Java中有個概念,叫生產者與消費者。
假設生產者不停輪流生產蘋果和香蕉,其中蘋果的價格是10元/kg,香蕉的價格是5元/kg,消費者則是對生產的商品進行購買,顯然,這裡的生產者生產的商品和消費者消費的商品是同一個商品。
定義產品類,生產者類,消費者類,因為需要共用商品,採取的策略是主方法中建立產品類對象,並將其作為參數傳入生產者和消費者的建構函式中。
//產品類
class Product{
String name; //名字
double price; //價格
}
//生產者
class Producer extends Thread{
Product p ; //產品
public Producer(Product p) {
this.p = p ;
}
@Override
public void run() {
int i = 0 ;
while(true){
if(i%2==0){
p.name = "蘋果";
p.price = 10;
}else{
p.name="香蕉";
p.price = 5;
}
System.out.println("生產者生產出了:"+ p.name+" 價格是:"+ p.price);
i++;
}
}
}
}
//消費者
class Customer extends Thread{
Product p;
public Customer(Product p) {
this.p = p;
}
@Override
public void run() {
while(true){
System.out.println("消費者消費了"+p.name+" 價格:"+ p.price);
}
}
}
//測試類別
public class Demo5 {
public static void main(String[] args) {
Product p = new Product(); //產品
//建立生產對象
Producer producer = new Producer(p);
//建立消費者
Customer customer = new Customer(p);
//調用start方法開啟線程
producer.start();
customer.start();
}
}
觀察代碼,我們會發現兩個問題
1.沒有生產出商品,消費者也來消費了
2.商品的價格發生了錯亂
瞭解線程以及其安全問題的話,這兩個問題並不難分析。
二、需求分析
顯然,我們需要生產者每生產一個就進入等待,喚醒消費者消費,消費者消費一個,消費完後進入等待,喚醒生產者繼續生產,這樣的話,必須要等待生產者徹底生產完畢,消費者才能進行商品的消費,且價格不會發生錯亂。java提供了wait()和notify()|notifyAll()(喚醒所有鎖對象為標識符的線程池中所有的線程)方法解決這個問題,即等待喚醒機制。
//產品類
class Product{
String name; //名字
double price; //價格
boolean flag = false; //產品是否生產完畢的標識,預設情況是沒有生產完成。
}
//生產者
class Producer extends Thread{
Product p ; //產品
public Producer(Product p) {
this.p = p ;
}
@Override
public void run() {
int i = 0 ;
while(true){
synchronized (p) {
if(p.flag==false){
if(i%2==0){
p.name = "蘋果";
p.price = 10;
}else{
p.name="香蕉";
p.price = 5;
}
System.out.println("生產者生產出了:"+ p.name+" 價格是:"+ p.price);
p.flag = true;
i++;
p.notifyAll(); //喚醒消費者去消費
}else{
//已經生產 完畢,等待消費者先去消費
try {
p.wait(); //生產者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
//消費者
class Customer extends Thread{
Product p;
public Customer(Product p) {
this.p = p;
}
@Override
public void run() {
while(true){
synchronized (p) {
if(p.flag==true){ //產品已經生產完畢
System.out.println("消費者消費了"+p.name+" 價格:"+ p.price);
p.flag = false;
p.notifyAll(); // 喚醒生產者去生產
}else{
//產品還沒有生產,應該 等待生產者先生產。
try {
p.wait(); //消費者也等待了...
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
//測試類別
public class Demo5 {
public static void main(String[] args) {
Product p = new Product(); //產品
//建立生產對象
Producer producer = new Producer(p);
//建立消費者
Customer customer = new Customer(p);
//調用start方法開啟線程
producer.start();
customer.start();
}
}
程式碼分析:假設生產者拿到鎖對象,此時flag為false,生產者生產出蘋果對象,flag至為true,此時喚醒消費者消費,然而,在以鎖對象p為標識符的線程池中並沒有消費者,此時生產者繼續持有鎖對象,然後發現flag為true,執行wait()方法,此時生產者釋放鎖對象p,並進入以p為標識符的線程池中等待,此時消費者擷取鎖,flag為true,消費完畢喚醒以鎖對象p為線程池中的等待線程當中的其中一個線程(無法指定喚醒線程),此時生產者被喚醒,此時消費者假設還是持有cpu執行權,繼續迴圈,發現flag為false,進入wait(),釋放鎖,生產者擷取cpu執行權,開始生產….
三、wait()和notify()的使用注意點
1. wait方法與notify方法是屬於Object對象的。
理由:若不是Object對象,那麼調用者對象即鎖對象則必須是線程對象嗎?顯然不必,鎖對象是任意對象。
2. wait方法與notify方法必須要在同步代碼塊或者是同步函數中才能使用。
理由:如果不存在同步代碼塊和同步函數則沒有鎖對象這個概念,而這兩個方法必須由鎖對象調用。
3. wait方法與notify方法必需要由鎖對象調用。
理由:因為需要以鎖對象為標識符建立一個線程池,生產者與消費者必須用同一個鎖,不同的鎖會建立不同的線程池。且不用鎖對象調用是報錯的,要求就是以鎖對象調用,否則就會報錯。