多線程安全(synchronized、三大特性、Java內存模型)


線程安全問題?

什么是線程安全問題?簡單的說,當多個線程在共享同一個變量,做讀寫的時候,會由於其他線程的干擾,導致數據誤差,就會出現線程安全問題。

比如說,多個窗口同時賣票這個案例:

 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();
    }
}
View Code

 點擊+號查看代碼,代碼中的執行結果是絕對正確的,我們是采用一個線程使用同步代碼塊,另一個線程使用同步函數的方式,看是否會發生數據錯誤,作為對比,下面的代碼中同步代碼塊我們不使用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();
    }
}
View Code

顯然,這段代碼最后會出現數據沖突的情況,因為兩個線程拿到的不是同一把鎖,也證明了同步函數鎖的是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();
    }
}
View Code

出現了數據錯誤,這里我們不做猜測,只做驗證,靜態的同步函數鎖的是當前類的字節碼文件,代碼驗證:

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();
    }
}
View Code

 多線程死鎖

 同步中嵌套同步,鎖沒有來得及釋放,一直等待,就導致死鎖。

下面這段代碼,多運行幾次就會出現死鎖,思路是開啟兩個線程,讓這兩個線程執行的代碼獲取的鎖的順序不同,第一個線程需要先獲得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();
    }
}
View Code

 多線程的三大特性

原子性

原子性就是在執行一個或者多個操作的過程中,要么全部執行完不被任何因素打斷,要么不執行。比如銀行轉賬,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


免責聲明!

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



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