關於多線程並發環境下的數據安全的問題


1.為什么是重點?

  以后在開發中,項目都是運行在服務器當中,而服務器已經將線程的定義、線程對象的創建、線程的啟動等,都已經實現完了。這些代碼都不需要編寫,最重要的是要知道:編寫的程序需要放到一個多線程的環境下運行,更需要關注這些數據在多線程並發的環境下是否是安全的。

2.什么時候數據在多線程並發的環境下會存在安全問題?

  三個條件:

  (1)條件1:多線程並發

  (2)條件2:多線程有共享的數據

  (3)條件3:共享的數據有修改的行為

  滿足以上3個條件之后,就會存在線程安全問題。

3.如何解決線程安全問題?

  當多線程並發的環境下,有共享數據,並且這個數據還會被修改,此時就存在線程安全問題。

  此時應當使線程排隊執行(不能並發)。用排隊執行解決線程安全問題,這種機制稱為:線程同步機制。(這是專業術語的叫法,實際上就是線程不能並發了,必須排隊執行)。線程同步(也就是線程排隊)會犧牲一部分效率,但是數據安全是第一位的。

4.線程同步涉及到的兩個專業術語

  4.1 異步編程模型

    線程t1和t2各自執行各自的,相互不管,誰也不需要等誰。(其實就是多線程並發,效率較高)

  4.2 同步編程模型

    線程t1和t2,在某一線程執行的時候,另一個線程必須等待正在執行的線程,一直到其執行完畢為止。兩個線程之間發生了等待關系,線程排隊執行,效率較低。

5.例子分析

  

 1 package thread_safe;
 2 
 3 public class Account {
 4     
 5     //賬戶
 6     private String sctno;
 7     
 8     //余額
 9     private double balance;
10 
11     public Account() {
12     
13     }
14 
15     public Account(String sctno, double balance) {
16         this.sctno = sctno;
17         this.balance = balance;
18     }
19 
20     public String getSctno() {
21         return sctno;
22     }
23 
24     public void setSctno(String sctno) {
25         this.sctno = sctno;
26     }
27 
28     public double getBalance() {
29         return balance;
30     }
31 
32     public void setBalance(double balance) {
33         this.balance = balance;
34     }
35     
36     //取款方法
37     public void withdraw(double money){
38         //t1和t2並發這個方法;t1、t2是兩個棧。兩個棧操作堆中同一個對象
39         //取款前的余額
40         double before=this.getBalance();
41         double after=before-money;//取款后的余額
42         //更新余額
43         //若t1執行到這里,但還沒來得及執行第44行代碼,t2線程進來withdraw()方法了,此時一定出現問題。
44         this.setBalance(after);
45         
46     }
47 
48 }
 1 package thread_safe;
 2 
 3 public class AccountThread extends Thread {
 4     
 5     //兩個線程必須共享同一個賬戶對象
 6     private Account act;
 7     
 8     //通過構造方法傳遞過來賬戶對象
 9     public AccountThread(Account act){
10         this.act=act;
11     }
12     public void run(){
13         //run()方法執行取款操作
14         //假設取款5000
15         double money=5000;
16         //取款
17         act.withdraw(money);
18         System.out.println(Thread.currentThread().getName()+"對賬戶"+act.getSctno()+"取款成功,余額:"+act.getBalance());
19     }
20 }
 1 package thread_safe;
 2 
 3 public class Test {
 4     public static void main(String[] args){
 5         
 6         //創建一個賬戶對象
 7         Account act=new Account("act-001",10000);
 8         
 9         //創建兩個線程
10         Thread t1=new AccountThread(act);
11         Thread t2=new AccountThread(act);
12         
13         //設置名字
14         t1.setName("t1");
15         t2.setName("t2");
16         
17         //啟動線程取款
18         t1.start();
19         t2.start();
20         
21     }
22 
23 }

