黑馬程序員——淺談java中的多線程

一、多線程概述

        要理解多線程,就必須理解線程。而要理解線程,就必須知道進程。

1、 進程

        是一個正在執行的程序。

        每一個進程執行都有一個執行順序。該順序是一個執行路徑,或者叫一個控制單元。

2、線程

         就是進程中的一個獨立的控制單元。線程在控制着進程的執行。只要進程中有一個線程在執行,進程就不會結束。

        一個進程中至少有一個線程。

3、多線程

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

4、多線程存在的意義

        多線程的出現能讓程序產生同時運行效果。可以提高程序執行效率。

         例如:在java.exe進程執行主線程時,如果程序代碼特別多,在堆內存中產生了很多對象,而同時對象調用完後,就成了垃圾。如果垃圾過多就有可能是堆內存出現內存不足的現象,只是如果只有一個線程工作的話,程序的執行將會很低效。而如果有另一個線程幫助處理的話,如垃圾回收機制線程來幫助回收垃圾的話,程序的運行將變得更有效率。

5、計算機CPU的運行原理

         我們電腦上有很多的程序在同時進行,就好像cpu在同時處理這所以程序一樣。但是,在一個時刻,單核的cpu只能運行一個程序。而我們看到的同時運行效果,只是cpu在多個進程間做着快速切換動作。

         cpu執行哪個程序,是毫無規律性的。這也是多線程的一個特性:隨機性。哪個線程被cpu執行,或者說搶到了cpu的執行權,哪個線程就執行。而cpu不會只執行一個,當執行一個一會後,又會去執行另一個,或者說另一個搶走了cpu的執行權。至於究竟是怎麼樣執行的,只能由cpu決定。

 

二、創建線程的方式

        創建線程共有兩種方式:繼承方式和實現方式(簡單的說)。

1、 繼承方式

        通過查找java的幫助文檔API,我們發現java中已經提供了對線程這類事物的描述的類——Thread類。這第一種方式就是通過繼承Thread類,然後複寫其run方法的方式來創建線程。

創建步驟:

        a,定義類繼承Thread

        b,複寫Thread中的run方法。

             目的:將自定義代碼存儲在run方法中,讓線程運行。

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

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

注:如果對象直接調用run方法,等同於只有一個線程在執行,自定義的線程並沒有啓動。

覆蓋run方法的原因:

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

執行是隨機、交替執行的,每一次運行的結果都會不同。

       

2、 實現方式

        使用繼承方式有一個弊端,那就是如果該類本來就繼承了其他父類,那麼就無法通過Thread類來創建線程了。這樣就有了第二種創建線程的方式:實現Runnable接口,並複習其中run方法的方式。

創建步驟:

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

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

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

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

       爲什麼要將Runnable接口的子類對象傳遞給Thread的構造函數?

        因爲,自定義的run方法所屬的對象是Runnable接口的子類對象。所以要讓線程去指定對象的run方法,就必須明確該run方法所屬對象。

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

實現方式好處:避免了單繼承的侷限性。在定義線程時,建議使用實現方式。 

程序示例:

/*
//一、通過繼承Thread創建線程
class Ticket extends Thread
{
	private static int tick = 100; 
	public void run()
	{
		while(true)
		{
			if(tick>0)
				System.out.println(Thread.currentThread().getName()+"..."+tick--); 
		}
	}
}
*/

//二、通過實現runnable接口
class Ticket implements Runnable//extends Thread
{
	private static int tick = 100; 

	Object obj = new Object(); 
	public void run()
	{
		while(true)
		{
			synchronized(obj)
			{
				if(tick>0)
				{
					try
					{
						Thread.sleep(10); 	
					}
					catch (Exception e)
					{
					}
					System.out.println(Thread.currentThread().getName()+"..."+tick--); 
				}
			}
		}
	}
}

class  TicketDemo
{
	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(); 

		/*
		Ticket t1 = new Ticket(); 
		Ticket t2 = new Ticket();
		Ticket t3 = new Ticket();
		Ticket t4 = new Ticket();

		t1.start(); 
		t2.start(); 
		t3.start(); 
		t4.start(); 
		*/

	}
}

三、兩種方式的區別和線程的幾種狀態

1、兩種創建方式的區別

        繼承Thread:線程代碼存放在Thread子類run方法中。

        實現Runnable:線程代碼存放在接口子類run方法中。      

