天天看點

java程式員從笨鳥到菜鳥之(四十二)線程再涉

線程的引出

1 現在有一個需求:

某電影院目前正在上映賀歲大片(國産淩淩漆,大話西遊),共有1000張票,而它有3個售票視窗售票,請設計一個程式模拟該電影院售票。

 分析:

(1)3個視窗售票是開啟了三個線程,完成各自的任務

(2)3個視窗要多1000張票進行操作(賣票)

執行個體1

線程類

package 賣票;


public class SellTicket extends Thread {
	//這1000張票應該被三個線程共用,是以用static修飾
	//這1000張票不讓外界修改資料,是以用private修飾
	private static int ticket=1000;
	
	public SellTicket(String name){
		super(name);
	}
	
	
	@Override
	public void run() {
		//賣票
		while(ticket>0){
			
			System.out.println(Thread.currentThread().getName()+"賣第"+(ticket--)+"張票");
		}
		
	}
}
           

測試類

package 賣票;

/**
 * @author Orange
 * @version 1.8
 */
public class SellTicketDemo {
	
	public static void main(String[] args) {
		//建立三個賣票的線程對象
		SellTicket st1 = new SellTicket("售票視窗1");
		SellTicket st2 = new SellTicket("售票視窗2");
		SellTicket st3 = new SellTicket("售票視窗3");
		//開啟三個線程
		st1.start();
		st2.start();
		st3.start();
	}
}
           

執行結果:在多線程的環境下,三個線程彼此互不影響,互相搶占CPU來執行該線程的代碼;

發現問題:賣票不是逐漸遞減的去賣;先賣第1000張,然後賣第998張,與我們的需求有一定的差異

分析原因:每個線程都有一個獨立的程式計數器和方法調用棧

程式計數器:稱為PC計數器,當線程執行一個方法時,程式計數器指向方法中下一條要執行的位元組碼檔案

說明:隻有線程搶到了CPU,程式計數器才會執行指向的代碼(其實是位元組碼檔案);換句話說沒有搶到就暫時中斷,停留在此位置,直到搶到再從此位置開始執行位元組碼檔案。

方法調用棧:簡稱方法棧,跟蹤線程運作過程中的一系列的方法調用過程,每當線程調用一個方法(常見的是在run()方法中調用其他方法),就向方法棧壓入一個新桢(棧)

棧桢的組成部分:

(1)局部變量區:存放(調用方法中)局部變量和方法參數

(2)操作數棧:線程工作區,存放目前線程運算過程中的臨時資料

(3)棧資料區:為線程指令提供相關的資訊。例如:定位位于方法區(回顧方法區的的内容)和堆區的特定資料、正常退出或異常退出的方法

操作數棧:線程的工作區,存放目前線程運算過程中的生成的臨時資料

針對本問題:比如說線程1準備賣第400張票時(準備要執行輸出這條指令時),但是由于線程2搶到了CPU是以會先輸出第399張票,然後第398張票,然後被線程1重新搶到了CPU的執行權,從上一次程式計數器執行的指令開始執行(上次線程1暫停的位置執行,儲存第400張票的資訊)

撿到的知識:java指令開啟了java虛拟機程序時,JVM會建立一個主線程(作業系統完成的),該線程從程式入口main()方法開始執行

 需求2:為了模拟電影院賣票更真實的場景:,每一個視窗賣票應該延遲操作在接口自實作類中,在run()方法中讓每一個線程執行睡眠0.1秒

執行個體2

線程類變化了,其它不變

package 賣票;


public class SellTicket extends Thread {
	//這1000張票應該被三個線程共用,是以用static修飾
	//這1000張票不讓外界修改資料,是以用private修飾
	private static int ticket=100;
	
	public SellTicket(String name){
		super(name);
	}
	
	/* 模拟該電影院售票情況---無延時
	 * 為了模拟電影院賣票更真實的場景,每一個視窗賣票應該延遲操作
	 * 在接口自實作類中,在run()方法中讓每一個線程執行睡眠0.1秒
	 * 
	 * */
	public void run() {
		//賣票
		while(ticket>0){
			try {
				Thread.sleep(100);//加了一個這個
			} catch (InterruptedException e) {
				System.out.println("異常中斷");
			}
			//一張票可能會被賣多次,(加入延遲操作之後)---sleep()--一會看看情況
			System.out.println(Thread.currentThread().getName()+"賣第"+(ticket--)+"張票");
		}
		
	}
}
           

出現問題:

(1)一張票可能被賣多次(同票)

