黑馬程序員——Java基礎知識——多線程

------Java培訓、Android培訓、iOS培訓、.Net培訓、期待與您交流! 

一、線程

       在計算機中,每一個程序的運行,都是通過線程來實現的。我們可以把一個正在執行的程序稱爲進程,每一個進程的執行都有一個執行順序。該順序是一個執行路徑,或者叫控制單元。線程就是進程中的一個獨立的控制單元,線程在控制着進程的執行。只要進程中有一個線程在執行,進程就不會結束。一個進程中至少有一個線程。

       在多個程序運行時,CPU會隨機地在多個進程中快速切換,哪個線程搶到了CPU的執行權,哪個線程就運行。運行Java程序時,虛擬機啓動會有一個java.exe的進程,該進程中至少有一個線程負責java程序的執行。而且這個線程運行的代碼存在於main方法中,稱之爲主線程。虛擬機啓動除了執行主線程外,還有負責垃圾回收機制的線程。這種在在一個進程中有多個線程執行的方式,就是多線程。

       多線程的出現能讓程序產生同時運行的效果,提高程序的運行效率。例如:在虛擬機啓動後,進程中執行主線程時,一般會根據程序代碼,在堆內存中產生很多對象,而對象調用完後,就成了垃圾,如果不及時清理,垃圾過多容易造成內存不足,影響程序的運行。所以如果只有主線程運行,程序的效率可能會很低;而如果有一個負責垃圾回收機制的線程運行時,就會對堆內存中的垃圾進行清理,保證了內存的穩定,就保證了程序的運行效率。 

二、創建線程

       有兩種創建線程的方式:繼承Thread類和實現Runnable接口。

       (1)繼承Thread類 

             Thread類是Java提供的對線程這類事物描述的類,通過繼承Thread類,複寫其run方法來創建線程。步驟如下:

            1.定義一個類繼承Thread

           2.覆蓋Thread中的run方法。將自定義的代碼放在run方法中,讓線程運行時,執行這些代碼。

           3.創建這個類的對象。相當於創建一個線程。然後用該對象調用線程的start方法。該方法的作用是:啓動線程,調用run方法。注意如果直接用對象調用run方法,相當於沒有啓動創建的線程,還是隻有主線程在執行。

           覆蓋run方法的原因:Thread類用於描述線程。該類就定義了一個功能,用於存儲線程要執行的代碼。該存儲功能就是run方法。也就是說,Thread類中的run方法,用於存儲線程要運行的代碼。

            下面通過一段程序,演示如何用繼承方法創建線程,如下:

/**
需求:創建兩個線程,和主線程交替運行。
*/

class MyThread extends Thread
{
	//覆蓋父類的run方法,存入運行代碼
	public void run(){
            for(int x=0;x<1000;x++)  
		System.out.println(Thread.currentThread().getName()+"在運行");
	}
}

class ThreadDemo
{
	public static void main(String[] args) 
	{
		//創建線程
		MyThread mt1=new MyThread();
                MyThread mt2=new MyThread(); 
               //啓動線程
		mt1.start();
		mt2.start();
		for(int x=0;x<1000;x++)
		   System.out.println("Hello World!");
	}
}
       主線程和創建的線程會交替執行,因爲CPU是隨機地選擇要執行的線程。這種方法可以創建線程,但如果定義的類已經是其他類的子類了,就無法再繼承Thread類了,所以Java提供了另一中創建線程的方式,通過實現Runnable接口的方式。
       (2)實現Runnable接口

            實現Runnable接口,覆蓋run方法。這種創建線程的方式避免了單繼承的侷限性,在定義創建線程時,一般都使用這種方式。具體步驟如下:

            1.定義一個類實現Runnable的接口。

           2.覆蓋Runnable接口中的run方法。將線程要運行的代碼存放在該run方法中。

            3.通過Thread類創建線程對象,並將Runnable接口的子類對象作爲實際參數傳遞給Thread類的構造方法。這樣做是因爲自定義的run方法所屬的對象是Runnable接口的子類對象。所以要讓線程去指定對象的run方法,就必須明確該run方法所屬對象。

           4.調用Thread類中start方法啓動線程。start方法會自動調用Runnable接口子類的run方法。

           下面是一個簡單的售票程序,應用的就是實現這種創建線程的方式,如下