2、幾種狀態

        被創建:等待啓動,調用start啓動。

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

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

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

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

注:當已經從創建狀態到了運行狀態,再次調用start()方法時,就失去意義了,java運行時會提示線程狀態異常。

圖解:

   

四、線程安全問題

1、導致安全問題的出現的原因:

        當多條語句在操作同一線程共享數據時,一個線程對多條語句只執行了一部分,還沒用執行完,另一個線程參與進來執行。導致共享數據的錯誤。

簡單的說就兩點:

        a、多個線程訪問出現延遲。

        b、線程隨機性    

注:線程安全問題在理想狀態下,不容易出現,但一旦出現對軟件的影響是非常大的。

2、解決辦法——同步

        對多條操作共享數據的語句,只能讓一個線程都執行完。在執行過程中,其他線程不可以參與執行。

        在java中對於多線程的安全問題提供了專業的解決方式——synchronized(同步)

        這裏也有兩種解決方式,一種是同步代碼塊,還有就是同步函數。都是利用關鍵字synchronized來實現。

         a、同步代碼塊

        用法:

                  synchronized(對象)

                  {需要被同步的代碼}

        同步可以解決安全問題的根本原因就在那個對象上。其中對象如同鎖。持有鎖的線程可以在同步中執行。沒有持有鎖的線程即使獲取cpu的執行權,也進不去,因爲沒有獲取鎖。

示例:

/*	
給賣票程序示例加上同步代碼塊。
*/
class Ticket implements Runnable
{
	private int tick=100;
	Object obj = new Object();
	public void run()
	{
		while(true)
		{
			//給程序加同步,即鎖
			synchronized(obj)
			{
				if(tick>0)
				{
					try
					{	
						//使用線程中的sleep方法,模擬線程出現的安全問題
						//因爲sleep方法有異常聲明,所以這裏要對其進行處理
						Thread.sleep(10);
					}
					catch (Exception e)
					{
					}
					//顯示線程名及餘票數
					System.out.println(Thread.currentThread().getName()+"..tick="+tick--);
				}
			}	
		}
	}
}


/*	
給賣票程序示例加上同步代碼塊。
*/
class Ticket implements Runnable
{
	private int tick=100;
	Object obj = new Object();
	public void run()
	{
		while(true)
		{
			//給程序加同步,即鎖
			synchronized(obj)
			{
				if(tick>0)
				{
					try
					{	
						//使用線程中的sleep方法,模擬線程出現的安全問題
						//因爲sleep方法有異常聲明,所以這裏要對其進行處理
						Thread.sleep(10);
					}
					catch (Exception e)
					{
					}
					//顯示線程名及餘票數
					System.out.println(Thread.currentThread().getName()+"..tick="+tick--);
				}
			}	
		}
	}
}

        b,同步函數

        格式:

                在函數上加上synchronized修飾符即可。

        那麼同步函數用的是哪一個鎖呢?

        函數需要被對象調用。那麼函數都有一個所屬對象引用。就是this。所以同步函數使用的鎖是this

示例:

class Ticket1 implements Runnable//extends Thread
{
	private static int tick = 100; 
	Object obj = new Object();

	//用來標誌是線程是進去同步代碼塊還是同步函數
	boolean flag = true; 

	public void run()
	{
		if(flag)
		{
			while(true)
			{
				synchronized(obj)
				{
					if(tick>0)
					{
						try{Thread.sleep(10);}catch (Exception e){}
						System.out.println(Thread.currentThread().getName()+"...code.."+tick--); 
					}
				}
			}
		}
		else
			while(true)
				show(); 

	}

	public synchronized void show()//this
	{
		if(tick>0)
		{
			try{Thread.sleep(10);}catch (Exception e){}
			System.out.println(Thread.currentThread().getName()+"...show.."+tick--); 
		}
	}
}

class  ThisLockDemo
{
	public static void main(String[] args) 
	{
		Ticket1 t = new Ticket1(); 

		Thread t1 =  new Thread(t); //創建一個線程;
		Thread t2 =  new Thread(t); //創建一個線程;
		
		//兩個線程使用的不是同一個鎖,t1使用的是obj對象的鎖,t2使用的是this的
		t1.start(); 
		try{Thread.sleep(100);}catch (Exception e){}
		t.flag = false; 
		t2.start();
	}
}


3、同步的前提

        a,必須要有兩個或者兩個以上的線程。

        b,必須是多個線程使用同一個鎖。

