一個簡單的synchronized多線程問題、梳理與思考


一個程序,多個線程同時操作一個變量,給這個變量+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 }    
View Code

 輸出結果:

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
View Code

測試結果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
View Code

 

升級想法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 }
View Code

 

另外補充!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 }
View Code

測試會發現,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}代碼塊里面的東西在執行之前都需要獲取這把鎖,運行完之后馬上釋放鎖。


免責聲明!

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



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