理解多執行緒的根本:資源問題與鎖的物件

synchronized與Lock解決的是多執行緒訪問資源時的執行緒安全問題,那麼這個存線上程安全問題的多執行緒模型中,一定滿足一個條件:多個執行緒訪問一個共享資源。

通常的實現模型是,自定義一個資源類,類中儲存具體資源,以及定義資源訪問的方法,這些方法就是需要提供執行緒安全的。

synchronized的理解

這時使用synchronized有三種實現方式:

  1. synchronized宣告普通方法:呼叫將取得當前類物件的鎖
  2. synchronized宣告靜態方法:呼叫將取得當前類的Class的鎖
  3. synchronized宣告程式碼塊:進入時取得括號宣告的物件的鎖

普通方法和靜態方法的說明:普通和靜態都可以實現資源的互斥訪問,只是一般當資源是靜態變數時,靜態的同步方法才有意義。特別注意普通方法取得的是物件鎖,靜態方法取得的是類鎖,物件鎖和類鎖是不互斥的。

同步方法和同步程式碼塊的說明:當一個資源訪問方法並不是所有程式碼都需要進行同步,一部分程式碼可以先行執行的時候,可以用synchronized程式碼塊代替方法宣告。注意程式碼塊鎖定的是小括號中宣告的物件,取得的仍然是物件鎖,當最先進入同步程式碼塊的執行緒取得了這個資源類物件的鎖後,其他的執行緒就在程式碼塊外阻塞等待鎖的釋放。儘量使用同步程式碼塊代替同步方法可以縮小鎖的粒度。

附1:生產者消費者實現程式碼

/*
 * synchronized同步方法在資源類Property中,意味著produce和consume方法鎖定的是呼叫這個方法的Property物件,所以無論是多個生產者和消費者,還是一個生產者和一個消費者,只要啟動了多個執行緒就能實現效果
 */

class Property<T>{
	private static final int MAX_LENGTH = 5;
	private String properties[];
	private int index = 0;//貨物索引
	public Property() {
		this.properties = new String[MAX_LENGTH];
	}
	public synchronized void produce(String str) {
		if(this.index==5) {
			System.out.println("我是生產者:"+Thread.currentThread().getName()+",產品已滿,我無法生產,馬上釋放鎖");
			try {
				super.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		else {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			properties[index++] = str;
			System.out.println("[生產者:"+Thread.currentThread().getName()+"]生產了產品");
			super.notify();
		}
	}
	public synchronized void consume() {
		if(this.index==0) {
			System.out.println("我是消費者:"+Thread.currentThread().getName()+",沒有產品,我無法消費,馬上釋放鎖");
			try {
				super.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}else {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("[消費者:"+Thread.currentThread().getName()+"]消費了產品:"+this.properties[--this.index]);
			super.notify();
		}	
	}
}
class Producer implements Runnable{
	private Property pro;
	public Producer(Property pro) {
		this.pro = pro;
	}
	@Override
	public void run() {
		for(int x=0;x<10;x++) {
			this.pro.produce("["+Thread.currentThread().getName()+"]"+"的生產品");
		}
	}
}
class Consumer implements Runnable{
	private Property pro;
	public Consumer(Property pro) {
		this.pro = pro;
	}
	@Override
	public void run() {
		for(int x=0;x<10;x++) {
			this.pro.consume();
		}
	}
}

public class ThreadTest {
	public static void main(String[] args) {
		Property pro = new Property();
		Producer p = new Producer(pro);
		Consumer c = new Consumer(pro);
		for(int x=1;x<=5;x++) {
			new Thread(p,String.valueOf(x)).start();
		}
		for(int x=1;x<=5;x++) {
			new Thread(c,String.valueOf(x)).start();
		}
	}
}

理解Thread和Runnable的區別

Runnable介面解決的不僅僅是Thread類的單繼承侷限,在有些情況下,使用繼承Thread類的方式無法滿足同步要求。當可以單獨定義資源類時,將資源類的物件設為執行緒實現類(如定義MyThread extends Thread)的成員,無論使用Thread還是Runnbale,只要main方法中綁定了同一資源類物件,就可以實現同步訪問;但當沒有這個所謂的資源類,僅僅希望實現同步方法時,這時synchronized的作用只能移到run()方法上,這意味著繼承Thread時,每次new例項化一個新的執行緒物件,synchronized進入run()時取得的是各個不同的執行緒物件的鎖,是無法同步的,而Runnable實現就不同,可以通過先例項化一個Runnable實現類,用這一個Runnable物件去建立多個執行緒,這樣所有執行緒嘗試取得的就是這個Runnable物件的鎖,所以synchronized宣告run()方法仍然可以實現同步

附2:Thread和Runnable都可以進行資源同步控制

class Property{
	public synchronized void fun() {
		//同步操作
	}
}
class MyThread extends Thread{
	Property pro;//資源繫結線上程實現類內部
	public MyThread(Property pro) {
		this.pro = pro;
	}
	@Override
	public void run() {
		// 資源操作
	}
}
class MyRunnable implements Runnable{
	Property pro;//資源繫結線上程實現類內部
	public MyRunnable(Property pro) {
		this.pro = pro;
	}
	public void run() {
		// 資源操作
	}
}
public class PropertiesTest {
	public static void main(String[] args) {
		Property pro = new Property();//用同一個資源類物件構造執行緒物件
		Thread t1 = new MyThread(pro);
		Thread t2 = new MyThread(pro);
		t1.start();
		t2.start();
		Thread r1 = new Thread(new MyRunnable(pro));
		Thread r2 = new Thread(new MyRunnable(pro));
		r1.start();
		r2.start();
	}
}

附3:Thread無法實現單純的方法同步

class MyRunnable implements Runnable {
	@Override
	public synchronized void run() {
		for(int x=0;x<100;x++){
			System.out.println(Thread.currentThread().getName());
		}	
	}
}
class MyThread extends Thread{
	@Override
	public synchronized void run() {
		for(int x=0;x<100;x++){
			System.out.println(Thread.currentThread().getName());
		}	
	}
}
public class RunTest{	
	public static void main(String[] args) throws InterruptedException {
		/*for(int x=0;x<3;x++){	
			Thread t = new Thread(new MyRunnable(),String.valueOf(x));
			t.start();
		}*/
		for(int x=3;x<6;x++){	
			Thread t = new MyThread();
			t.setName(String.valueOf(x));
			t.start();
		}
	}
}

擷取部分結果:沒有實現同步
在這裡插入圖片描述

最後總結synchronized和Lock的區別

效能不一致:資源競爭激烈的情況下,Lock效能會比synchronized好,競爭不激烈的情況下,synchronized比Lock效能好。但jdk1.6後synchronized也加入了CAS的優化,效能大大提升。
一般認為synchronized關鍵字的實現是源自像訊號量之類的執行緒同步機制,在高併發狀態下,CPU消耗過多的時間線上程的排程上,從而造成了效能的極大浪費;Lock實現原理則是依賴於硬體,現代處理器都支援CAS指令。
機制不一致:synchronized是在JVM層面實現的,系統會監控鎖的釋放與否,無法手動中斷阻塞執行緒;Lock是基於程式碼實現的,可以非阻塞地獲取鎖,需要手動釋放,推薦在finally塊中釋放。
用法不一樣:synchronized可以用在程式碼塊上,方法上;Lock通過程式碼實現,有更精確的執行緒語義。synchronized只能以一個條件進行執行緒的休眠(wait)和喚醒(notify)控制;Lock可以獲取多個條件(Condition)進行更細緻的執行緒休眠(await)和喚醒(signal)的控制

 
 
 
(文章內容有過多方參考,來源過廣,不一一列出)