運行結果:

  可以看到,兩個余額都是5000,這就說明出現了問題。但是要注意但是,出現問題是個概率事件,即這種情況可能發生,也可能不發生,關鍵在於當某個線程即將執行 this.setBalance(after);這行代碼的時候,另一個線程是否已經執行withdraw()方法了,倘若另一個線程已經執行了withdraw()方法,那么即將執行this.setBalance(after) 這行代碼的線程執行完這行代碼后,肯定會出現問題。

  為了放大這個問題,可以在this.setBalance(after)之前設置一個延時,這樣一定出錯,問題顯示的也更加明顯:

 1 package thread_safe;
 2 
 3 public class Account {
 4     
 5     //賬戶
 6     private String sctno;
 7     
 8     //余額
 9     private double balance;
10 
11     public Account() {
12     
13     }
14 
15     public Account(String sctno, double balance) {
16         this.sctno = sctno;
17         this.balance = balance;
18     }
19 
20     public String getSctno() {
21         return sctno;
22     }
23 
24     public void setSctno(String sctno) {
25         this.sctno = sctno;
26     }
27 
28     public double getBalance() {
29         return balance;
30     }
31 
32     public void setBalance(double balance) {
33         this.balance = balance;
34     }
35     
36     //取款方法
37     public void withdraw(double money){
38         //t1和t2並發這個方法;t1、t2是兩個棧。兩個棧操作堆中同一個對象
39         //取款前的余額
40         double before=this.getBalance();
41         double after=before-money;//取款后的余額
42         
43         //進行1秒的睡眠,模擬網絡延時
44         try {
45             Thread.sleep(1000);
46         } catch (InterruptedException e) {
47             // TODO Auto-generated catch block
48             e.printStackTrace();
49         }
50         //更新余額
51         //若t1執行到這里,但還沒來得及執行第44行代碼,t2線程進來withdraw()方法了,此時一定出現問題。
52         this.setBalance(after);
53         
54     }
55 
56 }

  這樣之前所說的概率事件就成了一個肯定發生的事件,即每次運行都會出現問題。

 解決方法:

 1 public void withdraw(double money){
 2         
 3         //一下幾行代碼必須是線程排隊的,不能並發
 4         //一個線程將這里的代碼全部執行完畢后,另一個代碼才能進來
 5         /*
 6          * 線程同步機制的語法是:
 7          *  synchronized(){
 8          *      線程同步代碼塊
 9          *  
10          *  }
11          *  
12          *    synchronized后面小括號中傳的這個數據是相當重要的,這個數據必須是多線程共享的數據,才能達到多線程排隊
13          *    ()中寫的是想讓同步的線程,這里讓t1,t2兩個線程同步。
14          *  
15          *    
16          *    這里的共享對象是:賬戶對象 。賬戶對象是共享的,而這里的this就是賬戶對象
17          *    
18          *    在java中,任何一個對象都有“一把鎖”,其實這把鎖就是一個標記(100個對象100把鎖)
19          *    
20          *    下面代碼 的原理:
21          *     1.假設t1和t2線程並發,開始執行一下代碼的時候,肯定有一個先一個后
22          *     2.假設t1先執行了,遇到synchronized,這個時候自動找后面“共享對象”(也就是這里的Account)的對象鎖。找到之后並占有這把鎖,
23          *  然后執行同步代碼塊中的程序,在程序執行過程中一直占有這把鎖,直到同步代碼塊代碼結束,這把鎖才會釋放。
24          *     3. 當t2想占有對象的鎖的時候,發現t1已經占有,所以只能在同步代碼塊外等候t1執行完同步代碼塊里的程序,歸還對象鎖后,t2再占用對象的鎖,
25          *  然后進入同步代碼塊執行程序
26          *  
27          *     注:共享對象的選擇一定要選好,這個對象一定要是需要排隊執行的這些線程對象所共享的
28         */
29         synchronized(this){
30         double before=this.getBalance();
31         double after=before-money;//取款后的余額
32         
33         try {
34             Thread.sleep(1000);
35         } catch (InterruptedException e) {
36             
37             e.printStackTrace();
38         }
39         this.setBalance(after);
40         }
41         
42     }

運行結果:

思考:

 a.  在Account對象中再添加一個Obj對象(全局對象)

  

 1 public class Account {
 2     
 3     //賬戶
 4     private String sctno;
 5     
 6     //余額
 7     private double balance;
 8     
 9     
10     //對象
11     Object obj=new Object(); //實例變量,(Account)對象是多線程共享的,所以Account對象中的實例變量obj也是被這些線程共享的

  然后在synchronized()的括號內放入obj對象

 1 synchronized(obj){
 2         double before=this.getBalance();
 3         double after=before-money;//取款后的余額
 4         
 5         try {
 6             Thread.sleep(1000);
 7         } catch (InterruptedException e) {
 8             
 9             e.printStackTrace();
10         }
11         this.setBalance(after);
12         }
13         
14     }

