1. 程式人生 > >多執行緒的基本概念與執行緒安全問題

多執行緒的基本概念與執行緒安全問題

多執行緒:

基本概念

  • 程式:是一個可執行的檔案.

  • 程序:是一個正在執行的程式.在記憶體中開闢了一塊兒空間

  • 執行緒:負責程式的執行,可以看做程式執行的一條通道或者一個執行單元.所以我們通常將程序的工作理解成執行緒的工作.

  • 程序中可不可以沒有執行緒?

    • 必須有執行緒,至少有一個,當有一個執行緒存在的時候,我們稱為單執行緒,這個唯一的執行緒就是主執行緒(main執行緒)
  • 當有一個以上的執行緒存在的時候,我們稱為多執行緒.

  • 多執行緒存在的意義:為了實現同一時間做多件事情.

  • 任務區:我們將執行緒完成工作的方法稱為任務區

    • 每一個執行緒都有自己的任務區.
  • JVM是多執行緒嗎?

    • 一定是多執行緒
    • 至少有兩個
    • 主執行緒的任務區:main方法
    • 垃圾回收執行緒的任務區:finalize()方法
public class Demo7 {
	public static void main(String[] args) {//一個main執行緒
		new Test();
		
		/*
		 * 手動執行gc方法,執行垃圾回收器,觸發垃圾回收機制.
		 * 工作原理:執行gc方法,觸發垃圾回收機制,執行垃圾回收執行緒,呼叫finalize()方法
		 * 
		 * 多個執行緒是搶cpu的關係,cpu有隨機性
		 */
		System.gc();//兩個:主執行緒和垃圾回收執行緒
System.out.println("main"); }//執行緒是隨著任務的開始而開始結束而結束,只要任務沒有結束,執行緒就不會結束.當執行緒還在工作的時候,程序沒有辦法結束. } class Test{ /* * finalize()方法應該由系統呼叫,為了模擬多執行緒的使用環境,我們將它進行了重寫 * 正常情況下,當Test類的物件被釋放的時候,這個方法就會被呼叫 */ protected void finalize() throws Throwable { System.out.println("finalize"); } }

自己建立執行緒的原因

系統的執行緒無法完成我們自己的功能,我們就自己建立執行緒.系統將執行緒面向物件了,形成的類就是Thread.

  • Thread的任務區是run()方法

  • 注意:如果我們想讓run方法作為任務區,不能手動去呼叫,必須通過呼叫start方法,讓run自動執行.

  • 兩種建立執行緒的方式:

    1. 通過Thread直接建立執行緒
    2. 重寫的run方法,作為任務區
public class Demo8 {
//	public static void main(String[] args) {//為了方便研究,暫時忽略垃圾回收執行緒.認為這裡有一個執行緒--main執行緒
//		//1.通過Thread直接建立執行緒
//		//建立兩個執行緒
//		Thread thread1 = new Thread();
//		Thread thread2 = new Thread();
//		
//		//通過start方法讓執行緒工作
//		thread1.start();
//		thread2.start();//有三個執行緒,兩個子執行緒一個主執行緒
//		
//		System.out.println("main");
//	}
	
	public static void main(String[] args) {//為了方便研究,暫時忽略垃圾回收執行緒.認為這裡有一個執行緒--main執行緒
		//1.通過Thread的子類建立執行緒
		//建立兩個執行緒
		MyThread thread1 = new MyThread("bingbing");//thread-0
		MyThread thread2 = new MyThread("yingying");//thread-1
		
		//通過start方法讓執行緒工作
		thread1.start();
		thread2.start();//有三個執行緒,兩個子執行緒一個主執行緒
		
		for(int i=0;i<10;i++){
			System.out.println(Thread.currentThread().getName()+" main  i:"+i);
		}
		
		/*
		 * 當手動呼叫run時,run變成了普通方法,失去了任務區的功能.
		 * run內部對應的執行緒就是run方法被手動呼叫的位置對應的執行緒.
		 */
		//thread1.run();
	}
}

class MyThread extends Thread{
	String myname;
	public MyThread(String myname) {
		super();
		this.myname = myname;
	}
	@Override
	public void run() {
		/*
		 * 重寫的run方法,作為任務區
		 * Thread.currentThread():獲取的當前執行緒
		 * Thread.currentThread().getName():獲取的是當前執行緒的名字,系統給的名字.
		 */
		for(int i=0;i<10;i++){
			System.out.println(Thread.currentThread().getName()+"  "+myname+"  i:"+i);
		}
	}
}

執行緒的兩種建立方式:

