一個程序,多個線程同時操作一個變量,給這個變量+1()。功能很簡單,可是怎么樣去實現呢?這其中涉及到了哪些問題?
最基礎想法
見代碼:

1 public class Test extends Thread { 2 public static int amount = 0; 3 4 public void run() { 5 amount++; 6 } 7 8 public static void main(String[] args) { 9 int num_thread = 100; 10 for (int i = 0; i < num_thread; i++) { 11 new Test().start(); 12 } 13 try { 14 Thread.sleep(3 * 1000); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } 18 System.out.println(amount); 19 } 20 }
輸出結果:
num_thread = 100時,結果=100;
num_thread = 1000時,結果=1000;
num_thread = 10000時,結果=9995;
num_thread = 1000000時,結果=999936;
程序判定為不安全,當線程數比較少的時候,因為線程是先后啟動的,所以看起來沒有影響,一旦線程數增大,弊端畢露無疑。其實還有一個更簡單看出問題的方法,線程運行時,不是給變量+1,而是+1000*1000,再來看結果:
num_thread = 10時,結果=5034021;——線程數很少,但是結果不是想要的結果。
總之說明,這樣的多線程不安全!amount++這個方法並不是原子性的!
升級想法1.0:用volatile修飾的變量,線程在每次使用變量的時候,都會讀取變量修改后的最新值。
見代碼:
1 public class Test extends Thread { 2 public static volatile int amount = 0;//只是這里的變量聲明為volatile修飾 3 4 public void run() { 5 int i = 0; 6 while (i < 1000 * 1000) { 8 amount++; 9 i++; 10 } 11 } 12 13 public static void main(String[] args) { 14 int num_thread = 10; 15 for (int i = 0; i < num_thread; i++) { 16 new Test().start(); 17 } 18 try { 19 Thread.sleep(1 * 1000); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 System.out.println(amount); 24 } 25 }
輸出結果:
num_thread = 10時,結果=2375833;——結果仍然不是想要的。
那問題出在哪里了呢?處在了對volatile修飾符的理解上。(參考博客:java中volatile關鍵字的含義)
volatile很容易被誤用,被誤用來進行原子性操作。
在 java 垃圾回收整理一文中,描述了jvm運行時刻內存的分配。其中有一個內存區域是jvm虛擬機棧,每一個線程運行時都有一個線程棧,線程棧保存了線程運行時候變量值信息。當線程訪問某一個對象時候值的時候,首先通過對象的引用找到對應在堆內存的變量的值,然后把堆內存變量的具體值load到線程本地內存中,建立一個變量副本,之后線程就不再和對象在堆內存變量值有任何關系,而是直接修改副本變量的值,在修改完之后的某一個時刻(線程退出之前),自動把線程變量副本的值回寫到對象在堆中變量。這樣在堆中的對象的值就產生變化了。下面一幅圖描述這種交互:
read and load ——從主存復制變量到當前工作內存
use and assign ——執行代碼,改變共享變量值
store and write ——用工作內存數據刷新主存相關內容
其中use and assign 可以多次出現,但是這一些操作並不是原子性,也就是 在read load之后,如果主內存count變量發生修改之后,線程工作內存中的值由於已經加載,不會產生對應的變化,所以計算出來的結果會和預期不一樣。
對於volatile修飾的變量,jvm虛擬機只是保證從主內存加載到線程工作內存的值是最新的。
這項問題搞清楚之后可以繼續想法了。
升級想法3.0:同步代碼塊——通過 synchronized 關鍵字,所有加上synchronized 的塊語句,在多線程訪問的時候,同一時刻只能有一個線程能夠用synchronized 修飾的方法或者代碼塊。
java為了解決線程並發的問題,在語言內部引入了 同步塊 和 volatile 關鍵字機制。
對於synchronized的使用,又有不同的方式:同步代碼塊和同步方法
首先來看同步代碼塊的運用。語法見代碼:
synchronized(syncObject){ //code }
synchronized 塊是這樣一個代碼塊,其中的代碼必須獲得對象 syncObject 的鎖方能執行。由於可以針對任意代碼塊,且可任意指定上鎖的對象,故靈活性較高。
這里面的syncObject,可以是 類實例 或 類。
針對我們的場景,修改代碼如下:
1 public class Test extends Thread { 2 public static volatile Integer amount = new Integer(0);//修改為對象 3 4 public void run() { 5 int i = 0; 6 synchronized (amount) { 7 while (i < 1000 * 1000) { 9 amount++; 10 i++; 11 } 12 } 13 } 14 15 public static void main(String[] args) { 16 int num_thread = 10; 17 for (int i = 0; i < num_thread; i++) { 18 new Test().start(); 19 } 20 try { 21 Thread.sleep(1 * 1000); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 System.out.println(amount); 26 } 27 }
變動包括,amount的類型由int變為Interger,這樣amount才是一個可以被synchronized使用的Integer實例。
然而程序的輸出:1902241 —— 仍然不是我們想要的。問題出在了哪里?測試發現在synchronized后面sleep 10ms 以上同步成功,sleep 1ms 的話就不會成功!!!!!!!!!!!什么鬼!!!!!!!!詳情見代碼注釋:
1 public class Test implements Runnable { 2 // public static volatile AtomicInteger amount = new AtomicInteger(0); 3 private Integer amount = new Integer(0); 4 private InClass inClass = new InClass("adfas"); 5 6 public void run() { 7 synchronized (amount) { 8 // synchronized (inClass) {// 注意!!這里換成inClass就成功了,即便沒有sleep,簡直郁悶啊啊啊啊啊!!! 9 // synchronized (this){ 或者synchronized (Test.class)都可以,即便沒有sleep 10 System.out.println( 11 Thread.currentThread().getName() + "---------begin--------------" + System.currentTimeMillis()); 12 try { 13 Thread.sleep(1);//sleep(10)的話可以成功,查看輸出,程序輸出時間確實是遞增的!!! 14 } catch (Exception e) { 15 // TODO 自動生成的 catch 塊 16 e.printStackTrace(); 17 } 18 for (int i = 0; i < 200000;) { 19 // addOne(); 20 amount++; 21 i++; 22 } 23 System.out.println( 24 Thread.currentThread().getName() + "------------end-------------" + System.currentTimeMillis()); 25 } 26 } 27 28 public synchronized void addOne() { 29 amount++; 30 } 31 32 public static void main(String[] args) { 33 int num_thread = 50; 34 Test test = new Test(); 35 for (int i = 0; i < num_thread; i++) { 36 (new Thread(test)).start(); 37 } 38 try { 39 Thread.sleep(10 * 1000); 40 } catch (InterruptedException e) { 41 e.printStackTrace(); 42 } 43 System.out.println(test.amount); 44 } 45 46 class InClass { 47 public InClass(String name) { 48 // TODO 自動生成的構造函數存根 49 } 50 } 51 }
對於上面這個例子,是我最大的疑惑。講道理的話,amount是一個Integer的實例,我本認為synchronized(amount)鎖住的是amount這個實例的同步方法或者同步代碼塊,按道理來說對程序是沒有影響的,也就是synchronized並不起作用,但是使用inClass實例告訴我們,上面的想法是錯誤的(具體原因需要分析)!但是既然synchronized(syncObject)表明了可以用的話,amount抽象來看,和inClass是同一個東西,都是實例!但是amount為什么就不能用呢???感覺非常奇怪,貼一下測試結果:
測試結果1:synchronized(amount)沒有sleep(),可以明顯看出,代碼塊沒有被鎖住。另外,sleep(1)的時候,結果類似。

Thread-0---------begin--------------1483240909596 Thread-7---------begin--------------1483240909596 Thread-9---------begin--------------1483240909597 Thread-10---------begin--------------1483240909597 Thread-11---------begin--------------1483240909598 Thread-12---------begin--------------1483240909603 Thread-8---------begin--------------1483240909617 Thread-18---------begin--------------1483240909617 Thread-9------------end-------------1483240909628 Thread-17---------begin--------------1483240909628 Thread-10------------end-------------1483240909631 Thread-12------------end-------------1483240909631 Thread-16---------begin--------------1483240909632 Thread-20---------begin--------------1483240909632 Thread-11------------end-------------1483240909632 Thread-19---------begin--------------1483240909633 Thread-18------------end-------------1483240909636 Thread-13---------begin--------------1483240909636 Thread-14---------begin--------------1483240909632 Thread-15---------begin--------------1483240909651 Thread-48---------begin--------------1483240909651 Thread-49---------begin--------------1483240909654 Thread-20------------end-------------1483240909654 Thread-13------------end-------------1483240909655 Thread-22---------begin--------------1483240909651 Thread-30---------begin--------------1483240909651 Thread-14------------end-------------1483240909669 Thread-23---------begin--------------1483240909651 Thread-15------------end-------------1483240909670 Thread-16------------end-------------1483240909651 Thread-7------------end-------------1483240909642 Thread-31---------begin--------------1483240909651 Thread-48------------end-------------1483240909673 Thread-38---------begin--------------1483240909651 Thread-39---------begin--------------1483240909651 Thread-30------------end-------------1483240909674 Thread-46---------begin--------------1483240909651 Thread-22------------end-------------1483240909674 Thread-26---------begin--------------1483240909651 Thread-23------------end-------------1483240909686 Thread-34---------begin--------------1483240909650 Thread-42---------begin--------------1483240909650 Thread-21---------begin--------------1483240909650 Thread-25---------begin--------------1483240909650 Thread-38------------end-------------1483240909693 Thread-31------------end-------------1483240909694 Thread-28---------begin--------------1483240909650 Thread-26------------end-------------1483240909695 Thread-29---------begin--------------1483240909650 Thread-37---------begin--------------1483240909650 Thread-36---------begin--------------1483240909650 Thread-34------------end-------------1483240909705 Thread-25------------end-------------1483240909709 Thread-44---------begin--------------1483240909650 Thread-21------------end-------------1483240909711 Thread-33---------begin--------------1483240909650 Thread-28------------end-------------1483240909713 Thread-41---------begin--------------1483240909650 Thread-45---------begin--------------1483240909650 Thread-29------------end-------------1483240909713 Thread-35---------begin--------------1483240909650 Thread-42------------end-------------1483240909714 Thread-27---------begin--------------1483240909650 Thread-17------------end-------------1483240909650 Thread-24---------begin--------------1483240909650 Thread-32---------begin--------------1483240909650 Thread-44------------end-------------1483240909730 Thread-40---------begin--------------1483240909650 Thread-41------------end-------------1483240909732 Thread-35------------end-------------1483240909733 Thread-19------------end-------------1483240909650 Thread-43---------begin--------------1483240909650 Thread-0------------end-------------1483240909650 Thread-6---------begin--------------1483240909734 Thread-8------------end-------------1483240909644 Thread-45------------end-------------1483240909735 Thread-33------------end-------------1483240909733 Thread-37------------end-------------1483240909722 Thread-36------------end-------------1483240909722 Thread-39------------end-------------1483240909691 Thread-46------------end-------------1483240909691 Thread-49------------end-------------1483240909674 Thread-47---------begin--------------1483240909657 Thread-27------------end-------------1483240909740 Thread-24------------end-------------1483240909740 Thread-32------------end-------------1483240909741 Thread-40------------end-------------1483240909748 Thread-6------------end-------------1483240909751 Thread-5---------begin--------------1483240909751 Thread-43------------end-------------1483240909751 Thread-47------------end-------------1483240909751 Thread-5------------end-------------1483240909752 Thread-4---------begin--------------1483240909752 Thread-4------------end-------------1483240909754 Thread-3---------begin--------------1483240909754 Thread-3------------end-------------1483240909756 Thread-2---------begin--------------1483240909756 Thread-2------------end-------------1483240909757 Thread-1---------begin--------------1483240909757 Thread-1------------end-------------1483240909758 2589439
測試結果2:synchronized(amount)並且sleep(10),可以看出,代碼是被同步了的。為了看的更清楚,sleep(1000),結果類似。

Thread-0---------begin--------------1483241058337 Thread-0------------end-------------1483241058353 Thread-49---------begin--------------1483241058353 Thread-49------------end-------------1483241058365 Thread-48---------begin--------------1483241058365 Thread-48------------end-------------1483241058378 Thread-47---------begin--------------1483241058379 Thread-47------------end-------------1483241058390 Thread-46---------begin--------------1483241058390 Thread-46------------end-------------1483241058401 Thread-45---------begin--------------1483241058401 Thread-45------------end-------------1483241058412 Thread-44---------begin--------------1483241058412 Thread-44------------end-------------1483241058423 Thread-43---------begin--------------1483241058423 Thread-43------------end-------------1483241058434 Thread-42---------begin--------------1483241058434 Thread-42------------end-------------1483241058446 Thread-41---------begin--------------1483241058446 Thread-41------------end-------------1483241058461 Thread-40---------begin--------------1483241058461 Thread-40------------end-------------1483241058472 Thread-39---------begin--------------1483241058472 Thread-39------------end-------------1483241058483 Thread-38---------begin--------------1483241058483 Thread-38------------end-------------1483241058494 Thread-37---------begin--------------1483241058494 Thread-37------------end-------------1483241058504 Thread-36---------begin--------------1483241058504 Thread-36------------end-------------1483241058515 Thread-35---------begin--------------1483241058516 Thread-35------------end-------------1483241058526 Thread-34---------begin--------------1483241058526 Thread-34------------end-------------1483241058540 Thread-33---------begin--------------1483241058541 Thread-33------------end-------------1483241058552 Thread-32---------begin--------------1483241058552 Thread-32------------end-------------1483241058563 Thread-31---------begin--------------1483241058564 Thread-31------------end-------------1483241058577 Thread-29---------begin--------------1483241058577 Thread-29------------end-------------1483241058588 Thread-30---------begin--------------1483241058588 Thread-30------------end-------------1483241058598 Thread-28---------begin--------------1483241058599 Thread-28------------end-------------1483241058610 Thread-27---------begin--------------1483241058610 Thread-27------------end-------------1483241058621 Thread-26---------begin--------------1483241058621 Thread-26------------end-------------1483241058632 Thread-25---------begin--------------1483241058632 Thread-25------------end-------------1483241058643 Thread-24---------begin--------------1483241058643 Thread-24------------end-------------1483241058654 Thread-23---------begin--------------1483241058654 Thread-23------------end-------------1483241058665 Thread-22---------begin--------------1483241058665 Thread-22------------end-------------1483241058680 Thread-21---------begin--------------1483241058680 Thread-21------------end-------------1483241058693 Thread-20---------begin--------------1483241058693 Thread-20------------end-------------1483241058706 Thread-19---------begin--------------1483241058706 Thread-19------------end-------------1483241058718 Thread-18---------begin--------------1483241058718 Thread-18------------end-------------1483241058731 Thread-17---------begin--------------1483241058731 Thread-17------------end-------------1483241058743 Thread-16---------begin--------------1483241058743 Thread-16------------end-------------1483241058757 Thread-15---------begin--------------1483241058757 Thread-15------------end-------------1483241058770 Thread-14---------begin--------------1483241058772 Thread-14------------end-------------1483241058783 Thread-13---------begin--------------1483241058783 Thread-13------------end-------------1483241058794 Thread-12---------begin--------------1483241058794 Thread-12------------end-------------1483241058805 Thread-11---------begin--------------1483241058805 Thread-11------------end-------------1483241058816 Thread-10---------begin--------------1483241058817 Thread-10------------end-------------1483241058828 Thread-9---------begin--------------1483241058828 Thread-9------------end-------------1483241058839 Thread-8---------begin--------------1483241058839 Thread-8------------end-------------1483241058850 Thread-7---------begin--------------1483241058850 Thread-7------------end-------------1483241058861 Thread-6---------begin--------------1483241058861 Thread-6------------end-------------1483241058872 Thread-3---------begin--------------1483241058872 Thread-3------------end-------------1483241058883 Thread-5---------begin--------------1483241058883 Thread-5------------end-------------1483241058895 Thread-4---------begin--------------1483241058896 Thread-4------------end-------------1483241058907 Thread-2---------begin--------------1483241058907 Thread-2------------end-------------1483241058919 Thread-1---------begin--------------1483241058919 Thread-1------------end-------------1483241058930
升級想法3.1:基於上面出現的各種問題,可以把amount++這一步直接用一個synchronized修飾的方法代替,簡單明了,粗暴高效!
詳見上面的代碼里面的addOne()方法!但是這里其實是有問題的:畢竟真是程序中一般可能出現各種同步情況,很多時候同步代碼塊兒的靈活性非常好,而同步方法使用起來可能不方便;所以上面的問題還是需要解決,在網上很多的例子中我們看到的都是synchronized(this)和synchronized(Test.class),關於這兩種用法的對比,見之后的補充。當我們不想鎖住類的對象,只是想同步代碼塊的時候,可以考慮創建一個對象實例,如下圖所示:
升級想法4.0:java里面有些對數變量的操作是原子性的,
Java中的原子操作包括:
1)除long和double之外的基本類型的賦值操作
2)所有引用reference的賦值操作
3)java.concurrent.Atomic.* 包中所有類的一切操作
count++不是原子操作,是3個原子操作組合
1.讀取主存中的count值,賦值給一個局部成員變量tmp
2.tmp+1
3.將tmp賦值給count
方法:使用java.util.concurrent.AtomicInteger,詳見代碼!

1 import java.util.concurrent.atomic.AtomicInteger; 2 3 public class Test implements Runnable { 4 public static volatile AtomicInteger amount = new AtomicInteger(0); 5 6 public void run() { 7 System.out.println( 8 Thread.currentThread().getName() + "---------begin--------------" + System.currentTimeMillis()); 9 for (int i = 0; i < 200000;) { 10 amount.incrementAndGet(); 11 i++; 12 } 13 System.out.println( 14 Thread.currentThread().getName() + "------------end-------------" + System.currentTimeMillis()); 15 } 16 } 17 18 public static void main(String[] args) { 19 int num_thread = 50; 20 Test test = new Test(); 21 for (int i = 0; i < num_thread; i++) { 22 (new Thread(test)).start(); 23 } 24 try { 25 Thread.sleep(10 * 1000); 26 } catch (InterruptedException e) { 27 e.printStackTrace(); 28 } 29 System.out.println(test.amount); 30 } 31 32 }
另外補充!synchronized(this) VS synchronize(MyClass.class)
引用國外一個問答網站的精辟回答:
"
MyClass.class
and this
are different things, are different references to different objects.
this
- is the reference to particular this instance of class, and
MyClass.class
- is the reference to MyClass
description object.
This synchronization blocks differs in that the first will synchronize all threads that deal concretely with this instance of MyClass
, and the second one will synchronize all threads independently of which object on which this method was called.
"
翻譯過來就是:this同步的是一個具體的對象,所有由這個對象產生的線程在運行同一個方法時都會被阻塞(補充:所有的synchronized標示的方法用也會被阻塞,原因是等同1);MyClass.class同步的是當前類,獲取鎖的方法將阻塞所有這個類的實例對象,這些對象都無權調用該方法。理解這里的this和MyClass.class非常重要!比如對於上面的例子來說,如果我們的Test extends Thread,如果每次啟動線程都是new Test().start(),使用synchronized(this)是無效的(程序中amount要聲明為static,屬於類),因為每個線程都是一個獨立的對象產生的。(注意下面兩個等同)。
等同1:
public void test() { synchronized(this) { // todo your code } }
public synchronized void test() { // todo your code }
等同2:
如果某方法為類方法,即其修飾符為static,那么synchronized 意味着某個調用此方法的線程當前會擁有該類的鎖,只要該線程持續在當前方法內運行,其他線程依然無法獲得方法的使用權!
測試代碼:

1 public class TestSynchronized { 2 private InClass inClass = new InClass("name"); 3 4 public void test1() { 5 synchronized (inClass) { 6 int i = 5; 7 while (i-- > 0) { 8 System.out.println(Thread.currentThread().getName() + " : " + i); 9 try { 10 Thread.sleep(500); 11 } catch (InterruptedException ie) { 12 } 13 } 14 } 15 } 16 17 public synchronized void test2() { 18 19 int i = 5; 20 while (i-- > 0) { 21 System.out.println(Thread.currentThread().getName() + " : " + i); 22 try { 23 Thread.sleep(500); 24 } catch (InterruptedException ie) { 25 } 26 } 27 28 } 29 30 public void test3() { 31 synchronized (TestSynchronized.class) { 32 int i = 5; 33 while (i-- > 0) { 34 System.out.println(Thread.currentThread().getName() + " : " + i); 35 try { 36 Thread.sleep(500); 37 } catch (InterruptedException ie) { 38 } 39 } 40 } 41 } 42 43 public void test4() { 44 synchronized (this) { 45 int i = 5; 46 while (i-- > 0) { 47 System.out.println(Thread.currentThread().getName() + " : " + i); 48 try { 49 Thread.sleep(500); 50 } catch (InterruptedException ie) { 51 } 52 } 53 } 54 } 55 56 public synchronized void test5() { 57 58 int i = 5; 59 while (i-- > 0) { 60 System.out.println(Thread.currentThread().getName() + " : " + i); 61 try { 62 Thread.sleep(500); 63 } catch (InterruptedException ie) { 64 } 65 } 66 67 } 68 69 public static void main(String[] args) { 70 final TestSynchronized myt1 = new TestSynchronized(); 71 final TestSynchronized myt2 = new TestSynchronized(); 72 Thread test1 = new Thread(new Runnable() { 73 public void run() { 74 myt1.test2(); 75 } 76 }, "test1"); 77 try { 78 Thread.sleep(10); 79 } catch (InterruptedException e) { 80 // TODO 自動生成的 catch 塊 81 e.printStackTrace(); 82 } 83 Thread test2 = new Thread(new Runnable() { 84 public void run() { 85 myt1.test5(); 86 } 87 }, "test2"); 88 test1.start(); 89 test2.start(); 90 } 91 92 class InClass { 93 public InClass(String name) { 94 // TODO 自動生成的構造函數存根 95 } 96 } 97 }
測試會發現,synchronized(this)會鎖住代碼塊本身的方法、synchronized標示的方法和其它synchroized(this)的代碼塊;而synchronized(MyClass.class)只能鎖住不同對象對應的這一個方法塊兒!其他方法(即便是同步的)不會被鎖住!
紙上得來終覺淺,還盼諸君勤實踐啊!!!啊啊啊啊啊啊!!!!!
參考:synchronize類鎖和對象鎖詳解;深入理解java中的synchronized關鍵字;java中volatile關鍵字的含義(這個的評論要看一下)
-------------------------------------- 我 是 華 麗 的 分 割 線 ------------------------------------------
--->>>原因找到了!
關鍵在於amount is an instance of Integer,synchronized鎖的是amount這個對象,但是for循環中amount++這個操作會使得amount這個對象發生變化,這個通過hashcode可以看出來,對於instance of Integer,amount.intValue()==amount.hashcode().所以當這個對象變了之后,之前對象的鎖自然就沒用了,其它線程開始競爭新對象的鎖,由此造成了這樣的結果。
--->>>關於synchronized一點補充
synchronized的實現機制需要參照jvm,了解的知識比較多。synchronized(syncObject)類似於給某個對象加鎖,{\\code}代碼塊里面的東西在執行之前都需要獲取這把鎖,運行完之后馬上釋放鎖。