運行結果:

  b.在withdraw()方法里添加一個對象(局部對象)。

 1 package Thread_safe2;
 2 
 3 public class Account {
 4     
 5     //賬戶
 6     private String sctno;
 7     
 8     //余額
 9     private double balance;
10     
11     
12     //全局對象 obj
13     Object obj=new Object(); //實例變量,(Account)對象是多線程共享的,所以Account對象中的實例變量obj也是被這些線程共享的
14 
15     public Account() {
16     
17     }
18 
19     public Account(String sctno, double balance) {
20         this.sctno = sctno;
21         this.balance = balance;
22     }
23 
24     public String getSctno() {
25         return sctno;
26     }
27 
28     public void setSctno(String sctno) {
29         this.sctno = sctno;
30     }
31 
32     public double getBalance() {
33         return balance;
34     }
35 
36     public void setBalance(double balance) {
37         this.balance = balance;
38     }
39     
40     //取款方法
41     public void withdraw(double money){
42         //局部對象 obj2
43         Object obj2=new Object();
44         synchronized(obj2){
45         double before=this.getBalance();
46         double after=before-money;//取款后的余額
47         
48         try {
49             Thread.sleep(1000);
50         } catch (InterruptedException e) {
51             
52             e.printStackTrace();
53         }
54         this.setBalance(after);
55         }
56         
57     }
58 
59 }

運行結果:

 

分析原因 :obj2是局部變量,所以它不是共享對象

  c.在withdraw()方法里添加字符串“abc”

synchronized("abc"){
        double before=this.getBalance();
        double after=before-money;//取款后的余額
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            
            e.printStackTrace();
        }
        this.setBalance(after);
        }

  這種方法也行,因為“abc”在字符串常量池中,但是需要注意的是,寫“abc”是所有線程都同步,也就是說,如果這里再有幾個線程t3、t4。。。。。。,它們也會同步,如果只需要t1和t2同步,其他線程不希望同步,那么就不能采用這樣寫法。如:

 1 package Thread_safe2;
 2 
 3 public class Test {
 4     public static void main(String[] args){
 5         
 6         //創建一個賬戶對象
 7         Account act=new Account("act-001",10000);
 8         Account act1=new Account("act-002",10000000);
 9         
10         //創建兩個線程
11         Thread t1=new AccountThread(act);
12         Thread t2=new AccountThread(act);
13         
14         //額外創建一個線程
15         Thread t3=new AccountThread(act1);
16         
17         //設置名字
18         t1.setName("t1");
19         t2.setName("t2");
20         
21         t3.setName("t3");
22         
23         //啟動線程取款
24         t1.start();
25         t2.start();
26         t3.start();
27         
28     }
29 
30 }

  (1)synchronized()中寫this,這種情況下,只有t1與t2同步,也就是說,它們需要排隊,后執行者等前面的歸還對象鎖(t1與t2共享的對象是act,所以它們兩個要排隊使用act的對象鎖)后,才執行,而此時t3的對象是act1,括號中的this指的是當前對象,所以此時,t3與t1和t2都不同步。

  (2)synchronized()中寫“abc”,這種情況下,只有t1、t2、t3都同步,它們共同使用對象“abc”的鎖,所以需要排隊。

6.java中的三大變量

  實例變量:存在堆中

  靜態變量:存在方法區中

  局部變量:存在棧中

  以上三大變量中,局部變量永遠不會存在線程安全問題,因為局部變量在棧中,而一個線程一個棧,所以局部變量永遠不會被共享所以也就不會存在安全問題了。

  實例變量在堆中,靜態變量在方法區中,而堆和方法區都只有一個,所以會堆和方法區都是多線程共享的,所以可能存在安全問題。

  綜上所述:局部變量和常量(因為常量不可修改)都沒有線程安全問題,而成員變量(實例變量+靜態變量)會存在線程安全問題

7. 如果使用局部變量的話,使用StringBuilder還是StringBuffer?

  建議使用:StringBuilder。

  雖然StringBuffer是線程安全的,StringBuilder是非線程安全的,然而將它們用作局部變量的時候,因為局部變量不存在線程安全問題,StringBuffer因為是線程安全的,反而效率會降低(因為StringBuffer的方法上都加synchronized了,每次運行到這里的時候都會去“鎖池”里走一趟,所以會降低效率),所以這個時候應該使用StringBuilder。

  非線程安全:ArrayList、HashMap、HashSet

  線程安全:Vector、Hashtable

8. 總結

  synchronized有兩種寫法

    第一種:同步代碼塊。

      synchronized(線程共享對象){

        同步代碼塊  

      }

 

    第二種:在實例方法上使用synchronized。表示共享對象一定是this,並且同步代碼塊 是整個方法體

 

    第三種:在靜態方法上,使用synchronized。表示使用類鎖,類鎖永遠只有1把,就算創建了100個對象,那類鎖也只有一把。

 

               對象鎖:一個對象一把鎖,100個對象100把鎖

     類鎖:100個對象,也可能只是一把鎖

   4個例子:

  a.

 1 package exam;
 2 
 3 /*
 4  * doOther()的執行是否需要等doSome()結束?
 5  * 不需要,因為doOther()不需要占用this的鎖,即當doSome()占用this鎖執行的時候,不影響doOther()的執行
 6  */
 7 public class Exam01 {
 8     
 9     public static void main(String[] args) throws Exception{
10         
11         MyClass mc=new MyClass();
12         Thread t1=new MyThread(mc);
13         Thread t2=new MyThread(mc);
14         
15         t1.setName("t1");
16         t2.setName("t2");
17         
18         t1.start();
19         //睡眠的作用是保證t1先執行
20         Thread.sleep(1000);
21         t2.start();
22     }
23 
24 }
25 class MyThread extends Thread{
26     MyClass mc=new MyClass();
27     public MyThread(MyClass mc){
28         this.mc =mc;
29     }
30     public void run(){
31         if(Thread.currentThread().getName().equals("t1")){
32             mc.doSome();
33         }
34         if(Thread.currentThread().getName().equals("t2")){
35             mc.doOther();
36         }
37     }
38 }
39 class MyClass{
40     public synchronized void doSome(){
41         System.out.println("doSome begin");
42         
43         try {
44             Thread.sleep(1000*10);
45         } catch (InterruptedException e) {
46             // TODO Auto-generated catch block
47             e.printStackTrace();
48         }
49         System.out.println("doSome over");
50     }
51     
52     public void doOther(){
53         System.out.println("doOther begin");
54         System.out.println("doOther over");
55     }
56 }

  b.

 1 package exam;
 2 
 3 
 4 /*
 5  * doOther()的執行是否需要等doSome()結束?
 6  * 需要,因為等doSome()執行結束了,才會釋放this的鎖,然后doOther()拿到this的鎖了,才能繼續執行
 7  */
 8 public class Exam2 {
 9 public static void main(String[] args) throws Exception{
10         
11         MyClass1 mc=new MyClass1();
12         Thread t1=new MyThread1(mc);
13         Thread t2=new MyThread1(mc);
14         
15         t1.setName("t1");
16         t2.setName("t2");
17         
18         t1.start();
19         //睡眠的作用是保證t1先執行
20         Thread.sleep(1000);
21         t2.start();
22     }
23 
24 }
25 class MyThread1 extends Thread{
26     MyClass1 mc=new MyClass1();
27     public MyThread1(MyClass1 mc){
28         this.mc =mc;
29     }
30     public void run(){
31         if(Thread.currentThread().getName().equals("t1")){
32             mc.doSome();
33         }
34         if(Thread.currentThread().getName().equals("t2")){
35             mc.doOther();
36         }
37     }
38 }
39 class MyClass1{
40     public synchronized void doSome(){
41         System.out.println("doSome begin");
42         
43         try {
44             Thread.sleep(1000*10);
45         } catch (InterruptedException e) {
46             // TODO Auto-generated catch block
47             e.printStackTrace();
48         }
49         System.out.println("doSome over");
50     }
51     
52     public synchronized void doOther(){
53         System.out.println("doOther begin");
54         System.out.println("doOther over");
55     }
56 }

  c.

 1 package exam;
 2 
 3 /*
 4  * doOther()的執行是否需要等doSome()結束?
 5  * 不需要,因為MyClass對象是兩個,兩把鎖
 6  */
 7 
 8 public class Exam3 {
 9 public static void main(String[] args) throws Exception{
10         
11         MyClass2 mc1=new MyClass2();
12         MyClass2 mc2=new MyClass2();
13         Thread t1=new MyThread2(mc1);
14         Thread t2=new MyThread2(mc2);
15         
16         t1.setName("t1");
17         t2.setName("t2");
18         
19         t1.start();
20         //睡眠的作用是保證t1先執行
21         Thread.sleep(1000);
22         t2.start();
23     }
24 
25 }
26 class MyThread2 extends Thread{
27     MyClass2 mc=new MyClass2();
28     public MyThread2(MyClass2 mc){
29         this.mc =mc;
30     }
31     public void run(){
32         if(Thread.currentThread().getName().equals("t1")){
33             mc.doSome();
34         }
35         if(Thread.currentThread().getName().equals("t2")){
36             mc.doOther();
37         }
38     }
39 }
40 class MyClass2{
41     public synchronized void doSome(){
42         System.out.println("doSome begin");
43         
44         try {
45             Thread.sleep(1000*10);
46         } catch (InterruptedException e) {
47             // TODO Auto-generated catch block
48             e.printStackTrace();
49         }
50         System.out.println("doSome over");
51     }
52     
53     public synchronized void doOther(){
54         System.out.println("doOther begin");
55         System.out.println("doOther over");
56     }
57 
58 }

  d.

 1 package exam;
 2 /*
 3  * doOther()的執行是否需要等doSome()結束?
 4  * 需要,因為靜態方法是類鎖,不管創建了幾個對象,類鎖只有1把
 5  */
 6 public class Exam4 {
 7 public static void main(String[] args) throws Exception{
 8         
 9         MyClass3 mc1=new MyClass3();
10         MyClass3 mc2=new MyClass3();
11         Thread t1=new MyThread3(mc1);
12         Thread t2=new MyThread3(mc2);
13         
14         t1.setName("t1");
15         t2.setName("t2");
16         
17         t1.start();
18         //睡眠的作用是保證t1先執行
19         Thread.sleep(1000);
20         t2.start();
21     }
22 
23 }
24 class MyThread3 extends Thread{
25     MyClass3 mc=new MyClass3();
26     public MyThread3(MyClass3 mc){
27         this.mc =mc;
28     }
29     public void run(){
30         if(Thread.currentThread().getName().equals("t1")){
31             mc.doSome();
32         }
33         if(Thread.currentThread().getName().equals("t2")){
34             mc.doOther();
35         }
36     }
37 }
38 class MyClass3{
39     // synchronized 出現在靜態方法上找的是類鎖
40     public synchronized static void doSome(){
41         System.out.println("doSome begin");
42         
43         try {
44             Thread.sleep(1000*10);
45         } catch (InterruptedException e) {
46             // TODO Auto-generated catch block
47             e.printStackTrace();
48         }
49         System.out.println("doSome over");
50     }
51     
52     public synchronized static void doOther(){
53         System.out.println("doOther begin");
54         System.out.println("doOther over");
55     }
56 
57     
58 
59 }

 9.開發中如何解決線程安全問題?

  雖然使用synchronized()會解決線程安全問題,保證線程的同步,但是這種方法也有缺點:會降低用戶的吞吐量(並發量),使得程序執行效率降低,所以盡量避免使用這種方法。

  方法一:盡量使用局部變量代替實例變量和靜態變量。

  方法二:如果必須使用實例變量,可以考慮創建多個對象,這樣就可以避免共享實例變量的內存。(1個線程對應1個對象,100個線程對應100個對象,若線程之間不存在共享對象,也就不會出現安全問題)。

  方法三:如果不能使用局部變量,對象也不能創建多個,這個時候就只能使用synohronized.

10. 線程部分的其他內容

  10.1 守護線程

    java語言中,線程分為兩類,一類叫用戶線程,另一類叫守護線程(后台線程)。

    守護線程的特點:一般是個死循環;只要用戶線程結束,守護線程就會自動結束。垃圾回收線程就是一個守護線程,main方法就是一個用戶線程。

    守護線程用在什么地方?

  10.2 定時器

  10.3 實現線程的第三種方式:FutureTask方式,實現Callable接口。(JDK8新特性)

  10.4 關於Object類中的wait和notify方法。(生產者和消費者模式)


免責聲明!

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



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