天天看點

深入解析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();
    }
}      
深入解析synchronized

總結:

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

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);
    }
}      
深入解析synchronized

使用javap反彙編class檔案,得到下面的位元組碼指令:

深入解析synchronized

由此可見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

深入解析synchronized

程式代碼在執行過程中的先後順序,由于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;
        }
    }
}      

小結:

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      
深入解析synchronized

可重入原理

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");
    }
}      

反編譯後的結果

深入解析synchronized
深入解析synchronized

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,它們之間的關系轉換可以用下圖

表示:

深入解析synchronized

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系統的體系架構:

深入解析synchronized

從上圖可以看出,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對象不能直接調用,隻能通過反射獲得。

深入解析synchronized
深入解析synchronized

悲觀鎖從悲觀的角度出發:

總是假設最壞的情況,每次去拿資料的時候都認為别人會修改,是以每次在拿資料的時候都會上鎖,這樣别人想拿這個資料就會阻塞。是以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中,對象在記憶體中的布局分為三塊區域:對象頭、執行個體資料和對齊填充。如下圖所示:

對象頭

當一個線程嘗試通路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大小的,其存儲結構如下:

深入解析synchronized

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位元組的倍數,是以,當對象執行個體資料部分沒有對齊時,就需要通過對齊填充來補全。

深入解析synchronized
偏向鎖

什麼是偏向鎖

偏向鎖是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

深入解析synchronized

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

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

深入解析synchronized

如果 cas 失敗,有兩種情況

  • 如果是其它線程已經持有了該 Object 的輕量級鎖,這時表明有競争,進入鎖膨脹過程

    如果是自己執行了 synchronized 鎖重入,那麼再添加一條 Lock Record 作為重入的計數

深入解析synchronized

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

深入解析synchronized

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

  • 成功,則解鎖成功

    失敗,說明輕量級鎖進行了鎖膨脹或已經更新為重量級鎖,進入重量級鎖解鎖流程

輕量級鎖的釋放

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

  1. 取出在擷取輕量級鎖儲存在Displaced Mark Word中的資料。
  2. 用CAS操作将取出的資料替換目前對象的Mark Word中,如果成功,則說明釋放鎖成功。

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

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

輕量級鎖好處

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

鎖膨脹

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

  1. 當 Thread-1 進行輕量級加鎖時,Thread-0 已經對該對象加了輕量級鎖 。
  1. 這時 Thread-1 加輕量級鎖失敗,進入鎖膨脹流程
  • 即為 Object 對象申請 Monitor 鎖,讓 Object 指向重量級鎖位址

    然後自己進入 Monitor 的 EntryList BLOCKED

深入解析synchronized
  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();
 }
}      
鎖粗化
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());
 }
}      

繼續閱讀