溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

java自旋鎖和JVM對鎖如何優(yōu)化

發(fā)布時間:2022-09-23 09:57:35 來源:億速云 閱讀:152 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要講解了“java自旋鎖和JVM對鎖如何優(yōu)化”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“java自旋鎖和JVM對鎖如何優(yōu)化”吧!

    背景

    先上圖

    java自旋鎖和JVM對鎖如何優(yōu)化

    由此可見,非自旋鎖如果拿不到鎖會把線程阻塞,直到被喚醒;

    自旋鎖拿不到鎖會一直嘗試

    為什么要這樣?

    好處

    阻塞和喚醒線程都是需要高昂的開銷的,如果同步代碼塊中的內(nèi)容不復(fù)雜,那么可能轉(zhuǎn)換線程帶來的開銷比實際業(yè)務(wù)代碼執(zhí)行的開銷還要大。

    在很多場景下,可能我們的同步代碼塊的內(nèi)容并不多,所以需要的執(zhí)行時間也很短,如果我們僅僅為了這點時間就去切換線程狀態(tài),那么其實不如讓線程不切換狀態(tài),而是讓它自旋地嘗試獲取鎖,等待其他線程釋放鎖,有時我只需要稍等一下,就可以避免上下文切換等開銷,提高了效率。

    用一句話總結(jié)自旋鎖的好處,那就是自旋鎖用循環(huán)去不停地嘗試獲取鎖,讓線程始終處于 Runnable 狀態(tài),節(jié)省了線程狀態(tài)切換帶來的開銷。

    AtomicLong的實現(xiàn)

    getAndIncrement方法

    public final long getAndIncrement() {
        return unsafe.getAndAddLong(this, valueOffset, 1L);
    }
    public final long getAndAddLong(Object o, long offset, long delta) {
        long v;
        do {
            v = getLongVolatile(o, offset);
            //如果修改過程中遇到其他線程競爭導(dǎo)致沒修改成功,死循環(huán),直到修改成功為止
        } while (!compareAndSwapLong(o, offset, v, v + delta));
        return v;
    }

    實驗

    package com.reflect;
    import java.util.concurrent.atomic.AtomicReference;
    class ReentrantSpinLock {
        private AtomicReference<Thread> owner = new AtomicReference<>();
        private int count = 0;
        public void lock() {
            Thread t = Thread.currentThread();
            if (t == owner.get()) {
                ++count;
                return;
            }
            while (!owner.compareAndSet(null, t)) {
                System.out.println("自旋了");
            }
        }
        public void unlock() {
            Thread t = Thread.currentThread();
            if (t == owner.get()) {
                if (count > 0) {
                    --count;
                } else {
                    owner.set(null);
                }
            }
        }
        public static void main(String[] args) {
            ReentrantSpinLock spinLock = new ReentrantSpinLock();
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "開始嘗試獲 取自旋鎖");
                    spinLock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + "獲取到 了自旋鎖");
                        Thread.sleep(4000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        spinLock.unlock();
                        System.out.println(Thread.currentThread().getName() + "釋放了 了自旋鎖");
                    }
                }
            };
            Thread thread1 = new Thread(runnable);
            Thread thread2 = new Thread(runnable);
            thread1.start();
            thread2.start();
        }
    }

    很多"自旋了",說明自旋期間CPU依然在不停運轉(zhuǎn)

    缺點

    雖然避免了線程切換的開銷,但是在避免線程切換開銷的同時帶來新的開銷:不停嘗試獲取鎖,如果這個鎖一直不能被釋放那么這種嘗試知識無用的嘗試,浪費處理器資源,就是說一開始自旋鎖開銷低于線程切換,但是隨著時間增加,這種開銷后期甚至超過線程切換的開銷,得不償失

    適用場景

    • 并發(fā)不是特別高的場景

    • 臨界區(qū)比較短小的情況,利用避免線程切換提高效率

    如果臨界區(qū)很大,線程拿到鎖很久才釋放,那自旋會一直占用CPU但無法拿到鎖,浪費資源

    JVM對鎖做了哪些優(yōu)化?

    相比于 JDK 1.5,在 JDK 1.6 中 HotSopt 虛擬機(jī)對 synchronized 內(nèi)置鎖的性能進(jìn)行了很多優(yōu)化,包括自適應(yīng)的自旋、鎖消除、鎖粗化、偏向鎖、輕量級鎖等。有了這些優(yōu)化措施后,synchronized 鎖的性能得到了大幅提高,下面我們分別介紹這些具體的優(yōu)化。

    自適應(yīng)的自旋鎖

    在 JDK 1.6 中引入了自適應(yīng)的自旋鎖來解決長時間自旋的問題。自適應(yīng)意味著自旋的時間不再固定,而是會根據(jù)最近自旋嘗試的成功率、失敗率,以及當(dāng)前鎖的擁有者的狀態(tài)等多種因素來共同決定。自旋的持續(xù)時間是變化的,自旋鎖變 “聰明” 了。比如,如果最近嘗試自旋獲取某一把鎖成功了,那么下一次可能還會繼續(xù)使用自旋,并且允許自旋更長的時間;但是如果最近自旋獲取某一把鎖失敗了,那么可能會省略掉自旋的過程,以便減少無用的自旋,提高效率。

    鎖消除

    public class Person {
        private String name;
        private int age;
        public Person(String personName, int personAge) {
            name = personName;
            age = personAge;
        }
        public Person(Person p) {
            this(p.getName(), p.getAge());
        }
        public String getName() {
            return name;
        }
        public int getAge() {
            return age;
        }
    }
    class Employee {
        private Person person;
        public Person getPerson() {
            return new Person(person);
        }
        public void printEmployeeDetail(Employee emp) {
            Person person = emp.getPerson();
            System.out.println("Employee's name: " + person.getName() + "; age: " + person.getAge());
        }
    }

    在這段代碼中,我們看到下方的 Employee 類中的 getPerson() 方法,這個方法中使用了類里面的person 對象,并且新建一個和它屬性完全相同的新的 person 對象,目的是防止方法調(diào)用者修改原來的 person 對象。但是在這個例子中,其實是沒有任何必要新建對象的,因為我們的printEmployeeDetail() 方法沒有對這個對象做出任何的修改,僅僅是打印,既然如此,我們其實可以直接打印最開始的 person 對象,而無須新建一個新的。

    如果編譯器可以確定最開始的 person 對象不會被修改的話,它可能會優(yōu)化并且消除這個新建 person的過程。根據(jù)這樣的思想,接下來我們就來舉一個鎖消除的例子,,經(jīng)過逃逸分析之后,如果發(fā)現(xiàn)某些對象不可能被其他線程訪問到,那么就可以把它們當(dāng)成棧上數(shù)據(jù),棧上數(shù)據(jù)由于只有本線程可以訪問,自然是線程安全的,也就無需加鎖,所以會把這樣的鎖給自動去除掉。

    例如,我們的 StringBuffffer 的 append 方法如下所示:

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

    從代碼中可以看出,這個方法是被 synchronized 修飾的同步方法,因為它可能會被多個線程同時使用。

    但是在大多數(shù)情況下,它只會在一個線程內(nèi)被使用,如果編譯器能確定這個 StringBuffffer 對象只會在一個線程內(nèi)被使用,就代表肯定是線程安全的,那么我們的編譯器便會做出優(yōu)化,把對應(yīng)的synchronized 給消除,省去加鎖和解鎖的操作,以便增加整體的效率。

    鎖粗化

    釋放了鎖,緊接著什么都沒做,又重新獲取鎖

    public void lockCoarsening() { 
        synchronized (this) { 
        } 
        synchronized (this) { 
        } 
        synchronized (this) { 
        } 
    }

    那么其實這種釋放和重新獲取鎖是完全沒有必要的,如果我們把同步區(qū)域擴(kuò)大,也就是只在最開始加一次鎖,并且在最后直接解鎖,那么就可以把中間這些無意義的解鎖和加鎖的過程消除,相當(dāng)于是把幾個synchronized 塊合并為一個較大的同步塊。這樣做的好處在于在線程執(zhí)行這些代碼時,就無須頻繁申請與釋放鎖了,這樣就減少了性能開銷。

    不過,我們這樣做也有一個副作用,那就是我們會讓同步區(qū)域變大。如果在循環(huán)中我們也這樣做,如代碼所示:

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

    也就是我們在第一次循環(huán)的開始,就開始擴(kuò)大同步區(qū)域并持有鎖,直到最后一次循環(huán)結(jié)束,才結(jié)束同步代碼塊釋放鎖的話,這就會導(dǎo)致其他線程長時間無法獲得鎖。所以,這里的鎖粗化不適用于循環(huán)的場景,僅適用于非循環(huán)的場景。

    鎖粗化功能是默認(rèn)打開的,用 -XX:-EliminateLocks可以關(guān)閉該功能

    偏向鎖/ 輕量級鎖/ 重量級鎖

    這三種鎖是特指 synchronized 鎖的狀態(tài),通過在對象頭中的 mark word 來表明鎖的狀態(tài)

    • 偏向鎖

    對于偏向鎖而言,它的思想是如果自始至終,對于這把鎖都不存在競爭,那么其實就沒必要上鎖,只要打個標(biāo)記就行了。一個對象在被初始化后,如果還沒有任何線程來獲取它的鎖時,它就是可偏向的,當(dāng)有第一個線程來訪問它嘗試獲取鎖的時候,它就記錄下來這個線程,如果后面嘗試獲取鎖的線程正是這個偏向鎖的擁有者,就可以直接獲取鎖,開銷很小。

    • 輕量級鎖

    JVM 的開發(fā)者發(fā)現(xiàn)在很多情況下,synchronized 中的代碼塊是被多個線程交替執(zhí)行的,也就是說,并不存在實際的競爭,或者是只有短時間的鎖競爭,用 CAS 就可以解決。這種情況下,重量級鎖是沒必要的。輕量級鎖指當(dāng)鎖原來是偏向鎖的時候,被另一個線程所訪問,說明存在競爭,那么偏向鎖就會升級為輕量級鎖,線程會通過自旋的方式嘗試獲取鎖,不會阻塞

    • 重量級鎖

    這種鎖利用操作系統(tǒng)的同步機(jī)制實現(xiàn),所以開銷比較大。當(dāng)多個線程直接有實際競爭,并且鎖競爭時間比較長的時候,此時偏向鎖和輕量級鎖都不能滿足需求,鎖就會膨脹為重量級鎖。重量級鎖會讓其他申請卻拿不到鎖的線程進(jìn)入阻塞狀態(tài)。

    鎖升級

    偏向鎖性能最好,避免了 CAS 操作。而輕量級鎖利用自旋和 CAS 避免了重量級鎖帶來的線程阻塞和喚醒,性能中等。重量級鎖則會把獲取不到鎖的線程阻塞,性能最差。

    java自旋鎖和JVM對鎖如何優(yōu)化

    JVM 默認(rèn)會優(yōu)先使用偏向鎖,如果有必要的話才逐步升級,這大幅提高了鎖的性能

    感謝各位的閱讀,以上就是“java自旋鎖和JVM對鎖如何優(yōu)化”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對java自旋鎖和JVM對鎖如何優(yōu)化這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI