首先需要了解String的內存模型,有常量池,堆,那麼何時訪問的是常量池,何時又是訪問的堆,需要提前瞭解,當然下文也會介紹,此處就不在過多說明;另外線程的內存模型也要了解一下,尤其是線程棧。接下來我們就來做一個簡單的實驗,代碼如下:
public class Test { public static void main(String[] args) throws InterruptedException { final Test1 test1 = new Test1(); CountDownLatch latch = new CountDownLatch(20); for(int i=0;i<20;i++){ new Thread(new Runnable() { @Override public void run() { try { String a = new String("1"); test1.add(a); } catch (InterruptedException e) { e.printStackTrace(); } latch.countDown(); } }).start(); } latch.await(); System.out.println(test1.a); } } class Test1{ public int a =0; void add(String i) throws InterruptedException { synchronized (i){ int c=a+1; Thread.sleep(200); a=c; } } }
從上面看,開啓20個線程,分別對a進行加1操作,覺得這段代碼的執行結果會是多少?是20嗎?顯然不是。此處new String會在內存的堆中單獨創建對象,每個線程都是各自的對象,然後在synchronize中對這個對象加鎖,其實是互不競爭的,所以最後的運行結果可能是1,也可能是其他。那麼如何修改才能得到結果20呢?那就是讓20個線程相互競爭,可以通過String的intern獲取常量池中相同內容的對象,然後對該對象加鎖,也就是說,只要內容相同,返回的對象就是同一個,然後用synchronize加鎖就可以成功,只需調整一下同步塊的代碼即可:
synchronized (i.intern())
此時獲取到結果就是20。
有的人可能會說,直接在方法上同步就可以了,當然這種方式也是可以的,不過性能肯定要差,主要體現在兩方面來說:
1、對方法加同步,那整個方法執行完成後才被其他線程執行,鎖的粒度太大了。
2、鎖的範圍太大了,此次測試只是將i設置成爲相同了,所以20個線程都對這個i競爭,如果i不同,方法上的同步也會造成20個線程競爭;而採用i.intern(),可以把鎖給分散開,範圍更小,性能就會更好。