天天看點

黑馬程式員--java基礎複習之多線程及線程間通信

------ Java教育訓練、Android教育訓練、iOS教育訓練、.Net教育訓練、期待與您交流! -------

程序: 程序:是一個正在執行的程式 每個程序執行都有一個執行順序。該順序是一個執行路徑,或者叫一個控制單元

線程:

線程就是程序中的一個獨立的控制單元,線程在控制着程序的執行。一個程序中至少有一個線程。

多線程:  在java虛拟機啟動的時候會有一個java.exe的執行程式,也就是一個程序。

 該程序中至少有一個線程負責java程式的執行。而且這個線程運作的代碼存在于main方法中。該線程稱之為主線程。

 JVM啟動除了執行一個主線程,還有負責垃圾回收機制的線程。 像種在一個程序中有多個線程執行的方式,就叫做多線程。

多線程的好處:  多線程的出現能讓程式産生同時運作效果。可以提高程式執行效率。

就我們常見的,在電腦上,即能在記事本上打字,同時也能聽音樂。這個就是多線程的應用。又比如360安全衛士,當我們在清理垃圾的時候,同時也能掃描系統漏洞。這同僚是多線程的應用有。

1、如何在自定義的代碼中,自定義一個線程呢?

通過對API的查找,java已經提供了對線程這類事物的描述。就是Thread類。

建立線程的第一種方式:繼承Thread類

步驟:

1、定義類繼承Thread。

2、複寫Thread類中的run 方法

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

3、調用線程的start方法。該方法有兩個作用:啟動線程,調用run方法

如下:

class Test extends Thread
{		
	//複寫父類中的run方法
	public  void run()
	{
		for(int i=0;i<30;i++)
		{			
			System.out.println(Thread.currentThread().getName()+" run .."+i);
		}
	}
}
class ThreadDemo
{
	public static void  main(String[] agrs)
	{
		Test t1=new Test();
		t1.start();//開啟線程,并調用run方法
		Test t2=new Test();
		t2.start();
		for(int i=0;i<60;i++)
		{
			System.out.println("main run::"+i);
		}
		
		
	}
}
           

運作多次後,發現運作結果每一次都不同。因為多個線程都在擷取CPU的執行權。CPU執行到誰,誰就運作。

明确一點,在某一個時刻,隻能有一個程式在運作。(多核除外) cpu在做着快速的切換,以達到看上去是同時運作的效果。 我們可以形象地把多線程的運作行為看作在互相搶奪CPU的執行權。

這就是多線程的一個特性:随機性。輪到誰,誰執行。

為什麼要覆寫run方法呢?

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

也就是說Thread類中的run 方法,用于存儲線程要運作的代碼。 我們要利用線程運作我們自己的代碼,是以我們也要将代碼存放在我們自己的run方法中,是以可以直接調用父類中的run方法,并将其複寫。

線程都有自己預設的名稱。

Thread-編号  該編号從0開始

static Thread currentThread():擷取目前線程對象

getName():擷取線程名稱

設定線程名稱:setName或者構造函數

建立線程的第二種方式:實作 Runnable接口

步驟:

1、定義類實作Runnable接口 2、覆寫Runnable接口中的run方法。 将線程要運作的代碼存放在run方法中 3、通過Thread類建立線程對象。 4、将Runnable接口的子類對象作為實際參數傳遞給Thread類的構造函數。 為什麼要将Runnable接口的子類對象傳遞給Thread的構造函數。 因為,自定義的run方法所屬的對象是Runnable接口 的子類對象。 是以要讓線程去執行指定對象的run方法。 就必須明确該run方法所屬對象。 5、調用Thread類的start方法開戶線程并調用Runnable接口子類的Run方法

下面用一個賣票的小程式來說明:

//賣票線程
class Ticket implements Runnable
{
    int  num=100;
	Object obj=new Object();
	public void run()
	{
		while(true)
		{			
			if(num>0)
			{				
				System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
			}
			
		}
	}
	
	
}

