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的賦值操作仍然是一個原子操作。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM