Java基礎--多線程


一、進程和線程

    進程:每個進程都有獨立的代碼和數據空間,進程間的切換會有較大的開銷,一個進程包含1--n個線程。

      線程:同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程切換開銷小。

二、多線程的概念

        在java虛擬機啓動的時候會有一個java.exe的執行程序,也就是一個進程。該進程中至少有一個線程負責java程序的執行。而且這個線程運行的代碼存在於main方法中。該線程稱之爲主線程。JVM啓動除了執行一個主線程,還有負責垃圾回收機制線程。 像這種在一個進程中有多個線程執行的方式,就叫做多線程。 

三. 創建線程兩種方法

       1.繼承Thread類,重寫run()方法

        創建步驟:

          a.定義類繼承Thread。

          b.複寫Thread中的run方法。

          c.創建定義類的實例對象。相當於創建一個線程。

          d.用該對象調用線程的start方法。該方法的作用是:啓動線程,調用run方法。

//定義MyThread 類繼承Thread。
class MyThread extends Thread{
	//複寫Thread中的run方法。
	public void run(){
	for(int x=0;x<10;x++){
		System.out.println("x= "+x);
	}
  }
}
public class Test1 {

	public static void main(String[] args) {
     MyThread mt=new MyThread(); //創建線程MyThread的實例對象。
     mt.run();  //開啓線程
	}
}

    2.實現Runnable接口,重寫run()方法

          創建步驟:

           a.定義類實現Runnable的接口。

           b.覆蓋Runnable接口中的run方法。目的也是爲了將線程要運行的代碼存放在該run方法中。

           c.通過Thread類創建線程對象。

           d.將Runnable接口的子類對象作爲實參傳遞給Thread類的構造方法。

    //定義MyThread類實現Runnable的接口。
class MyThread implements Runnable{
   //覆蓋Runnable接口中的run方法。
	public void run() {
		for(int x=0;x<10;x++){
			System.out.println("x= "+x);
		}
	}
}
public class Test2 {

	public static void main(String[] args) {
      MyThread mt=new MyThread();  //創建線程MyThread的實例對象。
      Thread t=new Thread(mt);  //創建線程對象
      t.start();  //開啓線程
	}
}
四、實現Runnable接口的好處

        1.適合多個相同程序代碼的線程去處理同一資源的情況,把虛擬CPU(線程)同程序的代碼,數據有效的分離,較好地體現了 面向對象的設計思想。 

        2.可以避免由於Java的單繼承特性帶來的侷限。

        3.有利於程序的健壯性,代碼能夠被多個線程共享,代碼與數據是獨立的。

五.線程生命週期及狀態

      1.線程生命週期:

          生命週期開始:當Thread對象創建完成時,線程生命週期開始。

          生命週期結束:當run()方法中代碼正常執行完畢結束或者線程拋出一個未捕獲的異常或者錯誤時,線程生命週期結束。

       2.線程狀態:

          新建狀態:創建一個線程對象後,還沒有在其上調用start()方法。

          運行狀態:具有執行資格和執行權。

          阻塞狀態:有執行資格,但是沒有執行權。

          凍結狀態:遇到sleep(time)方法和wait()方法時,失去執行資格和執行權,sleep方法時間到或者調用notify()方法        時,獲得執行資格,變爲臨時狀態。

          消忙狀態:run方法執行結束,或者線程拋出一個未捕獲的異常,失去運行資格。

六.線程調度

       1.概念

          JAVA中多個線程是併發執行的,線程的執行必須先得到CPU的使用權,JAVA虛擬機會按照特定的機制爲程序中每個線程分配CPU的使用權。
       2.線程調度的模型

           兩種模型:分時調度和搶佔式調度

       3.線程休眠

          Thread.sleep(long millis)靜態方法強制當前正在執行的線程休暫停執行,以“減慢線程”。當線程睡眠時,在指定時間內是不會執行的。當睡眠時間到期,則返回到可運行狀態。

          3.1對線程休眠的總結:

           a).線程睡眠是幫助所有線程獲得運行機會的最好方法。

           b).線程睡眠到期自動甦醒,並返回到可運行狀態,不是運行狀態。sleep()中指定的時間是線程不會運行的最短時間。因此,sleep()方法不能保證該線程睡眠到期後就開始執行。

            c).sleep()是靜態方法,只能控制當前正在運行的線程。

程序示例:(模擬火車站售票案例)

class TicketWidow implements Runnable{
	private int tickets=10;//定義變量tickets,並賦值10
	Object obj=new Object();//定義任意一個對象,用作同步對象的鎖
	public void run(){
		while(true){
			synchronized(obj){//定義同步代碼塊
				try{
					Thread.sleep(10);//經過的線程休眠10秒
				}
				catch(InterruptedException e){
					e.printStackTrace();
				}
				if(tickets>0){
					System.out.println(Thread.currentThread().getName()+"出售   "+tickets--);
				}
				else //如果tickets小於10,退出循環
					break;
			}
		}
	}
}
public class LiXi {
	public static void main(String[] args) {
     TicketWidow tw=new TicketWidow();//創建TicketWidow對象
     //創建並開啓3個進程
     new Thread(tw,"線程一").start();
     new Thread(tw,"線程二").start();
     new Thread(tw,"線程三").start();
	}
}

        程序解析:此程序創建了一個TicketWidow對象,然後創建了三個線程,程序中調用了Thread.sleep(10)方法,目的是讓一個線程打印一次後休眠10ms,從而使其他線程獲得執行的機會,這樣就可以實現多個線程交替執行,確保三個線程訪問的是同一個Tickets變量。

      4.線程讓步

        線程的讓步是通過Thread.yield()來實現的。yield()方法的作用是:暫停當前正在執行的線程對象,並執行其他線程。

        yield()做的是讓當前運行線程回到可運行狀態,以允許具有相同優先級的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。

      5.線程插隊

         join()方法:當某個線程中調用了join()方法時,調用的線程將會被堵塞,直到被join()方法加入的線程執行完成後它才繼續運行。
           例如:
             Thread t = new MyThread();

              t.start();

              t.join();
七、多線程安全問題

     1.產生原因

        當程序的多條語句在操作線程共享數據時,由於線程的隨機性導致 一個線程對多條語句,執行了一部分還沒執行完,另一個線程搶奪到cpu執行權參與進來執行,此時就導致共享數據發生錯誤。比如買票例子中打印重票和錯票的情況。        

      2.解決方法

         對多條操作共享數據的語句進行同步,一個線程在執行過程中其他線程不可以參與進來。

      3.Java中多線程同步是什麼?

         同步是用來解決多線程的安全問題的,在多線程中,同步能控制對共享數據的訪問。如果沒有同步,當一個線程在修改一個共享數據時,而另外一個線程正在使用或者更新同一個共享數據,這樣容易導致程序出現錯誤的結果。

      4.什麼是鎖?鎖的作用是什麼?

         鎖就是對象。

         鎖的作用是保證線程同步,解決線程安全問題。持有鎖的線程可以在同步中執行,沒有鎖的線程即使獲得cpu執行權,也進不去。        

      5.同步的前提

         1)必須保證有兩個以上線程

         2)必須是多個線程使用同一個鎖,即多條語句在操作線程共享數據

         3)必須保證同步中只有一個線程在運行

      6.同步的好處和弊端

         好處:同步解決了多線程的安全問題。

         弊端:多線程都需要判斷鎖,比較消耗資源。

八、線程間通信

        1.概念

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

       2.爲什麼要通信

        多線程併發執行的時候, 如果需要指定線程等待或者喚醒指定線程, 那麼就需要通信。比如生產者消費者的問題,生產一個消費一個,生產的時候需要負責消費的進程等待,生產一個後完成後需要喚醒負責消費的線程,同時讓自己處於等待,消費的時候負責消費的線程被喚醒,消費完生產的產品後又將等待的生產線程喚醒,然後使自己線程處於等待。這樣來回通信,以達到生產一個消費一個的目的。                  

      3.怎麼通信

         在同步代碼塊中, 使用鎖對象的wait()方法可以讓當前線程等待, 直到有其他線程喚醒爲止. 使用鎖對象的notify()方法可以喚醒一個等待的線程,或者notifyAll喚醒所有等待的線程。

      4.Lock和Condition

          Condition 實現提供比synchronized方法和語句可獲得的更廣泛的鎖的操作,可支持多個相關的Condition對象

          Lock是個接口,鎖是控制多個線程對共享數據進行訪問的工具。

          JDK1.5中提供了多線程升級的解決方案: 將同步synchonized替換成了顯示的Lock操作,將Object中的wait、notify、notifyAll替換成了Condition對象,該對象可以Lock鎖進行獲取 

          Lock的方法摘要:

          void   lock() : 獲取鎖。

         Condition  newCondition() :返回綁定到此 Lock 實例的新 Condition 實例。

          void  unlock() :釋放鎖。

          Condition方法摘要:

          void await() :造成當前線程在接到信號或被中斷之前一直處於等待狀態。

          void  signal() :喚醒一個等待線程。         

          void  signalAll(): 喚醒所有等待線程。          

       5.停止線程

         停止線程的方法只有一種,就是run方法結束。開啓多線程運行,運行代碼通常是循環體,只要控制住循環,就可以讓run方法結束,也就是結束線程。 

          特殊情況:當線程屬於凍結狀態,就不會讀取循環控制標記,則線程就不會結束。爲解決該情況,可引入Thread類中的Interrupt方法結束線程的凍結狀態;當沒有指定的方式讓凍結線程恢復到運行狀態時,需要對凍結進行清除,強制讓線程恢復到運行狀態        

       6.interrupt

           void interrupt() 中斷線程:

           中斷狀態將被清除,它還將收到一個 InterruptedException

       7.守護線程(後臺線程)

           setDaemon(booleanon):將該線程標記爲守護線程或者用戶線程。

           特點:當主線程結束,守護線程自動結束。

程序示例:

//多生產者與多消費的示例
class Resource{
	private String name;
	private int count=1;
	private boolean flag=false;
	Lock lock=new ReentrantLock(); //創建一個鎖對象
	//通過已有的鎖獲取兩組監視器,一組監視生產者,一組監視消費者。
	Condition con1=lock.newCondition(); 
	Condition con2=lock.newCondition();
	public void set(String name){
		lock.lock();//獲取鎖
		try{
			while(flag)//如果flag標記爲true
			try{con1.await();}catch(Exception e){} //con1的監視器處於等待狀態
			this.name =name+count;
			count++;
			System.out.println(Thread.currentThread().getName()+"...生產了..."+this.name);
			flag=true; //將flag標記改爲true
			con2.signal();//喚醒con2監視器
		}finally{
			lock.unlock(); //釋放鎖
		}
	}
	public void out(){
		lock.lock();
		try{
		    while(!flag)//如果flag標記爲flase
			try{con2.await();}catch(Exception e){}//con2的監視器處於等待狀態
			System.out.println(Thread.currentThread().getName()+"...消費了..."+this.name);
			flag=false; //將flag標記改爲flase
			con1.signal(); //喚醒con1監視器
	}finally{
		lock.unlock(); //釋放鎖
	}
  }
}
//創建生產者
class Producer implements Runnable{
	private Resource r;
	Producer(Resource r){
		this.r=r;
	}
	public void run() {
		while(true){
			r.set("商品"); //調用r的set()方法,併爲name賦值爲"商品"
		}
	}	
}
//創建消費者
class Consumer implements Runnable{
	private Resource r;
	Consumer(Resource r){
		this.r=r;
	}
	public void run() {
		while(true){
	       r.out(); //調用r的out()方法
		}
	}
}
public class test1 {
	public static void main(String[] args) {
		//創建資源對象
		Resource r=new Resource();  
		//創建任務
		Producer p=new Producer(r);
		Consumer c=new Consumer(r);
		//創建線程,兩個生產者路徑,兩個消費者路徑
		Thread t0=new Thread(p);
		Thread t1=new Thread(p);
		Thread t2=new Thread(c);
		Thread t3=new Thread(c);
		//開啓線程
		t0.start();
		t1.start();
		t2.start();
		t3.start();
	}
}

程序部分運行結果:


問題:1.創建線程是爲什麼要複寫run方法?

          Thread類用於描述線程。Thread類定義了一個功能,用於存儲線程要運行的代碼,該存儲功能就是run方法。

            2.start()和run方法有什麼區別?

           調用start方法方可啓動線程,而run方法只是thread的一個普通方法,調用run方法不能實現多線程

         3.wait(),sleep()有什麼區別?

            wait():既可以指定時間也可以不指定時間,釋放cpu執行權,釋放鎖。

            sleep():必須指定睡眠時間,釋放cpu執行權,不釋放鎖。


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