class ThreadTest
{
	public static void main(String[] args)
	{
		Ticket t=new Ticket();//建立一個對象,然後再開始四個線程,模拟四個視窗賣票
		Thread th1=new Thread(t);//這裡是用到了多态 ,傳遞的是Runnable類型或其子類的對象
		Thread th2=new Thread(t);
		Thread th3=new Thread(t);
		Thread th4=new Thread(t);
		th1.start();
		th2.start();
		th3.start();
		th4.start();
	}
}
           

通過實作Runnable接口方式建立線程的好處: 避免了單繼承的局限性。(當一個類本身就有一個父類,則不能再繼承Thread類)

在定義線程時,建議使用實作方式。

差別:

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

實作Runnable,線程代碼存放在接口的子類的run方法中。

多線程中的安全問題

還是那個賣票程式,再看代碼:

<span style="font-size:14px;">//賣票線程
class Ticket implements Runnable
{
    int  num=100;
	Object obj=new Object();
	public void run()
	{
		while(true)
		{
				if(num>0)
				{
					//在此處sleep() 10毫秒,模拟CPU運作到此處的時候,将執行權交給其他程序
					try{Thread.sleep(10);} catch(Exception e){}
					System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
				}
		}
	}	
}

class ThreadTest
{
	public static void main(String[] args)
	{
		Ticket t=new Ticket();
		Thread th1=new Thread(t);
		Thread th2=new Thread(t);
		Thread th3=new Thread(t);
		Thread th4=new Thread(t);
		th1.start();
		th2.start();
		th3.start();
		th4.start();
	}
}</span>
           

結果:

黑馬程式員--java基礎複習之多線程及線程間通信

    從結果中可以看到,票号出現了負數和0,這明顯是不符合常理的。 造成這情況,是因為多線程的運作出現了安全問題

問題原因:當多條語句在操作同一個線程共享資料時,一個線程對多條語句隻執行了一部分,還沒執行完,另一個線程參與進來執 行,導緻共享資料的錯誤。

解決辦法:對多條操作共享資料的語句,隻能讓一個線程都執行完。在執行過程中,其他線程不可以參與執行。

1、java 對于多線程的安全問題提供了專業的解決方式,就是同步代碼塊

synchronized(對象)

{

需要被同步的代碼

}

對象如同鎖。持有鎖的線程可以在同步中執行。沒有持有鎖的線程即使擷取了CPU的執行權,也進不去,因為沒有鎖。

同步的前提:

1、必須要有兩個或兩個以上的線程

2、必須是多個線程使用同一個鎖

必須保證同步中至少有一個線程在運作。

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

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

同步代碼塊的使用,如下:

class Ticket implements Runnable
{
    int  num=100;
	Object obj=new Object();
	public void run()
	{
		while(true)
		{
			
			//同步
			synchronized(obj)
			{
				if(num>0)
				{
					try{Thread.sleep(10);} catch(Exception e){}
					System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
				}
			}
			
		}
	}
		
}
           

如上,同步代碼塊,關鍵字synchronized,這裡的鎖為任一對象。

2、解決線程安全問題的第二種方式。同步函數

格式:在函數上加上synchronized修飾符

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

如上面的代碼片段可以直接用同步代碼塊的方式完成,如下:

//賣票線程
class Ticket implements Runnable
{
    int  num=100;
	Object obj=new Object();
	public void run()
	{
		while(true)
		{
			this.Sale();
			/*
			//同步代碼塊
			synchronized(obj)
			{
				if(num>0)
				{
					try{Thread.sleep(10);} catch(Exception e){}
					System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
				}
			}
			*/
		}
	}
	
	public synchronized void Sale()
	{
		if(num>0)
		{
			try{Thread.sleep(10);} catch(Exception e){}
			System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
		}
	}
}
           

那麼問題來了,如何确認哪些代碼需要被同步呢?如何尋找多線程中的安全問題呢?         a,明确哪些代碼是多線程運作代碼。         b,明确共享資料。

        c,明确多線程運作代碼中哪些語句是操作共享資料的。

靜态同步函數

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

通過驗證,發現不再是this。因為靜态方法中也不可以定義this。

靜态進記憶體時,記憶體中沒有本類對象,但是一定有該類對應的位元組碼檔案對象。

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

驗證靜态同步函數的鎖為 類名.class

靜态的同步函數,使用的鎖是該方法所在類的位元組碼檔案對象。 類名.class

說到多線程的安全問題,想到之前的單例設計模式。 單例設計模式的特點就是對象的唯一性。當多線程通路單例設計模式中的懶漢式的時候。因為懶漢式中有一個判斷:if(s==null) s=new Single(); 這裡是兩行代碼,當多線程運作到第一句發生CPU轉移執行權的時候,會發生安全問題,這樣,對象就可能不是一個,而是多個了,是以根據多線程的安全問題,我們可以對懶漢式進行同步操作,以解決懶漢式中的安全問題。如下:

class Single
{
	// 構造函數私有化,就不能建立類的對象
	private Single(){}
	private static Single s=null;
	//每次都要判斷鎖,比較低效
	public static synchronized Single getInstance()
	{
		if(s==null)		
			s=new Single();		
		return s;
	}
}
           

但是,每次線程通路getInstance()方法時,都有一個判斷鎖的動作,這樣做效率比較低。是以又用同步代碼塊對懶漢式進行了優化:

class Single
{
	// 構造函數私有化,就不能建立類的對象
	private Single(){}
	private static Single s=null;
	public static Single getInstance()
	{
		//使用雙重判斷
		if(s==null)
		{
			//鎖為該類所屬位元組碼檔案對象
			synchronized(Single.class)
			{
				if(s==null)		
					s=new Single();		
				return s;
			}
		}
	}
}
           

這裡使用了雙重判斷,以免每次通路該函數時,在函數中要進行判斷鎖的操作,影響效率,使後面的通路隻需要判斷s是否為空即可。

死鎖 死鎖是什麼?死鎖即是同步中嵌套同步。如下例:

class Test implements Runnable
{
	boolean flag;
	Test(boolean flag)
	{
		this.flag=flag;
	}
	public void run()
	{
        if(flag)
		{
			synchronized(MyLock.obja)
			{
				System.out.println(" if obja");
				synchronized(MyLock.objb)
				{
					System.out.println(" if objb");
				}
			}
		}
		else
		{
			synchronized(MyLock.objb)
			{
				System.out.println(" else objb");
				synchronized(MyLock.obja)
				{
					System.out.println(" else obja");
				}
			}
		}	
	}
}

class MyLock
{
	static Object obja=new Object();
	static Object objb=new Object();
}

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

結果:

黑馬程式員--java基礎複習之多線程及線程間通信

線程間通信

線程間通信,即是多個線程操作同一資源,但操作的内容不同。 舉例,A和B去銀行辦個人業務,其中A是存錢,B是取錢。操作的都是銀行的金庫。

如下例,兩個線程操作一個共同資源,一個向資源内指派,一個從資源中取值。希望兩個線程互動,存一個,取一個

<span style="font-size:14px;">//共同資源
class Res
{   
	boolean flag=false;
	String name;
	String sex;
}

//輸入類,實作Runnable接口
class Input implements Runnable
{
	Res r;
	Input(Res r)
	{
		this.r=r;
	}
	public void run()
	{
		int x=0;
		while(true)
		{
			synchronized(r)
			{
				if(r.flag==false)
				{
					if(x==0)
					{
						r.name="zhangsa";
						r.sex="man";
					}
					else
					{
						r.name="李四";
						r.sex="女";
					}
					r.flag=true;
				}
				
				
			}
			x=(x+1)%2;
			
		}
	}
}

//輸出類
class Output implements Runnable
{
	Res r;
	Output(Res r)
	{
		this.r=r;
	}
	public void run()
	{
		while(true)
		{
			synchronized(r)
			{
				if(r.flag)
				{
					System.out.println(r.name+"::::"+r.sex);
					r.flag=false;
				}
				
			}
			
		}
	}
}

class InputOutputDemo
{
	public static void main(String[] args)
	{
		//建立一個資源對象
		Res r=new Res();
		
		Input in=new Input(r);
		Output out=new Output(r);
		
		Thread t1=new Thread(in);
		Thread t2=new Thread(out);
		
		t1.start();
		t2.start();
		
	}
}</span>
           

結果:

黑馬程式員--java基礎複習之多線程及線程間通信

上述代碼使用線程等待喚醒機制來優化:

//資源
class Res
{   
	boolean flag=false; //辨別,用于判斷目前線程是應做存取操作還是等待
	private String name;
	private String sex;
	
	//存入操作
	public synchronized void set(String name,String sex)
	{
		if(flag) //若已存入資源,則線程等待
			try{this.wait();}catch(Exception e){}  //線程等待。
		this.name=name;
		this.sex=sex;
		this.flag=true;
		//喚醒線程池中的其他等待線程
		this.notify();
	}
	//取出操作
	public synchronized void out()
	{		
		if(!flag)
			try{this.wait();} catch(Exception e){}		
		System.out.println(name+":::"+sex);
		this.flag=false;
		this.notify();
	}
}

//輸入類,實作Runnable接口
class Input implements Runnable
{
	Res r;
	Input(Res r)
	{
		this.r=r;
	}
	public void run()
	{
		int x=0;
		while(true)
		{
			
			if(x==0)					
				r.set("zhangsan","man");										
			else					
				r.set("李四","女");										
			x=(x+1)%2;
									
							
		}
	}
}

//輸出類
class Output implements Runnable
{
	private Res r;
	Output(Res r)
	{
		this.r=r;
	}
	public void run()
	{
		while(true)
		{
			r.out();			
		}
	}
}

class InputOutputDemo1
{
	public static void main(String[] args)
	{
		//建立一個資源對象
		Res r=new Res();
		
		new Thread(new Input(r)).start();
		new Thread(new Output(r)).start();
				
	}
}
           

wait(): notify()

notifyAll();

都使用在同步中,因為要對持有螢幕(鎖)的線程操作。是以要使用在同步中,因為隻有同步才具有鎖

為什麼這些操作線程的方法要定義在Objcect類中呢?

1、因為這些方法在操作同步中線程時,都必須要辨別它們所操作線程持有的鎖。

隻有同一個鎖上的被等待線程,可以被同一個鎖上的notify喚醒,不可以對不同鎖中的線程喚醒。也就是說,等待和喚醒必須是同一個鎖。

2、鎖可以是任意對象,是以可以被任意對象調用的方法定義在Object類中

JDK1.5中提供了多線程更新解決方法

将同步synchronized替換成顯式的Lock操作。 将Object中的wait,notify  notifyAll,替換成Condition對象。 該對象可以通過Lock鎖進行擷取.lock.newConditiom

通過下面一個代碼來實作,看看與synchronized有什麼不同:

import java.util.concurrent.locks.*;
class Res
{
	private String name;
	private int num=1;
	boolean flag=false;
	//建立Lock接口的子接口,父類引用指向子類對象
	private Lock lock=new ReentrantLock();
	//建立Condition對象,用來控制生産者的鎖,等待和喚醒本鎖上的線程
	private Condition condition_pro =lock.newCondition();
	//建立Condition對象,用來控制消費者的鎖,等待和喚醒本鎖上的線程
	private Condition condition_con =lock.newCondition();
	public  void set(String name) throws InterruptedException
	{
		lock.lock();
		
		//因為使用Conditiom的await方法,會抛出異常
			try
			{
				while(flag)
				condition_pro.await();//線程等待,抛出異常
				 this.name=name+(num++);
				System.out.println("生産者::::::"+this.name);
				condition_con.signal();//消費者線程喚醒
				flag=true;
			}
			//一定執行的操作:解開鎖
			finally
			{
				lock.unlock();
			}
			
		
	}
	
	public synchronized void out()throws InterruptedException
	{
		//鎖上鎖
		lock.lock();
		try
		{
			while(!flag)
				condition_con.await(); //消費者線程等待
		System.out.println("消費者:  "+name);
		condition_pro.signal();//生産者線程喚醒
		flag=false;
		}
		finally{
			lock.unlock(); //解開鎖
		}
		
	}
}

class Producter implements Runnable
{
	Res r;
	Producter(Res r)
	{
		this.r=r;
	}
	public void run() 
	{
		while(true)
		{
			try
			{
				r.set("商品");
			}
			catch(InterruptedException e)
			{
				
			}
			
		}
	}
}
class Consumer implements Runnable
{
	Res r;
	Consumer(Res r)
	{
		this.r=r;
	}
	public void run()
	{
		while(true)
			try
			{
				r.out();
			}
			catch(InterruptedException e)
			{}
			
	}
}

class ProConLockTest
{
	public static void main(String[] args)
	{
		Res r=new Res();
	
		Producter pro=new Producter(r);
		Consumer con=new Consumer(r);

		Thread t1=new Thread(pro);
		Thread t2=new Thread(con);
		Thread t3=new Thread(pro);
		Thread t4=new Thread(con);

		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
	
}
           

結果:

黑馬程式員--java基礎複習之多線程及線程間通信

停止線程

兩種方式: 1、定義循環結束标記  因為線程運作代碼一般都是循環,隻要控制了循環即可。如下:

class StopThread implements Runnable
{
	private boolean flag=true;
	public  void run()
	{
		while(flag)
		{			
			System.out.println("aaaaa");
		}
	}
	//改變标志的值
	public void chageFlag()
	{
		flag=false;
	}
}

class StopThreadDemo
{
	public static void main(String[] args)
	{
		StopThread st=new StopThread();
		
		Thread t1=new Thread(st);
		t1.start();
		
		int num=0;
		while(true)
		{
			if(num++==60)
			{
				st.chageFlag();
				break;
			}
			System.out.println("num="+num);
		}
	}
}
           

特殊情況:

當線程處于了當機狀态,就不會讀取到标記,那麼線程就不會結束。當沒有指定的方式讓當機的線程恢複到運作狀态時,這時需要對當機狀态進行清除。強制讓線程恢複到運作狀态中來。這樣就可以操作标記讓線程結束。

2、使用interrupt(中斷)方法  (stop方法已過時)

該方法中結束線程的當機狀态,使線程回到運作狀态中來。

//Thread 類提供該方法 interrupt();
*/

class StopThread implements Runnable
{
	private boolean flag=true;
	public  void run()
	{
		while(flag)
		{			
			try
			{
				wait();
			}
			catch(InterruptedException e)
			{
				flag=false;
			}
			System.out.println(Thread.currentThread().getName()+"....");
		}
	}
}

class StopThreadDemo
{
	public static void main(String[] args)
	{
		StopThread st=new StopThread();
		
		Thread t1=new Thread(st);
		t1.start();
		
		Thread t2=new Thread(st);
		t2.start();
		
		int num=0;
		while(true)
		{
			if(num++==60)
			{
				t1.interrupt();
				t2.interrupt();
				break;
			}
			System.out.println("num="+num);
		}
		System.out.println("over");
	}
}
           

多線程什麼時候用? 當某些代碼需要同時被執行時,就用單獨的線程進行封裝。

如下:

class UseMulThread
{
	public static void main(String[] args)
	{
		/*使用繼承方式(匿名内部類)*/
		
		new Thread(){
			public void run()
			{
				for(int i=0;i<50;i++)
				{
					System.out.println("我是線程一");
				}
			}
		}.start();
		
		
		/******使用實作Runnable方式(匿名内部類)*********/
		
		Runnable ra= new Runnable()
		{
			public void run()
			{
				for(int j=0;j<40;j++)
				{
					System.out.println("我是線程二");
				}
			}
		};
		
		new Thread(ra).start();
		
		
		/***************主線程*********************/
		for(int k=0;k<40;k++)
		{
			System.out.println("我是主線程");
		}
		
	}
}
           

join方法

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

  setPriority()方法用來設定優先級

        MAX_PRIORITY 最高優先級10

        MIN_PRIORITY   最低優先級1

        NORM_PRIORITY 配置設定給線程的預設優先級5

優先級高是擷取CPU機率大一些,優先級高并不一定就先執行。 yield()方法可以暫停目前線程,讓其他線程執行。

繼續閱讀