(2)可能出現負票:0、-1

 原因:延遲操作和線程随機性導緻

深入剖析:一種可能的情景;線程1先讀取了第1000張準備賣出,但由于睡眠,被線程2搶到了CPU(線程1雖然沒有賣出,但儲存着第1000張票的資訊,一旦線程1被喚醒(timeout)搶占到CPU還是要賣第1000張票),線程2準備賣第1000張票但由于睡眠被線程3搶到,線程3同理;最後線程1先輸出第1000張票,此時ticket本應變為999,但是底層尚未在方法區内變為999(底層指令,基本不會出現:大多數情況下變量的改變指令優于其它),線程2還是按照1000張票來輸出,此時ticket變為999;

關于負票:一種可能的情景:線程1準備賣第1張票,此時線程2搶到了CPU的執行權,進入了判斷的方法(ticket>0);然後線程3也搶到了進入了判斷的方法(ticket>0),但是線程1又搶到了,輸出票的資訊(此時票變成0了),線程2結束,此時線程1執行輸出票0,線程3輸出票-1

目前線程已經進入了判斷的方法(ticket>0),但是被其他線程搶到了,其她線程結束後,而變量也随之改變

----------------------------------------------------------------------------------------

線程的職責是執行一些操作,而大多數的操作都涉及資料的處理;

那麼出現上述問題的真正原因是:多個線程對共享的資料(ticket)進行操作,線程1在修改ticket的過程中,線程2也會修改變量ticket,ticket的變量僅僅代表一個臨時的結果,變量ticket的值處于不确定的狀态

出現的問題:專業術語(線程不安全),原子操作沒有封裝

原子操作:多條語句操作共享資源的代碼的操作(很重要!!!  很重要!!! 很重要!!!)

檢驗多線程安全問題的标準(以後在判斷一個多線程式是否有安全問題的标準)

(1)目前是否處于多線程的環境

(2)多線程環境是否有共享資源

(3)是否有多條語句操作共享資源

解決方案:線程1在操作共享資料的過程,讓線程2先等待,操作完後再讓線程2線上程1的基礎上操作;也即一個線程在操作原子操作的期間,采取措施使其它線程不能操作共享資源(處于阻塞狀态)

涉及到同步機制問題了:Java中的同步是指人為的控制和排程,保證共享資源的多線程通路成為線程安全,來保證結果的準确性;通常使用synchronized關鍵字的方法或者synchronized同步代碼塊----同步是指得是一個線程完整的執行原子操作(其它線程靠邊)

同步機制解決問題:解決共享資源競争---一個線程在操作共享資源時,其他線程隻能望眼欲穿的等待,隻有當已經操作完成共享資源的線程執行完同步代碼塊時,其他線程才有機會操作共享資源----類比上廁所排隊(除了忍受等待還有面臨搶廁所的問題)

補充:線程的安全優先級(同步)高于性能(并發),是以在保證安全的情況下(結果準确),提高性能;在開發中常見的措施是:在同步代碼塊中盡量包含較少的原子操作(共享資料的操作),使得一個線程盡快釋放鎖,減少其他線程等待鎖的時間,二者兼顧

有時間了補充一個打水的問題——安全與性能兼顧(二者是一對此消彼長的沖突)

真正解決上面的問題:将多條語句對共享資料進行操作(原子操作)的代碼用同步代碼塊封裝起來

  Java的同步機制的代碼形式

  使用同步代碼塊:synchronized(同步鎖對象){

  多條語句對共享資料的操作;

  }

 執行個體3

線程類

package 賣票2;


public class SellTicket implements Runnable {

	private   int ticket=100;
	private Object obj = new Object() ;
	
public void run() {
		
		// 模拟電影院一直有票
		while (true) {
			
			synchronized(obj){//t1進來,門關了,t2和t3線程都不會進來
				
				if (ticket> 0) {
					try {
						Thread.sleep(100) ;
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "正在出售第"
							+ (ticket--) + "張票");
					//視窗1正在出售第100張
					//視窗正在出售第99張
				}
			}//代碼執行完畢,t1線程出來,意味着門開了...  
			  
			
			
		}
	}
}
           

測試類

package 賣票2;

/**
 * @author Orange
 * @version 1.8
 */
public class SellTicketDemo {

	public static void main(String[] args) {
		//建立線程對象
		SellTicket st = new SellTicket();
		//多個線程對共享資料:st的操作
		Thread thread1 = new Thread(st);
		thread1.setName("售票視窗1");
		Thread thread2 = new Thread(st);
		thread2.setName("售票視窗2");
		Thread thread3 = new Thread(st);
		thread3.setName("售票視窗3");
		//開啟線程
		thread1.start();
		thread2.start();
		thread3.start();
	}
}
           