4、同步的利弊

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

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

5、如何尋找多線程中的安全問題

        a,明確哪些代碼是多線程運行代碼。

        b,明確共享數據。

        c,明確多線程運行代碼中哪些語句是操作共享數據的。

 

五、靜態函數的同步方式

        如果同步函數被靜態修飾後,使用的鎖是什麼呢?

        通過驗證,發現不在是this。因爲靜態方法中也不可以定義this。靜態進內存時,內存中沒有本類對象,但是一定有該類對應的字節碼文件對象。如:

        類名.class 該對象的類型是Class

這就是靜態函數所使用的鎖。而靜態的同步方法,使用的鎖是該方法所在類的字節碼文件對象。類名.class

經典示例:

class Ticket1 implements Runnable//extends Thread
{
	private static int tick = 100; 
	Object obj = new Object();

	//用來標誌是線程是進去同步代碼塊還是同步函數
	boolean flag = true; 

	public void run()
	{
		if(flag)
		{
			while(true)
			{
				synchronized(Ticket1.class)
				{
					if(tick>0)
					{
						try{Thread.sleep(10);}catch (Exception e){}
						System.out.println(Thread.currentThread().getName()+"...code.."+tick--); 
					}
				}
			}
		}
		else
			while(true)
				show(); 

	}

	public static synchronized void show()//靜態同步函數的鎖,是class對象
	{
		if(tick>0)
		{
			try{Thread.sleep(10);}catch (Exception e){}
			System.out.println(Thread.currentThread().getName()+"...show.."+tick--); 
		}
	}
}

class  StaticLockDemo
{
	public static void main(String[] args) 
	{
		Ticket1 t = new Ticket1(); 

		Thread t1 =  new Thread(t); //創建一個線程;
		Thread t2 =  new Thread(t); //創建一個線程;
		
		//兩個線程使用的不是同一個鎖,t1使用的是obj對象的鎖,t2使用的是this的
		t1.start(); 
		try{Thread.sleep(100);}catch (Exception e){}
		t.flag = false; 
		t2.start(); 
	}
}


/*
加同步的單例設計模式————懶漢式
*/
class Single
{
	private static Single s = null;
	private Single(){}
	public static void getInstance()
	{
		if(s==null)
		{
			synchronized(Single.class)
			{
				if(s==null)
					s = new Single();
			}
		}
		return s;
	}
}

 

六、死鎖

        當同步中嵌套同步時,就有可能出現死鎖現象。

示例:

class Test implements Runnable
{
	private boolean flag; 
	Test(boolean flag)
	{
		this.flag = flag; 
	}
	public void run()
	{
		if(flag)
		{	
			while(true)
			{
				synchronized(MyLock.locka)
				{
					System.out.println("if locka");
					synchronized(MyLock.lockb)
					{
						System.out.println("if lockb"); 
					}
				}
			}
		}
		else
		{
			while(true)
			{
				synchronized(MyLock.lockb)
				{
					System.out.println("else lockb");
					synchronized(MyLock.locka)
					{
						System.out.println("else locka"); 
					}
				}
			}
		}
	}
}

class MyLock
{
	static Object locka = new Object(); 
	static Object lockb = new Object(); 
}

class DeadLockTest
{
	public static void main(String[] args) 
	{
		Thread t1 = new Thread(new Test(true));
		Thread t2 = new Thread(new Test(false)); 
		
		t1.start(); 
		t2.start(); 
	}
}


七、線程間通信

        其實就是多個線程在操作同一個資源,但是操作的動作不同。

代碼示例:

/*
生產者消費者問題

一個生產者
一個消費者
共享一個緩存區
*/
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)
	{
		if(flag)
			try{wait(); } catch(Exception e){}
		this.name = name+"--"+count++; 
		System.out.println(Thread.currentThread().getName()+"...生產者.."+this.name); 
		flag = true; 
		this.notify(); 
	}

	public synchronized void out()
	{
		if(!flag)
			try{wait(); } catch(Exception e){}	//等待線程
		System.out.println(Thread.currentThread().getName()+"...消費者........."+this.name); 
		flag = false; 
		this.notify(); //喚醒線程池中的一個線程
	}
}

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,容易出現只喚醒本方線程的情況。導致程序中的所有線程都等待。
*/
class  ProducerConsumerDemo2
{
	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; 
		this.notifyAll(); 
	}

	public synchronized void out()
	{
		while(!flag)
			try{wait(); } catch(Exception e){}	//等待線程
		System.out.println(Thread.currentThread().getName()+"...消費者........."+this.name); 
		flag = false; 
		this.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(); 
		}
	}
}


 幾個小問題:

        1)wait(),notify(),notifyAll(),用來操作線程爲什麼定義在了Object類中?

                a,這些方法存在與同步中。

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

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

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

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

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

        3)爲甚麼要定義notifyAll

        因爲在需要喚醒對方線程時。如果只用notify,容易出現只喚醒本方線程的情況。導致程序中的所以線程都等待。

