有助於提升"鎖"性能的幾點建議

最近閱讀《java高併發編程一書》大概總結幾條,也是書中的內容java

1.減少鎖持有的時間

好比100我的去銀行辦理業務,要填一百張表,可是隻有一支筆,那麼很顯然,每一個人用筆的時間越短,效率也就月高:看代碼:node

/*
othercode1和othercode2很耗時間,裏面沒有涉及資源同步,只有mutexMethod方法要對資源同步,
全部優化代碼讓持有鎖時間儘可能短
*/

public synchronized void syncMethod(){
        othercode1();
        mutexMethod();
        othercode2();
}

public  void syncMethod(){
        othercode1();
        synchronized(this){
            mutexMethod();
        }
        othercode2();
}
//在jdk源碼裏面也很容易找到這種手段,好比處理正則表達式的Pattern類
public Matcher matcher(CharSequence input) {
        if (!compiled) {
            synchronized(this) {
                if (!compiled)
                    compile();
            }
        }
        Matcher m = new Matcher(this, input);
        return m;
}
//只有在表達式未編譯的時候進行局部加鎖,這種方法大大提升了matcher的執行效率和可靠性

注意:減小鎖的持有時間有助於下降鎖衝突的可能性,進而提高系統的併發能力正則表達式

2.減少鎖的力度

concurrentHashMap的實現,他的內部被分爲了若干個曉得hashmap,稱之爲段(SEGMENT),默認是16段編程

減少鎖粒度會引入一個新的問題,當須要獲取全局鎖的時候,其消耗的資源會較多,不如concurrenthashMap的size()方法.能夠看到計算size的時候須要計算所有有效的段的鎖性能優化

public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
}

final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

事實上計算size的時候會先使用無鎖的方式計算,若是失敗會採用這個方法,可是在高併發的場合concurrenthashmap的size依然要差於同步的hashmap.所以在相似於size獲取全局信息方法調用不頻繁的狀況下,這種減少粒度的的方法纔是真正意義上的提升系統併發量多線程

注意:所謂減少鎖粒度,就是指縮小鎖定對象的範圍,從而減少鎖衝突的可能性,進而提升系統性能併發

3.讀寫分離來替換獨佔鎖

在讀多寫少的狀況下,使用讀寫鎖能夠有效的提升系統性能 ReadWriteLock能夠提升系統性能app

package com.high.concurrency;

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author: zhangzeli
 * @date 8:43 2018/4/10
 * <P></P>
 */
public class ReadWriteLockDemo {
    private static Lock lock = new ReentrantLock();
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock =readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private int value;

    public Object handleRead(Lock lock) throws InterruptedException{
        try {
            lock.lock();
            Thread.sleep(1000);
            return value;
        }finally {
            lock.unlock();
        }
    }

