多線程、單例設計模式、死鎖

1  多線程

1.1  進程

   進程就是一個正在執行中的程序。

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


1.2  線程:

   線程就是進程中的一個獨立的控制單元。

      線程在控制着進程的執行,一個進程中至少有一個線程。


1.3  主線程

     Java虛擬機(JVM)啓動的時候會有一個進程java.exe,該進程中至少有一個線程負責java程序的執行,而且這個線程運行的代碼存在於main方法中,該線程稱之爲主線程。

其實JVM啓動不止一個線程,還有負責垃圾回收機制的線程。


1.4  創建線程:

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

 

創建線程的第一種方式:繼承Thread類,覆run()方法。

步驟:1,自定義類,並繼承Thread類。

          2、重寫Thread類中的run()方法。

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

          3、調用線程的start()方法。

               start()方法有兩個作用:啓動線程,調用run()方法。

代碼示例如下:

class Demo extends Thread {  //要創建線程必須繼承Thread類
	public void run(){
		for(int x=0;x<120;x++)
			System.out.println("Demo run--"+x);
	}
}
class ThreadDemo{
	public static void main(String[] args){
		Demo d = new Demo();  //創建好一個線程
		d.start();   //啓動線程,並調用run()方法。
		//d.run();   //這樣,僅僅是對象調用方法,而線程創建了,並沒有運行。
		
		for(int x=0;x<120;x++)
			System.out.println("Hello World--"+x);
	}
  }

發現運行結果每一次都不同。

因爲多個線程都獲取CPU執行權,CPU執行到誰,誰就運行。Demo線程和主線程爭奪CPU

明確一點,在某一個時刻,只能有一個程序在運行(多核除外)。

CPU在做着快速切換,以達到看上去是同時運行的效果。

我們可以形象的把多線程的運行形容爲在互相搶奪CPU的執行權(CPU資源);

這就是多線程的一個特性:隨機性。誰搶到誰執行,至於執行多長時間,CPU說的算。

 

爲什麼要覆蓋run()方法呢?

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

 

線程都有自己默認的名稱:

Thread-編號,該編號從0開始。

currentThread();  該方法獲取當前線程對象;

getName();  該方法獲取線程名稱

設置線程名稱:serName()或者構造函數。

 

1.5  簡單的賣票程序

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

 

創建線程的第二種方式:實現Runnable接口。

步驟:1、定義類實現Runnable接口。

          2、覆蓋Runnable接口中的run()方法。

               將線程要運行的代碼存放在該run()方法中。

          3、通過Thread類建立線程對象。

          4、將Runnable接口的子類對象作爲實際參數,傳遞給Thread類的構造函數。

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

               因爲,自定義的run()方法所屬的對象是Runnable接口的子類對象。

               所以要讓線程去執行指定對象的run()方法,就必須明確該run()方法所屬的對象。

          5、調用Thread類的start()方法,啓動線程,並調用Runnable接口子類的run方法。

 

第一種和第二種,即實現Runnable接口方式和繼承Thread類方式有什麼區別?

實現方式的好處:避免了單繼承的侷限性。

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

兩種方式的區別:

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

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

 

多線程的安全問題:

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

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

Java對於多線程的安全問題提供了專業的解決方式:同步代碼塊和同步函數。

 

同步代碼塊:

synchronized(對象)

{

需要被同步的代碼; //即操作共享數據的代碼,是需要被同步的代碼

}

    對象如同鎖,持有鎖的線程可以在同步中執行。

沒有持有鎖的線程即使獲取CPU的執行權,也進不去,因爲沒有獲取鎖。

 

同步的前提

1、必須要有兩個或兩個以上的線程。鎖住操作共享數據的代碼。

2、必須是多個線程使用同一個鎖(即同一個對象),必須保證同步中只能有一個線程在運行。

 

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

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

 

賣票程序代碼:

class Ticket implements Runnable {//extends Thread {
	private int tick = 100;
	Object obj = new Object(); //解決安全問題
	public void run() {
		while(true) {
			synchronized(obj){ //同步代碼塊,解決安全問題
				if(tick>0) {
					try{Thread.sleep(10);}catch(Exception e){} //sleep()拋出一個異常。出現-1、-2,出現安全問題
					System.out.println(Thread.currentThread().getName()+"sale: "+tick--);
				}
			}
		}
	}
}
class ThreadDemo2 {
	public static void main(String[] args){
		Ticket t = new Ticket();  //t不是線程,因爲與Thread類無關
		
		Thread t1 = new Thread(t); //創建一個線程,並把Runnable子類對象傳遞給Thread構造函數
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		System.out.println("---main---");
	}
  }


1.6  同步函數:

synchronized作爲修飾符放在函數聲明中,此函數就具有同步功能(相當於同步代碼塊)。

這個被synchronized修飾的函數就是同步函數。

 

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

函數需要被對象調用,那麼函數都有一個所屬對象的引用,就是this

所以同步函數使用的同步鎖是this

牢記同步的兩個前提,如果加同步後還有問題,就查看是否滿足同步到前提。

 

靜態同步函數:

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

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

 

靜態進內存時,內存中沒有本類對象,但是一定有該類對應的字節碼文件對象。getClass()方法

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

靜態的同步方法,使用的同步鎖是該方法所在類的字節碼文件對象:類名.class

 

需求:銀行有一個金庫,有兩個儲戶分別存300元,每次存100,存三次。

目的:該程序是否有安全問題,如果有,如何解決?

如何找問題(需要同步的代碼怎麼找):

1、明確哪些代碼是多線程運行代碼。

2、明確共享數據。

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

 

代碼:

class Bank {
	private int sum;
	//Object obj = new Object();
	public synchronized void add(int n){  //用synchronized修飾,add爲同步函數
		//synchronized(obj){
			sum = sum + n;
			try{Thread.sleep(10);} catch(Exception e){}
			System.out.println("sum="+sum);
		//}
	}
}
class Cus implements Runnable {
	private Bank b = new Bank();
	public void run() {
		for(int x=0; x<3; x++) {
			b.add(100);
		}
	}
}
class ThreadDemo3 {
	public static void main(String[] args) {
		Cus c = new Cus();
		Thread t1 = new Thread(c);
		Thread t2 = new Thread(c);
		t1.start();
		t2.start();
	}
  }

2  單例設計模式

設計模式:解決某一類問題最行之有效的方法。

 

單例設計模式:一個類在內存只存在一個對象,想要保證對象唯一。

1、爲了避免其他程序過多建立該類對象,先禁止其他程序建立該類對象

2、還爲了讓其他程序可以訪問到該類對象,只好在本類中,自定義一個對象。

3、爲了方便其他程序對自定義對象的訪問,可以對外提供一些訪問方式。

 

這三部分怎麼用代碼體現呢?

1、將構造函數私有化

2、在類中創建一個本類對象

3、提供一個方法可以獲取到該對象。

 

對於事物該怎麼描述,還怎麼描述。

當需要將該事物的對象保證在內存中唯一時,就將以上的三步加上即可。

 

2.1  餓漢式 :

    餓漢式:先初始化對象。

Single類一進內存,就已經創建好了對象。

 

class Single {

      private Single(){}

      private static final Single s = new Single();

      public static Single getInstance() {

              return s;

      }

}

 

2.2  懶漢式

懶漢式:對象是方法被調用時,才初始化,也叫做對象的延時加載。

Single類進內存,對象還沒有存在,只有調用了getInstance()方法時,才建立對象。

 

class Single {

      private Single(){}

      private static Single s = null;

      public static Single getInstance() {

            if(s==null)

                s = new Single();

           return s;

      }

}

 

代碼示例:

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;
	}
}
class SingleDemo {
	public static void main(String[] args){
		System.out.println(Single.getInstance().getClass());
	}
}

3  死鎖

死鎖:同步中嵌套同步,會發生死鎖。

比如,A鎖的同步代碼中需要B鎖,而B鎖的同步代碼中需要A鎖,

 就會產生衝突,發生死鎖。

代碼示例:

class Ticket implements Runnable {
	private int tick = 1000;
	Object obj = new Object();
	boolean flag = true;
	
	public void run() {
		if(flag) {
			while(true) {
				synchronized(obj) {  //obj鎖
					show();          //需要this鎖,而此時this鎖被使用
				}                    //同步中嵌套同步,出現死鎖
			}
		}
		else
			while(true)
				show();
	}
	public synchronized void show() {  //this鎖
		synchronized(obj) {            //需要obj鎖,而此時obj鎖被使用
			if(tick > 0) {
				try{Thread.sleep(10);}catch(Exception e){}
				System.out.println(Thread.currentThread().getName()+"....code: "+tick--);
			}
		}
	}
}
class DeadLockDemo {
	public static void main(String[] args) {
		Ticket t = new Ticket();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		t1.start();
		try{Thread.sleep(10);}catch(Exception e){}
		t.flag = false;
		t2.start();
	}
}

小練習:寫一個死鎖程序。

提示:多線程,同步鎖,死鎖。

class Test implements Runnable {
	private boolean flag;
	Test(boolean flag){
		this.flag = flag;
	}
	public void run() {
		if(flag){
			synchronized(MyLock.locka){    //使用a鎖
				System.out.println("if locka");
				synchronized(MyLock.lockb){  //需要b鎖,而此時b鎖在被使用
					System.out.println("if lockb");
				}
			}
		}
		else{
			synchronized(MyLock.lockb){    //使用b鎖
				System.out.println("else lockb");
				synchronized(MyLock.locka){   //需要a鎖,而此時a鎖在被使用
					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();
	}
}




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