注意:while(true)和synchronized不同的位置時分析(很重要,暫時沒有搞懂)

同步鎖對象的說明:

(1)同步鎖對象可以是什麼樣的對象?

          可以是Object類型,任意的Java類(自定義)

(2)對象的同步鎖是概念上的鎖,也稱之為以一個以對象為标記的鎖

線程之間保持同步的了解:不同的線程在執行同一對象的同步代碼塊,是以要獲得每個對象的鎖而互相牽制

同步代碼塊與同步方法的等價形式(二者的轉換,this)----有時間了補充

如果一個方法被synchronized修飾特點:每次隻能有一個線程執行此類中的同步方法,隻有一個線程喲!!!;隻有線程退出同步方法,其它線程才能執行同步方法

線程同步的特征:

(1)同步代碼塊和非同步代碼塊操作共享資源,依然會對共享資源競争。通俗一點:一個線程執行同步代碼塊,另一個線程可以執行非同步代碼塊對共享資源競争

(2)每個對象都有唯一的同步鎖

(3)在靜态方法前面使用synchronized修飾符,是在類的層面(每個被加載到java虛拟機的方法區的類也有唯一的同步鎖),通俗了解:同一時間隻有一個線程可以調用它們中的任意方法(靜态方法),也即:如果線程1調用它們中的一個方法,另一個線程2試圖調用它們當中的方法,線程2挂起,直到線程1執行完靜态代碼塊,線程2被喚醒,執行相應的靜态方法

(4)synchronized聲明不會被繼承;子類重寫父類用synchronized修飾的方法,如果子類方法不用synchronized修飾将不會同步,否則你懂的

問題:非靜态代碼塊和靜态代碼塊的差別?

(1)靜态方法(被synchronized修飾):在類範圍是同步的(了解上:原來所有的執行個體都可以不通過對象直接通路方法;現在所有的執行個體在該方法沒有去鎖的情況,不能去通路其它的靜态方法);同一時間内隻能有一個線程可以調用它們的任意一個方法,也就是說:如果一個線程通路任意的靜态方法,如果有另一個線程中通路(試圖)任意的靜态方法時,另一個線程會挂起(處于類的鎖池中),直到一個線程的靜态同步代碼塊執行完,才會喚醒,執行另一個線程的靜态方法-----等于是為類中所有的靜态同步方法加了一把鎖(靜态同步方法共進退)

(2)非靜态的方法(被synchronized修飾):在同一個對象是同步的。也就是說這個類的某一個确定對象,線程1調用該對象的同步方法時,線程2試圖在該對象上調用某個同步的非靜态方法,線程2會挂起(位于對象的鎖池中),直到線程1執行完畢同步非靜态方法後,線程2被喚醒,執行相應的非靜态方法(存在競争嗎?)通俗講:在一個對象上試圖有多個線程調用該方法,隻有一個線程會進入該方法,其餘線程必須挂起。注意:不同對象調用各自的非靜态方法,不會挂起

何時使用同步方法?

(1)方法修改關鍵屬性值的時候

(2)方法通路多個屬性的值,并根據多個屬性的值進行邏輯判斷時候

(3)其它情況,視需要而定

萬事有利有弊,同步機制弊端:

 (1)同步雖然保證了線程的安全性,但同時意味着執行效率低(每一個線程在搶占到CPU的執行權,會去将(門)關閉,别的線程進不來)

 (2)容易出現死鎖現象

死鎖

概念:當一個線程等待由另一個線程持有的鎖,而後者正在等待已被第一個線程持有的鎖,就會發生死鎖。通俗講:互相等待彼此釋放鎖對象

說明:Jva虛拟機不檢測也不試圖避免這種情況,是以就成了我們的責任,哎!!!

補充:開啟了兩個線程,兩個線程分别持有不同對象的鎖,然後一個線程在鎖對象的同步代碼塊中試圖通路另一個鎖對象造成的

執行個體5

鎖對象

package org.westos_02;

public class MyLock {
	//建立兩把鎖對象
	public static final Object objA = new Object() ;
	public static final Object objB = new Object() ;
}
           

死鎖的線程

package org.westos_02;

public class DieLock extends Thread {
	
	//定義一個成員變量
	private boolean flag ;
	
	public DieLock(boolean flag){
		this.flag = flag ;
	}
	
	//重寫run()方法
	