/**
需求:簡單的賣票程序。
多個窗口同時買票。
*/

class Ticket implements Runnable
{
	private int ticket = 100;
        //複寫run方法
        public void run()
	{
		while(true)
		{
			if(ticket>0)
			{
			System.out.println(Thread.currentThread().getName()+"....sale : "+ ticket--);
			}
		}
	}
}
class  TicketDemo
{
	public static void main(String[] args) 
	{

		Ticket t = new Ticket();
                //創建4個線程,表示4個同時售票的窗口
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);
		//啓動線程,4個窗口開始同時售票。
                t1.start();
		t2.start();
		t3.start();
		t4.start();	

	}
}
      這種創建線程的方式,是把線程要運行的代碼放在Runnable接口的子類的run方法中;而繼承Thread類是把要運行的代碼放在Thread的子類的run方法中。在以後的操作中一般用到的都是實現Runnable接口的方式。
 三、線程的運行狀態

          首先看一下表示線程運行狀態的圖例:



        從上圖可以看出,線程從創建、運行、到最後結束整個過程中的各種狀態,具體有:

       被創建:等待啓動,調用start啓動。如果線程已經啓動,處在運行時,再次調用start方法,沒有意義,會提示線程狀態異常。

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

       臨時狀態(阻塞):有執行資格,但沒有執行權。

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

       消亡狀態:stop()方法,或者run方法結束。

四、線程安全

        當多條語句在操作多個線程的共享數據時,當一個線程對多條語句只執行了一部分,還沒有執行完時,另一個線程可能就會參與進來執行,這樣會導致共享數據的錯誤。線程的安全問題一旦出現對程序的影響很大。所以Java中提供瞭解決線程安全問題的方法,叫做同步(synchronized),就是對多條操作共享數據的語句,只能讓一個線程都執行完。在執行過程中,其他線程不可以參與執行,這樣就解決了線程的安全問題。同步中分爲兩種解決方法,一種是同步代碼塊,另一種是同步函數

       (1)同步代碼塊

          格式:synchronized(對象){需要被同步的代碼}

          就以上面售票的程序爲例,利用同步代碼塊,確保線程安全。如下:

        

class Ticket implements Runnable
{
	private  int ticket = 100;
	//定義用於使用同步的對象。
        Object obj = new Object();
	public void run()
	{
		while(true)
		{
			//給程序實現同步
                   synchronized(obj)
			{
				if(ticket>0)
				{
				  System.out.println(Thread.currentThread().getName()+"....sale : "+ ticket--);
				}
			}
		}
	}
}
class  TicketDemo2
{
	public static void main(String[] args) 
	{

		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
     }
}
           在同步代碼塊中,對象就如同一把鎖。持有鎖的線程纔可以在同步中執行。沒有持有鎖的線程即使獲得CPU的執行權,也進不去,因爲沒有獲取鎖。

       (2)同步函數

             格式:就是在函數上加上synchronized即可。因爲非靜態函數需要被對象調用,所以非靜態函數中都有一個所屬對象引用,即this。同步函數使用的鎖就this

              下面還以售票的程序,來用同步函數的格式,實現同步。如下:

class Ticket implements Runnable
{
	private  int ticket = 100;
	public void run()
	{
		while(true)
		     show();	
	}		
	  //通過同步函數,實現同步。	
	  public synchronized void show()
	 {
            if(ticket>0)
		System.out.println(Thread.currentThread().getName()+"....sale : "+ ticket--);
	  }
}
class  TicketDemo2
{
	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();
       }
}
        無論使用上面兩種方法的哪一種,都必須保證存在兩個或兩個以上的線程,並且這些線程都是在使用同一個鎖,否則實現不了不同,線程還是會有問題。在使用同步時要明確哪些是多線程的運行代碼,哪些是多線程的共享數據以及運行代碼中哪些是用來操作共享數據,明確了這些關鍵點之後,定義的同步會保證線程的安全
