Java中關於synchronized淺析

進程

我們都知道計算機的核心是CPU,它承擔了所有的計算任務;而操作系統是計算機的管理者,它負責任務的調度、資源的分配和管理,統領整個計算機硬件;應用程序側是具有某種功能的程序,程序是運行於操作系統之上的。

進程是一個具有一定獨立功能的程序在一個數據集上的一次動態執行的過程,是操作系統進行資源分配和調度的一個獨立單位,是應用程序運行的載體。進程是一種抽象的概念,從來沒有統一的標準定義。進程一般由程序、數據集合和進程控制塊三部分組成。程序用於描述進程要完成的功能,是控制進程執行的指令集;數據集合是程序在執行時所需要的數據和工作區;程序控制塊(Program Control Block,簡稱PCB),包含進程的描述信息和控制信息,是進程存在的唯一標誌。

進程具有的特徵:

動態性:進程是程序的一次執行過程,是臨時的,有生命期的,是動態產生,動態消亡的;
併發性:任何進程都可以同其他進程一起併發執行;
獨立性:進程是系統進行資源分配和調度的一個獨立單位;
結構性:進程由程序、數據和進程控制塊三部分組成。
線程
在早期的操作系統中並沒有線程的概念,進程是能擁有資源和獨立運行的最小單位,也是程序執行的最小單位。任務調度採用的是時間片輪轉的搶佔式調度方式,而進程是任務調度的最小單位,每個進程有各自獨立的一塊內存,使得各個進程之間內存地址相互隔離。

後來,隨着計算機的發展,對CPU的要求越來越高,進程之間的切換開銷較大,已經無法滿足越來越複雜的程序的要求了。於是就發明了線程,線程是程序執行中一個單一的順序控制流程,是程序執行流的最小單元,是處理器調度和分派的基本單位。一個進程可以有一個或多個線程,各個線程之間共享程序的內存空間(也就是所在進程的內存空間)。一個標準的線程由線程ID、當前指令指針(PC)、寄存器和堆棧組成。而進程由內存空間(代碼、數據、進程空間、打開的文件)和一個或多個線程組成。

進程與線程的區別

前面講了進程與線程,但可能你還覺得迷糊,感覺他們很類似。的確,進程與線程有着千絲萬縷的關係,下面就讓我們一起來理一理:

線程是程序執行的最小單位,而進程是操作系統分配資源的最小單位;
一個進程由一個或多個線程組成,線程是一個進程中代碼的不同執行路線;
進程之間相互獨立,但同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)及一些進程級的資源(如打開文件和信號),某進程內的線程在其它進程不可見;
調度和切換:線程上下文切換比進程上下文切換要快得多。
synchronized關鍵字
synchronized是Java中的關鍵字,是一種同步鎖。

修飾代碼塊

一個線程訪問一個對象中的synchronized(this)同步代碼塊時,其他試圖訪問該對象的線程將被阻塞。

代碼清單,synchronized修飾代碼塊用法

package synchronize;
public class SyncThread implements Runnable{
	private static int count=0;
	
	public SyncThread() {
		count=0;
	}
	
	public void run() {
		synchronized (this) {
			for(int i=0;i<5;i++){
				System.out.println(Thread.currentThread().getName()+" : "+(count++));
				try {
					Thread.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
		
	}
	public int getCount(){
		return count;
	}
}

代碼清單,synchronized修飾代碼塊測試用例

SyncThread syncThread1 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "SyscThread1");
Thread thread2 = new Thread(syncThread1, "SyscThread2");
thread1.start();
thread2.start();

打印輸出結果:
在這裏插入圖片描述

當兩個併發線程(thread1和thread2)訪問同一個對象(syncThread)中的synchronized代碼塊時,在同一時刻只能有一個線程得到執行,另一個線程受阻塞,必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。Thread1和thread2是互斥的,因爲在執行synchronized代碼塊時會鎖定當前的對象,只有執行完該代碼塊才能釋放該對象鎖,下一個線程才能執行並鎖定該對象。

修飾方法

Synchronized修飾一個方法很簡單,就是在方法的前面加synchronized,

public synchronized void method(){//todo};

synchronized修飾方法和修飾一個代碼塊類似,只是作用範圍不一樣,修飾代碼塊是大括號括起來的範圍,而修飾方法範圍是整個函數。

在用synchronized修飾方法時要注意以下幾點:

雖然可以使用synchronized來定義方法,但synchronized並不屬於方法定義的一部分,因此,synchronized關鍵字不能被繼承。如果在父類中的某個方法使用了synchronized關鍵字,而在子類中覆蓋了這個方法,在子類中的這個方法默認情況下並不是同步的,而必須顯式地在子類的這個方法中加上synchronized關鍵字纔可以。當然,還可以在子類方法中調用父類中相應的方法,這樣雖然子類中的方法不是同步的,但子類調用了父類的同步方法,因此,子類的方法也就相當於同步了。這兩種方式的例子代碼如下:

在子類方法中加上synchronized關鍵字

class Parent {
   public synchronized void method() { }
}
class Child extends Parent {
   public synchronized void method() { }
}

在子類方法中調用父類的同步方法

class Parent {
   public synchronized void method() {   }
}
class Child extends Parent {
   public void method() { super.method();   }
}
  1. 在定義接口方法時不能使用synchronized關鍵字。
  2. 構造方法不能使用synchronized關鍵字,但可以使用synchronized代碼塊來進行同步。

修飾一個靜態的方法
Synchronized也可修飾一個靜態方法,用法如下:

public synchronized static void method() {
   // todo
}

我們知道靜態方法是屬於類的而不屬於對象的。同樣的,synchronized修飾的靜態方法鎖定的是這個類的所有對象。

代碼清單,修飾靜態方法

package synchronize;
public class synchronizestaticmethod implements Runnable {
	private static int count;
	public synchronizestaticmethod() {
		count = 0;
	}
	public synchronized static void method() {
		for (int i = 0; i < 5; i++) {
			try {
				System.out.println(Thread.currentThread().getName() + ":"
						+ (count++));
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	public synchronized void run() {
		method();
	}
}

代碼清單,修飾靜態方法測試用例

synchronizestaticmethod syncThread1 = new synchronizestaticmethod();
synchronizestaticmethod syncThread2 = new synchronizestaticmethod();
Thread thread1 = new Thread(syncThread1, "SyscThread1");
Thread thread2 = new Thread(syncThread2, "SyscThread2");
thread1.start();
thread2.start();

在這裏插入圖片描述

syncThread1和syncThread2是synchronizestaticmethod 的兩個對象,但在thread1和thread2併發執行時卻保持了線程同步。這是因爲run中調用了靜態方法method,而靜態方法是屬於類的,所以syncThread1和syncThread2相當於用了同一把鎖。這與下面代碼是不同的。

代碼清單,修飾方法

package synchronize;
public class synchronizestaticmethod implements Runnable {
	private static int count;
	public synchronizestaticmethod() {
		count = 0;
	}
	public synchronized void run() {
		for (int i = 0; i < 5; i++) {
			try {
				System.out.println(Thread.currentThread().getName() + ":"
						+ (count++));
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

在這裏插入圖片描述

這時創建了兩個synchronizestaticmethod 的對象syncThread1和syncThread2,線程thread1執行的是syncThread1對象中的synchronized代碼(run),而線程thread2執行的是syncThread2對象中的synchronized代碼(run);我們知道synchronized鎖定的是對象,這時會有兩把鎖分別鎖定syncThread1對象和syncThread2對象,而這兩把鎖是互不干擾的,不形成互斥,所以兩個線程可以同時執行。

修飾類

class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo
      }
   }
}

代碼清單,修飾一個類

public class synchronizestaticmethod implements Runnable {
	private static int count;
	public synchronizestaticmethod() {
		count = 0;
	}
	public static void method() {
		synchronized (synchronizestaticmethod.class) {
			for (int i = 0; i < 5; i++) {
				try {
					System.out.println(Thread.currentThread().getName() + ":"
							+ (count++));
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	public synchronized void run() {
		method();
	}
}

在這裏插入圖片描述

synchronized作用於一個類T時,是給這個類T加鎖,T的所有對象用的是同一把鎖。

總結

  1. 無論synchronized關鍵字加在方法上還是對象上,如果它作用的對象是非靜態的,則它取得的鎖是對象;如果synchronized作用的對象是一個靜態方法或一個類,則它取得的鎖是對類,該類所有的對象同一把鎖。
  2. 每個對象只有一個鎖(lock)與之相關聯,誰拿到這個鎖誰就可以運行它所控制的那段代碼。
  3. 實現同步是要很大的系統開銷作爲代價的,甚至可能造成死鎖,所以儘量避免無謂的同步控制。

關於作者

專注於 Android 開發多年,喜歡寫 blog 記錄總結學習經驗,blog 同步更新於本人的公衆號,歡迎大家關注,一起交流學習~

在這裏插入圖片描述

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