	@Override
	public void run() {
		//dl1,dl2線程
		if(flag){
			synchronized(MyLock.objA){
				System.out.println("if objA");
				synchronized (MyLock.objB) {
					System.out.println("if objB");
				}
			}//代碼執行完畢,objA鎖相當于才能被釋放掉
		}else {
			//dl2
			synchronized (MyLock.objB) {
				System.out.println("else objB");
				synchronized(MyLock.objA){
					System.out.println("else objA");
				}
			}
		}
		
	}
}
           

測試類

package org.westos_02;

public class DieLockDemo {
	
	public static void main(String[] args) {
		
		//建立線程類對象
		DieLock dl1 = new DieLock(true) ;
		DieLock dl2 = new DieLock(false);
		
		//啟動線程
		dl1.start() ;
		dl2.start() ;
	}
	
}
           

造成的原因:線程中不通信(處于阻塞狀态);由此引出了線程通信的問題(都不主動,總得有個中間人---和事佬來協調--調解)---下面會講到

問題:如何盡量避免死鎖現象?

經驗之談:當幾個線程都要通路共享資源A、B、C時,保證每個線程都按照同樣的順序去通路它們,比如先通路A,再通路B,再通路C(不要壞了規矩)

Thread類容易死鎖方法

簡要說明:這些方法已經被廢棄(jdk1.2被廢除,但可以使用,不推薦)

(1)suspend()

說明:非靜态方法,使運作中線程放棄CPU,暫停運作,但是并不會釋放鎖對象,死鎖根源。

(2)resume()

說明:非靜态方法,使暫停的線程恢複運作

這兩個方法廢除原因(這兩個方法的危險性):

        1)容易導緻死鎖

        2)即使不是死鎖,一個線程(線程1)強行中斷另一個運作的線程(線程2),會造成另一個線程(線程2)操作的資料停留在邏輯上不合理的狀态,導緻線程不安全

舉例:線程1獲得了對象的鎖,正在執行一個同步代碼塊,如果線程2調用了線程1的suspend()方法,線程1會暫停,暫時放棄CPU,但是線程1不會釋放鎖對象;此時線程2試圖通路該鎖對象就會産生死鎖問題。

廢除了總得有替代方案吧,解決方案:

這也是為什麼不在Thread類中定義這兩個方法,其實Thread類本身已經有類似的兩個方法(問題太多)

在Object類中使用wait()方法和notify()方法來替代suspend()和resume()

特點:前者由線程自身執行一個對象的wait()方法,確定處理的資料穩定性,再進入阻塞狀态;同時會釋放鎖對象,避免了死鎖

(3)stop()

說明:強制終止一個線程,線程會終止,同時釋放鎖

廢棄的原因:不會造成死鎖問題,但會使共享資料停留在不穩定的中間狀态

舉例:假設線程1獲得了對象的鎖,執行一個同步代碼塊;線程2調用線程1的stop()方法,線程1就會終止,線程1在終止之前釋放它特有的鎖對象;避免了前面我們提到的前兩種方法的死鎖問題;但是如過線程2在調用線程1的stop()方法時,線程1正在執行一個原子操作,會操作共享資料,使共享資料停留在不安全的狀态;為了安全起見,隻有線程1本身才可以決定何時終止運作

在實際開發中:以程式設計的方式控制線程-----重點

通俗的講:一般在受控制的線程中定義一個标志變量,其它線程通過改變變量的值,來控制線程的暫停、恢複運作

線程通信

不同的線程執行不同的任務,任務有聯系,線程必須能夠通信,協調完成任務,同步問題(鎖對象互相牽制問題)

突然想到的:高山流水----并發,一唱一和---同步

Object類中有兩個與線程通信有關的方法

(1)線程挂起---wait()

 wait()概述:執行該方法的線程釋放鎖對象,java虛拟機把該線程放到對象的等待池(阻塞狀态),該線程等待其它線程将其喚醒(不會釋放鎖對象);釋放鎖對象(在保證安全的前提下,提高了并發性---性能)

(2)線程喚醒---notify()

 notify()概述:執行該方法的線程喚醒在等待池的線程,從對象的等待池中随機選擇一個線程放到對象的鎖池,待該線程的方法(同步)執行完畢,等待池中随機選擇的線程(上帝的選擇)就會執行相應的同步代碼塊-----排隊:等待執行該方法的線程執行完畢,再執行随機線程

 注意:如果對象的等待池中沒有任何線程(即:沒有可以移到對象鎖池中的線程),notify()方法什麼也不做