2JDK1.5中提供了多線程升級解決方案。

   

/*
生產者消費者問題JDK1.5升級版

JDK1.5中提供了多線程的升級解決方案。
將同步synchronized替換成顯示Lock操作。
將object中的wait,notify,notifyAll 替換成了condition對象。
該對象可以用Lock鎖進行獲取。
該示例中,實現了本方只喚醒對方操作。

*/
import java.util.concurrent.locks.*; 

class  ProducerConsumerDemo3
{
	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 con_pro = lock.newCondition(); 
	private Condition con_con = lock.newCondition(); 

	public  void set(String name) throws InterruptedException
	{
		lock.lock(); 
		try
		{
			while(flag)
				con_pro.await(); //生產者等待
			this.name = name+"--"+count++; 
			System.out.println(Thread.currentThread().getName()+"...生產者.."+this.name); 
			flag = true; 
			con_con.signal(); //喚醒消費者 
		}
		finally
		{
			lock.unlock(); 
		}
	}

	public  void out()	throws InterruptedException
	{
		lock.lock();
		try
		{
			while(!flag)
				con_con.await();//消費者線程等待 
			System.out.println(Thread.currentThread().getName()+"...消費者........."+this.name); 
			flag = false; 
			con_pro.signal(); //喚醒生產者線程
		}
		finally
		{
			lock.unlock(); 
		}
	}
}

class Producer implements Runnable
{
	private Resource res; 
	Producer(Resource res)
	{
		this.res = res; 
	}
	public void run()
	{
		while(true)
		{
			try
			{
				res.set("商品"); 
			}
			catch (InterruptedException e)
			{
			}
			
		}
	}
}

class Consumer implements Runnable 
{
	private Resource res; 
	Consumer(Resource res)
	{
		this.res = res; 
	}
	public void run()
	{
		while(true)
		{
			try
			{
				res.out(); 
			}
			catch (InterruptedException e)
			{
			} 
		}
	}
}


八、停止線程

        JDK 1.5版本之前,有stop停止線程的方法,但升級之後,此方法已經過時。

/*
線程結束

stop方法已經過時了,如何停止線程呢?
只有一種方法,run方法結束。
開啓多線程運行,運行代碼通常是循環結構。

只要控制住循環,就可以讓run方法結束,也就是結束線程。


特殊情況:
當線程處於了凍結狀態時
就不會讀取到標記,那麼線程就不會結束。

當沒有指定的方式讓凍結的線程恢復到運行狀態時,這時就需要對凍結進行清除
強制讓線程恢復到運行狀態中來,這樣就可以操作標記讓線程結束。

Thread類提供了該方法 interrupt(); 

*/

/*
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 StopThread implements Runnable
{
	private boolean flag = true; 
	public synchronized void run()
	{
		while(flag)
		{
			try
			{
				wait(); 
			}
			catch (InterruptedException e)
			{
				System.out.println(Thread.currentThread().getName()+"...Exception");
				flag = false; 
			}
			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++ == 100)
		{ 
			//st.changeFlag();	//設置標記,中斷線程
			//t1.interrupt();	//線程中斷
			//t2.interrupt();	//線程中斷
			break; 
		}
		System.out.println(Thread.currentThread().getName()+"..."+num); 
		}
	}
}


擴展小知識:

1join方法

        當A線程執行到了b線程的.join()方法時,A線程就會等待,等B線程都執行完,A線程纔會執行。(此時B和其他線程交替運行。)join可以用來臨時加入線程執行。

2setPriority()方法用來設置優先級

        MAX_PRIORITY 最高優先級10

        MIN_PRIORITY   最低優先級1

        NORM_PRIORITY 分配給線程的默認優先級

3yield()方法可以暫停當前線程,讓其他線程執行。

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