Java-多線程-基礎 - && synchronized && volatile

public class T implements Runnable {
    private int cnt = 10;
    @Override
    public void run() {
        cnt--;
        System.out.println(Thread.currentThread().getName() + " cnt = " + cnt);
    }
    public static void main(String[] args){
        T t = new T();
        for (int i = 0; i < 5; i++) {
            new Thread(t, "THREAD " + i).start();
        }
    }
}

輸出:

THREAD 1 cnt = 8
THREAD 2 cnt = 7
THREAD 0 cnt = 9
THREAD 3 cnt = 6
THREAD 4 cnt = 5

開了5個線程,每一個線程都去將cnt減一次,但是由於是多線程,可能存在的問題是,當第一個線程執行cnt--的時候,此時還沒有打印的時候,第二個線程就又來了,將cnt--,然後才執行了第一個線程得輸出,所以第一次輸出就出現了8的錯誤。

並且上面的通過繼承Runnable 接口並實現其run 得方法,進行多線程得編程。

synchronized關鍵字

同步鎖,其作用就是將當線程序執行的對像鎖住,不讓其他線程來執行這個對象,當第一個線程執行結束之後再將這個對象給第二個線程。這樣就不會出現上面的現象。

public class T implements Runnable {
    private int cnt = 10;
    @Override
    public synchronized void  run() {
        cnt--;
        System.out.println(Thread.currentThread().getName() + " cnt = " + cnt);
    }
    public static void main(String[] args){
        T t = new T();
        for (int i = 0; i < 5; i++) {
            new Thread(t, "THREAD " + i).start();
        }
    }
}

synchronized關鍵字鎖住的是對象,而不是當前執行的方法代碼,如下代碼

public class T implements Runnable {
    private int cnt = 10;
    @Override
    public synchronized void  run() {
        cnt--;
        System.out.println(Thread.currentThread().getName() + " cnt = " + cnt);
    }
    public static void main(String[] args){
        for (int i = 0; i < 5; i++) {
            T t = new T();
            new Thread(t, "THREAD " + i).start();
        }
    }
}

執行之後輸出的都

THREAD 0 cnt = 9
THREAD 4 cnt = 9
THREAD 3 cnt = 9
THREAD 2 cnt = 9
THREAD 1 cnt = 9

每個線程每次執行都new 一個新的對象,每個線程都去鎖住new 出來的t,共5個,但是並不知道哪個線程先被cpu執行,所以THREAD亂的。

如下程序:

public class T  {
    public synchronized void m1() {
        System.out.println(Thread.currentThread().getName() + " m1 start");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {e.printStackTrace();}
        System.out.println(Thread.currentThread().getName() + " m1 end");
    }
    public void m2() {
        System.out.println(Thread.currentThread().getName() + " m2 start");
    }
    public static void main(String[] args){
        T t = new T();
        new Thread(() -> t.m1(), "m1").start();
        new Thread(() -> t.m2(), "m2").start();
    }
}
輸出:
m1 m1 start
m2 m2 start
m1 m1 end

這個程序主要說明的地方是

  1. 使用 new Thread(() -> t.m1(), "m1").start(); 創建多線程得編碼方式。還可以寫爲 new Thread(t::m1(), "m1").start();
  2. 在同步鎖方法執行的過程中,還是可以去執行非同步的方法的。在第一個線程同步方法m1休眠得時候,第二個線程又去執行非同步方法。也就是說多個線程執行加同步鎖方法的時候會排隊,但是多個線程中加同步鎖和不加同步鎖的方法執行的時候不會存在等待機制。

如下最典型的例子,同步鎖去寫數據,非同步鎖去讀數據,讀出來得數據並不是我們所希望的。

public class Account {
    private String name;
    private Integer balance=0;
    public synchronized void set(String name, Integer balance) {
        this.name = name;
        try {  Thread.sleep(2000);
        } catch (InterruptedException e) { e.printStackTrace();}
        this.balance = balance;
    }
    public Integer getBalance() {return balance;}
    public static void main(String[] args){
        Account account = new Account();
        new Thread(() -> account.set("zhangsan", 1000)).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {e.printStackTrace();}
        System.out.println("第一次讀取 "+account.getBalance());
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {e.printStackTrace();}
        System.out.println("第二次讀取 "+account.getBalance());
    }
}
輸出結果:
第一次讀取 0
第二次讀取 1000

這就是上面提到的『髒讀』的問題,set 修改balance得時候此時還沒有set值得時候,getBalance 來讀取它,結果是0,二第二次讀取的時候,已經賦值了,就是1000,這就是多線程同步鎖只會取鎖住set 方法,但是在執行這個方法的時候,其他非同步鎖得方法還是可以執行的,

解決上面『髒讀』問題方法是在 getBalance 方法上也加一把鎖,這樣,set 方法在執行的時候鎖住了 account對象,getBalnce方法就不能去執行了,直到第一個線程結束

一個線程已經擁有某個對象的鎖,再次申請的時候仍然會得到該對象的鎖.

public class Account {
    private synchronized void m1(){
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2();
    }
    private synchronized void m2(){
        System.out.println("m2 start");
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args){
        Account account = new Account();
        new Thread(() ->account.m1()).start();
    }
}

對t執行m1的時候,需要在t上面加把鎖,拿到這個鎖了,開始執行,執行鎖定的過程之中,調用了m2();調用m2的過程中,發現m2也是需要申請一把鎖,而申請的這把鎖就是當前自己已經持有的這把鎖;嚴格來講,這把鎖m1已經持有了,m2還能持有嗎?由於是在同一個線程裏面,這個是沒關係的。它可以再去申請我自己已經擁有的這把鎖,實際上就在這把鎖上加個數字,從1變成2,鎖定了2次。總而言之,再去申請當前持有的這把鎖沒問題,仍然會得到該對象的鎖。一個同步方法可以調用另外一個同步方法,一個線程已經擁有某個對象的鎖,再次申請的時候仍然會得到該對象的鎖,也就是說synchronized獲得的鎖是可重入的。

重入鎖的另外一種情形,繼承中子類的同步方法調用父類的同步方法,結論和上面的都是一樣的。

public class Account {
     synchronized void m1(){
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args){
        TT t = new TT();
        new Thread(() ->t.m()).start();
    }
}
class TT extends Account{
    synchronized void m(){
        System.out.println("child m start");
        super.m1();
        System.out.println("child m end");
    }
}
輸出:
child m start
m1 start
child m end

synchronized 同步方法如果遇到異常就會釋放。所以在併發處理的時候,需要異常的小心,不然就會產生不一致得問題,如在web app中多個servlet多線程共同訪問同一個資源的時候,如果異常處理不合適,那麼當第一個線程拋出異常,而其他就會靜茹同步代碼區(之前那個釋放了,所以其他可以進入),可能會訪問到上次只處理到一半的有問題的數據。

public class Account {

    private int cnt=0;
     synchronized void m1(){
        while (true){
            cnt ++;
            System.out.println(Thread.currentThread().getName() + ": " + cnt);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (cnt==5){
                int i=1/0;
                // 這裏會拋出異常,這個線程就會結束
            }

            // 處理異常,catch住,然後不釋放鎖
            //if (cnt==5){
            //    try {
            //    int i=1/0;
            //    // 這裏會拋出異常,這個線程就會結束
            //    }catch (Exception e){
            //        System.out.println(e.getMessage());
            //    }
            //}
        }
    }
    public static void main(String[] args){
        Account t = new Account();
        new Thread(() ->t.m1(), "t1").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() ->t.m1(), "t2").start();
    }
}
輸出:
t1: 1
t1: 2
t1: 3
t1: 4
t1: 5
t2: 6
Exception in thread "t1" java.lang.ArithmeticException: / by zero
	at Basic.Account.m1(Account.java:21)
	at Basic.Account.lambda$main$0(Account.java:39)
	at java.lang.Thread.run(Thread.java:748)
t2: 7
處理異常後的輸出
t1: 1
t1: 2
t1: 3
t1: 4
t1: 5
/ by zero
t1: 6
t1: 7

這裏打印就可以看出,這種因爲異常二導致的線程退出,線程t1可能是在處理某事情,例如修改數據庫,數據修改列一半結果就遇到異常退出了,而第二個線程如果是來讀第一個線程修改了的東西,那麼就會 『髒讀』,所以處理異常是一個非常腰小心的事情。

一個處理的方法就是,當拋出異常之後去catch,讓鎖不釋放,繼續執行。

volatile關鍵字

public class Account {
    volatile boolean running = true;
    void m(){
        System.out.println("m start");
        while (running){
        }
        System.out.println("m end");
    }
    public static void main(String[] args){
        Account account = new Account();
        new Thread(() -> account.m()).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.running=false;
    }
}

volatile 關鍵字,是一個變量在多個線程間可見,加入現在有A,B兩個線程都會使用到同一個變量,那麼java默認是A線程保留一份copy,這樣如果B線程修改列該變量,則A線程未必直到,而使用volatile關鍵字,會讓所有線程都會讀到變量修改。在執行每個線程得時候,每個線程都會去堆內存中去讀取running值,這個功能在使用synchronized也是有的,但是volatile並不會去鎖住對象,也就是說volatile 是要比synchronized要輕量。

不加volatile得情況下,上面程序執行的過程是這樣的:

線程之間要讓running這個值進行可見,這裏要涉及到java的內存模型,java對於線程處理的內存模型;

在jmm(java memory model)裏面有個內存它叫主內存,我們所熟識的棧內存,堆內存都可以認爲是主內存;每一個線程在執行的過程之中,它有一個線程自己的一塊內存,(實際上不能認爲這塊是內存,有可能它是內存,還有cpu上的緩衝區,是一個統稱,就是線程存放它自己變量的一塊內存),如果兩個cpu在運行不同線程的話,每個線程上都有自己的一塊緩衝區,緩衝區就是把主內存JMM裏面的內容讀過來在緩衝區裏面進行修改,如果+1,+1加了好多次再寫回去;

現在有個running在主內存裏面,值是true,佔一個字節;

第一個線程啓動的時候會把這個字節copy到自己的緩衝區裏面,cpu在處理的過程之中就不再去主內存裏面讀了;它在運行這個線程的過程之中,由於這個cpu非常的忙,在while(running)裏面,沒空再去主線程裏面去刷一下running值了;它一直讀自己緩存裏面的內容,running永遠是true;

第二個主線程裏面,它首先也是把running讀到它自己的緩衝區,然後把running改成false,發現running已經改了那就把running寫回到主內存裏面去;寫回到主內存之後,但是第一個線程它沒有在主內存重新讀啊,所以第一個線程永遠結束不了;

加了volatile之後的情況是這樣的

加了volatile,第一個線程運行中,不是要求你每次while(running)循環的時候都要到主內存裏面讀一次running的值,而是說一旦主內存running這個值發生改變後會通知別的線程,說你們的緩衝區裏面內容過期了請重新讀一下,第一個線程再去讀的時候running已經改了,所以線程結束了。

加了volatile的意思就是當running改了後會通知其他的所有線程的緩衝區,說你們那邊的值已經過期了,請你們再去主內存裏面重新讀一下。

而並不是通知所有的線程cpu執行的時候每次用的時候都要去主內存讀一下,不是,是寫完之後進行緩存過期通知。

要保證線程之間的可見性,那麼需要對兩個線程共同訪問的變量加上volatile;如果不想加volatile那隻能用synchronized;但volatile的效率要比synchronized高的多;所以在很多高併發的框架裏面好多的volatile關鍵字都在用;比如JDK的併發容器的源碼;能用volatile的時候就不要加鎖,程序的併發性就要提高很多;

volatile並不能保證多個線程共同修改running變量時所帶來的不一致問題,

也就是說volatile不能替代synchronized。

volatile只保證可見性,並不保證原子性。(原子性計算不可以在拆分 如:int i = 4;但是 a+=1; 這就是可拆分)

synchronized既保證可見性,又保證原子性;但效率要比volatile低不少。

如果只需要保證可見性的時候,使用volatile,不要使用synchronized。

synchronized得優化

使用synchrinized 得同步代碼快中的語句越少越好。

如在上面的代碼中都是對整個方法進行使用synchrinized修飾,可以改進爲:

synchronized(this) {
            count ++;
}

這樣同步代碼塊,這時候不應該給整個方法上鎖,採用細粒度得鎖,使得線程爭用得時間變短,從而提高效率。

應該避免鎖定對象的引用變爲另外的對象

public class Account {
    volatile boolean running = true;

    Object o = new Object();

    void m(){
        synchronized (o){
            while (true){
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args){
        Account t = new Account();

        new Thread(t::m, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread t2 = new Thread(t::m, "t2");
        t.o = new Object();  ////--------****------////

        t2.start();
    }

在有 * 標註的那行,將鎖的對象切換,從事是的線程2開始執行,如果對象沒有發送改變,那麼線程2將不會得到執行,鎖的對象發生改變,就不需要鎖原來的對象,直接鎖新對象就行了;而新對象還沒有鎖的,所以t2線程就被執行了;這就證明這個鎖是鎖在什麼地方?是鎖在堆內存裏new出來的對象上,不是鎖在棧內存裏頭o的引用,不是鎖的引用,而是鎖new出來的真正的對象;鎖的信息是記錄在堆內存裏的

在多線程編程得時候,需要注意的是,不要以字符常量作爲鎖得對象。

public class T {   
    String s1 = "Hello";
    String s2 = "Hello";
    void m1() {
        synchronized(s1) {           
        }
    }   
    void m2() {
        synchronized(s2) {
            
        }
    }
}
 * 不要以字符串常量作爲鎖定對象
 * 在下面的例子中,m1和m2其實鎖定的是同一個對象
 * 這種情況還會發生比較詭異的現象,比如你用到了一個類庫,在該類庫中代碼鎖定了字符串“Hello”,
 * 但是你讀不到源碼,所以你在自己的代碼中也鎖定了"Hello",這時候就有可能發生非常詭異的死鎖阻塞,
 * 因爲你的程序和你用到的類庫不經意間使用了同一把鎖

上面提到的,同步鎖鎖得是堆內存裏面new 出來的對象,雖然m1和m2是兩個變量,但是指向得是到堆內存中的同一個對象

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章