(3)線程喚醒---notifyAll()

 notifyAll()方法概述:會把對象等待池中所有的線程轉移到對象的鎖池中(不太常用)---此舉等于讓這些線程競争

  注意:對象的等待池中沒有線程,則什麼也不做

假設問題:線程1和線程2操作同一個對象t,兩個線程通過對象t的wait()和notify()方法進行通信

常見的模型:生産者和消費者模型、銀行存儲模型

執行個體4   生産者和消費者模型----"販賣人口"

Person類

package 販賣人口;

/**
 * @author Orange
 * @version 1.8
 * 說明一點:這兩個鎖對象都是:同一個對象
 */
public class Person {

	//人的屬性:名字和年齡
	private String name;
	private int age;
	private boolean flag;//線程通信的關鍵
	
	//對人的操作----其它線程類對人執行的任務
	//得到人---生産(人)
	public synchronized void set(String name,int age){
		//首先判斷是否有資料,如果有資料該怎麼辦?沒有資料怎麼辦?
		//如果有資料,通知人販子來買(讓人販子線程喚醒,此線程睡眠),生産線程處于挂起狀态(直到檢測到沒有資料,則重新生産)
		if(flag){
			try {
				this.wait();//自己暫時停止生産,通知其他線程來消費
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//沒有資料,則開始生産資料,同時辨別位改變(通知消費者線程消費)
		this.name=name;
		this.age=age;
		flag=true;
		this.notify();
	}
	
	public synchronized void get(){
		//判斷是否已經生産了資料
		//(1)沒有資料,消費者線程則等待
		if(!flag){
			try {
				this.wait();//沒有可消費的資料了,通知生産
			} catch (InterruptedException e) {
				
			}
		}
		//否則消費
		System.out.println("恭喜你購買了年齡為"+age+"的"+name);
		//消費完了,該通知生産了
		flag=false;
		this.notify();
	}
}
           

生産者線程

package 販賣人口;

/**
 * @author Orange
 * @version 1.8
 */
public class SetPerson extends Thread {

	private Person student;

	public SetPerson() {

	}

	public SetPerson(Person student) {
		this.student = student;
	}
    private int i;
	@Override
	public void run() {
		//自己沒有加while(true)導緻執行了一次
		//始終在等待生産資料
		while(true){
			if(i%3==0){
				student.set("高圓圓", 18);
			}else if(i%3==1){
				student.set("陳圓圓", 16);
			}else{
				student.set("宋圓圓", 17);
			}
			i++;
		}
	}
}
           

消費者線程

package 販賣人口;

/**
 * @author Orange
 * @version 1.8
 */
public class GetPerson extends Thread {
	
	private Person student;

	public GetPerson() {

	}

	public GetPerson(Person student) {
		this.student = student;
	}

	@Override
	public void run() {
		//始終在等待擷取資料
		while(true){
		  student.get();
		}
	}
}
           

測試類

package 販賣人口;

/**
 * @author Orange
 * @version 1.8
 */
public class Test {

	public static void main(String[] args) {
		//建立兩個線程要操作的同一個對象
		Person person = new Person();
		//建立兩個線程,來操作對象
		SetPerson sp1 = new SetPerson(person);
		GetPerson sp2 = new GetPerson(person);
		//開啟兩個線程
		sp1.start();
		sp2.start();
	}
}
           

看懂了,沒看懂的話整個流程再過一遍。首先生産者和消費者線程會搶占CPU,我們也不知道誰先搶到;假設消費者線程先搶到,由于判斷發現沒有資料就會處于靜默狀态,生産者線程搶到了,生産者線程通過判斷發現還沒生産資料,就開始生産資料,生産資料後通過改變标志位以及notify()通知消費者線程啟動消費線程的任務,消費者線程消費完了修改标志位以及notify()通知生産者開始生産,循環下去。。。;關于生産者線程先搶到,大家可以自行推理,人人都是福爾摩斯

發現問題:在編寫代碼 過程中,發現沒有while(true)隻執行了一次,線程就Gameover了;此語句的作用是時刻等着生産和消費

通俗了解(強調n邊不為過):兩個人共同擁有一把鎖(對此鎖進行操作),是以同一時刻隻有一個人可以擁有鎖,其他人隻能等待

友情提示:本代碼純屬娛樂,不以标題制造輿論,俺是合法公民!!!

執行個體5 銀行存儲模型(有時間了補充)

下一章節的内容---涉及到進階屬性

線程組

Lock(外部鎖)

Callable接口(第三種實作方式)