多線程




  • 多線程概述
    • 進程:正在進行中的程序。
    • 線程:進程中的獨立控制單元。線程控制着進程的執行。
    • 一個進程中至少有一個線程。
    • jvm啓動時有一個java.exe進程,該進程至少有一個主線程負責程序的執行,這個線程運行的代碼存在於main函數中,該線程稱之爲主線程。
    • jvm啓動時,除了主線程,還有負責垃圾回收的線程。
    • 多線程程序:有不止一個線程(執行路徑)的程序就是多線程程序,如迅雷。
    • 多線程存在的意義:
      • 使程序中不同部分的代碼產生“同時運行”的效果。

  • 線程的創建方式

  • 繼承Thread類
  • 創建線程的步驟:
    1. 定義類繼承Thread。
    2. 覆寫Thread類中的run方法。
      • 目的:將自定義代碼存儲在run()方法中,讓線程運行。
    3. 調用線程的start方法
      • 該方法有兩個作用:啓動線程,調用run方法。
      • 如果直接調用run方法,相當於僅僅是對象調用普通方法。線程雖然創建了,但是並沒有運行。
  • Thread類
    • Thread類用於描述線程。
    • Thread類中的run()方法用於存儲線程要運行的代碼。
    • start()方法用於開啓線程,並執行該線程的run()方法。
  • 練習
    • 需求:創建兩個線程,和主線程交替運行。
    • 代碼:
      package cn.itcast.heima;
      
      public class Test {
      	public static void main(String[] args) {
      		SubThread st1 = new SubThread();
      		SubThread st2 = new SubThread();
      		st1.start();
      		st2.start();
      		
      		for (int i = 0; i < 10; i++) {
      			System.out.println(Thread.currentThread().getName() + "run..." + i);
      		}
      	}
      }
      
      class SubThread extends Thread{
      	@Override
      	public void run() {
      		for (int i = 0; i < 10; i++) {
      			System.out.println(Thread.currentThread().getName() + "run..." + i);
      		}
      	}
      }
      打印結果:

  • 實現Runnable接口
  • 創建線程的步驟:
    1. 定義類實現Runnable接口。
    2. 覆蓋Runnable接口中的run()方法。
    3. 創建類Thread的線程對象。
    4. 將Runnable子類對象作爲實際參數傳遞給Thread類的構造函數。
    5. 通過Thread對象的start()方法開啓線程並調用Runnable接口子類的run()方法。
  • 優勢:
    • 避免了單繼承的侷限性
    • 可以創建多個線程共享Runnable子類對象的數據,避免了共享數據定義爲靜態。
  • 例子:
    • 需求:簡單的賣票程序。實現多個窗口同時賣票。
    • 如果按照繼承Thread類的方式實現多線程,代碼如下:
      package cn.itcast.heima;
      
      public class Test {
      	public static void main(String[] args) {
      		
      		//開放四個窗口售票
      		Ticket t1 = new Ticket();
      		Ticket t2 = new Ticket();
      		Ticket t3 = new Ticket();
      		Ticket t4 = new Ticket();
      		t1.start();
      		t2.start();
      		t3.start();
      		t4.start();
      	}
      }
      
      class Ticket extends Thread{
      	private static int ticket = 20;//總共20張票
      	@Override
      	public void run() {
      		while (ticket > 0) {
      			System.out.println(Thread.currentThread().getName() + "...sale:" + ticket--);
      		}
      	}
      }

      打印結果:


      這種方式需要將票數定義爲靜態成員。
      缺陷:ticket成員變量生命週期過長,且Ticket類不能繼承其它類。
    • 可以將其改爲實現Runnable接口的方式實現多線程,代碼如下:
      package cn.itcast.heima;
      
      public class Test {
      	public static void main(String[] args) {
      		Ticket t = new Ticket();
      		
      		//開放四個窗口售票
      		Thread t1 = new Thread(t);
      		Thread t2 = new Thread(t);
      		Thread t3 = new Thread(t);
      		Thread t4 = new Thread(t);
      		t1.start();
      		t2.start();
      		t3.start();
      		t4.start();
      	}
      }
      
      class Ticket implements Runnable{
      	private int ticket = 20;//總共20張票
      	@Override
      	public void run() {
      		while (ticket > 0) {
      			System.out.println(Thread.currentThread().getName() + "...sale:" + ticket--);
      		}
      	}
      }
      打印結果:


      這種方式避免了將ticket成員變量定義爲static,並且避免了單繼承的侷限性。
    • 實際開發中,建議使用實現Runnable接口的方式實現多線程。

  • 兩種方式區別:
  • 繼承Thread類:線程代碼存放在Thread子類的run()方法中。
  • 實現Runnable接口:線程代碼存放在Runnable接口子類的run()方法中。

  • 多線程的特性:隨機性
    • 對於多線程程序,每一次的運行結果都不相同。
    • 因爲多個線程都需要獲取CPU的執行權,CPU執行到哪個線程,哪個線程就運行。
    • 在某一時刻,只能有一個線程在運行,CPU在做着快速的切換,以達到看上去是同時運行的效果。

  • 線程的五種狀態
    • 線程的五種狀態:被創建,運行,凍結,臨時(阻塞),消亡。關係如下圖:

  • 獲取線程對象以及名稱
    • 線程有默認的名稱:Thread-編號(從0開始)。
    • currentThread():獲取當前線程對象(靜態)。
    • getName():獲取線程名稱。
    • 設置線程名稱:setName()或者構造函數。

  • 多線程的安全問題:同步
    • 問題原因:
      • 多個線程在操作同一個共享數據時,一個線程對多條語句只執行了一部分,另一個線程就參與進來,導致共享數據出現問題。
    • 解決辦法:
      • 一個線程在執行過程中,其它線程不可以參與執行。
    • Java對於多線程的安全問題提供了專業的解決方案:同步代碼塊。
    • 同步代碼塊格式:

    • 判斷需要被同步的代碼:
      • 操作共享數據的代碼就是需要被同步的代碼。
    • 同步代碼塊的原理:
      • 對象如同鎖。持有鎖的線程纔可以進入同步代碼塊執行語句。不持有鎖的線程即使獲得了CPU的執行權,也不能進入同步代碼塊執行語句。
      • 火車上的衛生間的例子。
    • 同步的前提:
      • 同步需要兩個或者兩個以上的線程。
      • 多個線程使用的是同一個鎖。
    • 同步的好處:
      • 解決了多線程的安全問題。
    • 同步的弊端:
      • 當線程相當多時,因爲每個線程都會去判斷同步上的鎖,這是很耗費資源的,無形 中會降低程序的運行效率。
    • 同步函數
      • 格式:
        在函數上加上synchronized修飾符即可。
      • 同步函數使用的鎖是this。要保證同步代碼塊和同步函數用的是同一個鎖,同步代碼塊的參數必須是this。
    • 靜態同步函數
      • 靜態同步函數的鎖是該方法所在類的字節碼文件對象:類名.class。
    • 死鎖
      • 死鎖的出現:同步中嵌套同步。
      • 實際開發中,要避免出現死鎖問題。

  • 線程間通信

  • 概念
    • 線程間通信就是多個線程在操作同一個資源,但是操作的動作不同。

  • 等待喚醒機制
    • 多線程操作共享數據時要考慮使用等待喚醒機制。
    • 等待線程存放在線程池中,notify()通常喚醒線程池中的最先等待的線程,notifyAll()喚醒線程池中的所有線程。
    • wait(),notify(),notifyAll()都要用在同步中,因爲要對持有監視器(鎖)的線程操作。
    • wait(),notify(),notifyAll()都是Object類中的方法,因爲這些方法在操作線程時,都必須要標識他們所操作線程持有的鎖。
      一個被等待的線程,只能被持有同一個鎖的線程喚醒,也就是說,等待和喚醒必須是同一個鎖。
      爲鎖可以是任意對象,所以可以被任意對象調用的方法定義在Object類中。
    • wait和sleep比較:
      • wait():釋放cpu執行權,釋放鎖。
      • sleep():釋放cpu執行權,不釋放鎖。到達休眠時間後線程將繼續執行,直到完成。若在休眠期另一線程中斷該線程,則該線程退出。

  • 生產者消費者例子
    • 需求:生產者和消費者分別創建兩個線程,保證生產者進程和消費者進程能夠交替執行。
    • 代碼:
      package cn.itcast.heima;
      
      public class Test {
      	public static void main(String[] args) {
      		Resource r = new Resource();//商品
      		
      		Producer pro = new Producer(r);//生產者
      		Consumer con = new Consumer(r);//消費者
      		
      		Thread t1 = new Thread(pro);//生產線程
      		Thread t2 = new Thread(pro);//生產線程
      		Thread t3 = new Thread(con);//消費線程
      		Thread t4 = new Thread(con);//消費線程
      		t1.start();
      		t2.start();
      		t3.start();
      		t4.start();
      	}
      }
      
      //商品類
      class Resource{
      	private String name;//商品名稱
      	private int count = 1;//商品編號
      	private boolean flag = false;//判斷商品是否還在
      	
      	//生產商品
      	public synchronized void set(String name){
      		//如果商品還在,生產線程等待
      		while (flag) {
      			try {
      				wait();
      			} catch (InterruptedException e) {
      				e.printStackTrace();
      			}
      		}
      		this.name = name + "---" + count++;
      		System.out.println(Thread.currentThread().getName() + "...生產者..." + this.name);
      		flag = true;//商品生產出來了
      		notifyAll();//喚醒所有等待線程
      	}
      	
      	//消費商品
      	public synchronized void out(){
      		//如果商品沒了,消費線程等待
      		while (!flag) {
      			try {
      				wait();
      			} catch (InterruptedException e) {
      				e.printStackTrace();
      			}
      		}
      		System.out.println(Thread.currentThread().getName() + "...消費者......" + this.name);
      		flag = false;//商品被拿走了
      		notifyAll();//喚醒所有等待線程
      	}
      }
      
      //生產者類
      class Producer implements Runnable{
      	private Resource res;
      	
      	public Producer(Resource res) {
      		this.res = res;
      	}
      	
      	@Override
      	public void run() {
      		while (true) {
      			res.set("商品");
      		}
      	}
      }
      
      //消費者類
      class Consumer implements Runnable{
      	private Resource res;
      	
      	public Consumer(Resource res) {
      		this.res = res;
      	}
      	@Override
      	public void run() {
      		while (true) {
      			res.out();
      		}
      	}
      }
      打印結果:
    • 需要定義while判斷標記flag:
      • 原因:讓被喚醒的線程再一次判斷標記,否則會導致重複生產或者重複消費。
    • 並且爲了避免線程全部凍結,需要在解鎖之前喚醒所有線程:
      • 原因:需要喚醒對方線程。因爲只用notify()容易出現只喚醒本方線程的情況,導致程序中所有線程都掛起。

  • JDK5.0升級版
    • 定義了Lock接口,代替了同步函數和同步代碼塊。
    • 定義了Condition接口,代替了Object中的wait(),notify(),notifyAll()方法。Condition對象可以通過Lock接口子類的newCondition()方法獲取。
    • 一個Lock可以對應多個Condition對象,該方式可以對指定的某個Condition對象對應的線程進行等待和喚醒處理,而不需要喚醒所有線程。
    • 生產者消費者例子的JDK5.0升級版代碼:
      package cn.itcast.heima;
      
      import java.util.concurrent.locks.Condition;
      import java.util.concurrent.locks.Lock;
      import java.util.concurrent.locks.ReentrantLock;
      
      public class Test {
      	public static void main(String[] args) {
      		Resource r = new Resource();//商品
      		
      		Producer pro = new Producer(r);//生產商品
      		Consumer con = new Consumer(r);//消費商品
      		
      		Thread t1 = new Thread(pro);//生產線程
      		Thread t2 = new Thread(pro);//生產線程
      		Thread t3 = new Thread(con);//消費線程
      		Thread t4 = new Thread(con);//消費線程
      		t1.start();
      		t2.start();
      		t3.start();
      		t4.start();
      	}
      }
      
      //商品類
      class Resource{
      	private String name;//商品名稱
      	private int count = 1;//商品編號
      	private boolean flag = false;//判斷商品是否還在
      	
      	private Lock lock = new ReentrantLock();//定義鎖對象
      	private Condition condition_pro = lock.newCondition();//定義生產線程的Condition對象
      	private Condition condition_con = lock.newCondition();//定義消費線程的Condition對象
      	
      	//生產商品
      	public void set(String name){
      		try{
      			lock.lock();//上鎖
      			//如果商品還在,生產線程等待
      			while (flag) {
      				try {
      					condition_pro.await();//生產線程等待
      				} catch (InterruptedException e) {
      					e.printStackTrace();
      				}
      			}
      			this.name = name + "---" + count++;
      			System.out.println(Thread.currentThread().getName() + "...生產者..." + this.name);
      			flag = true;//商品生產出來了
      			condition_con.signal();//喚醒消費線程
      		}finally{
      			lock.unlock();//解鎖
      		}
      	}
      	
      	//消費商品
      	public void out(){
      		try {
      			lock.lock();//上鎖
      			//如果商品沒了,消費線程等待
      			while (!flag) {
      				try {
      					condition_con.await();//消費線程等待
      				} catch (InterruptedException e) {
      					e.printStackTrace();
      				}
      			}
      			System.out.println(Thread.currentThread().getName()
      					+ "...消費者......" + this.name);
      			flag = false;//商品被拿走了
      			condition_pro.signal();//喚醒生產線程
      		} finally {
      			lock.unlock();//解鎖
      		}
      	}
      }
      
      //生產者類
      class Producer implements Runnable{
      	private Resource res;
      	
      	public Producer(Resource res) {
      		this.res = res;
      	}
      	
      	@Override
      	public void run() {
      		while (true) {
      			res.set("商品");
      		}
      	}
      }
      
      //消費者類
      class Consumer implements Runnable{
      	private Resource res;
      	
      	public Consumer(Resource res) {
      		this.res = res;
      	}
      	@Override
      	public void run() {
      		while (true) {
      			res.out();
      		}
      	}
      }
      打印結果:

  • 線程生命控制

  • 停止線程
    • stop()方法已經過時。
    • 如何停止線程:
      • 只有一種方法,run()方法結束。
      • 開啓多線程運行,運行代碼通常都是循環結構。只要控制住循環,就可以結束run()方法,從而結束線程。
    • Thread類提供的interrupt()方法,可以中斷線程的凍結狀態。
      當沒有指定的方式讓凍結的線程恢復到運行狀態時,就需要對凍結狀態進行清除。強制線程恢復到運行狀態,並在異常處理中通過改變標記讓線程結束。

  • 守護線程
    • 守護線程相當於後臺線程,當前臺線程全部結束後,後臺線程自動結束,JVM退出。
    • 如果一個線程的運行依賴於另一個線程的運行,當第一個線程結束時,希望第二個線程也隨之結束,則第二個線程可以標記爲守護線程。
    • 設置守護線程的方法:
      • 在線程開啓前調用setDaemon(true)方法。

  • 聯合線程
    • 當A線程執行到了B線程的join()方法時,A就會等待,直到B線程都執行完,A線程纔會繼續執行。
    • join()方法可以用來臨時加入線程執行。

  • 優先級和yield方法
    • 優先級
      • 線程被哪個線程開啓,就屬於哪個線程組。
      • 線程默認優先級爲5。優先級取值範圍1-10。
      • Thread類的toString()方法返回線程名稱、優先級和線程組。
      • 優先級代表搶佔cpu的頻率。
      • setPriority方法:改變線程優先級。
    • yield方法
      • yield方法:暫停當前正在執行的線程對象,並執行其他線程。
      • 可以實現當前線程執行完後讓出CPU執行權,達到不同線程交替執行的效果。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章