鎖的優(yōu)化及注意事項(xiàng)(一)

一、提高鎖性能的幾點(diǎn)建議

鎖的競(jìng)爭(zhēng)會(huì)導(dǎo)致程序整體性能的下降,如何降低鎖競(jìng)爭(zhēng)帶來的副作用是我們必須考慮的。下面提出幾點(diǎn)鎖優(yōu)化的建議:

1.1 減小鎖持有時(shí)間

單個(gè)線程對(duì)鎖的持有時(shí)間與系統(tǒng)的性能密切相關(guān)。如果線程持有鎖的時(shí)間越長(zhǎng),那么鎖的競(jìng)爭(zhēng)程度就會(huì)越激烈。因此,應(yīng)盡可能減少線程對(duì)某個(gè)鎖的占有時(shí)間,進(jìn)而減少線程間互斥的可能。看下面這段代碼:

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

假設(shè)只有mutexMethod()有同步需要,而othercode1()othercode2()不需要做同步控制。如果othercode1()othercode2()都是重量級(jí)的方法,那么就會(huì)花費(fèi)較長(zhǎng)的CPU時(shí)間。改進(jìn)后的代碼如下:

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

只對(duì)需要同步的方法進(jìn)行同步控制,這樣鎖的占用時(shí)間會(huì)大大減少,進(jìn)而提高系統(tǒng)的并行性能。

1.2 減小鎖粒度

對(duì)于HashMap來說,最重要的兩個(gè)方法就是get()和put()。一種最自然的的想法就是對(duì)整個(gè)HashMap加鎖,必然可以得到一個(gè)線程安全的對(duì)象。但是這樣做,我們就認(rèn)為加鎖粒度太大。對(duì)于ConcurrentHashMap,它內(nèi)部進(jìn)一步細(xì)分了若干個(gè)小的HashMap,稱之為段(SEGMENT)。默認(rèn)情況下,一個(gè)ConcurrentHashMap被進(jìn)一步細(xì)分為16個(gè)段。

如果需要在ConcurrentHashMap中增加一個(gè)新的表項(xiàng),并不是將整個(gè)HashMap加鎖,而是首先根據(jù)hashcode得到該表項(xiàng)應(yīng)該被存放到哪個(gè)段中,然后對(duì)該段加鎖,并完成put()操作。在多線程環(huán)境中,如果多個(gè)線程同時(shí)進(jìn)行put()操作,只要被加入的表項(xiàng)不存放在同一個(gè)段中,則線程間便可以做到真正的并行。下面代碼展示了put()操作的過程:

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    //獲取段的序號(hào)
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
      (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
        //得到段
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

但是這樣會(huì)存在一個(gè)問題:當(dāng)系統(tǒng)需要取得全局鎖時(shí),其消耗的資源會(huì)比較多。仍然以ConcurrentHashMap類為例,雖然其put()方法很好地分離了鎖,但是當(dāng)試圖訪問ConcurrentHashMap全局信息時(shí),就會(huì)需要同時(shí)取得所有段的鎖方能順利實(shí)施。比如ConcurrentHashMap的size()方法,它將返回ConcurrentHashMap的有效表項(xiàng)的數(shù)量,即ConcurrentHashMap的全部有效表項(xiàng)之和。要獲得這個(gè)信息需要取得所有子段的鎖。下面是size()方法的部分代碼:

sum = 0;
for(int i=0; i<segments.length; ++i)
    segments[i].lock();
for(int i=0; i<segments.length; ++i)
    sum += segments[i].count;
for(int i=0; i<segments.length; ++i)
    segments[i].unlock();

可以看到在計(jì)算總數(shù)時(shí),先要獲得所有段的鎖,然后再求和。但是,ConcurrentHashMap的size()方法并不總是這樣執(zhí)行,事實(shí)上,size()方法會(huì)先使用無鎖的方式求和,如果失敗才會(huì)嘗試這種加鎖的方法。

1.3 讀寫分離鎖來替換獨(dú)占鎖

在讀多寫少的場(chǎng)合,讀寫鎖對(duì)系統(tǒng)性能是很有好處的。因?yàn)槿绻到y(tǒng)在讀寫數(shù)據(jù)時(shí)均使用獨(dú)占鎖,那么讀操作和寫操作間、讀操作和讀操作間、寫操作和寫操作間均不能做到真正的并發(fā),并且需要相互等待。而讀操作本身不會(huì)影響數(shù)據(jù)的完整性和一致性。因此,理論上,在大部分情況下,應(yīng)該可以允許多線程同時(shí)讀,讀寫鎖正是實(shí)現(xiàn)了這種功能。

1.4 鎖分離

鎖分離是讀寫鎖的而進(jìn)一步延伸。 一個(gè)典型的案例就是java.util.concurrent.LinkedBlockingQueue的實(shí)現(xiàn)。
在LinkedBlockingQueue的實(shí)現(xiàn)中,take()函數(shù)和put()函數(shù)分別實(shí)現(xiàn)了從隊(duì)列中取得數(shù)據(jù)和往隊(duì)列中增加數(shù)據(jù)的功能。雖然兩個(gè)函數(shù)都對(duì)當(dāng)前隊(duì)列進(jìn)行了修改操作,但由于LinkedBlockingQueue是基于鏈表的,因此,兩個(gè)操作分別作用于隊(duì)列的前端和尾端,從理論上說,并不沖突。

如果使用獨(dú)占鎖,則要求在兩個(gè)操作進(jìn)行時(shí)獲取當(dāng)前隊(duì)列的獨(dú)占鎖,那么take()和put()操作就不可能真正的并發(fā),在運(yùn)行時(shí),它們會(huì)彼此等待對(duì)方釋放鎖資源。在這種情況下,鎖競(jìng)爭(zhēng)會(huì)相對(duì)比較激烈,從而影響程序在高并發(fā)時(shí)的性能。因此,在JDK的實(shí)現(xiàn)中,并沒有采用這樣的方式,取而代之的是兩把不同的鎖,分離了take()和put()操作。

/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();

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

/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

take()put()函數(shù)相互獨(dú)立,不存在鎖的競(jìng)爭(zhēng)關(guān)系。只需要在take()和take()間、put()和put()間分別對(duì)takeLock和putLock進(jìn)行競(jìng)爭(zhēng)。從而,削弱了鎖競(jìng)爭(zhēng)的可能性。
函數(shù)take()的實(shí)現(xiàn)如下:

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly(); //不能有兩個(gè)線程同時(shí)取數(shù)據(jù)
    try {
        while (count.get() == 0) { //如果當(dāng)前沒有可用數(shù)據(jù),一直等待
            notEmpty.await(); //等待,put()操作的通知
        }
        x = dequeue(); //取得第一個(gè)數(shù)據(jù)
        c = count.getAndDecrement(); //數(shù)量減1,原子操作
        if (c > 1)
            notEmpty.signal(); //通知其他take()操作
        } finally {
            takeLock.unlock(); //釋放鎖
        }
    if (c == capacity)
        signalNotFull(); //通知put()操作,已有空余空間
    return x;
}

函數(shù)put()的實(shí)現(xiàn)如下:

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    int c = -1;
    Node<E> node = new Node(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly(); //不能有兩個(gè)線程同時(shí)進(jìn)行put()
    try {
        while (count.get() == capacity) { //如果隊(duì)列已經(jīng)滿了
            notFull.await(); //等待
        }
        enqueue(node); //插入數(shù)據(jù)
        c = count.getAndIncrement(); //更新總數(shù),變量c是count加1前的值
        if (c + 1 < capacity)
            notFull.signal(); //有足夠的空間,通知其他線程
    } finally {
        putLock.unlock(); //釋放鎖
    }
    if (c == 0)
    signalNotEmpty(); //插入成功后,通知take()操作取數(shù)據(jù)
}

1.4 鎖粗化

通常情況下,為了保證多線程間的有效并發(fā),會(huì)要求每個(gè)線程持有鎖的時(shí)間盡量短,即在使用完公共資源后,應(yīng)該立即釋放鎖。只有這樣,等待在這個(gè)鎖上的其他線程才能盡早地獲得資源執(zhí)行任務(wù)。但是,如果對(duì)同一個(gè)鎖不停地進(jìn)行請(qǐng)求、同步和釋放,其本身也會(huì)消耗系統(tǒng)寶貴的資源,反而不利于性能的優(yōu)化。

為此,虛擬機(jī)在遇到一連串連讀地對(duì)同一鎖不斷進(jìn)行請(qǐng)求和釋放的操作時(shí),便會(huì)把所有的鎖作整合成對(duì)鎖的一次請(qǐng)求,從而減少對(duì)鎖的請(qǐng)求同步次數(shù),這個(gè)操作叫做鎖是粗化。比如代碼段:

public void demoMethod() {
    synchronized(lock) {
        //do sth
    }
    //做其他不需要的同步的工作,但能很快執(zhí)行完畢
    synchronized(lock) {
        //do sth
    }
}

按照鎖粗化的思想,整合后代碼如下:

public void demoMethod() {
    synchronized(lock) {
        //do sth
        //做其他不需要的同步的工作,但能很快執(zhí)行完畢
    }
}

二、Java虛擬機(jī)對(duì)鎖優(yōu)化所做的努力

2.1 偏向鎖

偏向鎖的核心思想是:如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入了偏向模式。當(dāng)這個(gè)線程再次請(qǐng)求鎖的時(shí)候無需再去做任何同步操作,節(jié)省了鎖的申請(qǐng)操作,提高程序的性能。偏向鎖不適合鎖競(jìng)爭(zhēng)激烈的情況。使用Java虛擬機(jī)參數(shù)-XX:UseBiasedLocking可以開啟偏向鎖。

2.2 輕量級(jí)鎖

如果偏向鎖失敗,虛擬機(jī)并不會(huì)立即掛起線程。它還會(huì)使用一種稱為輕量級(jí)鎖的優(yōu)化手段。輕量級(jí)鎖的操作也很輕便,它只是簡(jiǎn)單地將對(duì)象頭部作為指針,指向持有鎖的線程堆棧的內(nèi)部,來判斷一個(gè)線程是否持有對(duì)象鎖。如果線程獲得輕量級(jí)鎖成功,則可以順利進(jìn)入臨界區(qū)。如果輕量級(jí)鎖加鎖失敗,則表示其他線程搶先爭(zhēng)奪到了鎖,那么當(dāng)前線程的鎖請(qǐng)求就會(huì)膨脹為重量級(jí)鎖。

2.3 自旋鎖

鎖膨脹后,虛擬機(jī)為了避免線程真實(shí)地在操作系統(tǒng)層面掛起,虛擬機(jī)還會(huì)在做最后的努力--自旋鎖。由于當(dāng)前線程暫時(shí)無法獲得鎖,但是什么時(shí)候可以獲得鎖是一個(gè)未知數(shù)。也許在幾個(gè)CPU時(shí)鐘周期后,就可以得到鎖。如果這樣,簡(jiǎn)單粗暴地掛起線程可能是一種得不償失的操作。因此,系統(tǒng)會(huì)進(jìn)行一次賭注:它會(huì)假設(shè)在不久的將來,線程可以得到這把鎖。因此,虛擬機(jī)會(huì)讓當(dāng)前線程做幾個(gè)空循環(huán),在經(jīng)過若干次循環(huán)后,如果可以得到鎖,那么就順利進(jìn)入臨界區(qū)。如果還不能得到鎖,才會(huì)真實(shí)地將線程在操作系統(tǒng)層面掛起。

2.4 鎖清除

鎖消除是一種更徹底的鎖優(yōu)化。Java虛擬機(jī)在JIT編譯時(shí),通過對(duì)上下文的掃描,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖。通過鎖消除,可以節(jié)省毫無意義的請(qǐng)求鎖時(shí)間。
例如,在一個(gè)不可能存在并發(fā)競(jìng)爭(zhēng)的場(chǎng)合使用Vector,而Vector內(nèi)部使用了Synchronized請(qǐng)求鎖。比如下面的代碼:

public String[] createStrings() {
    Vector<String> v = new Vector<String>();
    for(int i=0; i<100; i++) {
        v.add(Integer.toString(i));
    }
    return v.toArray(new String[]{});
}

v屬于線程私有數(shù)據(jù),不可能被其它線程訪問。這種情況下,Vector內(nèi)部所有加鎖同步都是沒有必要的,虛擬機(jī)檢測(cè)到這種情況就會(huì)將這些無用的鎖清除掉。

三、ThreadLocal

ThreadLocal是一個(gè)本地線程副本變量工具類。主要用于將私有線程和該線程存放的副本對(duì)象做一個(gè)映射,各個(gè)線程之間的變量互不干擾,在高并發(fā)場(chǎng)景下,可以實(shí)現(xiàn)無狀態(tài)的調(diào)用,特別適用于各個(gè)線程依賴不通的變量值完成操作的場(chǎng)景。

3.1 ThreadLocal實(shí)現(xiàn)原理

ThreadLocal原理圖
  • 每個(gè)Thread線程內(nèi)部都有一個(gè)Map;
  • Map里面存儲(chǔ)線程本地對(duì)象(key)和線程的變量副本(value)
  • Thread內(nèi)部的Map是由ThreadLocal維護(hù)的,由ThreadLocal負(fù)責(zé)向map獲取和設(shè)置線程的變量值。

所以對(duì)于不同的線程,每次獲取副本值時(shí),別的線程并不能獲取到當(dāng)前線程的副本值,形成了副本的隔離,互不干擾。

Thread線程內(nèi)部的Map在類中描述如下:

public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal如何保證這些對(duì)象只被當(dāng)前線程所訪問,我們需要關(guān)注的是ThreadLocal的set()方法和get()方法。

  1. get()方法

get()方法用于獲取當(dāng)前線程的副本變量值。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
    return null;
}

get()方法是先取得當(dāng)前線程的ThreadLocalMap對(duì)象。然后,通過將自己作為key取得內(nèi)部的實(shí)際數(shù)據(jù)。

  1. set()方法

set()方法用于保存當(dāng)前線程的副本變量值。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

在set時(shí),首先獲得當(dāng)前線程對(duì)象,然后通過getMap()拿到線程的ThreadLocalMap,并將值設(shè)入ThreadLocalMap中。

當(dāng)線程退出時(shí),Thread類會(huì)進(jìn)行一些清理工作,其中就包括清理ThreadLocalMap:

private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
}

如果使用線程池,意味著當(dāng)前線程未必會(huì)退出(比如固定大小的線程池,線程總是存在)如果這樣,將一些大大的對(duì)象設(shè)置到ThreadLocal中(它實(shí)際保存在線程持有的threadLocals Map內(nèi)),可能會(huì)使系統(tǒng)出現(xiàn)內(nèi)存泄漏的可能。此時(shí),如果希望及時(shí)回收對(duì)象,最好使用ThreadLocal.remove()方法將整個(gè)變量移除。

既然Key是弱引用,那么我們要做的事,就是在調(diào)用ThreadLocal的get()、set()方法時(shí)完成后再調(diào)用remove方法,將Entry節(jié)點(diǎn)和Map的引用關(guān)系移除,這樣整個(gè)Entry對(duì)象在GC Roots分析后就變成不可達(dá)了,下次GC的時(shí)候就可以被回收。如果使用ThreadLocal的set方法之后,沒有顯示的調(diào)用remove方法,就有可能發(fā)生內(nèi)存泄露,所以養(yǎng)成良好的編程習(xí)慣十分重要,使用完ThreadLocal之后,記得調(diào)用remove方法。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文是我自己在秋招復(fù)習(xí)時(shí)的讀書筆記,整理的知識(shí)點(diǎn),也是為了防止忘記,尊重勞動(dòng)成果,轉(zhuǎn)載注明出處哦!如果你也喜歡,那...
    波波波先森閱讀 11,596評(píng)論 4 56
  • Java SE 基礎(chǔ): 封裝、繼承、多態(tài) 封裝: 概念:就是把對(duì)象的屬性和操作(或服務(wù))結(jié)合為一個(gè)獨(dú)立的整體,并盡...
    Jayden_Cao閱讀 2,234評(píng)論 0 8
  • 進(jìn)程和線程 進(jìn)程 所有運(yùn)行中的任務(wù)通常對(duì)應(yīng)一個(gè)進(jìn)程,當(dāng)一個(gè)程序進(jìn)入內(nèi)存運(yùn)行時(shí),即變成一個(gè)進(jìn)程.進(jìn)程是處于運(yùn)行過程中...
    勝浩_ae28閱讀 5,257評(píng)論 0 23
  • 前幾天看到了一篇文章說到王者榮耀,進(jìn)而提到中國(guó)人的浪費(fèi)時(shí)間的問題,覺的中國(guó)人玩游戲吃瓜子等都是在浪費(fèi)時(shí)間。你認(rèn)為什...
    洞穴人Jane閱讀 2,767評(píng)論 1 3
  • 琴弦輕顫,余音未絕。 “《秋思》?怎么突然彈起這支曲子了?”他問。 “映了時(shí)節(jié),倒也不錯(cuò)。”望窗外,秋意漸濃。 “...
    丹青妙筆畫紅塵閱讀 525評(píng)論 0 4

友情鏈接更多精彩內(nèi)容