當多個線程同時對同一個對象中的實例變量進行并發訪問時可能會產生線程安全問題。產生的后果就是”臟讀”,即收到的數據其實是被更改過的。 如果訪問的是方法中的變量,則不存在”非線程安全”問題 可以通過以下幾種方式來解決,在對對象及變量并發訪問過程中的安全問題 1. synchronize同步方法 2. 同步語句塊 3. volatile關鍵字
如果兩個線程同時訪問同一個對象的方法,不加控制,會出現意外的結果。通過用synchronize修飾方法,可以取得對象鎖,那個線程先訪問就先持有對象鎖,其余的線程只能等待。 首先是沒有用synchronize修飾的情況
public class HasSelfPRivateNum { private int num = 0; public void addI(String username){ try{ if (username.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(username + " num=" + num); }catch (Exception e){ e.printStackTrace(); } }}public class SelfPrivateThreadA extends Thread{ private HasSelfPrivateNum num; public SelfPrivateThreadA(HasSelfPrivateNum num){ this.num = num; } @Override public void run() { super.run(); num.addI("a"); }}public class SelfPrivateThreadB extends Thread{ private HasSelfPrivateNum num; public SelfPrivateThreadB(HasSelfPrivateNum num){ this.num = num; } @Override public void run() { super.run(); num.addI("b"); }}測試類
public class HasSelfPrivateNumTest extends TestCase { public void testAddI() throws Exception { HasSelfPrivateNum numA = new HasSelfPrivateNum(); //HasSelfPrivateNum numB = new HasSelfPrivateNum(); SelfPrivateThreadA threadA = new SelfPrivateThreadA(numA); threadA.start(); SelfPrivateThreadB threadB = new SelfPrivateThreadB(numA); threadB.start(); Thread.sleep(1000 * 3); }}預期結果應該是a num=100 b num=200 但是實際結果如下:
a set over!b set over!b num=200a num=200用synchronize修飾方法addI()方法之后結果如下:
a set over!a num=100b set over!b num=200多個對象多個鎖
取消測試類中注釋的代碼,因為2個線程訪問的是2個不同的對象,2個線程仍然是異步執行。
synchronize修飾方法添加的是對象鎖
當2個線程同時訪問一個類中,2個不同的用synchronize修飾的方法時,有一個方法被訪問,另一個仍舊不能訪問。因為synchronize修飾方法添加的是對象鎖
如果數據的設置和獲取方法不是同步的,可以在任意時刻進行調用,可能會出現”臟讀”情況,可以通過在設置和獲取方法之前用synchronize修飾解決
synchronize擁有鎖重入的功能 鎖重入:即當一個線程獲得一個對象鎖之后,再次請求該對象可以再次得到該對象的鎖。即synchronize方法/塊的內部調用本類的其他synchronize方法/塊時,可以永遠得到鎖的。 子類繼承父類的時候,子類可以通過”可重入鎖”調用父類的同步方法
出現異常,鎖會自用釋放
同步不具有繼承性
對于上面的同步方法而言,其實是有些弊端的,如果同步方法是需要執行一個很長時間的任務,那么多線程在排隊處理同步方法時就會等待很久,但是一個方法中,其實并不是所有的代碼都需要同步處理的,只有可能會發生線程不安全的代碼才需要同步。這時,可以采用synchronized來修飾語句塊讓關鍵的代碼進行同步。用synchronized修飾同步塊,其格式如下:
synchronized(對象){ //語句塊 } 這里的對象,可以是當前類的對象this,也可以是任意的一個Object對象,或者間接繼承自Object的對象,只要保證synchronized修飾的對象被多線程訪問的是同一個,而不是每次調用方法的時候都是新生成就就可以。但是特別注意String對象,因為JVM有String常量池的原因,所以相同內容的字符串實際上就是同一個對象,在用同步語句塊的時候盡可能不用String。 下面,看一個例子來說明同步語句塊的用法和與同步方法的區別:
public class LongTimeTask { private String getData1; private String getData2; public void doLongTimeTask(){ try{ System.out.println("begin task"); Thread.sleep(3000); String privateGetData1 = "長時間處理任務后從遠程返回的值 1 threadName=" + Thread.currentThread().getName(); String privateGetData2 = "長時間處理任務后從遠程返回的值 2 threadName=" + Thread.currentThread().getName(); synchronized (this){ getData1 = privateGetData1; getData2 = privateGetData2; } System.out.println(getData1); System.out.println(getData2); System.out.println("end task"); }catch (InterruptedException e){ e.printStackTrace(); } }}public class LongTimeServiceThreadA extends Thread{ private LongTimeTask task; public LongTimeServiceThreadA(LongTimeTask task){ super(); this.task = task; } @Override public void run() { super.run(); CommonUtils.beginTime1 = System.currentTimeMillis(); task.doLongTimeTask(); CommonUtils.endTime1 = System.currentTimeMillis(); }}public class LongTimeServiceThreadB extends Thread{ private LongTimeTask task; public LongTimeServiceThreadB(LongTimeTask task){ super(); this.task = task; } @Override public void run() { super.run(); CommonUtils.beginTime2 = System.currentTimeMillis(); task.doLongTimeTask(); CommonUtils.endTime2 = System.currentTimeMillis(); }}測試類:
public class LongTimeServiceThreadATest extends TestCase { public void testRun() throws Exception { LongTimeTask task = new LongTimeTask(); LongTimeServiceThreadA threadA = new LongTimeServiceThreadA(task); threadA.start(); LongTimeServiceThreadB threadB = new LongTimeServiceThreadB(task); threadB.start(); try{ Thread.sleep(1000 * 10); }catch (InterruptedException e){ e.printStackTrace(); } long beginTime = CommonUtils.beginTime1; if (CommonUtils.beginTime2 < CommonUtils.beginTime1){ beginTime = CommonUtils.beginTime2; } long endTime = CommonUtils.endTime1; if (CommonUtils.endTime2 < CommonUtils.endTime1){ endTime = CommonUtils.endTime2; } System.out.println("耗時:" + ((endTime - beginTime) / 1000)); Thread.sleep(1000 * 20); }}結果如下:
begin taskbegin task長時間處理任務后從遠程返回的值 1 threadName=Thread-1長時間處理任務后從遠程返回的值 2 threadName=Thread-1end task長時間處理任務后從遠程返回的值 1 threadName=Thread-1長時間處理任務后從遠程返回的值 2 threadName=Thread-1end task耗時:3兩個線程并發處理耗時任務只用了3s, 因為只在賦值的時候進行同步處理,同步語句塊以外的部分都是多個線程異步處理的。 下面,說一下同步語句塊的一些特性:
當多個線程同時執行synchronized(x){}同步代碼塊時呈同步效果。當其他線程執行x對象中的synchronized同步方法時呈同步效果。當其他線程執行x對象中的synchronized(this)代碼塊時也呈現同步效果。細說一下每個特性,第一個特性上面的例子已經闡述了,就不多說了。第二個特性,因為同步語句塊也是對象鎖,所有當對x加鎖的時候,x對象內的同步方法也呈現同步效果,當x為this的時候,該對象內的其他同步方法也要等待同步語句塊執行完,才能執行。第三個特性和上面x為this是不一樣的,第三個特性說的是,x對象中有一個方法,該方法中有一個synchronized(this)的語句塊的時候,也呈現同步效果。即A線程調用了對x加鎖的同步語句塊的方法,B線程在調用該x對象的synchronized(this)代碼塊是有先后的同步關系。
上面說同步語句塊比同步方法在某些方法中執行更有效率,同步語句塊還有一個優點,就是如果兩個方法都是同步方法,第一個方法無限在執行的時候,第二個方法就永遠不會被執行。這時可以對兩個方法做同步語句塊的處理,設置不同的鎖對象,則可以實現兩個方法異步執行。
對類加鎖的同步處理
和對象加鎖的同步處理一致,對類加鎖的方式也有兩種,一種是synchronized修飾靜態方法,另一種是使用synchronized(X.class)同步語句塊。在執行上看,和對象鎖一致都是同步執行的效果,但是和對象鎖卻有本質的不同,對對象加鎖是訪問同一個對象的時候成同步的狀態,不同的對象就不會。但是對類加鎖是用這個類的靜態方法都是呈現同步狀態。 下面,看這個例子:
public class StaticService { synchronized public static void printA(){ try{ System.out.println(" 線程名稱為:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 進入printA"); Thread.sleep(1000 * 3); System.out.println(" 線程名稱為:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 離開printA"); }catch (InterruptedException e){ e.printStackTrace(); } } synchronized public static void printB(){ System.out.println(" 線程名稱為:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 進入printB"); System.out.println(" 線程名稱為:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 離開printB"); } synchronized public void printC(){ System.out.println(" 線程名稱為:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 進入printC"); System.out.println(" 線程名稱為:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 離開printC"); }}測試類:
public class StaticServiceTest extends TestCase { public void testPrint() throws Exception{ new Thread(new Runnable() { public void run() { StaticService.printA(); } }).start(); new Thread(new Runnable() { public void run() { StaticService.printB(); } }).start(); new Thread(new Runnable() { public void run() { new StaticService().printC(); } }).start(); Thread.sleep(1000 * 3); }}結果如下:
線程名稱為:Thread-0 在 1487684345462 進入printA線程名稱為:Thread-2 在 1487684345462 進入printC線程名稱為:Thread-2 在 1487684345462 離開printC線程名稱為:Thread-0 在 1487684348465 離開printA線程名稱為:Thread-1 在 1487684348466 進入printB線程名稱為:Thread-1 在 1487684348466 離開printB很明顯的看出來,對類加鎖和對對象加鎖兩者方法是異步執行的,而對類加鎖的兩個方法是呈現同步執行。 其特性也和同步對象鎖一樣。
鎖對象鎖的是該對象的內存地址,其存儲的內容改變,并不會讓多線程并發的時候認為這是不同的鎖。所以改變鎖對象的內容,并不會同步失效。
主要作用是使變量在多個線程間可見
在多線程爭搶對象的時候,處理該對象的變量的方式是在主內存中讀取該變量的值到線程私有的內存中,然后對該變量做處理,處理后將值在寫入到主內存中。上面舉的例子,之所以出現結果與預期不一致都是因為線程自己將值復制到自己的私有棧后修改結果而不知道其他線程的修改結果。如果我們不用同步的話,我們就需要一個能保持可見的,知道其他線程修改結果的方法。JDK提供了volatile關鍵字,來保持可見性,關鍵字volatile的作用是強制從公共堆棧中取得變量的值,而不是從線程私有數據棧中取得變量值。但是該關鍵字并不能保證原子性。
volatile不支持原子性。 synchronize與volatile的比較: 1. volatile是線程同步的輕量級實現,所以性能比synchronize好。但是volatile只能修飾變量,synchronize可以修飾方法。 2. volatile不會發生阻塞,synchronize會出現阻塞 3. volatile保證數據可見性,不能保證原子性;synchronize可以保證原子性,也可以間接保證可見性,因為他會將私有內存和公共內存中的數據做同步。 4. volatile解決變量在多個線程之間的可見性,synchronize解決多個線程之間訪問資源的同步性。
原子操作:一個完整的操作,操作一旦開始就一直運行到結束
原子操作也不一定完全安全 因為有的情況下雖然方法雖然是原子的,但是方法和方法之間的調用卻不是原子的。仍然需要同步去解決問題。
新聞熱點
疑難解答