    public void handleWrite(Lock lock,int index) throws InterruptedException{
        try {
            lock.lock();
            Thread.sleep(1000);
            value =index;
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final ReadWriteLockDemo demo = new ReadWriteLockDemo();
        Runnable readRunnale = new Runnable() {
            @Override
            public void run() {
                try {
                    demo.handleRead(lock);
                    //demo.handleRead(readLock);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Runnable write = new Runnable() {
            @Override
            public void run() {
                try {
                    //demo.handleWrite(writeLock,new Random().nextInt());
                    demo.handleWrite(lock,new Random().nextInt());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        for(int i=0;i<18;i++){
            new Thread(readRunnale).start();
        }
        for(int i=18;i<20;i++){
            new Thread(write).start();
        }
    }
}

差別很明顯.less

3.鎖分離

    已LinkedBlockingQueue爲例,take函數和put函數分別實現了衝隊列取和往隊列加數據,雖然兩個方法都對隊列進項了修改,可是LinkedBlockingQueue是基於鏈表的因此一個操做的是頭,一個是隊列尾端,從理論狀況下將並不衝突dom

    若是使用獨佔鎖則take和put就不能完成真正的併發,因此jdk並無才用這種方式取而代之的是兩把不一樣的鎖分離了put和take的操做,下面看源碼

/** Lock held by take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();//take函數須要持有takeLock

    /** Wait queue for waiting takes */
    private final Condition notEmpty = takeLock.newCondition();

    /** Lock held by put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();//put函數須要持有putLock

    /** Wait queue for waiting puts */
    private final Condition notFull = putLock.newCondition();
public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly(); //不能有兩個線程同時取數據
        try {
            while (count.get() == 0) { //若是當前沒有可用數據,一直等待
                notEmpty.await();      //等待,put操做的通知
            }
            x = dequeue();         //取得第一個數據
            c = count.getAndDecrement();//數量減一,原子操做由於回合put同時訪問count.注意變量c是count減一
            if (c > 1)
                notEmpty.signal();  //通知其餘take操做
        } finally {
            takeLock.unlock(); //釋放鎖
        }
        if (c == capacity)
            signalNotFull(); //通知put,已有空餘空間
        return x;
    }


public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();//不能有兩個線程同時進行put
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             */
            while (count.get() == capacity) { //若是隊列已滿
                notFull.await();   //等待
            }
            enqueue(node);  //插入數據
            c = count.getAndIncrement(); //更新總數,變量c是count加1前的值
            if (c + 1 < capacity)
                notFull.signal();   //有足夠的空間,通知其餘線程
        } finally {
            putLock.unlock();  //釋放鎖
        }
        if (c == 0)
            signalNotEmpty();  //插入成功後,通知take操做
    }

4.鎖粗化

虛擬機在遇到一連串地對同一鎖不斷進行請求和釋放的操做時,便會把全部的鎖操做整合成對鎖的一次請求,從而減少對鎖的請求同步次數,這個操做叫鎖粗話,好比

for (int i=0;i<20;i++){
     synchronized (lock){
                
     }
}

//優化後
synchronized (lock){
    for (int i=0;i<20;i++){
     
                
     }
}

注意:性能優化就是根據運行時的真實狀況對各個資源點進行權衡折中的過程,鎖粗話的思想和減小鎖持有時間是相反的,可是在不一樣的場合,他們的效果並不相同,因此你們要根據實際狀況,進行權衡

5.java虛擬機對鎖優化所作的努力

5.1鎖偏向

偏向鎖,簡單的講,就是在鎖對象的對象頭中有個ThreaddId字段,這個字段若是是空的,第一次獲取鎖的時候,就將自身的ThreadId寫入到鎖的ThreadId字段內,將鎖頭內的是否偏向鎖的狀態位置1.這樣下次獲取鎖的時候,直接檢查ThreadId是否和自身線程Id一致,若是一致,則認爲當前線程已經獲取了鎖,所以不需再次獲取鎖,略過了輕量級鎖和重量級鎖的加鎖階段。提升了效率。

可是偏向鎖也有一個問題,就是當鎖有競爭關係的時候,須要解除偏向鎖,使鎖進入競爭的狀態

參數-XX:+UseBiasedLocking

Java偏向鎖(Biased Locking)是Java6引入的一項多線程優化。它經過消除資源無競爭狀況下的同步原語,
進一步提升了程序的運行性能。偏向鎖,顧名思義,它會偏向於第一個訪問鎖的線程,若是在接下來的運行過程當中,
該鎖沒有被其餘的線程訪問,則持有偏向鎖的線程將永遠不須要觸發同步。若是在運行過程當中,遇到了其餘線程搶佔鎖,
則持有偏向鎖的線程會被掛起,JVM會嘗試消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖。(偏向鎖只能在單線程下起做用)
所以 流程是這樣的 偏向鎖->輕量級鎖->重量級鎖

5.2輕量級鎖

輕量級鎖加鎖:線程在執行同步塊以前,JVM會先在當前線程的棧楨中建立用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。

而後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。若是成功,當前線程得到鎖,若是失敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

輕量級鎖解鎖:輕量級解鎖時,會使用原子的CAS操做來將Displaced Mark Word替換回到對象頭,若是成功,則表示沒有競爭發生。

若是失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。

注:輕量級鎖會一直保持,喚醒老是發生在輕量級鎖解鎖的時候,由於加鎖的時候已經成功CAS操做;而CAS失敗的線程,會當即鎖膨脹,並阻塞等待喚醒。(詳見下圖)

下圖是兩個線程同時爭奪鎖,致使鎖膨脹的流程圖。

鎖不會降級

自旋其實就是虛擬機爲了不線程真實的在操做系統層掛起,虛擬機讓當前線程作空輪詢或許是幾個cpu時間週期,若是還沒辦法獲取鎖則在掛起.

由於自旋會消耗CPU,爲了不無用的自旋(好比得到鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,
就不會再恢復到輕量級鎖狀態。
當鎖處於這個狀態下,其餘線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖以後會喚醒這些線程,
被喚醒的線程就會進行新一輪的奪鎖之爭。

5.3鎖消除

鎖消除是Java虛擬機在JIT編譯是,經過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,經過鎖消除,能夠節省毫無心義的請求鎖時間

public class TestLockEliminate {
    public static String getString(String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }

    public static void main(String[] args) {
        long tsStart = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            getString("TestLockEliminate ", "Suffix");
        }
        System.out.println("一共耗費:" + (System.currentTimeMillis() - tsStart) + " ms");
    }
}

getString()方法中的StringBuffer數以函數內部的局部變量,進做用於方法內部,不可能逃逸出該方法,所以他就不可能被多個線程同時訪問,也就沒有資源的競爭,可是StringBuffer的append操做卻須要執行同步操做:

@Override
public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
}

逃逸分析和鎖消除分別可使用參數-XX:+DoEscapeAnalysis和-XX:+EliminateLocks(鎖消除必須在-server模式下)開啓。使用以下參數運行上面的程序:

這裏寫圖片描述

使用以下命令運行程序:-XX:+DoEscapeAnalysis -XX:+EliminateLocks

這裏寫圖片描述

鎖的優缺點對比 

優勢

缺點

適用場景

偏向鎖

加鎖和解鎖不須要額外的消耗,和執行非同步方法比僅存在納秒級的差距。

若是線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。

適用於只有一個線程訪問同步塊場景。

輕量級鎖

競爭的線程不會阻塞,提升了程序的響應速度。

若是始終得不到鎖競爭的線程使用自旋會消耗CPU。

追求響應時間。

同步塊執行速度很是快。

重量級鎖

線程競爭不使用自旋,不會消耗CPU。

線程阻塞,響應時間緩慢。

追求吞吐量。

同步塊執行速度較長。

相關文章
相關標籤/搜索