  1. 建立執行緒的第一種方式:通過建立Thread類的子類—讓run留在了執行緒了內部,造成任務與執行緒的繫結,操作不方便
  2. 建立執行緒的第二種方式:讓執行緒與任務分離—將run從執行緒中獨立出來.好處:操作更方便,那個執行緒想工作,我就把任務交給誰

例項:實現四個售票員售票

  • 分析:建立4個執行緒–模擬四個售票員
  • 任務:只需要一個
  • 資料:只需要一個

建立執行緒的第一種方式

public class Demo2 {
	public static void main(String[] args) {
		//建立執行緒物件
		Seller seller1 = new Seller();
		Seller seller2 = new Seller();
		Seller seller3 = new Seller();
		Seller seller4 = new Seller();
		
		//開啟執行緒
		seller1.start();
		seller2.start();
		seller3.start();
		seller4.start();
	}
}

class Seller extends Thread{
	static int num = 40;//為了實現num的共享
	public void run() {
		for(int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName()+"  i:"+"  "+ (--num));
		}
	}
}

建立執行緒的第二種方式

public class Demo2 {
	public static void main(String[] args) {
		
		//建立任務物件
		Ticket ticket = new Ticket();
		//建立執行緒並關聯同一個任務
		//如果我們建立了自己獨立的任務類,執行緒會優先呼叫我們手動傳入執行緒的任務類物件的run方法,不會再去呼叫Thread類的run方法
		Thread thread1 = new Thread(ticket);
		Thread thread2 = new Thread(ticket);
		Thread thread3 = new Thread(ticket);
		Thread thread4 = new Thread(ticket);

		thread1.start();
		thread2.start();
		thread3.start();
		thread4.start();

	}
}

//建立任務類
class Ticket implements Runnable {
	//因為Ticket物件被四個執行緒共享,所以num作為屬性也被共享了
	int num = 40;
	public void run() {
		for(int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName()+"  i:"+"  "+ (--num));
		}
	}
}

執行緒安全問題:

  • 分析:4個執行緒共用了一個數據,出現了-1,-2,-3等錯誤的資料

  • 具體分析:

  1. 共用了一個數據
  2. 共享語句有多條,一個執行緒使用cpu,沒有使用完,cpu被搶走,當再次搶到cpu的時候,直接執行後面的語句,造成了錯誤的發生.
  • 解決:

    • 在程式碼中使用同步程式碼塊兒(同步鎖)
    • 解釋:在某一段任務中,同一時間只允許一個執行緒執行任務,其他的執行緒即使搶到了cpu,也無法進入當前的任務區間,只有噹噹前的執行緒將任務執行完後,其他的執行緒才能有資格進入
  • 同步程式碼塊兒的構成:

synchronized((物件)){

	  同步的程式碼
    }
  • 對作為鎖的物件的要求:

    1. 必須是物件
    2. 必須保證被多個執行緒共享
  • 可以充當鎖的:

    1. 一個普通的物件
    2. 當前物件的引用–this
    3. 類的位元組碼檔案
  • 同步程式碼塊兒的特點:

    1. 可以保證執行緒的安全
    2. 由於每次都要進行判斷處理,所以降低了執行效率
  • 總結:什麼時候使用同步程式碼塊兒

    1. 多個執行緒共享一個數據
    2. 至少有兩個執行緒
//第二種:執行緒與任務分離
public class Demo3 {
	public static void main(String[] args) {
		//建立任務物件
		Ticket1 ticket = new Ticket1();
		//建立執行緒物件並關聯同一個任務
		//如果我們建立了自己獨立的任務類,執行緒會優先呼叫我們手動傳入執行緒的任務類物件的run方法,不會再去呼叫Thread預設的run方法
		Thread seller1 = new Thread(ticket);
		Thread seller2 = new Thread(ticket);
		Thread seller3 = new Thread(ticket);
		Thread seller4 = new Thread(ticket);
		
		//開啟執行緒
		seller1.start();
		seller2.start();
		seller3.start();
		seller4.start();
	}
}

//建立任務類
class Ticket1 implements Runnable{
	//因為Ticket物件被四個執行緒共享,所以num作為屬性也被共享了
 	int num = 20;
 	boolean flag = false;
 	//讓object充當鎖
 	//作為鎖要滿足兩個條件:1.必須是物件   2.必須供所有的執行緒共享.
 	//可以作為鎖的有:1.任意一個例項物件   2.this   3.位元組碼檔案物件
 	Object object = new Object();
	public void run() {
		while (!flag) {
			synchronized (object) {//同步程式碼塊兒--讓執行緒之間互斥
				//製造一個延遲,相當於讓當前執行run的執行緒休息一會兒(臨時讓出cpu)
				try {
					Thread.sleep(100);//100是時間,單位是毫秒
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				
				if (num >0) {
					System.out.println(Thread.currentThread().getName()+"  "+ --num);
				}else {
					flag = true;
				}
			}
		}

	}
}

例項:兩個人向同一個賬戶裡面存錢

  • 一人存三次,每次存100

  • 注意:

  1. 當在一個類中同時存在多個synchronized修飾的程式碼塊兒或函式時,要想安全,就必須讓他們後面的物件一致。因為只有同一把鎖才能安全。
  • 同步函式的鎖:this
  1. 靜態同步函式在進記憶體的時候不會建立物件,但是存在其所屬類的位元組碼檔案物件,屬於class型別的物件,所以靜態同步函式的鎖是其所屬類的位元組碼檔案物件

理解synchronized關鍵字

  1. synchronized關鍵字的作用域有二種:

    1. 是某個物件例項內,synchronized aMethod(){}可以防止多個執行緒同時訪問這個物件 的synchronized方法(如果一個物件有多個synchronized方法,只要一個執行緒訪問了其中的一個synchronized方法,其它執行緒不能同時訪問這個物件中任何一個synchronized方法)。這時,不同的物件例項的synchronized方法是不相干擾的。也就是說,其它執行緒照樣可以同時訪問相同類的另一個物件例項中的synchronized方法;
    2. 是某個類的範圍,synchronized static aStaticMethod{}防止多個執行緒同時訪問 這個類中的synchronized static 方法。它可以對類的所有物件例項起作用。
  2. 除了方法前用synchronized關鍵字,synchronized關鍵字還可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。用法是:synchronized(this){/區塊/},它的作用域是當前物件;

  3. synchronized關鍵字是不能繼承的,也就是說,基類的方法synchronized f(){} 在繼承類中並不自動是synchronizedf(){},而是變成了f(){}。繼承類需要你顯式的指定它的某個方法為synchronized方法.

public class Demo4 {
	public static void main(String[] args) {
		//1.建立任務類物件
		CunQian cunQian = new CunQian();
		//2.建立執行緒並繫結任務
		Thread thread1 = new Thread(cunQian);
		Thread thread2 = new Thread(cunQian);
		//3.開啟執行緒
		thread1.start();
		thread2.start();
	}
}

class Bank{
	int sum;//放的是當前賬戶的錢
	//使用同步程式碼塊兒
//	public void addMoney(int money){
//		synchronized (this) {
//			sum+=money;
//			System.out.println(sum);
//	    }
//	}
	//使用同步函式
	//非靜態的同步函式
	//相當於預設在synchronized後面跟著this充當鎖
//	public synchronized void addMoney(int money){
//		sum+=money;
//		System.out.println(sum);
//	}
	
	//靜態的同步函式
	//相當於預設在synchronized後面跟著當前的類的位元組碼檔案充當鎖----Bank.class
	public synchronized static void addMoney(int money){

	}
}

//建立任務類
class CunQian implements Runnable{
	Bank bank = new Bank();
	public void run() {
		for(int i=0;i<3;i++){
			bank.addMoney(100);
		}
	}
}

單例模式與執行緒

public class Demo5 {

}

//懶漢式
class SingleInstance1{
	private  static SingleInstance1 singleInstance = null;
	private SingleInstance1(){
		
	}
	//因為同步程式碼塊兒的效率高於同步函式,所以儘量使用同步程式碼塊兒
	public static SingleInstance1 getInstance() {
		if (singleInstance == null) {//目的:儘量減少執行緒安全程式碼的判斷次數,提高效率
			
			synchronized (SingleInstance1.class) {
				if (singleInstance == null) {
					singleInstance = new SingleInstance1();
				}
			}
		}
		return singleInstance;
	}
}

//餓漢式
class SingleInstance{
	private final static SingleInstance singleInstance = new SingleInstance();
	private SingleInstance(){
		
	}
	public static SingleInstance getInstance() {
		return singleInstance;
	}
}

class Test implements Runnable{
	@Override
	public void run() {
		SingleInstance1 singleInstance1 = SingleInstance1.getInstance();
		
	}
}

Thread物件作為引數


public class Demo6 {
	public static void main(String[] args) {
		Thread thread1 = new Thread();
		//1.這裡是可以的,這裡將thread1當做了任務類物件,執行的時候呼叫的是thread1內部的run方法
		Thread thread2 = new Thread(thread1);
		thread2.start();
		
		//2.建立Thread類的匿名子類物件充當執行緒類
		new Thread(){
			public void run() {
				System.out.println("haha");
			};
		}.start();
	}
}


執行緒的停止

如何讓他的任務結束

  1. 通過一個標識結束執行緒

  2. 通過呼叫stop方法結束執行緒----有固有的安全問題,已經過時,不建議再使用

  3. 呼叫interrupt()方法結束執行緒

    原理:執行緒可以呼叫wait()方法,讓當前的執行緒處於鈍化的狀態(會立刻釋放cpu,並且處於無法搶cpu的狀態,但是當前的執行緒並沒有死亡) 注意點:wait方法必須在同步狀態下使用. 呼叫interrupt方法就是將處於wait狀態的執行緒停止.

通過一個標識結束執行緒


public class Demo7 {
	public static void main(String[] args) {
		Test1 test1  = new Test1();
		Thread thread = new Thread(test1);
		thread.start();
		
		//讓主執行緒睡一會兒
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		int i=0;
		while (true) {
			if (++i == 10) {
				test1.flag = false;//當主執行緒執行到某個階段的時候,讓flag值變成false,控制while迴圈的結束,從而控制子執行緒的結束
			
				break;//目的:為了讓主執行緒結束
			}
		}
	}
}

class Test1 implements Runnable{
	boolean flag = true;
	public void run() {
		while (flag) {
			System.out.println(Thread.currentThread().getName()+"   "+"我們很happy");
		}
	}
}

呼叫interrupt()方法結束執行緒

public class Demo7 {
	public static void main(String[] args) {
		Test1 test1  = new Test1();
		Thread thread = new Thread(test1);
		thread.start();
		
		//讓主執行緒睡一會兒
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		int i=0;
		while (true) {
			if (++i == 10) {
			    thread.interrupt();//當呼叫這個方法的時候,會觸發wait方法的interruptedException異常,我們就可以在捕獲異常的	
			    					//時候將flag值變成false,從而結束迴圈,結束任務,結束執行緒
				break;//目的:為了讓主執行緒結束
			}
		}
	}
}

class Test1 implements Runnable{
	boolean flag = true;
	public synchronized void run() {
		while (flag) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				flag = false;
				System.out.println("InterruptedException");
			}//wait方法由鎖物件來呼叫
			System.out.println(Thread.currentThread().getName()+"   "+"我們很happy");
		}
	}
}