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方法。(生產者和消費者模式)
