首先感謝授課XXX老師。 1.什麼是線程安全問題 當多個線程共用同一個全局變數,做寫的操作時候,可能會受到其他線程的干擾,導致數據出現問題,這種現象就叫做線程安全問題。做讀的時候不會產生線程安全問題。 什麼安全:多個線程同時共用一個全局變數,做寫操作的時候會發生線程安全。 多個線程共用同一個局部變數 ...
首先感謝授課XXX老師。
1.什麼是線程安全問題
當多個線程共用同一個全局變數,做寫的操作時候,可能會受到其他線程的干擾,導致數據出現問題,這種現象就叫做線程安全問題。做讀的時候不會產生線程安全問題。
什麼安全:多個線程同時共用一個全局變數,做寫操作的時候會發生線程安全。
多個線程共用同一個局部變數做寫的操作時候,不會發生線程安全問題。
2.如何解決簡單的線程安全問題(鎖)
所謂簡單就是不涉及分散式,集群等行列。
因為用以下幾種解決方法完全得不到解決(實際上也就是兩種)!!!
① 使用 synchronized 代碼塊
② 使用 synchronized 函數
③ 使用 靜態同步代碼塊
搶奪cpu資源高的線程會首先拿到鎖,然後進入邏輯中進行操作,其他線程只能進行等待,然後這個線程執行完畢後釋放鎖,這個時候其他線程會爭奪這個鎖,也就會產生資源爭奪的問題,所以synchronized 的效率是很低的,然後搶奪cpu資源高的線程再次進入邏輯進行操作,以此類推。
案例
以搶車票為例,代碼會儘量簡潔。
100張火車票,兩個售票視窗。同時出售火車票,在不解決安全問題的情況下看一看會出現什麼問題。
1 package com.mydream.cn; 2 3 class Train extends Thread { 4 int count = 1; // 售票次數 5 @Override 6 public void run() { 7 while (count<=100) { 8 try { 9 Thread.currentThread().sleep(30);//為了讓程式滿足併發條件,讓他進入睡眠狀態。好讓後面同時進行 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 sale(); 14 } 15 } 16 // 售票方法邏輯 17 public void sale() { 18 System.out.println(count); 19 count++; 20 } 21 } 22 public class TrainDemo { 23 public static void main(String[] args) { 24 Train t = new Train(); 25 Thread th2 = new Thread(t); 26 Thread th1 = new Thread(t); 27 th1.start(); 28 th2.start(); 29 } 30 }
運行結果是出現了101張票!並且在開始時候還出現了重覆的票數,例如1,1,3,這種情況,並不是我們想象的1~100。
那麼是什麼原因呢???
思路分析:
首先定義了一個全局變數count然後開啟兩個線程但是是一個對象,也就是同時操作一個全局變數。在主函數main中運行他們,th1線程和th2線程因為有一個sleep函數會等待30毫秒,這樣就有機會創造出代碼併發的可能性。
分析一
1,1,3,這種不按順序走的邏輯思維。首先兩個線程都得到釋放,那麼就同時進入system.out列印方法中,然後就會列印出1 , 1 ,3這種不安順序的代碼。這種想起來應該很簡單的。也就是說同時進入的時候因為都是>=100,第一次一起進入你列印 1 我也列印 1
分析二
第101張票出現的情況,按照上面思路,當運行到第100張的時候,第一個線程列印出了100,然後進行了+1操作,而在判斷程式中第二個線程已經進入到sale()邏輯方法中了,此時他是100,因為地換一個線程的+1操作,導致他變成了101,然後因為判斷是>=100所以後面的進不來了。才導致會列印出101的情況。
解決方法一
1 // 售票方法邏輯 2 public void sale() { 3 synchronized(this){ 4 // 再次判斷邏輯,因為進入到這裡可能都是100,再次進行判斷即可 5 if (count<=100) { 6 System.out.println(count); 7 count++; 8 } 9 } 10 }
synchronized 代碼塊,在售票方法邏輯中加入代碼塊,並且寫入判斷邏輯,可能有人會說為什麼還要在寫入判斷邏輯?我直接寫入判斷邏輯不加synchronized 不是一樣麽,請記住synchronized 是用來進行線程安全同步的,既然是同步那麼就會安全,可是synchronized 已經進入了sale()方法中了,那麼我要是同時都是100進入那也沒毛病啊~~~~。所以在此加入邏輯就會防止101的出現。
synchronized 代碼塊中括弧中的變數該放什麼呢?答案是放什麼都可以,只要線程中使用的是同一把鎖就可以了。
例如:
1 private Object obj = new Object(); 2 3 // 售票方法邏輯 4 public void sale() { 5 synchronized(obj){ 6 // 再次判斷邏輯,因為進入到這裡可能都是100,再次進行判斷即可 7 if (count<=100) { 8 System.out.println(count); 9 count++; 10 } 11 } 12 }
解決方法二
1 // 售票方法邏輯 2 public synchronized void sale() { 3 if (count<=100) { 4 System.out.println(count); 5 count++; 6 } 7 }
synchronized 函數,synchronized 原理就是使用this當鎖,我們可以假設一種情況,也是兩個線程,分別用不同的鎖,這裡不同的鎖就是 synchronized 函數 和 synchronized 代碼塊用this當鎖,如果發現是同步的,那麼就證明瞭 synchronized 函數 是用this當鎖的,相反就不是,在這裡答案是是的。
解決方式三
static synchronized 函數 就是在synchronized 函數 前面 加上 static。如果一旦加入了static 那麼 synchronized 代碼塊this鎖 不會 和 它同步了。
static 關鍵字是比所有代碼都先編譯的,所以也就不會有this的說法,難道你能在static修飾的方法中調用this麽?可能是不可以的了。
那麼 這種鎖 用的是什麼呢?答案是 .class 文件 。如何測試呢?相同的方法,把 synchronized 代碼塊 鎖換成 類名.class 測試是否同步。答案是同步的。
模擬 this 和 同步函數是否相同
1 class ThreadTrain2 implements Runnable { 2 // 總共有一百張火車 當一變數被static修飾的話存放在永久區,當class文件被載入的時候就會被初始化。 3 private static int train1Count = 100; 4 private Object oj = new Object(); 5 public boolean flag = true; 6 @Override 7 public void run() { 8 // 為了能夠模擬程式一直在搶票的話。 where 9 if (flag) { 10 //執行同步代碼塊this鎖 11 while (train1Count > 0) { 12 13 synchronized (this) { 14 if(train1Count>0){ 15 try { 16 Thread.sleep(50); 17 } catch (Exception e) { 18 // TODO: handle exception 19 } 20 System.out.println(Thread.currentThread().getName()+ ",出售第" + (100 - train1Count + 1) + "票"); 21 train1Count--; 22 } 23 } 24 25 } 26 } 27 else{ 28 // 同步函數 29 while (train1Count > 0) { 30 31 // 出售火車票 32 sale(); 33 } 34 } 35 36 } 37 38 public synchronized void sale() { 39 40 // 同步代碼塊 synchronized 包裹需要線程安全的問題。 41 // synchronized (oj) { 42 if (train1Count > 0) { 43 try { 44 Thread.sleep(50); 45 } catch (Exception e) { 46 // TODO: handle exception 47 } 48 System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - train1Count + 1) + "票"); 49 train1Count--; 50 } 51 // } 52 53 } 54 } 55 56 public class ThreadDemo2 { 57 58 public static void main(String[] args) throws InterruptedException { 59 ThreadTrain2 threadTrain2 = new ThreadTrain2(); 60 Thread t1 = new Thread(threadTrain2, "視窗①"); 61 Thread t2 = new Thread(threadTrain2, "視窗②"); 62 t1.start(); 63 Thread.sleep(40); 64 threadTrain2.flag=false; 65 t2.start(); 66 } 67 68 }
有多線程安全的解決那麼就會出現一些問題,出現死鎖狀態,也就是說兩個鎖的狀態是你等我解鎖,我等你解鎖。直接上代碼
package com.itmayiedu; class ThreadTrain3 implements Runnable { // 總共有一百張火車 當一變數被static修飾的話存放在永久區,當class文件被載入的時候就會被初始化。 private static int train1Count = 100; private Object oj = new Object(); public boolean flag = true; @Override public void run() { // 為了能夠模擬程式一直在搶票的話。 where if (flag) { // 執行同步代碼塊this鎖 while (true) { synchronized (oj) { sale(); } } } else { // 同步函數 while (true) { // 出售火車票 sale(); } } } public synchronized void sale() { // 同步代碼塊 synchronized 包裹需要線程安全的問題。 synchronized (oj) { if (train1Count > 0) { try { Thread.sleep(50); } catch (Exception e) { // TODO: handle exception } System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - train1Count + 1) + "票"); train1Count--; } } } } public class ThreadDemo3 { public static void main(String[] args) throws InterruptedException { ThreadTrain3 threadTrain3 = new ThreadTrain3(); Thread t1 = new Thread(threadTrain3, "視窗①"); Thread t2 = new Thread(threadTrain3, "視窗②"); t1.start(); Thread.sleep(40); threadTrain3.flag = false; t2.start(); } }
什麼是死鎖?同步中嵌套同步,導致鎖無法釋放
上面鎖中用到了兩個鎖,一個是this一個是obj,分析運行程式,現進入true然後馬上進入false
true 得到obj鎖 進入sale() 得到 this 鎖
false 得到this0鎖 進入sale() 得到 obj 鎖
然後就可能會出現 交叉想等待,這種想法要好好理解,並且重要! 抽象一點就是this被ojb鎖掉了 另一個 ojb被this鎖掉了 然後互相等待解鎖,可是完全等不到,就會出現多線程死鎖情況。
多線程有三大特性
原子性、可見性、有序性
什麼是原子性
即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
一個很經典的例子就是銀行賬戶轉賬問題:
比如從賬戶A向賬戶B轉1000元,那麼必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。這2個操作必須要具備原子性才能保證不出現一些意外的問題。
我們操作數據也是如此,比如i = i+1;其中就包括,讀取i的值,計算i,寫入i。這行代碼在Java中是不具備原子性的,則多線程運行肯定會出問題,所以也需要我們使用同步和lock這些東西來確保這個特性了。
原子性其實就是保證數據一致、線程安全一部分,
什麼是可見性
當多個線程訪問同一個變數時,一個線程修改了這個變數的值,其他線程能夠立即看得到修改的值。
若兩個線程在不同的cpu,那麼線程1改變了i的值還沒刷新到主存,線程2又使用了i,那麼這個i值肯定還是之前的,線程1對變數的修改線程沒看到這就是可見性問題。
什麼是有序性
程式執行的順序按照代碼的先後順序執行。
一般來說處理器為了提高程式運行效率,可能會對輸入代碼進行優化,它不保證程式中各個語句的執行先後順序同代碼中的順序一致,但是它會保證程式最終執行結果和代碼順序執行的結果是一致的。如下:
int a = 10; //語句1
int r = 2; //語句2
a = a + 3; //語句3
r = a*a; //語句4
則因為重排序,他還可能執行順序為 2-1-3-4,1-3-2-4
但絕不可能 2-1-4-3,因為這打破了依賴關係。
顯然重排序對單線程運行是不會有任何問題,而多線程就不一定了,所以我們在多線程編程時就得考慮這個問題了。