線程安全問題?
什么是線程安全問題?簡單的說,當多個線程在共享同一個變量,做讀寫的時候,會由於其他線程的干擾,導致數據誤差,就會出現線程安全問題。
比如說,多個窗口同時賣票這個案例:
1 public class ThreadTrain2 implements Runnable { 2 private int tickets = 50; 3 @Override 4 public void run() { 5 while(tickets > 0){ 6 if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "賣了第" + (50 - tickets + 1) + "張票"); 7 tickets--; 8 } 9 } 10 } 11 } 12 public static void main(String[] args) { 13 ThreadTrain2 tt = new ThreadTrain2(); 14 Thread th1 = new Thread(tt, "1號窗口"); 15 Thread th2 = new Thread(tt, "2號窗口"); 16 th1.start(); 17 th2.start(); 18 } 19 }
模擬兩個窗口共同賣50張票,什么都不考慮,按照上面的寫法,運行的結果有時候並不是我們想要的,會完全亂了套。
我們該如何解決多線程安全問題?
使用多線程同步(synchronized)或者加鎖lock
什么是多線程同步?就是當多個線程共享同一個資源時,不會受到其他線程的干擾。
為什么這兩種方法可以解決線程的安全問題?
當把可能發生沖突的代碼包裹在synchronized或者lock里面后,同一時刻只會有一個線程執行該段代碼,其他線程必須等該線程執行完畢釋放鎖以后,才能去搶鎖,獲得鎖以后,才擁有執行權,這樣就解決的數據的沖突,實現了線程的安全。
賣票的案例同步后為:
1 public class ThreadTrain2 implements Runnable { 2 private int tickets = 50; 3 private static Object obj = new Object();//鎖的對象,可以是任意的對象 4 @Override 5 public void run() { 6 while(tickets > 0){
7 synchronized (obj) {// 同步代碼塊 8 if (tickets > 0) { 9 System.out.println(Thread.currentThread().getName() + "賣了第" + (50 - tickets + 1) + "張票"); 10 tickets--; 11 } 12 } 13 } 14 } 15 public static void main(String[] args) { 16 ThreadTrain2 tt = new ThreadTrain2(); 17 Thread th1 = new Thread(tt, "1號窗口"); 18 Thread th2 = new Thread(tt, "2號窗口"); 19 th1.start(); 20 th2.start(); 21 } 22 } 23
上面是同步代碼塊的加鎖方式,可以解決線程安全問題。同時,還有一種同步函數的方式,就是在方法上直接加synchronized,可以實現同樣的效果,那么現在有一個問題,在方法上加synchronized修飾,鎖的對象是什么呢???this。。下面來驗證一下為什么是this:

public class ThreadTrain1 implements Runnable { private int tickets = 100; private static Object obj = new Object(); private static boolean flag = true; @Override public void run() { if (flag) { try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized (this) {// 同步代碼塊 while (tickets > 0) { if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "賣了第" + (100 - tickets + 1) + "張票"); tickets--; } } } } else { while (tickets > 0) { sale(); } } } public synchronized void sale() { if (tickets > 0) { try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "賣了第" + (100 - tickets + 1) + "張票"); tickets--; } } public static void main(String[] args) { ThreadTrain1 tt = new ThreadTrain1(); Thread th1 = new Thread(tt, "1號窗口"); Thread th2 = new Thread(tt, "2號窗口"); th1.start(); try { Thread.sleep(40); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } ThreadTrain1.flag = false; th2.start(); } }
點擊+號查看代碼,代碼中的執行結果是絕對正確的,我們是采用一個線程使用同步代碼塊,另一個線程使用同步函數的方式,看是否會發生數據錯誤,作為對比,下面的代碼中同步代碼塊我們不使用this,而是使用obj這個對象:

public class ThreadTrain1 implements Runnable { private int tickets = 100; private static Object obj = new Object(); private static boolean flag = true; @Override public void run() { if (flag) { try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized (obj) {// 同步代碼塊 while (tickets > 0) { if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "賣了第" + (100 - tickets + 1) + "張票"); tickets--; } } } } else { while (tickets > 0) { sale(); } } } public synchronized void sale() { if (tickets > 0) { try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "賣了第" + (100 - tickets + 1) + "張票"); tickets--; } } public static void main(String[] args) { ThreadTrain1 tt = new ThreadTrain1(); Thread th1 = new Thread(tt, "1號窗口"); Thread th2 = new Thread(tt, "2號窗口"); th1.start(); try { Thread.sleep(40); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } ThreadTrain1.flag = false; th2.start(); } }
顯然,這段代碼最后會出現數據沖突的情況,因為兩個線程拿到的不是同一把鎖,也證明了同步函數鎖的是this。
明白了同步函數的鎖是this,那么加上static以后,鎖的對象會不會發生改變,還是依然是this???
先鎖this,驗證是否是this:

public class ThreadTrain1 implements Runnable { private static int tickets = 100; private static boolean flag = true; @Override public void run() { if (flag) { try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized (this) {// 同步代碼塊 while (tickets > 0) { if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "賣了第" + (100 - tickets + 1) + "張票"); tickets--; } } } } else { while (tickets > 0) { sale(); } } } public static synchronized void sale() { if (tickets > 0) { try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "賣了第" + (100 - tickets + 1) + "張票"); tickets--; } } public static void main(String[] args) { ThreadTrain1 tt = new ThreadTrain1(); Thread th1 = new Thread(tt, "1號窗口"); Thread th2 = new Thread(tt, "2號窗口"); th1.start(); try { Thread.sleep(40); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } ThreadTrain1.flag = false; th2.start(); } }
出現了數據錯誤,這里我們不做猜測,只做驗證,靜態的同步函數鎖的是當前類的字節碼文件,代碼驗證:

public class ThreadTrain1 implements Runnable { private static int tickets = 100; private static boolean flag = true; @Override public void run() { if (flag) { try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized (ThreadTrain1.class) {// 同步代碼塊 while (tickets > 0) { if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "賣了第" + (100 - tickets + 1) + "張票"); tickets--; } } } } else { while (tickets > 0) { sale(); } } } public static synchronized void sale() { if (tickets > 0) { try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "賣了第" + (100 - tickets + 1) + "張票"); tickets--; } } public static void main(String[] args) { ThreadTrain1 tt = new ThreadTrain1(); Thread th1 = new Thread(tt, "1號窗口"); Thread th2 = new Thread(tt, "2號窗口"); th1.start(); try { Thread.sleep(40); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } ThreadTrain1.flag = false; th2.start(); } }
多線程死鎖
同步中嵌套同步,鎖沒有來得及釋放,一直等待,就導致死鎖。
下面這段代碼,多運行幾次就會出現死鎖,思路是開啟兩個線程,讓這兩個線程執行的代碼獲取的鎖的順序不同,第一個線程需要先獲得obj對象鎖,然后再獲得this鎖,才可以執行代碼,然后釋放兩把鎖。線程2需要先獲得this鎖,再獲取obj對象鎖才可執行代碼,然后釋放兩把鎖。但是,當線程1獲得了obj鎖之后,線程2獲得了this鎖,這時候線程1需要獲得this鎖才可執行,但是線程2也無法獲取到obj對象鎖執行代碼並釋放,所以兩個線程都拿着一把鎖不釋放,這就產生了死鎖。

public class ThreadTrain3 implements Runnable { private static int tickets = 100; private static Object obj = new Object(); private static boolean flag = true; @Override public void run() { if (flag) { while (true) { System.out.println("111111"); synchronized (obj) {// 同步代碼塊 sale(); } } } else { while (true) { System.out.println(222222); sale(); } } } public synchronized void sale() { synchronized(obj){ if (tickets > 0) { try { Thread.sleep(50); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "賣了第" + (100 - tickets + 1) + "張票"); tickets--; } } } public static void main(String[] args) { ThreadTrain3 tt = new ThreadTrain3(); Thread th1 = new Thread(tt, "1號窗口"); Thread th2 = new Thread(tt, "2號窗口"); th1.start(); try { Thread.sleep(40); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } ThreadTrain3.flag = false; System.out.println(flag); th2.start(); } }
多線程的三大特性
原子性
原子性就是在執行一個或者多個操作的過程中,要么全部執行完不被任何因素打斷,要么不執行。比如銀行轉賬,A賬戶減去100元,B賬戶必須增加100元,對這兩個賬戶的操作必須保證原子性,才不會出現問題。還有比如:i=i+1的操作,需要先取出i,然后對i進行+1操作,然后再給i賦值,這個式子就不是原子性的,需要同步來實現數據的安全。
原子性就是為了保證數據一致,線程安全。
可見性
當多個線程訪問同一個變量時,一個線程修改了變量的值,其他的線程能立即看到,這就是可見性。
這里講一下Java內存模型?簡稱JMM,決定了一個線程與另一個線程是否可見,包括主內存(存放共享的全局變量)和私有本地內存(存放本地線程私有變量)
本地私有內存存放的是共享變量的副本,線程操作共享變量,首先操作的是自己本地內存的副本,當同一時刻只有一個線程操作共享變量時,該線程操作完畢本地內存,然后會刷新到主內存,然后主內存會通知另一個線程,進而更新;但是如果同一時刻有多個線程操作共享變量,會來不及更新主內存進而通知其他線程更新變量,就會出現沖突問題。
有序性
就是程序的執行順序會按照代碼先后順序進行執行,一般情況下,處理器由於要提高執行效率,對代碼進行重排序,運行的順序可能和代碼先后順序不同,但是結果一樣。單線程下不會出現問題,多線程就會出現問題了。
volatile
保證可見性,但是不保證原子性。
下面這個案例10個線程共享同一個count,進行+1操作:
public class VolatileTest extends Thread{ private volatile static int count = 0; @Override public void run() { for (int i = 0; i < 100; i++) { count++; } System.out.println(Thread.currentThread().getName()+":"+count); } public static void main(String[] args) { VolatileTest[] list = new VolatileTest[10]; for (int i = 0; i < list.length; i++) { list[i] = new VolatileTest(); } for (int i = 0; i < list.length; i++) { list[i].start(); } } }
多運行幾次,就會出現最后結果有不到1000的情況,也就證明了volatile不會保證原子性。
保證原子性,jdk1.5之后,並發包提供了很多原子類,例如AtomicInteger :
public class VolatileTest2 extends Thread{ private static AtomicInteger count = new AtomicInteger(); @Override public void run() { for (int i = 0; i < 100; i++) { count.incrementAndGet(); } System.out.println(Thread.currentThread().getName()+":"+count.get()); } public static void main(String[] args) { VolatileTest2[] list = new VolatileTest2[10]; for (int i = 0; i < list.length; i++) { list[i] = new VolatileTest2(); } for (int i = 0; i < list.length; i++) { list[i].start(); } } }
AtomicInteger解決了同步, 最后的結果最大的肯定是1000