面試題:深入解析synchronized


深入解析synchronized

1 常見的幾個並發問題

1.可見性問題

案例演示:一個線程根據boolean類型的標記flag, while循環,另一個線程改變這個flag變量的值,另一個線程並不會停止循環。

/**
 * @author WGR
 * @create 2020/12/22 -- 20:18
 */
public class Test01Visibility {

    private static boolean run = true;

    //
    public static void main(String[] args) throws InterruptedException {
        new Thread(
                () ->{
                    while (run) {
                    }
                }
        ).start();

        Thread.sleep(1000);
        new Thread(
                () ->{
                    run = false;
                    System.out.println("修改了");
                }
        ).start();
    }
}

image-20201222202534556

總結:

並發編程時,會出現可見性問題,當一個線程對共享變量進行了修改,另外的線程並沒有立即看到修改后的最新值。

2.原則性問題

原子性(Atomicity):在一次或多次操作中,要么所有的操作都執行並且不會受其他因素干擾而中斷,要么所有的操作都不執行。

/**
 * @author WGR
 * @create 2020/12/22 -- 20:27
 */
public class Test02Atomicity {

    private static Integer number = 0;

    public static void main(String[] args) throws InterruptedException {
        Runnable increment = () -> {
            for (int i = 0; i < 1000; i++) {
                number++;
            }
        };

        ArrayList<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            ts.add(t);
        }
        for (Thread t : ts) {
            t.join();
        }
        System.out.println("number = " + number);
    }
}

image-20201222203409328

使用javap反匯編class文件,得到下面的字節碼指令:

image-20201222203823353

由此可見number++是由多條語句組成,以上多條指令在一個線程的情況下是不會出問題的,但是在多線程情況下就可能會出現問題。比如一個線程在執行13:iadd時,另一個線程又執行9: getstatic。會導致兩次number++,實際上只加了1。
小結
並發編程時,會出現原子性問題,當一個線程對共享變量操作到一半時,另外的線程也有可能來操作共享變量,干擾了前一個線程的操作。

3.有序性

有序性(Ordering):是指程序中代碼的執行順序,Java在編譯時和運行時會對代碼進行優化,會導致程序最終的執行順序不一定就是我們編寫代碼時的順序。

jcstress是java並發壓測工具。https://wiki.openjdk.java.net/display/CodeTools/jcstress
修改pom文件,添加依賴:

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.openjdk.jcstress/jcstress-core -->
        <dependency>
            <groupId>org.openjdk.jcstress</groupId>
            <artifactId>jcstress-core</artifactId>
            <version>0.3</version>
            <scope>test</scope>
        </dependency>

    </dependencies>
/**
 * @author WGR
 * @create 2020/12/22 -- 20:48
 */
@JCStressTest
@Outcome(id = {"1","4"}, expect = Expect.ACCEPTABLE,desc = "ok")
@Outcome(id = "0",expect = Expect.ACCEPTABLE_INTERESTING,desc = "danger")
@State
public class Test03Orderliness {
    int num = 0;
    boolean ready = false;
    // 線程一執行的代碼
    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }
}

情況一:先進第一個線程,再執行第二個線程,結果為1

情況二:先進第二個線程,再進第一個線程,結果為4

情況三:進行指令重排序,先先讓ready=true,然后再執行線程一,結果為0

image-20201222211037120

小結
程序代碼在執行過程中的先后順序,由於Java在編譯期以及運行期的優化,導致了代碼的執行順序未必就是開發者編寫代碼時的順序。

2.synchronized

1.使用synchronized保證原子性

public class Test02Atomicity {

    private static Integer number = 0;
    private static  Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        Runnable increment = () -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (obj) {
                    number++;
                }
            }
        };

        ArrayList<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            ts.add(t);
        }
        for (Thread t : ts) {
            t.join();
        }
        System.out.println("number = " + number);
    }
}

小結:synchronized保證原子性的原理,synchronized保證只有一個線程拿到鎖,能夠進入同步代碼塊。

2.synchronized與可見性

public class Test01Visibility {

    private static boolean run = true;
    private static  Object obj = new Object();

    //
    public static void main(String[] args) throws InterruptedException {
        new Thread(
                () ->{
                    while (run) {
                        synchronized (obj){

                        }
                    }
                }
        ).start();

        Thread.sleep(1000);
        new Thread(
                () ->{
                    run = false;
                    System.out.println("修改了");
                }
        ).start();
    }
}

小結:synchronized保證可見性的原理,執行synchronized時,會對應lock原子操作會刷新工作內存中共享變量的值

3.synchronized保證有序性

synchronized保證有序性的原理,我們加synchronized后,依然會發生重排序,只不過,我們有同步代碼塊,可以保證只有一個線程執行同步代碼中的代碼。保證有序性

@JCStressTest
@Outcome(id = {"1"}, expect = Expect.ACCEPTABLE,desc = "ok")
@Outcome(id = "4",expect = Expect.ACCEPTABLE_INTERESTING,desc = "danger")
@State
public class Test03Orderliness {
    int num = 0;
    boolean ready = false;
    private static  Object obj = new Object();
    // 線程一執行的代碼
    @Actor
    public void actor1(I_Result r) {
        synchronized (obj){
            if(ready) {
                r.r1 = num + num;
            } else {
                r.r1 = 1;
            }
        }

    }
    @Actor
    public  void actor2(I_Result r) {
        synchronized (obj){
        num = 2;
        ready = true;
        }
    }
}

小結:
synchronized保證有序性的原理,我們加synchronized后,依然會發生重排序,只不過,我們有同步代碼塊,可以保證只有一個線程執行同步代碼中的代碼。保證有序性

3.synchronized的特性

可重入性

/**
 * @author WGR
 * @create 2020/12/22 -- 21:36
 */
public class Demo1 {
    public static void main(String[] args) {
        new MyThread().start();
        new MyThread().start();
    }

    public static void test01() {
        synchronized (MyThread.class) {
            String name = Thread.currentThread().getName();
            System.out.println(name + "進入了同步代碼塊2");
        }
    }
}

// 1.自定義一個線程類
class MyThread extends Thread {
    @Override
    public void run() {
        synchronized (MyThread.class) {
            System.out.println(getName() + "進入了同步代碼塊1");
            Demo1.test01();
        }
    }

}

結果:

Thread-0進入了同步代碼塊1
Thread-0進入了同步代碼塊2
Thread-1進入了同步代碼塊1
Thread-1進入了同步代碼塊2

image-20201222213321693

可重入原理
synchronized的鎖對象中有一個計數器(recursions變量)會記錄線程獲得幾次鎖.
可重入的好處

  1. 可以避免死鎖
  2. 可以讓我們更好的來封裝代碼

小結:synchronized是可重入鎖,內部鎖對象中會有一個計數器記錄線程獲取幾次鎖啦,在執行完同步代碼塊時,計數器的數量會-1,知道計數器的數量為0,就釋放這個鎖。

不可中斷性

一個線程獲得鎖后,另一個線程想要獲得鎖,必須處於阻塞或等待狀態,如果第一個線程不釋放鎖,第二個線程會一直阻塞或等待,不可被中斷。

/**
 * @author WGR
 * @create 2020/12/22 -- 21:41
 */
public class Demo02_Uninterruptible {
    private static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        // 1.定義一個Runnable
        Runnable run = () -> {
            // 2.在Runnable定義同步代碼塊
            synchronized (obj) {
                String name = Thread.currentThread().getName();
                System.out.println(name + "進入同步代碼塊");
                // 保證不退出同步代碼塊
                try {
                    Thread.sleep(888888);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 3.先開啟一個線程來執行同步代碼塊
        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);
        // 4.后開啟一個線程來執行同步代碼塊(阻塞狀態)
        Thread t2 = new Thread(run);
        t2.start();

        // 5.停止第二個線程
        System.out.println("停止線程前");
        t2.interrupt();
        System.out.println("停止線程后");
        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }
}

Thread-0進入同步代碼塊
停止線程前
停止線程后
TIMED_WAITING
RUNNABLE

ReentrantLock可中斷演示

/**
 * @author WGR
 * @create 2020/12/22 -- 21:45
 */
public class Demo03_Interruptible {
    private static Lock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        test01();
        //test02();
    }

    // 演示Lock可中斷
    public static void test02() throws InterruptedException {
        Runnable run = () -> {
            String name = Thread.currentThread().getName();
            boolean b = false;
            try {
                b = lock.tryLock(3, TimeUnit.SECONDS);
                if (b) {
                    System.out.println(name + "獲得鎖,進入鎖執行");
                    Thread.sleep(88888);
                } else {
                    System.out.println(name + "在指定時間沒有得到鎖做其他操作");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (b) {
                    lock.unlock();
                    System.out.println(name + "釋放鎖");
                }
            }
        };

        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);
        Thread t2 = new Thread(run);
        t2.start();

         System.out.println("停止t2線程前");
         t2.interrupt();
         System.out.println("停止t2線程后");

         Thread.sleep(1000);
         System.out.println(t1.getState());
         System.out.println(t2.getState());
    }

    // 演示Lock不可中斷
    public static void test01() throws InterruptedException {
        Runnable run = () -> {
            String name = Thread.currentThread().getName();
            try {
                lock.lock();
                System.out.println(name + "獲得鎖,進入鎖執行");
                Thread.sleep(88888);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println(name + "釋放鎖");
            }
        };

        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);
        Thread t2 = new Thread(run);
        t2.start();

        System.out.println("停止t2線程前");
        t2.interrupt();
        System.out.println("停止t2線程后");

        Thread.sleep(1000);
        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }
}

小結
不可中斷是指,當一個線程獲得鎖后,另一個線程一直處於阻塞或等待狀態,前一個線程不釋放鎖,后一個線程會一直阻塞或等待,不可被中斷。
synchronized屬於不可被中斷,Lock的lock方法是不可中斷的,Lock的tryLock方法是可中斷的

4.synchronized原理

1.反編譯

簡單的示例代碼

public class Demo01 {
    private static Object obj = new Object();

    public static void main(String[] args) {
        synchronized (obj) {
            System.out.println("1");
        }
    }

    public synchronized void test() {
        System.out.println("a");
    }
}

反編譯后的結果

image-20201222220344392

image-20201223115520741

monitorenter
首先我們來看一下JVM規范中對於monitorenter的描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter

  1. 若monior的進入數為0,線程可以進入monitor,並將monitor的進入數置為1。當前線程成為monitor的owner(所有者)
  2. 若線程已擁有monitor的所有權,允許它重入monitor,則進入monitor的進入數加1
  3. 若其他線程已經占有monitor的所有權,那么當前嘗試獲取monitor的所有權的線程會被阻塞,直到monitor的進入數變為0,才能重新嘗試獲取monitor的所有權。
  4. monitorenter小結:
    synchronized的鎖對象會關聯一個monitor,這個monitor不是我們主動創建的,是JVM的線程執行到這個同步代碼塊,發現鎖對象沒有monitor就會創建monitor,monitor內部有兩個重要的成員變量owner:擁有這把鎖的線程,recursions會記錄線程擁有鎖的次數,當一個線程擁有monitor后其他線程只能等待

monitorexit
首先我們來看一下JVM規范中對於monitorexit的描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit

翻譯過來:

  1. 能執行monitorexit指令的線程一定是擁有當前對象的monitor的所有權的線程。
  2. 執行monitorexit時會將monitor的進入數減1。當monitor的進入數減為0時,當前線程退出monitor,不再擁有monitor的所有權,此時其他被這個monitor阻塞的線程可以嘗試去獲取這個monitor的所有權
  3. monitorexit釋放鎖。
    monitorexit插入在方法結束處和異常處,JVM保證每個monitorenter必須有對應的monitorexit。

同步方法
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10
可以看到同步方法在反匯編后,會增加 ACC_SYNCHRONIZED 修飾。會隱式調用monitorenter和monitorexit。在執行同步方法前會調用monitorenter,在執行完同步方法后會調用monitorexit。

小結
通過javap反匯編我們看到synchronized使用編程了monitorentor和monitorexit兩個指令.每個鎖對象都會關聯一個monitor(監視器,它才是真正的鎖對象),它內部有兩個重要的成員變量owner會保存獲得鎖的線程,recursions會保存線程獲得鎖的次數,當執行到monitorexit時,recursions會-1,當計數器減到0時這個線程就會釋放鎖 。

面試題:synchronized與Lock的區別

  1. synchronized是關鍵字,而Lock是一個接口。
  2. synchronized會自動釋放鎖,而Lock必須手動釋放鎖。
  3. synchronized是不可中斷的,Lock可以中斷也可以不中斷。
  4. 通過Lock可以知道線程有沒有拿到鎖,而synchronized不能。
  5. synchronized能鎖住方法和代碼塊,而Loc只能鎖住代碼塊。
  6. Lock可以使用讀鎖提高多線程讀效率。
  7. synchronized是非公平鎖,ReentrantLock可以控制是否是公平鎖。

2.monitor監視器鎖

可以看出無論是synchronized代碼塊還是synchronized方法,其線程安全的語義實現最終依賴一個叫monitor的東西,那么這個神秘的東西是什么呢?下面讓我們來詳細介紹一下。在HotSpot虛擬機中,monitor是由ObjectMonitor實現的。其源碼是用c++來實現的,位於HotSpot虛擬機源碼ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。ObjectMonitor主要數據結構如下:

ObjectMonitor() {
    _header = NULL;
    _count = 0;
    _waiters = 0,
    _recursions = 0; // 線程的重入次數
    _object = NULL; // 存儲該monitor的對象
    _owner = NULL; // 標識擁有該monitor的線程
    _WaitSet = NULL; // 處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock = 0 ;
    _Responsible = NULL;
    _succ = NULL;
    _cxq = NULL; // 多線程競爭鎖時的單向列表
    FreeNext = NULL;
    _EntryList = NULL; // 處於等待鎖block狀態的線程,會被加入到該列表
    _SpinFreq = 0;
    _SpinClock = 0;
    OwnerIsThread = 0;
}
  1. _owner:初始時為NULL。當有線程占有該monitor時,owner標記為該線程的唯一標識。當線程釋放monitor時,owner又恢復為NULL。owner是一個臨界資源,JVM是通過CAS操作來保證其線程安全的。
  2. _cxq:競爭隊列,所有請求鎖的線程首先會被放在這個隊列中(單向鏈接)。_cxq是一個臨界資源,JVM通過CAS原子指令來修改_cxq隊列。修改前_cxq的舊值填入了node的next字段,_cxq指向新值(新線程)。因此_cxq是一個后進先出的stack(棧)。
  3. EntryList:cxq隊列中有資格成為候選資源的線程會被移動到該隊列中。
  4. _WaitSet:因為調用wait方法而被阻塞的線程會被放在該隊列中。

每一個Java對象都可以與一個監視器monitor關聯,我們可以把它理解成為一把鎖,當一個線程想要執行一段被synchronized圈起來的同步方法或者代碼塊時,該線程得先獲取到synchronized修飾的對象對應的monitor。
我們的Java代碼里不會顯示地去創造這么一個monitor對象,我們也無需創建,事實上可以這么理解:
monitor並不是隨着對象創建而創建的。我們是通過synchronized修飾符告訴JVM需要為我們的某個對象創建關聯的monitor對象。每個線程都存在兩個ObjectMonitor對象列表,分別為free和used列表。同時JVM中也維護着global locklist。當線程需要ObjectMonitor對象時,首先從線程自身的free表中申
請,若存在則使用,若不存在則從global list中申請。ObjectMonitor的數據結構中包含:_owner、_WaitSet和_EntryList,它們之間的關系轉換可以用下圖
表示:

image-20201222222435288

monitor競爭
1.執行monitorenter時,會調用InterpreterRuntime.cpp(位於:src/share/vm/interpreter/interpreterRuntime.cpp) 的 InterpreterRuntime::monitorenter函
數。具體代碼可參見HotSpot源碼。

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread,
BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
} H
andle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
} a
ssert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");

2.對於重量級鎖,monitorenter函數中會調用 ObjectSynchronizer::slow_enter
3.最終調用 ObjectMonitor::enter(位於:src/share/vm/runtime/objectMonitor.cpp),源碼如下:

void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ;
// 通過CAS操作嘗試把monitor的_owner字段設置為當前線程
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) {
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
} 
// 線程重入,recursions++
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
} 
// 如果當前線程是第一次進入該monitor,設置_recursions為1,_owner為當前線程
    if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_
recursions = 1 ;
// Commute owner from a thread-specific on-stack BasicLockObject address to
// a full-fledged "Thread *".
_owner = Self ;
OwnerIsThread = 1 ;
return ;
} 
// 省略一些代碼
for (;;) {
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()
// 如果獲取鎖失敗,則等待鎖的釋放;
EnterI (THREAD) ;
if (!ExitSuspendEquivalent(jt)) break ;
//
// We have acquired the contended monitor, but while we were
// waiting another thread suspended us. We don't want to enter
// the monitor while suspended because that would surprise the
// thread that suspended us.
//
_recursions = 0 ;
_succ = NULL ;
exit (false, Self) ;
jt->java_suspend_self();
} S
elf->set_current_pending_monitor(NULL);
}

此處省略鎖的自旋優化等操作,統一放在后面synchronzied優化中說。
以上代碼的具體流程概括如下:

  1. 通過CAS嘗試把monitor的owner字段設置為當前線程。
  2. 如果設置之前的owner指向當前線程,說明當前線程再次進入monitor,即重入鎖,執行recursions ++ ,記錄重入的次數。
  3. 如果當前線程是第一次進入該monitor,設置recursions為1,_owner為當前線程,該線程成功獲得鎖並返回。
  4. 如果獲取鎖失敗,則等待鎖的釋放

monitor等待
競爭失敗等待調用的是ObjectMonitor對象的EnterI方法(位於:src/share/vm/runtime/objectMonitor.cpp),源碼如下所示:

void ATTR ObjectMonitor::EnterI (TRAPS) {
Thread * Self = THREAD ;
    // Try the lock - TATAS
if (TryLock (Self) > 0) {
assert (_succ != Self , "invariant") ;
assert (_owner == Self , "invariant") ;
assert (_Responsible != Self , "invariant") ;
return ;
} i
f (TrySpin (Self) > 0) {
assert (_owner == Self , "invariant") ;
assert (_succ != Self , "invariant") ;
assert (_Responsible != Self , "invariant") ;
return ;
}
 // 省略部分代碼
// 當前線程被封裝成ObjectWaiter對象node,狀態設置成ObjectWaiter::TS_CXQ;
ObjectWaiter node(Self) ;
Self->_ParkEvent->reset() ;
node._prev = (ObjectWaiter *) 0xBAD ;
node.TState = ObjectWaiter::TS_CXQ ;
// 通過CAS把node節點push到_cxq列表中
ObjectWaiter * nxt ;
for (;;) {
node._next = nxt = _cxq ;
if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
// Interference - the CAS failed because _cxq changed. Just retry.
// As an optional optimization we retry the lock.
if (TryLock (Self) > 0) {
assert (_succ != Self , "invariant") ;
assert (_owner == Self , "invariant") ;
assert (_Responsible != Self , "invariant") ;
return ;
}
} 
// 省略部分代碼
for (;;) {
// 線程在被掛起前做一下掙扎,看能不能獲取到鎖
if (TryLock (Self) > 0) break ;
assert (_owner != Self, "invariant") ;
if ((SyncFlags & 2) && _Responsible == NULL) {
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
} 
// park self
if (_Responsible == Self || (SyncFlags & 1)) {
TEVENT (Inflated enter - park TIMED) ;
Self->_ParkEvent->park ((jlong) RecheckInterval) ;
// Increase the RecheckInterval, but clamp the value.
RecheckInterval *= 8 ;
if (RecheckInterval > 1000) RecheckInterval = 1000 ;
} else {
TEVENT (Inflated enter - park UNTIMED) ;
    // 通過park將當前線程掛起,等待被喚醒
Self->_ParkEvent->park() ;
} i
f (TryLock(Self) > 0) break ;
// 省略部分代碼
} 
// 省略部分代碼
}

當該線程被喚醒時,會從掛起的點繼續執行,通過 ObjectMonitor::TryLock 嘗試獲取鎖,TryLock方法實現如下:

int ObjectMonitor::TryLock (Thread * Self) {
for (;;) {
void * own = _owner ;
if (own != NULL) return 0 ;
if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
// Either guarantee _recursions == 0 or set _recursions = 0.
assert (_recursions == 0, "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert that OwnerIsThread == 1
return 1 ;
} 
// The lock had been free momentarily, but we lost the race to the lock.
// Interference -- the CAS failed.
// We can either return -1 or retry.
// Retry doesn't make as much sense because the lock was just acquired.
if (true) return -1 ;
}
}

以上代碼的具體流程概括如下:

  1. 當前線程被封裝成ObjectWaiter對象node,狀態設置成ObjectWaiter::TS_CXQ。
  2. 在for循環中,通過CAS把node節點push到_cxq列表中,同一時刻可能有多個線程把自己的node節點push到_cxq列表中。
  3. node節點push到_cxq列表之后,通過自旋嘗試獲取鎖,如果還是沒有獲取到鎖,則通過park將當前線程掛起,等待被喚醒。
  4. 當該線程被喚醒時,會從掛起的點繼續執行,通過 ObjectMonitor::TryLock 嘗試獲取鎖。

monitor釋放
當某個持有鎖的線程執行完同步代碼塊時,會進行鎖的釋放,給其它線程機會執行同步代碼,在HotSpot中,通過退出monitor的方式實現鎖的釋放,並通知被阻塞的線程,具體實現位於ObjectMonitor的exit方法中。(位於:src/share/vm/runtime/objectMonitor.cpp),源碼如下所示:

// 省略部分代碼
ObjectWaiter * w = NULL ;
int QMode = Knob_QMode ;
// qmode = 2:直接繞過EntryList隊列,從cxq隊列中獲取線程用於競爭鎖
if (QMode == 2 && _cxq != NULL) {
w = _cxq ;
assert (w != NULL, "invariant") ;
assert (w->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
ExitEpilog (Self, w) ;
return ;
} 
// qmode =3:cxq隊列插入EntryList尾部;
if (QMode == 3 && _cxq != NULL) {
w = _cxq ;
for (;;) {
assert (w != NULL, "Invariant") ;
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL,
&_
cxq, w) ;
if (u == w) break ;
w = u ;
} a
ssert (w != NULL , "invariant") ;
ObjectWaiter * q = NULL ;
ObjectWaiter * p ;
for (p = w ; p != NULL ; p = p->_next) {
guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
p->TState = ObjectWaiter::TS_ENTER ;
p->_prev = q ;
q = p ;
} O
bjectWaiter * Tail ;
for (Tail = _EntryList ; Tail != NULL && Tail->_next != NULL ; Tail =
Tail->_next) ;
if (Tail == NULL) {
_EntryList = w ;
} else {
Tail->_next = w ;
w->_prev = Tail ;
}
} 
// qmode =4:cxq隊列插入到_EntryList頭部
if (QMode == 4 && _cxq != NULL) {
w = _cxq ;
for (;;) {
assert (w != NULL, "Invariant") ;
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL,
&_
cxq, w) ;
if (u == w) break ;
w = u ;
  1. 退出同步代碼塊時會讓_recursions減1,當_recursions的值減為0時,說明線程釋放了鎖。
  2. 根據不同的策略(由QMode指定),從cxq或EntryList中獲取頭節點,通過ObjectMonitor::ExitEpilog 方法喚醒該節點封裝的線程,喚醒操作最終由unpark完成,實現如下:
void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) {
assert (_owner == Self, "invariant") ;
_succ = Knob_SuccEnabled ? Wakee->_thread : NULL ;
ParkEvent * Trigger = Wakee->_event ;
Wakee = NULL ;
// Drop the lock
OrderAccess::release_store_ptr (&_owner, NULL) ;
OrderAccess::fence() ; // ST _owner vs LD in
unpark()
if (SafepointSynchronize::do_call_back()) {
TEVENT (unpark before SAFEPOINT) ;
} D
TRACE_MONITOR_PROBE(contended__exit, this, object(), Self);
Trigger->unpark() ; // 喚醒之前被pack()掛起的線程.
// Maintain stats and report events to JVMTI
if (ObjectMonitor::_sync_Parks != NULL) {
ObjectMonitor::_sync_Parks->inc() ;
}
}

被喚醒的線程,會回到 void ATTR ObjectMonitor::EnterI (TRAPS) 的第600行,繼續執行monitor的競爭。

// park self
if (_Responsible == Self || (SyncFlags & 1)) {
TEVENT (Inflated enter - park TIMED) ;
Self->_ParkEvent->park ((jlong) RecheckInterval) ;
// Increase the RecheckInterval, but clamp the value.
RecheckInterval *= 8 ;
if (RecheckInterval > 1000) RecheckInterval = 1000 ;
} else {
TEVENT (Inflated enter - park UNTIMED) ;
Self->_ParkEvent->park() ;
} 
if (TryLock(Self) > 0) break ;

monitor是重量級鎖
可以看到ObjectMonitor的函數調用中會涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等內核函數,執行同步代碼塊,沒有競爭到鎖的對象會park()被掛起,競爭到鎖的線程會unpark()喚醒。這個時候就會存在操作系統用戶態和內核態的轉換,這種切換會消耗大量的系統資源。所以synchronized是Java語言中是一個重量級(Heavyweight)的操作。用戶態和和內核態是什么東西呢?要想了解用戶態和內核態還需要先了解一下Linux系統的體系架構:

image-20201222224047237

從上圖可以看出,Linux操作系統的體系架構分為:用戶空間(應用程序的活動空間)和內核。
內核:本質上可以理解為一種軟件,控制計算機的硬件資源,並提供上層應用程序運行的環境。
用戶空間:上層應用程序活動的空間。應用程序的執行必須依托於內核提供的資源,包括CPU資源、存儲資源、I/O資源等。
系統調用:為了使上層應用能夠訪問到這些資源,內核必須為上層應用提供訪問的接口:即系統調用。

所有進程初始都運行於用戶空間,此時即為用戶運行狀態(簡稱:用戶態);但是當它調用系統調用執行某些操作時,例如 I/O調用,此時需要陷入內核中運行,我們就稱進程處於內核運行態(或簡稱為內核態)。 系統調用的過程可以簡單理解為:

  1. 用戶態程序將一些數據值放在寄存器中, 或者使用參數創建一個堆棧, 以此表明需要操作系統提供的服務。
  2. 用戶態程序執行系統調用。
  3. CPU切換到內核態,並跳到位於內存指定位置的指令。
  4. 系統調用處理器(system call handler)會讀取程序放入內存的數據參數,並執行程序請求的服務。
  5. 系統調用完成后,操作系統會重置CPU為用戶態並返回系統調用的結果。
    由此可見用戶態切換至內核態需要傳遞許多變量,同時內核還需要保護 好用戶態在切換時的一些寄存器值、變量等,以備內核態切換回用戶態。這種切換就帶來了大量的系統資源消耗,這就是在synchronized未優化之前,效率低的原因。

5. JDK6 synchronized優化

1.CAS介紹

CAS的全稱是: Compare And Swap(比較相同再交換)。是現代CPU廣泛支持的一種對內存中的共享數據進行操作的一種特殊指令。CAS可以將比較和交換轉換為原子操作,這個原子操作直接由處理器保證。
CAS的作用:CAS可以將比較和交換轉換為原子操作,這個原子操作直接由CPU保證。CAS可以保證共享變量賦值時的原子操作。CAS操作依賴3個值:內存中的值V,舊的預估值X,要修改的新值B,如果舊的預估值X等於內存中的值V,就將新的值B保存到內存中。

/**
 * @author WGR
 * @create 2020/12/23 -- 9:27
 */
public class Demo1 {
    // 1.定義一個共享變量number
    private static AtomicInteger atomicInteger = new AtomicInteger();
    public static void main(String[] args) throws InterruptedException {
        // 2.對number進行1000的++操作
        Runnable increment = () -> {
            for (int i = 0; i < 1000; i++) {
                atomicInteger.incrementAndGet(); // 變量賦值的原子性
            }
        };

        List<Thread> list = new ArrayList<>();
        // 3.使用5個線程來進行
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            list.add(t);
        }

        for (Thread t : list) {
            t.join();
        }

        System.out.println("atomicInteger = " + atomicInteger.get());
    }
}

CAS 原理
通過剛才AtomicInteger的源碼我們可以看到,Unsafe類提供了原子操作。CAS會引起ABA問題,等有時間再研究一下
Unsafe類介紹
Unsafe類使Java擁有了像C語言的指針一樣操作內存空間的能力,同時也帶來了指針的問題。過度的使用Unsafe類會使得出錯的幾率變大,因此Java官方並不建議使用的,官方文檔也幾乎沒有。Unsafe對象不能直接調用,只能通過反射獲得。

image-20201223093229280

image-20201223093305729

悲觀鎖從悲觀的角度出發:
總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞。因此synchronized我們也將其稱之為悲觀鎖。JDK中的ReentrantLock也是一種悲觀鎖。性能較差!
樂觀鎖從樂觀的角度出發:
總是假設最好的情況,每次去拿數據的時候都認為別人不會修改,就算改了也沒關系,再重試即可。所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去修改這個數據,如何沒有人修改則更新,如果有人修改則重試。
CAS這種機制我們也可以將其稱之為樂觀鎖。綜合性能較好!

CAS獲取共享變量時,為了保證該變量的可見性,需要使用volatile修飾。結合CAS和volatile可以
實現無鎖並發,適用於競爭不激烈、多核 CPU 的場景下。

  1. 因為沒有使用 synchronized,所以線程不會陷入阻塞,這是效率提升的因素之一。
  2. 但如果競爭激烈,可以想到重試必然頻繁發生,反而效率會受影響。

2.synchronized 鎖升級過程

高效並發是從JDK 5到JDK 6的一個重要改進,HotSpot虛擬機開發團隊在這個版本上花費了大量的精力去實現各種鎖優化技術,包括偏向鎖( Biased Locking )、輕量級鎖( Lightweight Locking )和如適應性自旋(Adaptive Spinning)、鎖消除( Lock Elimination)、鎖粗化( Lock Coarsening )等,這些技術都是為了在線程之間更高效地共享數據,以及解決競爭問題,從而提高程序的執行效率。
無鎖--》偏向鎖--》輕量級鎖–》重量級鎖

Java 對象的布局

術語參考: http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
在JVM中,對象在內存中的布局分為三塊區域:對象頭、實例數據和對齊填充。如下圖所示:

image-20201223101133856

對象頭
當一個線程嘗試訪問synchronized修飾的代碼塊時,它首先要獲得鎖,那么這個鎖到底存在哪里呢?是存在鎖對象的對象頭中的。
HotSpot采用instanceOopDesc和arrayOopDesc來描述對象頭,arrayOopDesc對象用來描述數組類型。instanceOopDesc的定義的在Hotspot源碼的 instanceOop.hpp 文件中,另外,arrayOopDesc的定義對應 arrayOop.hpp 。

在普通實例對象中, oopDesc的定義包含兩個成員,分別是 _mark 和 _metadata_mark 表示對象標記、屬於markOop類型,也就是接下來要講解的Mark World,它記錄了對象和鎖有關的信息。
_metadata 表示類元信息,類元信息存儲的是對象指向它的類元數據(Klass)的首地址,其中Klass表示普通指針、 _compressed_klass 表示壓縮類指針。
對象頭由兩部分組成,一部分用於存儲自身的運行時數據,稱之為 Mark Word,另外一部分是類型指針,及對象指向它的類元數據的指針。
Mark Word
Mark Word用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等等,占用內存大小與虛擬機位長一致。Mark Word對應的類型是 markOop 。源碼位於 markOop.hpp 中。

在 64位虛擬機下,Mark Word是64bit大小的,其存儲結構如下:

image-20201223101302686

klass pointer
這一部分用於存儲對象的類型指針,該指針指向它的類元數據,JVM通過這個指針確定對象是哪個類的實例。該指針的位長度為JVM的一個字大小,即32位的JVM為32位,64位的JVM為64位。 如果應用的對象過多,使用64位的指針將浪費大量內存,統計而言,64位的JVM將會比32位的JVM多耗費50%的內存。為了節約內存可以使用選項 - XX:+UseCompressedOops 開啟指針壓縮,其中,oop即ordinaryobject pointer普通對象指針。開啟該選項后,下列指針將壓縮至32位:

  1. 每個Class的屬性指針(即靜態變量)
  2. 每個對象的屬性指針(即對象變量)
  3. 普通對象數組的每個元素指針

當然,也不是所有的指針都會壓縮,一些特殊類型的指針JVM不會優化,比如指向PermGen的Class對象指針(JDK8中指向元空間的Class對象指針)、本地變量、堆棧元素、入參、返回值和NULL指針等。對象頭 = Mark Word + 類型指針(未開啟指針壓縮的情況下)在32位系統中,Mark Word = 4 bytes,類型指針 =4bytes,對象頭 = 8 bytes = 64 bits;在 64位系統中,Mark Word = 8 bytes,類型指針 = 8bytes,對象頭 = 16 bytes = 128bits;
實例數據
就是類中定義的成員變量。
對齊填充
對齊填充並不是必然存在的,也沒有什么特別的意義,他僅僅起着占位符的作用,由於HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。而對象頭正好是8字節的倍數,因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

image-20201223101529063

偏向鎖

什么是偏向鎖
偏向鎖是JDK 6中的重要引進,因為HotSpot作者經過研究實踐發現,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低,引進了偏向鎖。
偏向鎖的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是這個鎖會偏向於第一個獲得它的線程,會在對象頭存儲鎖偏向的線程ID,以后該線程進入和退出同步塊時只需要檢查是否為偏向鎖、鎖標志位以及ThreadID即可。

不過一旦出現多個線程競爭時必須撤銷偏向鎖,所以撤銷偏向鎖消耗的性能必須小於之前節省下來的CAS原子操作的性能消耗,不然就得不償失了。
偏向鎖原理
當線程第一次訪問同步塊並獲取鎖時,偏向鎖處理流程如下:

  1. 虛擬機將會把對象頭中的標志位設為“01”,即偏向模式。
  2. 同時使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中 ,如果CAS操作成功,持有偏向鎖的線程以后每次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作,偏向鎖的效率高。

持有偏向鎖的線程以后每次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作,偏向鎖的效率高。

偏向鎖的撤銷

  1. 偏向鎖的撤銷動作必須等待全局安全點
  2. 暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態
  3. 撤銷偏向鎖,恢復到無鎖(標志位為 01)或輕量級鎖(標志位為 00)的狀態

偏向鎖在 Java 6之后是默認啟用的,但在應用程序啟動幾秒鍾之后才激活,可以使用 -XX:BiasedLockingStartupDelay=0 參數關閉延遲,如果確定應用程序中所有鎖通常情況下處於競爭狀態,可以通過 XX: -UseBiasedLocking=false 參數關閉偏向鎖。

偏向鎖好處
偏向鎖是在只有一個線程執行同步塊時進一步提高性能,適用於一個線程反復獲得同一鎖的情況。偏向鎖可以提高帶有同步但無競爭的程序性能。
它同樣是一個帶有效益權衡性質的優化,也就是說,它並不一定總是對程序運行有利,如果程序中大多數的鎖總是被多個不同的線程訪問比如線程池,那偏向模式就是多余的。
在JDK5中偏向鎖默認是關閉的,而到了JDK6中偏向鎖已經默認開啟。但在應用程序啟動幾秒鍾之后才激活,可以使用 - XX:BiasedLockingStartupDelay=0 參數關閉延遲,如果確定應用程序中所有鎖通常情況下處於競爭狀態,可以通過 XX: -UseBiasedLocking=false 參數關閉偏向鎖。

偏向鎖的原理是什么?

當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標志位設為“01”,即偏向模式。同時使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中 ,如果CAS操作成功,持有偏向鎖的線程以后每次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作,偏向鎖的效率高。

偏向鎖的好處是什么?

偏向鎖是在只有一個線程執行同步塊時進一步提高性能,適用於一個線程反復獲得同一鎖的情況。偏向鎖可以提高帶有同步但無競爭的程序性能。
輕量級鎖

輕量級鎖是JDK 6之中加入的新型鎖機制,它名字中的“輕量級”是相對於使用monitor的傳統鎖而言的,因此傳統的鎖機制就稱為“重量級”鎖。首先需要強調一點的是,輕量級鎖並不是用來代替重量級鎖的。引入輕量級鎖的目的:在多線程交替執行同步塊的情況下,盡量避免重量級鎖引起的性能消耗,但是如果多個線程在同一時刻進入臨界區,會導致輕量級鎖膨脹升級重量級鎖,所以輕量級鎖的出現並非是要替代重量級鎖。

輕量級鎖原理
當關閉偏向鎖功能或者多個線程競爭偏向鎖導致偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖,其步驟如下:

創建鎖記錄(Lock Record)對象,每個線程都的棧幀都會包含一個鎖記錄的結構,內部可以存儲鎖定對象的Mark Word

image-20201223110205763

讓鎖記錄中 Object reference 指向鎖對象,並嘗試用 cas 替換 Object 的 Mark Word,將 Mark Word 的值存入鎖記錄

image-20201223110255828

如果 cas 替換成功,對象頭中存儲了 鎖記錄地址和狀態 00 ,表示由該線程給對象加鎖,這時圖示如下

如果 cas 失敗,有兩種情況

  • 如果是其它線程已經持有了該 Object 的輕量級鎖,這時表明有競爭,進入鎖膨脹過程
    如果是自己執行了 synchronized 鎖重入,那么再添加一條 Lock Record 作為重入的計數
image-20201223110440704

當退出 synchronized 代碼塊(解鎖時)如果有取值為 null 的鎖記錄,表示有重入,這時重置鎖記錄,表示重入計數減一

image-20201229133129349

當退出 synchronized 代碼塊(解鎖時)鎖記錄的值不為 null,這時使用 cas 將 Mark Word 的值恢復給對象頭

  • 成功,則解鎖成功
    失敗,說明輕量級鎖進行了鎖膨脹或已經升級為重量級鎖,進入重量級鎖解鎖流程

輕量級鎖的釋放
輕量級鎖的釋放也是通過CAS操作來進行的,主要步驟如下:

  1. 取出在獲取輕量級鎖保存在Displaced Mark Word中的數據。

  2. 用CAS操作將取出的數據替換當前對象的Mark Word中,如果成功,則說明釋放鎖成功。

    3 . 如果CAS操作替換失敗,說明有其他線程嘗試獲取該鎖,則需要將輕量級鎖需要膨脹升級為重量級鎖。

對於輕量級鎖,其性能提升的依據是“對於絕大部分的鎖,在整個生命周期內都是不會存在競爭的”,如果打破這個依據則除了互斥的開銷外,還有額外的CAS操作,因此在有多線程競爭的情況下,輕量級鎖比重量級鎖更慢。

輕量級鎖好處
在多線程交替執行同步塊的情況下,可以避免重量級鎖引起的性能消耗。

鎖膨脹

如果在嘗試加輕量級鎖的過程中,CAS 操作無法成功,這時一種情況就是有其它線程為此對象加上了輕量級鎖(有競爭),這時需要進行鎖膨脹,將輕量級鎖變為重量級鎖。

  1. 當 Thread-1 進行輕量級加鎖時,Thread-0 已經對該對象加了輕量級鎖 。

image-20201229140534487

  1. 這時 Thread-1 加輕量級鎖失敗,進入鎖膨脹流程
  • 即為 Object 對象申請 Monitor 鎖,讓 Object 指向重量級鎖地址
    然后自己進入 Monitor 的 EntryList BLOCKED

image-20201229140615378

  1. 當 Thread-0 退出同步塊解鎖時,使用 cas 將 Mark Word 的值恢復給對象頭,失敗。這時會進入重量級解鎖流程,即按照 Monitor 地址找到 Monitor 對象,設置 Owner 為 null,喚醒 EntryList 中 BLOCKED 線程
自旋鎖

前面我們討論 monitor實現鎖的時候,知道monitor會阻塞和喚醒線程,線程的阻塞和喚醒需要CPU從用戶態轉為核心態,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作,這些操作給系統的並發性能帶來了很大的壓力。同時,虛擬機的開發團隊也注意到在許多應用上,共享數據的鎖定狀態只會持續很短的一段時間,為了這段時間阻塞和喚醒線程並不值得。如果物理機器有一個以上的處理器,能讓兩個或以上的線程同時並行執行,我們就可以讓后面請求鎖的那個線程“稍等一下”,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待,我們只需讓線程執行一個忙循環(自旋) , 這項技術就是所謂的自旋鎖。
自旋鎖在JDK 1.4.2中就已經引入 ,只不過默認是關閉的,可以使用-XX:+UseSpinning參數來開啟,在JDK 6中 就已經改為默認開啟了。自旋等待不能代替阻塞,且先不說對處理器數量的要求,自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時間的,因此,如果鎖被占用的時間很短,自旋等待的效果就會非常好,反之,如果鎖被占用的時間很長。那么自旋的線程只會白白消耗處理器資源,而不會做任何有用的工作,反而會帶來性 能上的浪費。因此,自旋等待的時間必須要有一定的限度,如果自旋超過了限定的次數仍然沒有成功獲得鎖,就應當使用傳統的方式去掛起線程了。自旋次數的默認值是10次,用戶可以使用參數-XX : PreBlockSpin來更改。

適應性自旋鎖

在JDK 6中引入了自適應的自旋鎖。自適應意味着自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那么虛擬機就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長的時間,比如100次循環。另外,如果對於某個鎖,自旋很少成功獲得過,那在以后要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。有了自適應自旋,隨着程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的狀況預測就會越來越准確,虛擬機就會變得越來越“聰明”了。

鎖消除

鎖消除是指虛擬機即時編譯器(JIT)在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要判定依據來源於逃逸分析的數據支持,如果判斷在一段代碼中,堆上的所有數據都不會逃逸出去從而被其他線程訪問到,那就可以把它們當做棧上數據對待,認為它們是線程私有的,同步加鎖自然就無須進行。變量是否逃逸,對於虛擬機來說需要使用數據流分析來確定,但是程序員自己應該是很清楚的,怎么會在明知道不存在數據爭用的情況下要求同步呢?實際上有許多同步措施並不是程序員自己加入的,同步的代碼在Java程序中的普遍程度也許超過了大部分讀者的想象。下面這段非常簡單的代碼僅僅是輸出3個字符串相加的結果,無論是源碼字面上還是程序語義上都沒有同步。

public class Demo01 {
  public static void main(String[] args) {
    contactString("aa", "bb", "cc");
 }
  public static String contactString(String s1, String s2, String s3) {
    return new StringBuffer().append(s1).append(s2).append(s3).toString();
 }
}

StringBuffer的append ( ) 是一個同步方法,鎖就是this也就是(new StringBuilder())。虛擬機發現它的動態作用域被限制在concatString( )方法內部。也就是說, new StringBuilder()對象的引用永遠不會“逃逸”到concatString ( )方法之外,其他線程無法訪問到它,因此,雖然這里有鎖,但是可以被安全地消除掉,在即時編譯之后,這段代碼就會忽略掉所有的同步而直接執行了。

鎖粗化

原則上,我們在編寫代碼的時候,總是推薦將同步塊的作用范圍限制得盡量小,只在共享數據的實際作用域中才進行同步,這樣是為了使得需要同步的操作數量盡可能變小,如果存在鎖競爭,那等待鎖的線程也能盡快拿到鎖。大部分情況下,上面的原則都是正確的,但是如果一系列的連續操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那即使沒有線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗。

public class Demo01 {
  public static void main(String[] args) {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < 100; i++) {
      sb.append("aa");
   }
    System.out.println(sb.toString());
 }
}

小結
JVM會探測到一連串細小的操作都使用同一個對象加鎖,將同步代碼塊的范圍放大,放到這串操作的外面,這樣只需要加一次鎖即可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM