1. 程式人生 > >Java多執行緒——物件及變數的併發訪問

Java多執行緒——物件及變數的併發訪問

Java多線系列文章是Java多執行緒的詳解介紹,對多執行緒還不熟悉的同學可以先去看一下我的這篇部落格Java基礎系列3:多執行緒超詳細總結,這篇部落格從巨集觀層面介紹了多執行緒的整體概況,接下來的幾篇文章是對多執行緒的深入剖析。

 

本篇文章主要介紹Java多執行緒中的同步,也就是如何在Java語言中寫出執行緒安全的程式,如何在Java語言中解決非執行緒安全的相關問題。多執行緒中的同步問題是學習多執行緒的重中之重,這個技術在其他的程式語言中也涉及,如C++或C#。

 

同步和非同步:

1、概念:

同步:A執行緒要請求某個資源,但是此資源正在被B執行緒使用中,因為同步機制存在,A執行緒請求不到,怎麼辦,A執行緒只能等待下去

非同步:A執行緒要請求某個資源,但是此資源正在被B執行緒使用中,因為沒有同步機制存在,A執行緒仍然請求的到,A執行緒無需等待

 

2、特點:

顯然,同步最最安全,最保險的。而非同步不安全,容易導致死鎖,這樣一個執行緒死掉就會導致整個程序崩潰,但沒有同步機制的存在,效能會有所提升

 

3、同步阻塞與非同步阻塞:

一個執行緒/程序經歷的5個狀態, 建立,就緒,執行,阻塞,終止。各個狀態的轉換條件如上圖,其中有個阻塞狀態,就是說當執行緒中呼叫某個函式,需要IO請求,或者暫時得不到競爭資源的,作業系統會把該執行緒阻塞起來,避免浪費CPU資源,等到得到了資源,再變成就緒狀態,等待CPU排程執行。

 

 

 

同步是指兩個執行緒的執行是相關的,其中一個執行緒要阻塞等待另外一個執行緒的執行。非同步的意思是兩個執行緒不相關,自己執行自己的。

 

 

執行緒安全問題:

定義:

執行緒安全是多執行緒程式設計時的計算機程式程式碼中的一個概念。在擁有共享資料的多條執行緒並行執行的程式中,執行緒安全的程式碼會通過同步機制保證各個執行緒都可以正常且正確的執行,不會出現資料汙染等意外情況。

執行緒安全問題概況來說有三方面:原子性、可見性和有序性。

 

原子性:

原子(Atomic)的字面意思是不可分割的(lndivisible)。對於涉及共享變數訪問的操作,若該操作從其執行執行緒以外的任意執行緒來看是不可分割的,那麼該操作就是原子操作,相應地我們稱該操作具有原子性(Atomicity)。

在生活中我們可以找到的一個原子操作的例子就是人們從ATM機提取現金:儘管從ATM軟體的角度來說,一筆取款交易涉及扣減戶主賬戶餘額、吐鈔器吐出鈔票、新增交易記錄等一系列操作,但是從使用者(我們)的角度來看ATM取款就是一個操作。該操作要麼成功了,即我們拿到現金(戶主賬戶的餘額會被扣減),這個操作發生過了;要麼失敗了,即我們沒有拿到現金,這個操作就像從來沒有發生過一樣(當然,戶主賬戶的餘額也不會被扣減)。除非ATM軟體有缺陷,否則我們不會遇到吐鈔口吐出部分現金而我們的賬戶餘額卻被扣除這樣的部分結果。在這個例子中,戶主賬戶餘額就相當於我們所說的共享變數,而ATM機及其使用者(人)就分別相當於上述定義中原子操作的執行執行緒和其他執行緒。

 

可見性:

在多執行緒環境下,一個執行緒對某個共享變數進行更新之後,後續訪問該變數的執行緒可能無法立刻讀取到這個更新的結果,甚至永遠也無法讀取到這個更新的結果。這就是執行緒安全問題的另外一個表現形式:可見性(Visibility)。

如果一個執行緒對某個共享變數進行更新之後,後續訪問該變數的執行緒可以讀取到該更新的結果,那麼我們就稱這個執行緒對該共享變數的更新對其他執行緒可見,否則我們就稱這個執行緒對該共享變數的更新對其他執行緒不可見。可見性就是指一個執行緒對共享變數的更新的結果對於讀取相應共享變數的執行緒而言是否可見的問題。多執行緒程式在可見性方面存在問題意味著某些執行緒讀取到了舊資料(Stale Data),而這可能導致程式出現我們所不期望的結果。

 

 如上圖所示,執行緒1修改X變數,是在自己工作記憶體中進行修改的,並未及時重新整理到主記憶體中,如果這時候執行緒2去讀取主記憶體中的資料X讀取到的還是0,但實際上X已經被修改成1了,這就是執行緒可見性有可能出現的問題。我們可以使用synchronized關鍵字來解決執行緒可見性問題。

 

有序性:

有序性(Ordering)指在什麼情況下一個處理器上執行的一個執行緒所執行的記憶體訪問操作在另外一個處理器上執行的其他執行緒看來是亂序的(Out of order)。所謂亂序,是指記憶體訪問操作的順序看起來像是發生了變化。

public class Singleton {
    private Singleton() { }
    private volatile static Singleton instance;
    public Singleton getInstance(){
        if(instance==null){
            synchronized (Singleton.class){
                if(instance==null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

  

上面程式碼中的instance=new Person(),這條語句實際上包含了三步操作

  1. 分配物件的記憶體空間;
  2. 初始化物件;
  3. 設定instance指向剛分配的記憶體地址

由於重排序的原因,可能會出現以下執行順序

 

 如果2和3進行了重排序的話,執行緒B進行判斷if(instance==null)時就會為true,而實際上這個instance並沒有初始化成功,顯而易見對執行緒B來說之後的操作就會出錯。我們可以使用volatile關鍵字來解決執行緒有序性問題

 

示例:

下面我們來看兩個執行緒安全的例子:

(1)、不共享資料的情況

    

class Mythread extends Thread{
	
	private int count=5;
	public Mythread(String name) {
		this.setName(name);
	}
	
	@Override
	public void run() {
		while(count>0) {
			count--;
			System.out.println("由 "+this.currentThread().getName()+" 計算,count="+count);
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		
		/**
		 * 下面建立了三個執行緒A,B,C
		 */
		Mythread A=new Mythread("A");
		Mythread B=new Mythread("B");
		Mythread C=new Mythread("C");
		A.start();
		B.start();
		C.start();
		
	}
	
}

  

執行結果:

由 B 計算,count=4
由 A 計算,count=4
由 C 計算,count=4
由 A 計算,count=3
由 A 計算,count=2
由 B 計算,count=3
由 A 計算,count=1
由 C 計算,count=3
由 A 計算,count=0
由 B 計算,count=2
由 C 計算,count=2
由 B 計算,count=1
由 C 計算,count=1
由 C 計算,count=0
由 B 計算,count=0

  

由結果可以看出,一共建立了三個執行緒,每個執行緒都有各自的count變數,自己減少自己的count變數的值。這樣的情況就是不共享變數,不會發生執行緒安全問題。

 

(2)、共享資料的情況:

 

 

 

 

class Mythread extends Thread{
	
	private int count=3;
	
	@Override
	public void run() {
		count--;
		System.out.println("由 "+this.currentThread().getName()+" 計算,count="+count);
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		//A,B,C三個執行緒共享一個變數
		Mythread thread=new Mythread();
		Thread A=new Thread(thread,"A");
		Thread B=new Thread(thread,"B");
		Thread C=new Thread(thread,"C");
		A.start();
		B.start();
		C.start();
	}
	
}

  

執行結果:注意,這裡的結果不一定是這樣,也有可能是其他

由 B 計算,count=0
由 A 計算,count=1
由 C 計算,count=0

  

由結果我們可以知道,B和C計算的值都為0,說明B和C對count進行了同樣的處理,產生了“非執行緒安全問題”。與我們想要的結果不同,我們希望值是依次遞減的。

在JVM中,實現count--實際上一共需要三步:

  1. 取得原有的count值
  2. 計算count-1
  3. 對count進行賦值

在這三步中如果有多個執行緒同時訪問就可能會出現非執行緒安全問題。假設A先執行來到第一步,獲取count值為3,然後進行減一操作,A執行完後,此時count的值為2;B和C同時取得count值為2,然後同時減一,此時count值為0,因為B和C都執行了減一操作,最後賦值的時候B和C都為0

那麼我們可不可以給執行緒設定一道“安檢”,類似於過機場安檢,每個人需要排隊進行安檢,不許搶先進行安檢。

我們將Mythread的run()方法改成如下:

public synchronized void run() {
	count--;
	System.out.println("由 "+this.currentThread().getName()+" 計算,count="+count);
}

  

現在執行結果如下:

由 A 計算,count=2
由 B 計算,count=1
由 C 計算,count=0

  

我們在上面的run()方法上面加上了synchronized關鍵字,現在的結果就是正確的了。下面來詳細介紹synchronized關鍵字。 

 

 

 

synchronized關鍵字:

一、synchronized同步方法:

在上面的例子中我們已經初步瞭解了“執行緒安全”與“非執行緒安全”相關的技術點,它們是學習多執行緒技術時一定會遇到的經典問題。“非執行緒安全”其實會在多個執行緒對同一個物件中的例項變數進行併發訪問時發生,產生的後果就是“髒讀”,也就是取到的資料其實是被更改過的。而“執行緒安全”就是以獲得的例項變數的值是經過同步處理的,不會出現髒讀的現象。

 

1、方法內的變數為執行緒安全的:

非執行緒安全問題存在於“例項變數”中,如果是方法內部的私有變數,則不存在非執行緒安全問題。

class NameTest{
	public void add(String name) {
		try {
			int  num=0;
			if(name.equals("a")) {
				num=100;
				System.out.println("a set over!");
				Thread.sleep(2000);
			}else {
				num=200;
				System.out.println("b set over");
			}
			System.out.println(name+" num="+num);
			
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
}


class ThreadA extends Thread{
	
	private NameTest nA;
	public ThreadA(NameTest nA) {
		this.nA=nA;
	}
	
	@Override
	public void run() {
		nA.add("a");
	}
	
}

class ThreadB extends Thread{
	
	private NameTest nB;
	public ThreadB(NameTest nB) {
		this.nB=nB;
	}
	
	@Override
	public void run() {
		nB.add("b");
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		NameTest n=new NameTest();
		ThreadA aThreadA=new ThreadA(n);
		aThreadA.start();
		ThreadB bThreadB=new ThreadB(n);
		bThreadB.start();
	}
	
}

  

執行結果:

a set over!
b set over
b num=200
a num=100

  

結果顯示,a num=100,b num=200;說明兩個執行緒之間並未發生非執行緒安全問題,因為他們操作都是之間內部的變數。

 

2、例項變數非執行緒安全:


還是上面的例子,我們只改一行程式碼,將NameTest修改如下,其他程式碼保持不變:

class NameTest{
	//將num修改為全域性變數
	private int num=0;
	
	public void add(String name) {
		try {
			
			if(name.equals("a")) {
				num=100;
				System.out.println("a set over!");
				Thread.sleep(2000);
			}else {
				num=200;
				System.out.println("b set over");
			}
			System.out.println(name+" num="+num);
			
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
}

  

現在我們來看一下執行結果:

a set over!
b set over
b num=200
a num=200

  

現在我們可以看到a和b的num值都為200,發生了執行緒安全問題。

這時我們只需要在add方法上加上 synchronized 關鍵字即可(public synchronized void add),此時的執行結果就正確了。

執行結果:
a set over!
a num=100
b set over
b num=200

  

實驗結論:在兩個執行緒訪問同一個物件中的同步方法時一定是執行緒安全的。本實驗由於是同步訪問,b必須等待a執行完了才可以執行,所以先打印出a,然後打印出b。

 

3、多個物件多個鎖:

class NameTest{
	//將num修改為全域性變數
	private int num=0;
	
	public synchronized void add(String name) {
		try {
			
			if(name.equals("a")) {
				num=100;
				System.out.println("a set over!");
				Thread.sleep(2000);
			}else {
				num=200;
				System.out.println("b set over");
			}
			System.out.println(name+" num="+num);
			
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
}


class ThreadA extends Thread{
	
	private NameTest nA;
	public ThreadA(NameTest nA) {
		this.nA=nA;
	}
	
	@Override
	public void run() {
		nA.add("a");
	}
	
}

class ThreadB extends Thread{
	
	private NameTest nB;
	public ThreadB(NameTest nB) {
		this.nB=nB;
	}
	
	@Override
	public void run() {
		nB.add("b");
	}
	
}



public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		//下面建立了兩個NameTest物件
		NameTest n1=new NameTest();
		NameTest n2=new NameTest();
		ThreadA a=new ThreadA(n1);
		a.start();
		ThreadB b=new ThreadB(n2);
		b.start();
	}
	
}

  

執行結果:

a set over!
b set over
b num=200
a num=100

  

有的讀者看到這裡可能有疑問了,add()方法不是已經用synchronized修飾了嗎?而synchronized修飾的方法時同步方法,那麼因為先把a執行完畢,再執行b,為什麼會實現這樣的結果?

請讀者仔細閱讀程式碼,上一個實驗中我們只建立了一個NameTest物件,而這個實驗中我們建立了兩個NameTest物件,兩個執行緒操作的是同一個類的不同例項,所以會產生這樣的結果。synchronized實現同步其實是給需要同步執行的程式碼加上了鎖,當A執行緒獲取到這把鎖後,其他執行緒便不能獲得到這個鎖,直到A執行完畢釋放鎖後,其他執行緒才可以去擁有這個鎖然後執行相應程式碼。

關鍵字synchronized取得的鎖都是物件鎖,而不是把一段程式碼或方法(函式)當作鎖,所以在上面的示例中,哪個執行緒先執行帶synchronized關鍵字的方法,哪個執行緒就持有該方法所屬物件的鎖Lock,那麼其他執行緒只能處於等待狀態。前提是多個執行緒訪問的是同一個物件。但如果多個執行緒訪問多個物件,則JVM便會建立多個鎖,上面的示例就是建立了兩個鎖。

 

4、synchronized方法與物件鎖:

上面的示例中我們初步接觸了鎖,下面我們來深入瞭解一下synchronized與鎖的關係。

class MyObject{
	public synchronized void methodA() {
		try {
			System.out.println("begin methon threadName= "+Thread.currentThread().getName());
			Thread.sleep(5000);
			System.out.println("end");
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	
	public void methodB(){
		
		try {
			System.out.println("begin methon threadName= "+Thread.currentThread().getName()+" begin time= "+System.currentTimeMillis());
			Thread.sleep(5000);
			System.out.println("end");
		}catch(Exception e) {
			e.printStackTrace();
		}
		
	}
}

//執行緒A
class ThreadA extends Thread{
	private MyObject object;
	public ThreadA(MyObject object) {
		this.object=object;
	}
	
	@Override
	public void run() {
		object.methodA();
	}
}


class ThreadB extends Thread{
	private MyObject object;
	public ThreadB(MyObject object) {
		this.object=object;
	}
	
	@Override
	public void run() {
		object.methodB();
	}
}




public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		
		MyObject object=new MyObject();
		ThreadA a=new ThreadA(object);
		a.setName("A");
		ThreadB b=new ThreadB(object);
		b.setName("B");
		a.start();
		b.start();
		
	}
	
}

  

上面程式碼中MyObject類中共有兩個方法methodA()和methodB()方法,其中methodA()方法加上了synchronized關鍵字,是同步方法,methodB()這是普通方法;現在有兩個執行緒類A和B,A執行緒中run方法呼叫的是MyObject類中的methodA()方法,B執行緒中run()方法呼叫的是MyObject類的methodB()方法,main()方法中建立了兩個執行緒,名稱為A和B,現在我們來看一下執行結果:

begin methon threadName= A
begin methon threadName= B begin time= 1574825571200
end
end

  

從結果可以看到,兩個執行緒並非同步執行。因為methodB()方法並非同步方法,所以當A執行緒啟動後,B執行緒依然可以呼叫methodB()方法。

下面我們將methodB()也加上synchronized關鍵字,再次執行看一下結果:

begin methon threadName= A
end
begin methon threadName= B begin time= 1574825932133
end

  

從這次的執行結果中我們可以清楚的看到A和B同步執行,那麼這是為什麼呢?執行緒A和B呼叫的不是同一個方法啊?我們再來仔細研究一下建立執行緒的程式碼:

MyObject object=new MyObject();
ThreadA a=new ThreadA(object);
a.setName("A");
ThreadB b=new ThreadB(object);
b.setName("B");
a.start();
b.start();

  

首先我們建立了一個MyObject類的例項,然後建立了執行緒A和B的例項,我們可以看到建立執行緒傳入的引數是相同的,都是object,所以這兩個執行緒執行時持有的是同一把鎖object,所以我們看到的執行結果是同步的。

假如現在我們把建立執行緒的程式碼改成下面這樣,大家思考結果會是什麼?

ThreadA a=new ThreadA(new MyObject());
a.setName("A");
ThreadB b=new ThreadB(new MyObject());
b.setName("B");
a.start();
b.start();

  

對,結果是不同步的,因為執行緒A和B用的不是同一把鎖

 

5、synchronized重入鎖:

“可重入鎖”的概念是:自己可以再次獲取自己的內部鎖。比如有一個執行緒獲得了該物件的鎖還沒有釋放,當其再次想要獲取這個鎖時依然可以獲取,如果是不可重入鎖的話,就會造成死鎖。

關鍵字synchronized擁有鎖重入的功能,也就是在使用synchronized的時候,當一個執行緒得到一個物件鎖後,該執行緒再次此物件的鎖依然是可以得到該物件的鎖。

class Service{
	public synchronized void service1() {
		System.out.println("service1");
		service2();
	}
	
	public synchronized void service2() {
		System.out.println("service2");
		service3();
	}
	
	public synchronized void  service3() {
		System.out.println("service3");
	}
}

class MyThread extends Thread{
	@Override
	public void run() {
		Service service=new Service();
		service.service1();
	}
}




public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		MyThread thread=new MyThread();
		thread.start();
	}
	
}

  

執行結果:

service1
service2
service3

  

二、synchronized同步程式碼塊:

synchronized同步程式碼塊實現的功能其實和synchronized同步方法是一樣的,但是在使用synchronized宣告方法時會有一些弊端,比如A執行緒呼叫同步方法執行很長時間,那麼B執行緒就必須等待很長時間,這樣效率就很低。

synchronized同步程式碼塊就可以將需要進行同步的程式碼放入同步程式碼塊中,而其他執行緒安全的程式碼則放到程式碼塊之外執行,這樣就可以提升效率,下面我們來看一段程式碼:

 

 1、同步方法的弊端:

class Commonutils{
	public static long beginTime1;
	public static long endTime1;
	public static long beginTime2;
	public static long endTime2;
	
}


//處理任務類
class Task{
	private String getData1;
	private String getData2;
	public synchronized void doLongTimeTask() {
		try {
			System.out.println("begin task");
			Thread.sleep(3000);
			getData1="長時間處理任務後從遠端返回的值1 threadName="+Thread.currentThread().getName();
			getData2="長時間處理任務後從遠端返回的值2 threadName="+Thread.currentThread().getName();
			System.out.println(getData1);
			System.out.println(getData2);
			System.out.println("end task");
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
}


class MyThread1 extends Thread{
	private Task task;
	public MyThread1(Task task) {
		this.task=task;
	}
	
	@Override
	public void run() {
		Commonutils.beginTime1=System.currentTimeMillis();
		task.doLongTimeTask();
		Commonutils.endTime1=System.currentTimeMillis();
	}
}

class MyThread2 extends Thread{
	private Task task;
	
	public MyThread2(Task task) {
		this.task=task;
	}
	
	@Override
	public void run() {
		Commonutils.beginTime2=System.currentTimeMillis();
		task.doLongTimeTask();
		Commonutils.endTime2=System.currentTimeMillis();
	}
}




public class Test01 {

	public static void main(String[] args) {
		Task task=new Task();
		MyThread1 thread1=new MyThread1(task);
		thread1.start();
		MyThread2 thread2=new MyThread2(task);
		thread2.start();
		try {
			Thread.sleep(10000);
		}catch(Exception e) {
			e.printStackTrace();
		}
		long beginTime=Commonutils.beginTime1;
		if(Commonutils.beginTime2<Commonutils.beginTime1) {
			beginTime=Commonutils.beginTime2;
		}
		long endTime=Commonutils.endTime1;
		if(Commonutils.endTime2>Commonutils.endTime1) {
			beginTime=Commonutils.endTime2;
		}
		System.out.println("耗時:"+((endTime-beginTime)/1000));
		
		
		
	}
	
}

  

執行結果:

begin task
長時間處理任務後從遠端返回的值1 threadName=Thread-0
長時間處理任務後從遠端返回的值2 threadName=Thread-0
end task
begin task
長時間處理任務後從遠端返回的值1 threadName=Thread-1
長時間處理任務後從遠端返回的值2 threadName=Thread-1
end task
耗時:6

  

上面的程式碼中Task類的doLongTimeTask()方法模擬了一個長時間的操作,MyThread1和MyThread2類的run()方法分別執行了這個任務,由於doLongTimeTask()方法時同步方法,所以當兩個執行緒啟動後必須按照先後順序去執行,時間較長,效率較低。我們想要的結果就是getData1和getData2賦值的過程不會出現執行緒安全問題,因此我們可以考慮使用synchronized同步程式碼塊來解決這個問題。

 

2、synchronized同步程式碼塊的使用:

當兩個併發執行緒訪問同一個物件中的同步程式碼塊時,一段時間內只能有一個執行緒被執行,另一個執行緒必須等待當前執行緒執行完這個程式碼塊後才能執行該程式碼塊。

我們現在將上面程式碼中Task類的程式碼修改為用synchronized同步程式碼塊實現,其他程式碼不變:

class Task{
	private String getData1;
	private String getData2;
	public void doLongTimeTask() {
		try {
			System.out.println("begin task");
			Thread.sleep(3000);
			String p1="長時間處理任務後從遠端返回的值1 threadName="+Thread.currentThread().getName();
			String p2="長時間處理任務後從遠端返回的值2 threadName="+Thread.currentThread().getName();
			synchronized(this) {
				getData1=p1;
				getData2=p2;
			}
			System.out.println(getData1);
			System.out.println(getData2);
			System.out.println("end task");
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
}

  

將getData1和getData2賦值的過程用同步程式碼塊包裹起來,這樣整個賦值過程就是執行緒安全的。

執行結果:

begin task
begin task
長時間處理任務後從遠端返回的值1 threadName=Thread-0
長時間處理任務後從遠端返回的值2 threadName=Thread-1
end task
長時間處理任務後從遠端返回的值1 threadName=Thread-1
長時間處理任務後從遠端返回的值2 threadName=Thread-1
end task
耗時:3

  

從執行結果可以看出,使用同步程式碼塊的時間明顯比較短。

 

3、synchronized程式碼塊之間的同步性:

synchronized程式碼塊和synchronized方法一樣,預設使用的都是同一把鎖,所以兩個同步程式碼塊之間也是同步的。我們來看如下程式碼:

class ObjectService{
	public void serviceA() {
		try {
			//這裡使用synchronized同步程式碼塊實現執行緒安全
			synchronized(this) {
				System.out.println("A begin time= "+System.currentTimeMillis());
				Thread.sleep(2000);
				System.out.println("A end time= "+System.currentTimeMillis());
			}
			
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	
	public void serviceB() {
		synchronized(this) {
			System.out.println("B begin time= "+System.currentTimeMillis());
			System.out.println("B end time= "+System.currentTimeMillis());
		}
	}
}

class ThreadA extends Thread{
	private ObjectService service;
	public ThreadA(ObjectService service) {
		this.service=service;
	}
	
	@Override
	public void run() {
		service.serviceA();
	}
}

class ThreadB extends Thread{
	private ObjectService service;
	public ThreadB(ObjectService service) {
		this.service=service;
	}
	
	@Override
	public void run() {
		service.serviceB();
	}
}




public class Test {
	
	public static void main(String[] args) {
		ObjectService service=new ObjectService();
		ThreadA a=new ThreadA(service);
		a.setName("a");
		a.start();
		ThreadB b=new ThreadB(service);
		b.setName("b");
		b.start();
	}

}

  

ObjectService中共有兩個方法,serviceA和serviceB,兩個方法中都是用了synchronized同步程式碼塊,ThreadA和ThreadB是兩個執行緒,ThreadA執行緒中執行了serviceA()方法,ThreadB執行緒中執行了serviceB()方法,我們現在來觀察結果是否是同步的:

執行結果:

A begin time= 1574837894142
A end time= 1574837896151
B begin time= 1574837896151
B end time= 1574837896151

  

從結果我們可以看到,程式碼是同步執行的,先執行的serviceA()方法,再執行的serviceB()方法,由此可見synchronized(this)同步程式碼塊持有的是同一個鎖

 

4、synchronized(this)同步程式碼塊中的this:

細心的讀者可能已經發現了,在上面的synchronized同步程式碼塊中都加上了一個this,synchronized(this),那麼這個this代表的是什麼呢?

學過java的同學應該都知道,this代表的是當前物件。和synchronized同步方法一樣,synchronized(this)同步程式碼塊也是鎖定當前物件的。那麼這裡的可以使用其他物件替代this呢?當然可以。

Java支援使用“任意物件”作為“物件監視器”來實現同步功能,這個任意物件大多數是例項變數及方法的引數,使用格式為synchronized(非this物件)。

class Service{
	private String username;
	private String password;
	private String anyString=new String();
	public void setUsernamePassword(String username,String password) {
		try {
			synchronized(anyString) {
				System.out.println("執行緒名稱為:"+Thread.currentThread().getName()+"在"+System.currentTimeMillis()+"進入程式碼塊");
				Thread.sleep(3000);
				System.out.println("執行緒名稱為:"+Thread.currentThread().getName()+"在"+System.currentTimeMillis()+"離開程式碼塊");
			}
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
}


class ThreadA extends Thread{
	private Service service;
	public ThreadA(Service service) {
		this.service=service;
	}
	
	@Override
	public void run() {
		service.setUsernamePassword("a","aa");
	}
}

class ThreadB extends Thread{
	private Service service;
	public ThreadB(Service service) {
		this.service=service;
	}
	
	@Override
	public void run() {
		service.setUsernamePassword("b","bb");
	}
}




public class Test {
	
	public static void main(String[] args) {
		Service service=new Service();
		ThreadA a=new ThreadA(service);
		a.setName("A");	
		a.start();
		ThreadB b=new ThreadB(service);
		b.setName("B");
		b.start();
	}

}

  

執行結果:

執行緒名稱為:A在1574839242431進入程式碼塊
執行緒名稱為:A在1574839245431離開程式碼塊
執行緒名稱為:B在1574839245431進入程式碼塊
執行緒名稱為:B在1574839248438離開程式碼塊

  

這段程式碼中的同步程式碼塊我們使用的是String物件作為鎖,並沒有使用,也可以實現同步。這樣做還有一個好處,就是使用其他物件作為鎖與使用this鎖之間是非同步的,不與其他的this鎖爭搶資源,可以提升效率。

小結:

  1. 當多個執行緒同時執行synchronized(x){}同步程式碼塊時呈現的是同步
  2. 當其他執行緒執行x物件中的synchronized同步方法呈線同步效果
  3. 當其他執行緒執行x物件方法裡面的synchronized(this)程式碼塊也呈現同步效果

 

volatile關鍵字:

一旦一個共享變數(類的成員變數、類的靜態成員變數)被volatile修飾之後,那麼就具備了兩層語義:

  1)保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。

  2)禁止進行指令重排序。

下面看一段程式碼:

//執行緒1
boolean stop = false;
while(!stop){
    doSomething();
}
 
//執行緒2
stop = true;

  

這段程式碼一開始執行緒1開始執行,stop的值為false,接著執行緒2執行將stop的值修改為true。在前面執行緒安全問題那裡介紹了執行緒可見性,那麼當執行緒2更改了stop變數的值之後,但是還沒來得及寫入主存當中,執行緒2轉去做其他事情了,那麼執行緒1由於不知道執行緒2對stop變數的更改,因此還會一直迴圈下去。

這時我們可以使用volatile關鍵字來解決可見性問題:

第一:使用volatile關鍵字會強制將修改的值立即寫入主存;

第二:使用volatile關鍵字的話,當執行緒2進行修改時,會導致執行緒1的工作記憶體中快取變數stop的快取行無效(反映到硬體層的話,就是CPU的L1或者L2快取中對應的快取行無效);

第三:由於執行緒1的工作記憶體中快取變數stop的快取行無效,所以執行緒1再次讀取變數stop的值時會去主存讀取。

加上volatile關鍵字後,執行緒2修改變數後會強制將stop的值重新整理到主記憶體中,執行緒1也會強制去主記憶體中讀取資料,這樣就不會出現可見性問題了。

 

volatile只能解決執行緒可見性問題,並不能解決執行緒原子性問題。

現在有這樣一個賦值操作:

volatile Map aMap=new HashMap();

這個賦值操作可以分解為以下幾步:

objRef=allocate(HaspMap.class);//子操作1:分配物件所需儲存空間

invokeConstructor(objRef);//子操作2:初始化objRef引用的物件

aMap=objRef;//子操作3:將物件的引用寫入變數

雖然volatile關鍵字僅保障其中的子操作③是一個原子操作,但是由於子操作①和子操作②僅涉及區域性變數而未涉及共享變數,因此對變數aMap的賦值操作仍然是一個原子操作。

&n