同步解決了多線程的安全問題,但多個線程需要判斷鎖,較爲消耗資源。

       注意,因爲靜態函數中沒有被對象調用,所以它內部沒有對象引用,這時對一個靜態函數同步,它的鎖肯定不是this,而是它所屬的類對應的字節碼文件對象。格式爲類名.class。因爲靜態函數進內存時,內存中一定有它所在的類的字節碼文件對象。所以在靜態函數同步時,就以字節碼文件對象作爲鎖。

        在我們之前學到的單例設計模式中,懶漢式需要方法調用時,纔會創建對象。但如果是在多線程中調用此方法時,就容易出現安全問題,保證不了對象在內存中的唯一性,所以這時也要使用到同步。如下:

class Single
{
	private static Single s = null;
	private Single(){}
        public static  Single getInstance()
	{
		if(s==null)
		{       //使用同步代碼塊,效率稍高
			synchronized(Single.class)
			{
				if(s==null)
					s = new Single();
			}
		}
		return s;
	}
}
五、線程間通信

       就是多個線程在操作同一個資源,但是操作的動作不同。爲了實現一個流程,不同的操作動作需要交替,這時需要用到wait、notify、notifyAll等方法。如下:

class Resource
{
  private String name;
  private String sex;
  //定義判斷標識
  boolean flag;
  //定義同步函數,表示輸入
  public synchronized void input(String name,String sex)
  {   //如果已有資源,等待輸入
	  if(!flag)
		  try{wait();}catch(Exception e){}
	  this.name=name;
	  this.sex=sex;
	  flag=false;
	  //喚醒等待線程
	  notify();
  }
  //表示輸出
  public synchronized void output()
  {
	 //等待輸出
	 if(flag)
		 try{wait();}catch(Exception e){}
	 System.out.println(Thread.currentThread().getName()+name+"........."+sex);	
	 flag=true;
	 //喚醒等待線程
	 notify();
  }

}
//定義輸入線程
class InputDemo implements Runnable
{
	private Resource res;
	InputDemo(Resource res)
	{
		this.res=res;
	}
	public void run(){
		int x=0;
		while(true){
		if(x==0)
		     res.input("小明","男");
		else
			 res.input("haha","man");
		x=(x+1)%2;
		
		}
	}
}
//定義輸出線程
class OutputDemo implements Runnable
{
	private Resource res;

	OutputDemo(Resource res)
	{
		this.res=res;
	}
	public void run(){
		while(true)
		  res.output();
	}
}
class Test
{
	public static void main(String[]args)
	{
		Resource res=new Resource();
		//創建並啓動線程
		new Thread(new InputDemo(res)).start();
		new Thread(new OutputDemo(res)).start();
	}
}
執行結果爲在控制檯上交替打印"線程名"“小明”...........“男”和"線程名"“hehe”..........“man”。
       兩個需要知道的知識點: (1)wait(),notify(),notifyAll(),用來操作線程爲什麼定義在了Object類中?

                                                             1.這些方法存在與同步中。

                                                             2.使用這些方法時必須要標識所屬的同步的鎖。同一個鎖上wait的線程,只可以被同一個鎖上的notify喚醒。

                                                             3. 鎖可以是任意對象,所以任意對象調用的方法一定定義Object類中。

                                                    (2)wait(),sleep()有什麼區別?

                                                               wait():釋放cpu執行權,釋放鎖。

                                                               sleep():釋放cpu執行權,不釋放鎖。

        在實際開發中,有時存在每個不同的操作動作都有多個線程在執行。如下:

/**
需求:多個生產者生產商品,每生產一件商品消費者就購買該商品。
*/
class ProducerConsumerDemo 
{
	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(Exception e){}
		this.name = name+"--"+count++;
     System.out.println(Thread.currentThread().getName()+"...生產者.."+this.name);
		flag = true;
		//喚醒所有等待的線程
		notifyAll();
	} 
	public synchronized void out()
	{
		//循環判斷被喚醒線程的標識
		while(!flag)
			try{wait();}catch(Exception e){}
   System.out.println(Thread.currentThread().getName()+"...消費者........."+this.name);
		flag = false;
		//喚醒所有等待的線程
		notifyAll();
	}
}
//定義線程 表示生產
class Producer implements Runnable
{
	private Resource res;

	Producer(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
		{
			res.set("+商品+");
		}
	}
}
//定義線程 表示消費
class Consumer implements Runnable
{
	private Resource res;

