Java——一篇文章瞭解高併發編程基礎知識點

synchronized關鍵字

synchronized 的含義:

  • Java中每一個對象都可以成爲一個監視器(Monitor), 該Monitor由一個鎖(lock), 一個等待隊列(waiting queue ), 一個入口隊列( entry queue).
  • 對於一個對象的方法, 如果沒有synchronized關鍵字, 該方法可以被任意數量的線程,在任意時刻調用。
  • 對於添加了synchronized關鍵字的方法,任意時刻只能被唯一的一個獲得了對象實例鎖的線程調用。
  • synchronized用於實現多線程的同步操作
  • 給某個對象加鎖;
  • synchronized鎖定的是一個對象,而不是代碼塊,該對象可以由我們指定;
  • 某個方法從頭到尾都需要鎖定,則synchronized可以寫在方法上,鎖定的還是this對象;
  • synchronized如果用在一個靜態方法上,相當於鎖定的是T.class這個對象;
  • 被synchronized包圍的代碼塊可以視爲一個原子操作,但是其中發生未處理的異常,則鎖會被自動釋放,此時其他線程就能重入該方法;
  • 當可以理解鎖是加給某個對象的本質就不難分析出諸如:“同步方法和非同步方法是否可以同時調用”的問題。當然可以,比如t1的m1方法是同步方法,t2的m2不是同步方法,t1的m1執行的間隙,調度器也可以調度t2執行m2,因爲m1執行需要獲得當前對象的鎖,而m2不需要,即使鎖被佔用,也一樣可以執行;抑或“同步非靜態方法和同步靜態方法是否可以同時執行?”,一樣前者鎖定的是this對象,後者鎖定的是當前類的類對象,是兩塊不同的空間,所以當然可以同時執行;
  • 對業務“寫方法”加鎖,對“讀”方法不加鎖,會出現髒讀的問題;
  • 同一個類中(兩個方法都是加鎖在this上),一個同步方法可以調用另外一個同步方法,因爲調用方法獲得鎖之後,被調用的方法在該方法中申請該鎖仍然可以獲得,synchronized是可重入的
  • 調用父類的synchronized方法,鎖住的是子類的對象,而不是父類的對象。所以子類同步方法可以調用父類的同步方法(有疑問?) 對於同步方法,誰調用鎖定的對象就是誰,子類方法中通過super調用父類同步方法,JVM認爲調用者依然是子類;
  • 同步方法調用非同步方法,非同步方法依然可以重入;

volatile關鍵字

1、保證內存可見性;2、防止指令重排;此外需注意volatile並不保證操作的原子性。

  • A、B線程都用到一個變量V,java默認是A、B線程中都保留一份copy,這樣如果B修改了變量V,線程A未必知道。使用Volatile關鍵字,會讓所有線程操作V前去再讀一次變量V。(使用volatile關鍵字,會強制所有線程去堆內存中讀取變量V)
  • volatile不能保證多個線程共同修改同一個變量V所帶來的不一致的問題,也就是說volatile不能替代sychronized。
  • synchronized既可以保證可見性也可以保證原子性,而volatile只能保證可見性;
  • 參考

AtomicXXX類

  • 爲解決多線程對一個數字變量遞增遞減操作的原子性和可見性可以使用AtomicXXX類。AtomicXXX類本身方法都是具有原子性的,但是不能保證多個方法連續調用時的原子性;

synchronized優化

  • synchronized優化,同步代碼塊語句越少越好。
  • 鎖定某個對象o,如果o的屬性發生改變,不影響鎖的使用。但是如果o變成另一個對象,則鎖定的對象發生改變鎖失效;應當避免將鎖定對象引用變成另外一個對象
  • 不要以字符串常量作爲鎖定對象;
//s1和s2其實是用一個對象,
//都在字符串常量池裏
String s1 = "lock";
String s2 = "lock";
synchronized(s1)
synchronized(s2)
 //下面這倆個不是同一個對象
String s1 = new String("hello");
String s2 = new String("hello");

wait和notify/notifyAll

  • 是Object的方法
  • wait會釋放鎖,notify不會釋放鎖,只會喚醒其他掛起的線程,等待調度器調度;
  • 爲什麼wait()和notify()/notifyAll()需要搭配synchonized關鍵字使用;如果不搭配使用:
// 線程A 的代碼
while(!condition){ 
// 不能使用 if ,
//因爲存在一些特殊情況,
//使得線程沒有收到notify
//時也能退出等待狀態
    wait();
}
// do something

// 線程 B 的代碼
if(!condition){ 
    // do something ...
    condition = true;
    notify();
}
  • 現在考慮, 如果wait() 和 notify() 的操作沒有相應的同步機制, 則會發生如下情況:
  1. 【線程A】 進入了 while 循環後突然被掛起
  2. 【線程B】 執行完畢了 condition = true; notify(); 的操作, 此時【線程A】的 wait() 操作尚未被執行, notify() 操作沒有產生任何效果
  3. 【線程A】執行wait() 操作, 進入等待狀態,如果沒有額外的 notify() 操作, 該線程將持續在 condition = true 的情形下, 持續處於等待狀態得不到執行。
  • 永遠不要在循環之外調用wait方法【《Effective Java》第二版中文版第69條244頁】
public synchronized void put(T t){
        while (lists.size()==MAX){//想想爲什麼用while而不用if
            try{
                this.wait();
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
        lists.add(t);//防止在執行這句話前別的線程向容器裏put了一個,size到MAX了,此時在執行就超出容器MAX容量了,用while可以循環檢查,避免這種情況
        ++count;
        this.notifyAll();//爲什麼要notifyAll,因爲notify只會叫醒一個線程,可能還會是生產者,如果每次都叫醒生產者,就會導致程序執行不下去

    }

    public synchronized T get(){
        T t = null;
        while (lists.size() == 0){
            try{
                this.wait();//一般wait會和while聯用
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        t = lists.removeFirst();
        count--;
        this.notifyAll();//effective java 永遠用notifyAll不要用notify
        return t;
    }
  • 假如有兩個生產者線程程序滿足條件都執行完wait()之後處於掛起狀態,此時有個消費者消費了一個,並notifyAll()喚起所有線程;

  • 如果是用的while,在執行完wait()掛起時還在while循環體中,被喚醒後被調度,會再次執行while判斷,決定是否要執行生產;

  • 而用if,在執行完wait()掛起時還在if中,但是不會再去判斷容器是否滿了,會直接生產向容器中增加元素,可能導致溢出;

  • 喚醒線程一定要用notifyAll();因爲notify只會叫醒一個線程,可能還會是生產者,如果每次都叫醒生產者,就會導致程序執行不下去

  • wait和notify/notifyAll整個通信過程比較複雜

Latch(門閂)

使用Latch(門閂)替代wait notify來進行通知

  • 好處是通信方式簡單,同時也可以指定等待時間
  • 使用await和countdown方法代替wait和notify
  • CountDownLatch不涉及鎖定,當count的值爲零時當前線程繼續運行
  • 當不涉及同步,只涉及線程通信的時候,用synchronized+wait/notify就顯得太重了
  • 這是就應該考慮countdownlatch/cyclicbarrier/semaphore

ReentrantLock重入鎖

①Lock lock = new ②ReentrantLock();
③lock.lock();
④lock.unlock();//必須要手動釋放鎖
  • 可以替代synchronized,使用reentrantlock可以完成和synchronized相同的功能;
  • reentrantlock需要手動釋放鎖;
  • 使用synchronized鎖定如果遇到異常,jvm會自動釋放鎖,但是reentrantlock必須手動釋放鎖;
  • 通常在finally中進行釋放鎖。
  • 使用reentrantlock可以進行嘗試鎖定(嘗試獲得鎖),不管是否獲得鎖,方法都繼續執行
  • 可以根據tryLock的返回值來判斷是否鎖定,然後執行不同的業務邏輯
  • tryLock可以指定時間,tryLock(time),嘗試等待幾秒(一段時間)去獲得鎖,然後繼續執行。由於tryLock(time)拋出異常,所以注意unlock處理,必須放到finally裏去釋放鎖
  • 相比之下reentrantlock比synchronized更加靈活。
  • 使用reentrantlock可以在某個線程中調用lock.lockInterruptibly()方法,這樣lock會對interrupt方法做出響應,其他線程可以通知該線程中斷,不要再死等了;
  • Reentrantlock 可以使用公平鎖,之前的鎖都是效率優先屬於不公平的,由調度器選擇調度
//參數爲true標識爲公平鎖
ReentrantLock lock = new ReentrantLock(true);

Condition

Condition是在java 1.5中才出現的,它用來替代傳統的Object的wait()、notify()實現線程間的協作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()這種方式實現線程間協作更加安全和高效。

  • Conditon中的await()對應Object的wait();
  • Condition中的signal()對應Object的notify();
  • Condition中的signalAll()對應Object的notifyAll()
private Condition producer = lock.newCondition();
private Condition customer = lock.newCondition();//條件
producer.await();//生產者等着
customer.signalAll();//喚醒所有消費者
  • Condition的方式可以更加精確的指定哪些線程被喚醒,效率比notifyAll好

ThreadLocal

Java中的ThreadLocal類允許我們創建只能被同一個線程讀寫的變量。因此,如果一段代碼含有一個ThreadLocal變量的引用,即使兩個線程同時執行這段代碼,它們也無法訪問到對方的ThreadLocal變量。

  • 在不同線程中是兩個不同對象
  • 線程中使用完ThreadLocal變量後,要記得及時remove掉。ThreadLocal可能會導致內存泄漏(內存泄漏指由於疏忽或錯誤造成程序未能釋放已經不再使用的內存。);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章