學習Java第二十九天--多線程之線程安全

14.3 線程安全

14.3.1 線程安全問題

在這裏插入圖片描述

  • 需求:A線程將“Hello”存入數組的第一個空位;B線程將“World”存入數組的第一個空位;
  • 線程不安全: 當多線程併發訪問臨界資源時,如果破壞原子操作,可能會造成數據不一致;
  • 臨界資源:共享資源(同一對象),一次僅允許一個線程使用,纔可保證其正確性;
  • 原子操作:不可分割的多步操作,被視作一個整體,其操作和步驟不可打亂或缺省;

14.3.2 同步方式(1)

  • 同步代碼塊:
    synchronized(臨界資源對象){//對臨界資源對象加鎖
    //代碼(原子操作)
    }
  • 注:
    每個對象都有一個互斥鎖標記,用來分配給線程的;
    只有擁有對象互斥鎖標記的線程,才能進入該對象加鎖的同步代碼塊;
    線程退出同步代碼塊時,會釋放相應的互斥鎖標記;
public class TestSynchronized {

	public static void main(String[] args) {
		
		//臨界資源對象
		//臨界資源對象只有一把鎖
		Account acc = new Account("6002" , "1234" , 2000);
		
		//兩個線程對象,共享同一銀行卡資源對象
		Thread husband = new Thread(new Husband(acc) , "丈夫");
		Thread wife = new Thread(new Wife(acc) , "妻子");
		
		husband.start();
		wife.start();
	}

}
class Husband implements Runnable{
	Account acc;
	public Husband(Account acc) {
		this.acc = acc;
	}
	//線程任務:取款
	public void run() {
//		synchronized(acc) {//對臨界資源對象加鎖
			this.acc.withdrawal("6002", "1234", 500);
//		}
	}
}
class Wife implements Runnable{
	Account acc;
	public Wife(Account acc) {
		this.acc = acc;
	}
	//線程任務:取款
	public void run() {
//		synchronized(acc) {
			this.acc.withdrawal("6002", "1234", 1200);//原子操作
		}
//	}
}

//銀行賬戶
class Account{
	String cardNo;
	String password;
	double balance;
	
	public Account(String cardNo , String password , double balance) {
		super();
		this.cardNo = cardNo;
		this.password = password;
		this.balance = balance;
	}
	
	//取款(原子操作,從插卡驗證,到取款成功的一系列步驟,不可缺少或打斷)
	public void withdrawal(String no , String pwd , double money) {
		//
		synchronized(this) {//對當前共享實例加同步鎖
			System.out.println(Thread.currentThread().getName()+"正在讀卡。。。");
			if(no.equals(this.cardNo) && pwd.equals(this.password)) {
				System.out.println(Thread.currentThread().getName()+"驗證成功。。。");
				if(money > this.balance) {
					System.out.println(Thread.currentThread().getName()+" 卡內餘額不足");
				}else{
					
					try {
						Thread.sleep(1000);//模擬ATM數錢時間
					}catch(InterruptedException e){
						e.printStackTrace();
					}
					this.balance = this.balance - money;
					System.out.println(Thread.currentThread().getName()+"當前餘額爲:"+this.balance);
				}
				
			}else {
				System.out.println(Thread.currentThread().getName()+"卡號或密碼錯誤");
			}
		}
	}
}

輸出結果:

丈夫正在讀卡。。。
丈夫驗證成功。。。
丈夫當前餘額爲:1500.0
妻子正在讀卡。。。
妻子驗證成功。。。
妻子當前餘額爲:300.0

14.3.3 線程的狀態(阻塞)

在這裏插入圖片描述

  • 注:JDK5之後,就緒、運行統稱Runnable

14.3.4 同步方法2

  • 同步方法
    synchronized 返回值類型 方法名稱(形參列表0){//對當前對象(this)加鎖
    //代碼(原子操作)
    }
  • 注:
    只有擁有對象互斥鎖標記的線程,才能進入該對象加鎖的同步方法中。
    線程退出同步方法時,會釋放相應的互斥鎖標記

14.3.5 同步規則

注意:

  • 只有在調用包含同步代碼塊的方法,或者同步方法時,才需要對象的鎖標記;
  • 如調用不包含同步代碼塊的方法,或普通方法時,則不需要鎖標記,可直接調用;

已知JDK中線程安全的類:

  • StringBuffer
  • Vector
  • Hashtable
  • 以上類中的公開方法,均爲synchronized修飾的同步方法;

14.3.6 經典問題