	Consumer(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
		{
			res.out();
		}
	}
}
     對於多個生產者和消費者,爲什麼要定義while判斷標示:因爲被喚醒的等待線程可能有多個,讓被喚醒的線程再一次判斷標識。
     定義notifyAll是因爲需要喚醒對方線程,只定義notify,容易只喚醒本方線程,導致程序中的線程都停掉。

     JDK1.5中提供了多線程升級解決方案。將同步synchronized替換成顯示的Lock操作,將Object中的wait、notify、notifyAll,替換成了Condition對象。該對象可以通過Lock鎖獲取,並支持多個相關的Condition對象。在這種方法中,實現了本方只喚醒對方的操作。如下:

/**
需求:多個生產者生產商品,每生產一件商品消費者就購買該商品。
*/
class ReflectTest1
{
	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;
	//定義Lock對象
	Lock lock=new ReentrantLock();
	//創建Conditon對象,用來喚醒對方線程。
	Condition condition_con=lock.newCondition();
	Condition condition_pro=lock.newCondition();
	public void set(String name)
	{   //實現鎖
		lock.lock();
	    try{
		while(flag)
			try{condition_pro.await();}catch(Exception e){}
		this.name = name+"--"+count++;
           System.out.println(Thread.currentThread().getName()+"...生產者.."+this.name);
		flag = true;
		//喚醒對方線程
		condition_con.signal();
	        }
        //釋放鎖的動作一定要執行
	    finally{
			lock.unlock();
		}
	} 
	public synchronized void out()
	{
		//實現鎖
		lock.lock();
		try{
		while(!flag)
			try{condition_con.await();}catch(Exception e){}
    System.out.println(Thread.currentThread().getName()+"...消費者........."+this.name);
		flag = false;
		//喚醒對方線程
		condition_pro.signal();
		}
		//釋放鎖
		finally{
			lock.unlock();
		}
	}
}
//定義線程 表示生產
class Producer implements Runnable
{
	private Resource res;
        Producer(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
		{
			res.set("+商品+");
		}
	}
}
//定義線程 表示消費
class Consumer implements Runnable
{
	private Resource res;

	Consumer(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
		{
			res.out();
		}
	}
}
注意,釋放鎖的動作一定要執行,所以放在finally語句中。

六、停止線程

      停止線程只有一種方法,就是讓run方法結束。

      開啓線程運行,運行代碼通常是循環結構,只要控制住循環,就可以讓run方法結束,線程就結束了,這時一般需要通過定義標識,通過標識的變化來實現。如下:

class StopThread implements Runnable
{
	private boolean flag =true;
	public  void run()
	{       //通過標識,控制循環
		while(flag)
			System.out.println(Thread.currentThread().getName()+"....run");
	}
	//定義改變標識的方法
	public void changeFlag()
	{
		flag = false;
	}
}
class  StopThreadDemo
{
	public static void main(String[] args) 
	{
		StopThread st = new StopThread();
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);
<span style="white-space:pre">	</span>        int num = 0;
<span style="white-space:pre">		</span>while(true)
		{
			if(num++ == 60)
			{       //調用改變標識的辦法,結束線程。
				st.changeFlag();
				break;
			}
			System.out.println(Thread.currentThread().getName()+"......."+num);
		}
	}
}

  但是當線程處於凍結狀態時,就不會讀到標識,線程就不會結束。這時需要對凍結進行清除,強制讓線程恢復到運行狀態上來,然後再通過操作標識,讓線程結束,完成清除動作,需要Thread類中的interrupt方法。如下:

class StopThread implements Runnable
{
	private boolean flag =true;
	public  void run()
	{
		while(flag)
		{       //線程凍結
			try{Thread.sleep(30)}catch(Exception e){}
			System.out.println(Thread.currentThread().getName()+"....run");
		}
	}
	//定義改變標識的方法
	public void changeFlag()
	{
		flag = false;
	}
}




class  StopThreadDemo
{
	public static void main(String[] args) 
	{
		StopThread st = new StopThread();
		
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);

		t1.start();
		t2.start();

		int num = 0;

		while(true)
		{
			if(num++ == 60)
			{
				//清除凍結狀態
				t1.interrupt();
				t2.interrupt();
				//改變標識
				st.changeFlag();	
			}
			System.out.println(Thread.currentThread().getName()+"......."+num);
		}
		System.out.println("over");
	}
}


-------------Java培訓、Android培訓、iOS培訓、.Net培訓、期待與您交流! -------

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