本次內容主要講synchronized、volatile和ThreadLocal。
1、synchronized內置鎖
線程開始運行,擁有自己的棧空間,就如同一個腳本一樣,按照既定的代碼一步一步地執行,直到終止。但是,每個運行中的線程,如果僅僅是孤立地運行,那么價值會很少,如果多個線程能夠相互配合完成工作,包括數據之間的共享、協同處理事情。這將會帶來巨大的價值。
Java支持多個線程同時訪問一個對象或者對象的成員變量,關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處於方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性,又稱為內置鎖機制。synchronized使用的3種情況:
(1)用在實例方法上,此時鎖住的是InstanceSyn這個類的實例對象,先來看正確的使用方式,每個線程持有的是同一個InstanceSyn實例。
public class InstanceSyn { private List<Integer> list = new ArrayList<>(); public synchronized void instance() { System.out.println(Thread.currentThread().getName() + " 調用instance()"); list.add(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 結束instance()"); } static class InstanceSynTest extends Thread { private InstanceSyn instanceSyn; public InstanceSynTest(InstanceSyn instanceSyn) { this.instanceSyn = instanceSyn; } public void run() { instanceSyn.instance(); } } public static void main(String[] args) { InstanceSyn instanceSyn = new InstanceSyn(); for (int i = 0; i < 3; i++) { Thread thread = new InstanceSynTest(instanceSyn); thread.start(); } } }
可以看到3個線程按同步方式對instance()方法進行訪問:
再看一下synchronized是怎么失效的,只是挪動了一行代碼,此時3個線程持有的是不同的InstanceSyn實例,導致同步失效。
import java.util.ArrayList; import java.util.List; public class InstanceSyn { private List<Integer> list = new ArrayList<>(); public synchronized void instance() { System.out.println(Thread.currentThread().getName() + " 調用instance()"); list.add(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 結束instance()"); } static class InstanceSynTest extends Thread { private InstanceSyn instanceSyn; public InstanceSynTest(InstanceSyn instanceSyn) { this.instanceSyn = instanceSyn; } public void run() { instanceSyn.instance(); } } public static void main(String[] args) { for (int i = 0; i < 3; i++) { InstanceSyn instanceSyn = new InstanceSyn(); Thread thread = new InstanceSynTest(instanceSyn); thread.start(); } } }
此時的輸出:
(2)用在代碼塊上
① synchronized(this),此時鎖住的是InstanceSyn這個類的實例對象,和synchronized用在實例方法上原理一樣,失效的情況也一樣,這里就不具體演示了。
import java.util.ArrayList; import java.util.List; public class InstanceSyn { private List<Integer> list = new ArrayList<>(); public void instance() { //do something synchronized (this) { System.out.println(Thread.currentThread().getName() + " 開始訪問同步塊"); list.add(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 結束訪問同步塊"); } //do something } static class InstanceSynTest extends Thread { private InstanceSyn instanceSyn; public InstanceSynTest(InstanceSyn instanceSyn) { this.instanceSyn = instanceSyn; } public void run() { instanceSyn.instance(); } } public static void main(String[] args) { InstanceSyn instanceSyn = new InstanceSyn(); for (int i = 0; i < 3; i++) { Thread thread = new InstanceSynTest(instanceSyn); thread.start(); } } }
② 鎖住一個靜態對象,由於靜態變量是屬於整個類,不屬於某個類的實例,全局唯一,所以就不會出現上面synchronized同步失效的情況。
import java.util.ArrayList; import java.util.List; public class InstanceSyn { private List<Integer> list = new ArrayList<>(); private static Object object = new Object(); public void instance() { //do something synchronized (object) { System.out.println(Thread.currentThread().getName() + " 開始訪問同步塊"); list.add(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 結束訪問同步塊"); } //do something } static class InstanceSynTest extends Thread { private InstanceSyn instanceSyn; public InstanceSynTest(InstanceSyn instanceSyn) { this.instanceSyn = instanceSyn; } public void run() { instanceSyn.instance(); } } public static void main(String[] args) { // InstanceSyn instanceSyn = new InstanceSyn(); for (int i = 0; i < 3; i++) { InstanceSyn instanceSyn = new InstanceSyn();//盡管每個線程持有的不是同一個實例對象,但是由於鎖住的是靜態對象,所以也可以正確執行同步操作 Thread thread = new InstanceSynTest(instanceSyn); thread.start(); } } }
盡管每個線程持有的不是同一個實例對象,但是由於鎖住的是靜態對象,所以也可以正確執行同步操作。代碼輸出如下:
(3)用在靜態方法上,此時鎖住的是InstanceSyn這個類的Class對象,InstanceSyn的Class對象全局唯一,所以就算每個線程持有的InstanceSyn不一樣,也可以進行同步訪問操作。
import java.util.ArrayList; import java.util.List; public class InstanceSyn { private static List<Integer> list = new ArrayList<>(); public static synchronized void instance() { System.out.println(Thread.currentThread().getName() + " 調用instance()"); list.add(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 結束instance()"); } static class InstanceSynTest extends Thread { private InstanceSyn instanceSyn; public InstanceSynTest(InstanceSyn instanceSyn) { this.instanceSyn = instanceSyn; } public void run() { instanceSyn.instance(); } } public static void main(String[] args) { // InstanceSyn instanceSyn = new InstanceSyn(); for (int i = 0; i < 3; i++) { InstanceSyn instanceSyn = new InstanceSyn();//盡管每個線程持有的不是同一個實例對象,但是由於鎖住的是類對象,所以也可以正確執行同步操作 Thread thread = new InstanceSynTest(instanceSyn); thread.start(); } } }
程序輸出:
注:實例鎖和類鎖
實例鎖是用於對象實例方法,類鎖是用於類的靜態方法上的。我們知道,類的對象實例可以有很多個,但是每個類只有一個Class對象,所以不同對象實例的對象鎖是互不干擾的。有一點必須注意的是,其實類鎖只是一個概念上的東西,並不是真實存在的,類鎖其實鎖的是每個類的對應的Class對象。類鎖和對象鎖之間也是互不干擾的,下面用代碼說明。
import java.util.ArrayList; import java.util.List; public class InstanceSyn { private static List<Integer> list = new ArrayList<>(); private static Object object = new Object(); public void instance() { synchronized (object) { System.out.println(Thread.currentThread().getName() + " 開始訪問instance同步塊"); list.add(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 開始訪問instance同步塊"); } } public void instance2() { synchronized (this) { System.out.println(Thread.currentThread().getName() + " 開始訪問instance2同步塊"); list.add(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 開始訪問instance2同步塊"); } } public static synchronized void instance3() { System.out.println(Thread.currentThread().getName() + " 開始訪問instance3同步塊"); list.add(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 開始訪問instance3同步塊"); } /** * 調用鎖靜態對象的方法 */ static class InstanceSynTest extends Thread { private InstanceSyn instanceSyn; public InstanceSynTest(InstanceSyn instanceSyn) { this.instanceSyn = instanceSyn; } public void run() { instanceSyn.instance(); } } /** * 調用鎖this的方法 */ static class InstanceSynTest2 extends Thread { private InstanceSyn instanceSyn; public InstanceSynTest2(InstanceSyn instanceSyn) { this.instanceSyn = instanceSyn; } public void run() { instanceSyn.instance2(); } } /** * 調用鎖類對象的方法 */ static class InstanceSynTest3 extends Thread { private InstanceSyn instanceSyn; public InstanceSynTest3(InstanceSyn instanceSyn) { this.instanceSyn = instanceSyn; } public void run() { instanceSyn.instance3(); } } public static void main(String[] args) { InstanceSyn instanceSyn = new InstanceSyn(); Thread thread1 = new InstanceSynTest(instanceSyn); Thread thread2 = new InstanceSynTest2(instanceSyn); Thread thread3 = new InstanceSynTest3(instanceSyn); thread1.start(); thread2.start(); thread3.start(); } }
從程序輸出可以看到,3個線程並沒有同步訪問。雖然這3個方法在同一個類中,但是由於3個方法鎖住的對象不一樣,所以他們之間互不干擾,不會進行同步訪問。
2、 volatile
volatile保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。volatile底層采用了MESI緩存一致性協議來實現。M代表修改modify,E代表獨占exclusive,S代表共享share,I代表失效invalid。下面畫圖進行說明。
E狀態:獨一份,且有效。在共享內存中有一個變量X,值是1。當只有一個線程把X讀取到自己的工作內存中時,X處於獨占狀態,僅被1個線程持有。
S狀態:每個線程工作內存中的變量值都是一樣的。
M和I狀態:一個線程修把X的值由1修改成5,是M狀態;另外一個線程通過CPU總線嗅探機制得知X的值已經改變,使自己工作內存中X的值失效,是I狀態。
下面用一段代碼來演示volatile的用法。
public class VolatileCase { private volatile static boolean ready = false; private static int number; private static class PrintThread extends Thread { @Override public void run() { System.out.println("PrintThread is running......."); while (!ready) { } System.out.println("number = " + number); } } public static void main(String[] args) { new PrintThread().start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } number = 51; ready = true; try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("main is ended!"); } }
從程序執行情況可以看出,主線程在執行3秒后,PrintThread線程得知ready狀態變成true,迅速退出循環。不加volatile關鍵字時,PrintThread線程無法得知ready狀態變成true,從而不會退出循環。
剛剛演示的是volatile修飾一個簡單變量,那么volatile來修飾一個復雜對象的時候又是什么樣的呢?我們通過代碼來測試,定義一個Entity類和一個測試類。
public class VolatileEntity {int first = 0; int first = 0; private static class VolatileEntityInstance { private static VolatileEntity instance = new VolatileEntity(); } public static VolatileEntity getInstance() { return VolatileEntityInstance.instance; } }
1 public class VolatileEntityTest { 2 private volatile static VolatileEntity volatileEntity = VolatileEntity.getInstance(); 3 4 public static void main(String args[]) { 5 //讀線程 6 new Thread(() -> { 7 int localValue = volatileEntity.first; 8 while (localValue < 3) { 9 if (volatileEntity.first != localValue) { 10 System.out.printf("first is update to [%d]\n", volatileEntity.first); 11 localValue = volatileEntity.first; 12 } 13 } 14 }, "read").start(); 15 16 //寫線程 17 new Thread(() -> { 18 int localValue = volatileEntity.first; 19 while (localValue < 3) { 20 System.out.printf("first will be changed to [%d]\n", ++localValue); 21 volatileEntity.first = localValue; 22 try { 23 Thread.sleep(1); 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 } 28 }, "write").start(); 29 } 30 }
運行代碼可以輸出以下信息:
可以看到first字段在一個線程發生改變時,另外一個線程可以檢測到它發生的變化。可以認為volatile修飾的對象,對象里面的每一個字段也被volatile修飾了。再看看使用volatile修飾數組的情況。
public class VolatileArray { static volatile int[] array = new int[]{0, 0}; public static void main(String args[]) { //讀線程 new Thread(() -> { int localValue = 0; while (true) { if (array[0] > localValue) { System.out.printf("array[0] is update to [%d]\n", array[0]); localValue = array[0]; } } }, "read").start(); //寫線程 new Thread(() -> { for (int i = 1; i <= 5; i++) { System.out.printf("array[0] will be changed to [%d]\n", i); array[0] = i; try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }, "write").start(); } }
程序輸出:
如果把修飾數組的volatile關鍵字去掉,再次執行程序輸出如下:
由此可以得出一個結論,volatile修飾的數組,可以看作是對其中每一個元素使用了volatile關鍵字。
注:volatile不能保證原子性,多個線程同時寫會造成數據不安全問題,下面使用例子說明。
public class VolatileNotSafe { private volatile long count = 0; public long getCount() { return count; } public void setCount(long count) { this.count = count; } public void increase() { count++; } //工作線程 private static class CountIncrease extends Thread { private VolatileNotSafe volatileNotSafe; public CountIncrease(VolatileNotSafe volatileNotSafe) { this.volatileNotSafe = volatileNotSafe; } @Override public void run() { for (int i = 0; i < 10000; i++) { volatileNotSafe.increase(); } } } public static void main(String[] args) throws InterruptedException { VolatileNotSafe volatileNotSafe = new VolatileNotSafe(); for (int i = 0; i < 5; i++) { CountIncrease counter = new CountIncrease(volatileNotSafe); counter.start(); } Thread.sleep(2000); System.out.println(volatileNotSafe.count); } }
程序說明:啟動5個線程對共享數據count進行一個累加操作,每個線程累加1萬次。線程安全情況下,count的輸出應該是50000。來看看這段代的輸出:
多次運行這段代碼,可以看到count的結果是小於等於50000,所以volatile不能保證數據在多個線程下同時寫的線程安全,具體原因后面單獨介紹。
3、ThreadLocal
ThreadLocal和synchronized都用於解決多線程並發訪問,但是他們之間有本質的區別。synchronized是利用鎖機制,使方法或代碼塊在同一時間只能由一個線程訪問,其他沒有搶到鎖的線程處於阻塞狀態。而ThreadLocal為每個線程都提供了變量的副本,使得每個線程在某一時間訪問到的是不同的對象,這樣就隔離了多個線程對數據的共享。看一下TheadLocal的實現,先上一張圖。
簡單的對圖做一個說明:在Thread這個類中,有一個ThreadLocalMap的成員變量,ThreadLocalMap這個類是ThreadLocal的一個內部類。ThreadLocalMap中有一個Entry數組用來保存數據,因為可能有多個變量需要線程隔離訪問。Entry這個類類似於map的key-value結構,key就是ThreadLocal,value是需要隔離訪問的變量。再通過源碼看看,看下ThreadLocal最常用方法:
public class ThreadLocal<T> { public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } protected T initialValue() { return null; } }
get()方法返回當前線程所對應的線程局部變量;set()方法設置當前線程的線程局部變量的值;remove()方法將當前線程局部變量的值刪除,目的是為了減少內存的占用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束后,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量並不是必須的操作,但它可以加快內存回收的速度。initialValue()方法返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,並且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。具體的實現原理在這里不詳細展開,后面單獨介紹。下面用代碼演示下ThreadLocal的用法:
public class UseThreadLocal { static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static class TestThread implements Runnable { int id; public TestThread(int id) { this.id = id; } public void run() { threadLocal.set("線程-" + id); System.out.println(Thread.currentThread().getName() + "的threadLocal :" + threadLocal.get()); threadLocal.remove(); } } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 3; i++) { new Thread(new TestThread(i)).start(); } Thread.sleep(5); } }
這段代碼的main()方法中啟動了3個線程,分別給threadLocal變量賦值,程序輸出的結果不一樣,可以看出每個線程對threadLocal變量是隔離訪問的。
注:使用ThreadLocal的坑
看下面代碼:
public class ThreadLocalUnSafe implements Runnable { public ThreadLocalUnSafe(Number number) { this.number = number; } public Number number; public static ThreadLocal<Number> threadLocal = new ThreadLocal<>(); public void run() { threadLocal.set(number); Number numberInner = threadLocal.get(); numberInner.setNumber(numberInner.getNumber() + 1);//每個線程計數加一 try { Thread.sleep(2); //休眠2毫秒,模擬實際業務 } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "=" + threadLocal.get().getNumber()); } public static void main(String[] args) { Number number = new Number(0); for (int i = 0; i < 5; i++) { new Thread(new ThreadLocalUnSafe(number)).start(); } try { Thread.sleep(10); //休眠10毫秒,保證5個線程全部啟動 } catch (Exception e) { e.printStackTrace(); } } private static class Number { private int number; public Number(int number) { this.number = number; } public int getNumber() { return number; } public void setNumber(int num) { this.number = num; } @Override public String toString() { return "Number [number=" + number + "]"; } } }
main()方法中啟動5個線程,每個線程對Number對象中的number字段加1,number默認為0,所以每個線程輸出的number應該是1。看看實際輸出:
納尼!!!怎么跟我想的不是一個東西~~~怎么全部變成5了。難道他們沒有獨自保存自己的Number副本嗎?為什么其他線程還是能夠修改這個值?仔細考察ThreadLocal的代碼,我們發現ThreadLocalMap中保存的其實是對象的一個引用,這樣的話,當有其他線程對這個引用指向的對象實例做修改時,其實也同時影響了所有的線程持有的對象引用所指向的同一個對象實例。這也就是為什么上面的程序為什么會輸出一樣的結果,5個線程中保存的是同一Number對象的引用,在線程睡眠的時候,其他線程將number變量進行了修改,而修改的對象Number的實例是同一份,因此它們最終輸出的結果是相同的。
想要上面的程序正常工作,其實也非常簡單,用法是讓每個線程的ThreadLocal持有不同的Number對象,使用剛剛提到的initialValue()方法即可,代碼如下:
public class ThreadLocalUnSafe implements Runnable { private static class Number { private int number; public Number(int number) { this.number = number; } public int getNumber() { return number; } public void setNumber(int num) { this.number = num; } @Override public String toString() { return "Number [number=" + number + "]"; } } //重寫initialValue()方法 public static ThreadLocal<ThreadLocalUnSafe.Number> threadLocal = new ThreadLocal<ThreadLocalUnSafe.Number>() { @Override protected ThreadLocalUnSafe.Number initialValue() { return new ThreadLocalUnSafe.Number(0); } }; public void run() { Number numberInner = threadLocal.get(); numberInner.setNumber(numberInner.getNumber() + 1);//每個線程計數加一 try { Thread.sleep(2); //休眠2毫秒,模擬實際業務 } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "=" + threadLocal.get().getNumber()); } public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(new ThreadLocalUnSafe()).start(); } try { Thread.sleep(10); //休眠10毫秒,保證5個線程全部啟動 } catch (Exception e) { e.printStackTrace(); } } }
執行代碼,輸出的是我們想要的結果,上面的坑成功填平。
線程之間的協作在下一篇文章中介紹,在閱讀過程中如發現描述有誤,請指出,謝謝。