鎖的優化及注意事項

鎖優化的思路及方法html

一旦用到鎖,就說明這是阻塞式的,因此在併發度上來講,通常都會比無鎖的狀況低一些。java

這裏所講的鎖優化,是指在阻塞式的狀況下,經過優化讓性能不會變得太差。固然,不管怎樣優化,理論上來講性能都會比無鎖的狀況差一點。git

總結來講,鎖優化的思路和方法有以下幾種:程序員

  • 減小鎖的持有時間
  • 減少鎖粒度
  • 鎖分離
  • 鎖粗化
  • 鎖消除

減小鎖的持有時間

只對須要同步的代碼加鎖!github

只有在真正須要同步加鎖的時候才加鎖,以此減小鎖的持有時間,有助於減低鎖衝突的可能性,進而提高系統的併發能力。正則表達式

public synchronized void syncMethod(){
    otherCode1();
    mutextMethod();
    otherCode2();
}

像以上這代代碼,在進入方法以前就須要獲取鎖,此時其餘線程就要在方法外等待。編程

這裏優化的一點在於,要減小其餘線程等待的時間,因此,在在有線程安全要求的程序上加鎖。優化後的代碼以下:api

public void syncMethod(){
    otherCode1();
    
    synchronized (this) {
        mutextMethod();
    }
        
    otherCode2();
}

好比JDK中的處理正則表達式的java.util.regex.Pattern類安全

/**
 * Creates a matcher that will match the given input against this pattern.
 *
 * @param  input
 *         The character sequence to be matched
 *
 * @return  A new matcher for this pattern
 */
public Matcher matcher(CharSequence input) {
    if (!compiled) {
        synchronized(this) {
            if (!compiled)
                compile();
        }
    }
    Matcher m = new Matcher(this, input);
    return m;
}

 

減小鎖粒度

縮小鎖定對象的範圍,從而減小衝突的可能性。數據結構

下面以HashTable和ConcurrentHashMap爲例進行說明:

HashTable的數據結構看起來以下所示:

HashTable是使用了鎖來保證線程安全的,而且全部同步操做使用的都是用一個鎖對象。這樣如有n個線程同時要執行get,這n個線程要串行等待來獲取鎖,此時加鎖鎖住的是整個HashTable對象。

而JDK1.7中,ConcurrentHashMap採用了Segment + HashEntry的方式進行實現,結構以下:

 

對於ConcurrentHashMap來講,它減小鎖粒度就是將其內部結構再次分紅多個Segment,其中Segment在實現上繼承了ReentrantLock,這樣就自帶了鎖的功能。put時,只須要對key所在的Segment加鎖,而其餘Segment能夠並行讀寫,所以在保證線程安全的同時,兼顧了效率。只有在須要修改全局信息時,才須要對所有Segment加鎖。

 

鎖分離

根據功能將獨佔鎖分離!

鎖分離最多見的例子就是ReadWriteLock。與ReentrantLock獨佔所相比,它根據功能將獨佔鎖分離成讀鎖和寫鎖,這樣能夠作到讀讀不互斥,讀寫互斥,寫寫互斥,既保證了線程安全,又提升了性能。相似的還有LinkedBlockingQueue,take和put方法就是使用了takeLock、putLock兩把鎖實現,以此使得take和put操做能夠併發執行。

鎖粗化

爲了提升併發效率,咱們要減少持有鎖的時間,而後再釋放鎖。但凡事都有一個度,反覆對鎖進行請求也會浪費資源,下降性能。若是遇到一連串連續對同一鎖進行請求,那麼咱們就須要把全部鎖請求整合成對鎖的一次請求,這就是鎖的粗化。

synchronized (this) {
	for(int i = 0; i < 10000; i++) {
		count++;
	}
}

for(int i = 0; i < 10000; i++) {
	synchronized (this) {
		count++
	}
}

 

虛擬機內的鎖優化

Java虛擬機針對鎖優化,提供了偏向鎖、輕量級鎖、自旋鎖和鎖消除四種機制。

偏向鎖

所謂的偏向,就是偏愛,即鎖會偏向於當前已經佔有鎖的線程 。

核心思想是:若是一個線程得到了鎖,那麼所就進入偏向模式。當這個線程再次請求鎖時,無需再作任何同步操做,就能夠直接得到鎖。這樣就節省了有關鎖申請的操做,從而提升了程序的性能。所以,對於幾乎沒有鎖競爭的場合,偏向鎖有比較好的優化效果,由於連續屢次極有多是同一個線程請求相同的鎖。而對於鎖競爭比較激烈的場合,其效果不佳,由於在競爭激烈的場合,最有可能的狀況是每次都是不一樣的線程來請求相同的鎖。這種場景下,偏向模式會時效,還不如不啓用偏向鎖(每次都要加一次是否偏向的判斷)。

啓用偏向鎖:-XX:+UseBiasedLocking

輕量級鎖

java的多線程安全是基於Lock機制實現的,而Lock的性能每每不如人意。緣由是,monitorenter與monitorexit這兩個控制多線程同步的bytecode原語,是JVM依賴操做系統互斥(mutex)來實現的。

互斥是一種會致使線程掛起,並在較短的時間內又須要從新調度回原線程的,較爲消耗資源的操做。

爲了優化Java的Lock機制,從Java6開始引入了輕量級鎖的概念。

輕量級鎖(Lightweight Locking)本意是爲了減小多線程進入互斥的概率,並非要替代互斥。

若是偏向鎖失敗,那麼系統會利用CPU原語Compare-And-Swap(CAS)來嘗試加鎖的操做,嘗試在進入互斥前,進行補救。它存在的目的是儘量不用動用操做系統層面的互斥,由於那個性能會比較差。

若是輕量級鎖失敗,表示存在競爭,升級爲重量級鎖(常規鎖),就是操做系統層面的同步方法。在沒有鎖競爭的狀況,輕量級鎖減小傳統鎖使用OS互斥量產生的性能損耗。在競爭很是激烈時(輕量級鎖老是失敗),輕量級鎖會多作不少額外操做,致使性能降低。

自旋鎖

鎖膨脹以後,虛擬機爲了不線程真實的系統層面掛起線程,虛擬機還會作最後的嘗試——自旋鎖。

因爲當前線程暫時沒法獲取鎖,可是何時可以得到鎖也是一個未知數,也許在將來的幾個CPU週期以後就能夠得到鎖,這種狀況下,簡單粗暴的把線程掛起多是一種得不償失的操做。所以,基於這種假設,虛擬機會讓當前線程作幾個空循環(這也是自旋的含義),而且不停地嘗試拿到這個鎖。若是通過若干次循環後,能夠得到到,那麼就順利的進入臨界區。若是還不能得到鎖,此時自旋鎖就會膨脹爲重量級鎖,真實的在OS層面掛起線程。

因此在每一個線程對於鎖的持有時間不多時,自旋鎖可以儘可能避免線程在OS層被掛起,這也是自旋鎖提高系統性能的關鍵所在。

JDK1.7中,自旋鎖爲內置實現。

 

偏向鎖、輕量級鎖、自旋鎖總結

偏向鎖、輕量級鎖和自旋鎖鎖不是Java語言層面的鎖優化方法,是內置在JVM當中的。

偏向鎖是爲了不某個線程反覆得到/釋放同一把鎖時的性能消耗,若是仍然是同個線程去得到這個鎖,嘗試偏向鎖時會直接進入同步塊,不須要再次得到鎖。

輕量級鎖和自旋鎖都是爲了不直接調用操做系統層面的互斥操做,由於掛起線程是一個很耗資源的操做。

爲了儘可能避免使用重量級鎖(操做系統層面的互斥),首先會嘗試輕量級鎖,輕量級鎖會嘗試使用CAS操做來得到鎖,若是輕量級鎖得到失敗,說明存在競爭。可是也許很快就能得到鎖,就會嘗試自旋鎖,將線程作幾個空循環,每次循環時都不斷嘗試得到鎖。若是自旋鎖也失敗,那麼只能升級成重量級鎖。

 可見偏向鎖,輕量級鎖,自旋鎖都是樂觀鎖

鎖消除

鎖消除是在編譯器級別作的事情。在即時編譯時,經過對運行上下文進行掃描,去除不可能存在共享資源競爭的鎖。經過鎖消除,能夠節省無心義的請求鎖時間。

也許你會以爲奇怪,既然有些對象不可能被多線程訪問,那爲何要加鎖呢?寫代碼時直接不加鎖不就行了?

其實有時候,這些鎖並非程序員所寫的,有的是JDK實現中就有鎖的,好比Vector和StringBuffer這樣的類,它們中的不少方法都是有鎖的。當咱們在一些不會有線程安全的狀況下使用這些類的方法時,達到某些條件時,編譯器會將鎖消除來提升性能。

下面以StringBuffer爲例說明:

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

/**
 * @throws IndexOutOfBoundsException {@inheritDoc}
 * @since      1.5
 */
@Override
public synchronized StringBuffer append(CharSequence s, int start, int end)
{
    toStringCache = null;
    super.append(s, start, end);
    return this;
}

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

/**
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
@Override
public synchronized StringBuffer append(char[] str, int offset, int len) {
    toStringCache = null;
    super.append(str, offset, len);
    return this;
}

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

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

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

可見,StringBuffer的append方法都使用了synchronize關鍵字修飾,都是同步的,使用時都須要得到鎖。

public String createString() {
    StringBuffer sb = new StringBuffer();
    for (int i=0; i<10000; i++) {
        sb.append("aaa");
        sb.append("bbb");
    }
    return sb.toString();
}

上述StringBuffer對象,只在createString方法中使用,所以它是一個局部變量。局部變量是在線程棧上分配的,屬於線程的私有資源,所以不可能被其餘線程訪問,這種狀況下,StringBuffer內部的全部加鎖同步都是不必的,若是虛擬機檢測到這種狀況,就會將這些不用的鎖去除。

鎖消除涉及到的一箱關鍵技術叫作逃逸分析。逃逸分析就是觀察一個變量是否會逃出某個做用域。

逃逸分析必須在-server模式下運行,使用-XX:+DoEscapeAnalysis打開,使用-XX:+EliminateLocks參數打開鎖消除。

ThreadLocal

ThreadLocal是解決線程安全問題一個很好的思路,它經過爲每一個線程提供一個獨立的變量副本解決了變量併發訪問的衝突問題。在不少狀況下,ThreadLocal比直接使用synchronized同步機制解決線程安全問題更簡單,更方便,且結果程序擁有更高的併發性。
在同步機制中,經過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的。

ThreadLocal則從另外一個角度來解決多線程的併發訪問。ThreadLocal會爲每個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。由於每個線程都擁有本身的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,能夠把不安全的變量封裝進ThreadLocal。

package com.lixiuyu.demo;

import java.text.SimpleDateFormat;

/**
 * Created by lixiuyu on 2017/6/20.
 */

import java.text.ParseException;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SimpleDateFormatDemo {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static class ParseDate implements Runnable {
        int i = 0;

        public ParseDate(int i) {
            this.i = i;
        }

        public void run() {
            try {
                Date t = sdf.parse("2016-02-16 17:00:" + i % 60);
                System.out.println(i + ":" + t);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService es = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10000; i++) {
            es.execute(new ParseDate(i));
        }
    }

}

因爲SimpleDateFormat是非線程安全的,所以某些狀況下可能會出現相似以下的異常:

上述代碼的一種可行的優化方案是在sdf.parse()先後加鎖。這裏咱們使用ThreadLocal來優化上述代碼:

public class SimpleDateFormatDemo {
    private static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();

    public static class ParseDate implements Runnable {
        int i = 0;

        public ParseDate(int i) {
            this.i = i;
        }

        public void run() {
            try {
                if (tl.get() == null) {
                    tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
                }

                Date t = tl.get().parse("2017-06-20 17:00:" + i % 60);
                System.out.println(i + ":" + t);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    }

}

從這裏也能夠看出,爲每個線程分配不一樣的私有對象的工做並非ThreadLocal來完成的,而是須要再應用層面保證,ThreadLocal只是起到了簡單的容器做用。

無鎖

鎖是一種悲觀的策略,它老是假設每一次對臨界區的操做都會產生衝突,所以必須對操做當心翼翼。若是有多個線程同時須要訪問臨界區,處於安全考慮,寧肯犧牲性能讓線程排隊等待,因此說鎖會阻塞線程執行。

與鎖相比,無鎖是一種樂觀的策略,他假設會資源的訪問是沒有衝突的,既然沒有衝突,天然無需等待,因此全部的線程均可以在不停頓的狀態下執行。

固然,衝突是不可能避免發生的,那麼遇到衝突怎麼辦呢?無鎖策略使用了一種叫作比較交換的技術(CAS、Compare And Set)在鑑別線程衝突,一旦檢測到衝突,就重試當前操做直至沒有衝突爲止。

CAS指令是個原子化的操做,它包含三個參數:CAS(param, expectValue, newValue);

param:要更新的變量

expectValue:預期值

newValue:新值

當且僅當變量param的值等於expectValue時,纔將param的值改成newValue。若是param的值跟expectValue不一樣,表示已經有其餘線程作了更新,當前線程什麼都不作。

jdk併發包中的atomic包,裏面實現了一些直接使用CAS操做的線程安全的類型,如AtomicInteger、AtomicLong等。

    private volatile int value;// 初始化值

    /**
     * 建立一個AtomicInteger,初始值value爲initialValue
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    /**
     * 建立一個AtomicInteger,初始值value爲0
     */
    public AtomicInteger() {
    }

    /**
     * 返回value
     */
    public final int get() {
        return value;
    }

    /**
     * 爲value設值(基於value),而其餘操做是基於舊值<--get()
     */
    public final void set(int newValue) {
        value = newValue;
    }

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    
    /**
     * 基於CAS爲舊值設定新值,採用無限循環,直到設置成功爲止
     * 
     * @return 返回舊值
     */
    public final int getAndSet(int newValue) {
        for (;;) {
            int current = get();// 獲取當前值(舊值)
            if (compareAndSet(current, newValue))// CAS新值替代舊值
                return current;// 返回舊值
        }
    }

    /**
     * 當前值+1,採用無限循環,直到+1成功爲止
     * @return the previous value 返回舊值
     */
    public final int getAndIncrement() {
        for (;;) {
            int current = get();//獲取當前值
            int next = current + 1;//當前值+1
            if (compareAndSet(current, next))//基於CAS賦值
                return current;
        }
    }

    /**
     * 當前值-1,採用無限循環,直到-1成功爲止 
     * @return the previous value 返回舊值
     */
    public final int getAndDecrement() {
        for (;;) {
            int current = get();
            int next = current - 1;
            if (compareAndSet(current, next))
                return current;
        }
    }

    /**
     * 當前值+delta,採用無限循環,直到+delta成功爲止 
     * @return the previous value  返回舊值
     */
    public final int getAndAdd(int delta) {
        for (;;) {
            int current = get();
            int next = current + delta;
            if (compareAndSet(current, next))
                return current;
        }
    }

    /**
     * 當前值+1, 採用無限循環,直到+1成功爲止
     * @return the updated value 返回新值
     */
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;//返回新值
        }
    }

    /**
     * 當前值-1, 採用無限循環,直到-1成功爲止 
     * @return the updated value 返回新值
     */
    public final int decrementAndGet() {
        for (;;) {
            int current = get();
            int next = current - 1;
            if (compareAndSet(current, next))
                return next;//返回新值
        }
    }

    /**
     * 當前值+delta,採用無限循環,直到+delta成功爲止  
     * @return the updated value 返回新值
     */
    public final int addAndGet(int delta) {
        for (;;) {
            int current = get();
            int next = current + delta;
            if (compareAndSet(current, next))
                return next;//返回新值
        }
    }

    /**
     * 獲取當前值
     */
    public int intValue() {
        return get();
    }

 

參考資料

http://www.importnew.com/21353.html

http://www.10tiao.com/html/194/201703/2651478260/1.html

http://www.cnblogs.com/ten951/p/6212285.html

https://sakuraffy.github.io/intercurrent_lock_majorizing/

http://www.cnblogs.com/java-zhao/p/5140158.html

《Java高併發程序設計》

《Java併發編程的藝術》

相關文章
相關標籤/搜索