死鎖:

  • 當前第一個線程擁有A對象鎖標記,並等待B對象鎖標記,同時第二個線程擁有B對象鎖標記,並等待A對象鎖標記時,產生死鎖;
  • 一個線程可以同時擁有多個對象的鎖標記,當線程阻塞時,不會釋放已經擁有的鎖標記,由此可能造成死鎖;
public class TestDeadLock {

	public static void main(String[] args)  {
		LeftChopstick left = new LeftChopstick();
		RightChopstick right = new RightChopstick();
		
		Thread boy = new Thread(new Boy(left , right));
		Thread girl = new Thread(new Girl(left , right));
		
		girl.start();
		boy.start();
	}

}

class LeftChopstick{
	String name = "左筷子";
}
class RightChopstick{
	String name = "右筷子";
}
class Boy implements Runnable{
	LeftChopstick left;
	RightChopstick right;
	public Boy(LeftChopstick left , RightChopstick right) {
		this.left = left;
		this.right = right;
	}
	public void run() {
		System.out.println("男孩要拿筷子:");
		synchronized(left) {//拿到左筷子資源,加鎖
			try {
				//高風亮節,把筷子讓出去了
				left.wait();
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("男孩拿到了左筷子,開始拿右筷子");
			synchronized(right) {//拿到右筷子資源,加鎖
				System.out.println("男孩拿到了右筷子,開始吃飯");
			}
		}
	}
}
class Girl implements Runnable{
	LeftChopstick left;
	RightChopstick right;
	public Girl(LeftChopstick left , RightChopstick right) {
		this.left = left;
		this.right = right;
	}
	public void run() {
		System.out.println("女孩要拿筷子:");
		synchronized(right) {//拿到右筷子資源,加鎖
			System.out.println("女孩拿到了右筷子,開始拿左筷子");
			synchronized(left) {//拿到左筷子資源,加鎖
				System.out.println("女孩拿到了左筷子,開始吃飯");
				left.notify();//女孩吃完後,喚醒等待左筷子鎖的的男孩線程
			}
		}
	}
}

輸出結果:

男孩要拿筷子:
女孩要拿筷子:
女孩拿到了右筷子,開始拿左筷子
女孩拿到了左筷子,開始吃飯
男孩拿到了左筷子,開始拿右筷子
男孩拿到了右筷子,開始吃飯
  • 生產者、消費者:
    若干個生產者在生產產品,這些產品將提供給若干個消費者去消費,爲了使生產者和消費者能併發執行,在兩者之間設置一個能存儲多個產品的緩衝區,生產者將生產的產品放入緩衝區中,消費者從緩衝區中取走產品進行消費,顯然生產者和消費者之間必須保持同步,即不允許消費者到一個空的緩衝區中取產品,也不允許生產者向一個滿的緩衝區中放入產品;
public class TestProductCustomer {

	public static void main(String[] args) {
		Shop shop = new Shop();//共享資源對象
		Thread p = new Thread(new Product(shop) , "生產者");
		Thread c = new Thread(new Customer(shop) , "消費者");

		p.start();
		c.start();
	}

}
//商場
class Shop{
	Goods goods;
	boolean flag;//標識商品釋放充足
	//生產者調用的存的方法
	public synchronized void saveGoods(Goods goods) {
		//1.判斷商品是否充足
		if(flag == true) {//商品充足,生產者不用生產,而等待消費者買完
			System.out.println("生產者:商品充足,要等待了");
			try {
				this.wait();//進入等待狀態
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
		//商品不足,生產者生產商品,存入商場
		System.out.println(Thread.currentThread().getName()+"生產者在商場裏存放"+goods.getId()+"件商品");
		this.goods = goods;
		flag = true;//已經有商品了,可以讓消費者購買了
		this.notifyAll();//將等待隊列的消費者喚醒,可以購買
	}
	//消費者調用的取的方法
	public synchronized void buyGoods() throws InterruptedException{
		if(flag == false) {//沒有商品了,消費者需要等待
			System.out.println("消費者,商品不充足,要等待了");
			this.wait();//消費者進入等待隊列,等待生產者生產商品後,喚醒
		}
		//正常購買商品
		System.out.println(Thread.currentThread().getName()+"購買了"+goods.getId()+"件商品");
		//商品賣完了,標識沒貨了
		this.goods = null;
		flag = false;
		//喚醒生產者生產商品
		this.notifyAll();
	}
	
}
//商品
class Goods{
	private int id;
	public Goods(int id) {
		this.id = id;
	}
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
}
//生產者
class Product implements Runnable{
	Shop shop;//商場
	public Product(Shop shop) {
		this.shop = shop;
	}
	public void run() {
		//通過循環生產商品存放到shop裏
		for(int i = 1 ; i <= 4 ; i++) {
			//生產者線程調用存商品的方法,傳一個商品對象
			this.shop.saveGoods(new Goods(i));
		}
	}
}
//消費者
class Customer implements Runnable{
	Shop shop;//商場
	public Customer(Shop shop) {
		this.shop = shop;
	}
	public void run() {
		
		for(int i = 1 ; i <= 4 ; i++) {
			try {
				this.shop.buyGoods();
				System.out.println();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}	
}

輸出結果:

消費者,商品不充足,要等待了
生產者生產者在商場裏存放1件商品
生產者:商品充足,要等待了
消費者購買了1件商品

生產者生產者在商場裏存放2件商品
生產者:商品充足,要等待了
消費者購買了2件商品

消費者,商品不充足,要等待了
生產者生產者在商場裏存放3件商品
生產者:商品充足,要等待了
消費者購買了3件商品

生產者生產者在商場裏存放4件商品
消費者購買了4件商品

14.3.7 線程通信

等待:

  • public final void wait()
  • public final void wait(long timeout)
  • 必須在對obj加鎖的同步代碼塊中。在一個線程中,調用obj.wait()時,此線程會釋放其擁有的所有鎖標記。同時此線程阻塞在o的等待隊列中。釋放鎖,進入等待隊列。

通知:

  • public final void notify()
  • public final void notifyAll()
  • 必須在對obj加鎖的同步代碼塊中。從obj的Waiting中釋放一個或全部線程。對本身沒有任何影響。
public class TestWaitNotify {

	public static void main(String[] args) throws Exception{
		
		Object obj = new Object();
		MyThread t1 = new MyThread(obj);
		MyThread2 t2 = new MyThread2(obj);
		
		t2.start();
		t1.start();
		
		//主線程通知完兩個線程後,休眠
		Thread.sleep(2000);
		synchronized(obj) {
			System.out.println(Thread.currentThread().getName()+"進入到同步代碼塊");
			
//			obj.wait();//主線程獲得鎖,也主動釋放
			//此時此刻等待隊列裏由兩個線程
//			obj.notify();//在obj的等待隊列中,隨機喚醒一個線程
			obj.notifyAll();//將obj的等待隊列的所有線程都喚醒
			System.out.println(Thread.currentThread().getName()+"退出同步代碼塊");
		}
	}

}
//一個線程持有A對象的鎖,需要B對象的鎖,另一個持有B,想要A
//簡單的,一個線程持有A對象的鎖,另一個線程也想要:阻塞了
class MyThread extends Thread{
	Object obj;
	public MyThread(Object obj) {
		this.obj = obj;
	}
	public void run() {
		synchronized(obj) {
			System.out.println(Thread.currentThread().getName()+"進入到同步代碼塊");
			//Thread-0先拿到了鎖,高風亮節,讓給其它線程先拿
			try {
				obj.wait();//主動釋放當前持有的鎖,並進入無限期等待
			}catch(InterruptedException e1){
				e1.printStackTrace();
			}
			try {
				Thread.sleep(1000);
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"退出了同步代碼塊");
		}
	}
}

class MyThread2 extends Thread{
	Object obj;
	public MyThread2(Object obj) {
		this.obj = obj;
	}
	public void run() {
		synchronized(obj) {
			try {
				obj.wait();
			} catch (InterruptedException e1) {
				e1.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"進入到同步代碼塊");
			try {
				Thread.sleep(1000);
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"退出了同步代碼塊");
//			obj.notify();//在obj這個共享對象的等待隊列中,喚醒一個正在等待拿鎖的線程
		}
	}
}

輸出結果:

Thread-0進入到同步代碼塊
main進入到同步代碼塊
main退出同步代碼塊
Thread-0退出了同步代碼塊
Thread-1進入到同步代碼塊
Thread-1退出了同步代碼塊

14.3.8 總結

線程的創建:

  • 方式1:繼承Thread類
  • 方式2:實現Runnable接口(一個任務Task),傳入給Thread對象並執行;

線程安全:

  • 同步代碼塊:爲方法中的局部代碼(原子操作)加鎖;
  • 同步方法:爲方法中的所有代碼(原子操作)加鎖;

線程間的通信:

  • wait() / wait(long timeout) :等待
  • notify() / notifyAll(): 通知
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章