前言html
JMM即java內存模型,JMM研究的就是多線程下Java代碼的執行順序,共享變量的讀寫。它定義了Java虛擬機在計算機內存中的工做方式。從抽象角度看,JMM定義了線程和主存之間的抽象關係:線程以前的共享變量存儲在主內存中,每一個線程有個私有的本地內存,本地內存中存儲了該線程讀寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存、寫緩衝區、寄存器以及其餘硬件和編譯器優化。java
先拋出兩個問題:node
參考文獻:c++
Java Language Specification Chapter 17. Threads and Locksgit
JSR-133: JavaTM Memory Model and Thread Specification程序員
Doug Lea' s JSR-133 cookbook算法
書籍:《Java Concurrency in Practice》sql
併發測試框架:jcstressshell
猜猜一下代碼在多線程的狀況下,會發生什麼樣的狀況?數組
boolean stop; @Actor public void a1() { while(!stop){ } } @Signal void a2() { stop = true; }
int balance = 10; @Actor public void deposit() { balance += 5; } @Actor public void withdraw() { balance -= 5; } @Arbiter public void query(I_Result r) { r.r1 = balance; }
int a; int b; @Actor public void actor1(II_Result r) { b = 1; r.r2 = a; } @Actor public void actor2(II_Result r) { a = 2; r.r1 = b; }
爲了方便測試,改造下代碼:
package com.study.demo6; import java.util.concurrent.TimeUnit; public class WhileTest { static boolean stop; public static void a1() { while (true) { boolean b = stop; if (b) { break; } } } public static void main(String[] args) { new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } stop = true; System.out.println("stop>>>>>>>true!"); }).start(); a1(); } }
運行結果:
發現main主線程中,調用了啊a1()方法,子線程1秒後,對stop修改了true,按正常邏輯,死循環應該會break終止了,可是實際上運行,咱們發現,一直在循環中,並未終止!
提示:
先用 -XX:+PrintCompilation 來查看即時編譯狀況(% 的含義 On-Stack-Replacement(OSR))
再嘗試用 -Xint 強制解釋執行
代碼演示
package com.study.demo6; import java.util.Arrays; import java.util.List; public class AddSubTest { static int balance = 10; private static void add(){ balance+=5; } private static void sub(){ balance-=5; } public static void main(String[] args) throws InterruptedException { List<Thread> threadList = Arrays.asList(new Thread(AddSubTest::add), new Thread(AddSubTest::sub)); threadList.forEach(Thread::start); for (Thread thread : threadList) { thread.join(); } System.out.println(balance); } }
這回用一下ASM 工具,能夠看到源碼第10 行的 balance += 5 的字節碼以下
LINENUMBER 8 L0 GETSTATIC TestAddSub.balance : I ICONST_5 IADD PUTSTATIC TestAddSub.balance : I
而第13 行的 balance -= 5 字節碼以下
LINENUMBER 12 L0 GETSTATIC TestAddSub.balance : I ICONST_5 ISUB PUTSTATIC TestAddSub.balance : I
換成僞代後
static int balance = 10; private static void add(){ //balance+=5; int b = balance; b += 5; balance = b; } private static void sub(){ //balance-=5; int c = balance; c -= 5; balance = c; }
可能出現的執行順序以下:
case1: 線程1和2串行
int b = balance; // 線程1 b += 5; // 線程1 balance = b; // 線程1 int c = balance; // 線程2 c -= 5; // 線程2 balance = c; // 線程2
case2:線程1和線程2同時拿到10,線程1執行完,線程2再執行完
int c = balance; // 線程2 int b = balance; // 線程1 b += 5; // 線程1 balance = b; // 線程1 c -= 5; // 線程2 balance = c; // 線程2
case3:線程1和線程2同時拿到10,線程2執行完,線程1再執行完
int b = balance; // 線程1 int c = balance; // 線程2 c -= 5; // 線程2 balance = c; // 線程2 b += 5; // 線程1 balance = b; // 線程1
代碼演示:
package com.study.demo6; public class FourthResultTest { int a; int b; private void actor1(IIResult r){ b=1; r.r2 = a; } private void actor2(IIResult r){ a=2; r.r1 = b; } }
可能出現的結果
case1:
b = 1; // 線程1 r.r2 = a; // 線程1 a = 2; // 線程2 r.r1 = b; // 線程2 // 結果 r1==1, r2==0
case2:
a = 2; // 線程2 r.r1 = b; // 線程2 b = 1; // 線程1 r.r2 = a; // 線程1 // 結果 r1==0, r2==2
case3:
a = 2; // 線程2 b = 1; // 線程1 r.r2 = a; // 線程1 r.r1 = b; // 線程2 // 結果 r1==1, r2==2
case4:這種結果是否是超乎你的預期了?這是由於多是編譯器調整了指令執行順序
r.r2 = a; // 線程1 a = 2; // 線程2 r.r1 = b; // 線程2 b = 1; // 線程1 // 結果 r1==0, r2==0
若是讓一個線程老是佔用CPU 是不合理的,全部任務調度器會讓線程分時使用CPU
編譯器以及硬件層面都會作層層優化,提高性能
Compiler/JIT 優化
Processor 流水線優化
Cache 優化
case1:
//優化前 x=1 y="universe" x=2 //優化後 y="universe" x=2
case2:
//優化前 for(i=0;i<max;i++){ z += a[i] } //優化後 t = z for(i=0;i<max;i++){ t += a[i] } z = t
case3:
//優化前 if(x>=0){ y = 1; // ... } //優化後 y = 1; if(x>=0){ // ... }
流水線在CPU 的一個時鐘週期內會執行多個指令的不一樣部分
非流水線操做
假設有三條指令
---|---|---| 1 2 3
每條指令執行花費300ps 時間,最後將結果存入寄存器須要20ps
一秒能運行的指令數爲
流水線操做
仔細分析就會發現,能夠把每一個指令細分爲三個階段
A|B|C| // 1 A|B|C| // 2 A|B|C| // 3
增長一些寄存器,緩存每一階段的結果,這樣就能夠在執行 指令1-C 階段時,同時執行 指令2-B 以及 指令3-A
一秒能運行的指令數爲
MESI (CPU緩存一致性)協議 引入緩存的反作用在於同一份數據可能保存了副本,一致性該如何保證呢?
就上文所說的第四種可能:r1 和r2 有沒有可能同時爲0
r.r1 = b; // 線程2 與 a = 2 重排 r.r2 = a; // 線程1 與 a = 1 重排 b = 1; // 線程1 a = 2; // 線程2
下面從緩存的角度分析,注意假定指令沒有重排
b = 1; // 線程1 - 寫入 CPU-0 的 store buffer a = 2; // 線程2 - 寫入 CPU-1 的 store buffer r.r1 = b; // 線程2 - 立刻執行 r.r2 = a; // 線程1 - 立刻執行 // 線程1 - 將 store buffer 中的 b = 1 寫入 cache, 晚了 // 線程2 - 將 store buffer 中的 a = 2 寫入 cache, 晚了
以上介紹了多線程讀寫共享變量可能發生的哪些問題?但對於程序員而言,咱們不該當關注到底是編譯器優化、Processor 優化、緩存優化。不然,就好像打開了潘多拉魔盒!
A memory model describes, given a program and an execution trace of that program, whether the execution trace is a legal execution of the program. A high level, informal overview of the memory model shows it to be a set of rules for when writes by one thread are visible to another thread.
多線程下,共享變量的讀寫順序是頭等大事,內存模型就是多線程下對共享變量的一組讀寫規則
在多線程下,沒有關係依賴的代碼,在操做共享變量時(至少有一個線程寫),並不能保證按編寫順序(Program Order)執行,這稱爲發生了競態條件(Race Conditon)。
例如
有共享變量 x,線程 1 執行
r.r1 = y; r.r2 = x;
線程 2 執行
x = 1; y = 1;
最終的結果多是 r11 而 r20
競態條件是爲了更好的 data race free。
若要保證多線程下,每一個線程的執行順序(Synchronization Order)按編寫順序(Program Order)執行,那麼必須使用 Synchronization Actions 來保證,這些 SA 有
lock,unlock
volatile 方式讀寫變量
VarHandle 方式讀寫變量
Synchronization Order 也稱之爲 Total Order
例如
用 volatile 修飾共享變量 y,線程 1 執行
r.r1 = y; r.r2 = x;
線程 2 執行
x = 1; y = 1;
最終的結果就不可能是 r11 而 r20
錯誤的認識,線程 1 執行
synchronized(LOCK) { r1 = x; //1 處 r2 = x; //2 處 }
線程 2 執行
synchronized(LOCK) { x = 1 }
並非說 //1 與 //2 處之間不能切換到線程 2,只是即便切換到了線程 2,由於線程 2 不能拿到 LOCK 鎖致使被阻塞,執行權又會輪到線程 1
用例1
int x; volatile int y;
以後採用
x = 10; //1 處 y = 20; //2 處
此時 //1 處代碼毫不會重排到 //2 處以後(只寫了 volatile 變量)
用例 2
int x; volatile int y;
執行下面的測試用例
@Actor public void a1(II_Result r) { y = 1; //1 處 r.r2 = x; //2 處 } @Actor public void a2(II_Result r) { x = 1; //3 處 r.r1 = y; //4 處 }
//1 //2 處的順序能夠保證(只寫了 volatile 變量),但 //3 //4 處的順序卻不能保證(只讀了 volatile 變量),仍會出現 r1r20 的問題
有時會很迷惑人,例以下面的例子
用例3
@Actor public void a1(II_Result r) { r.r2 = x; //1 處 y = 1; //2 處 } @Actor public void a2(II_Result r) { r.r1 = y; //3 處 x = 1; //4 處 }
這回 //1 //2 (只寫了 volatile 變量)//3 //4 處(只讀了 volatile 變量)的順序均能保證了,毫不會出現r1r21 的狀況
此外將用例 2 中兩個變量均用 volatile 修飾就不會出現 r1r20 的問題,所以也把所有都用 volatile 修飾稱爲total order,部分變量用 volatile 修飾稱爲 partial order
如果變量讀寫時發生線程切換(例如,線程 1 寫入 x,切換至線程 2,線程 2 讀取 x)在這些邊界的處理上若是有action1 先於 action 2 發生,那麼代碼能夠按肯定的順序執行,這稱之爲 Happens-Before Order 規則(Happens-Before Order 也稱之爲 Partial Order).
用公式表達就是:
含義爲:若是 action1 先於 action2 發生,那麼 action1 以前的共享變量的修改對於 action2 可見,且代碼按 PO順序執行
其中 $T_{n}$ 表明線程,而 x 未加說明,是普通共享變量,使用 volatile 會單獨說明
Causality 即因果律:代碼之間如存在依賴關係,即便沒有加 SA 操做,代碼的執行順序也是能夠預見的
回顧一下
多線程下,沒有依賴關係的代碼,在共享變量讀寫操做(至少有一個線程寫)時,並不能保證以編寫順序(Program Order)執行,這稱爲發生了競態條件(Race Condition)
若是有必定的依賴關係呢?
@JCStressTest @Outcome(id = {"0", "0"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE") @Outcome(expect = Expect.FORBIDDEN, desc = "FORBIDDEN") @State public class Case { int x; int y; @Actor public void a1(IIResult r) { r.r1 = x; y = r.r1; } @Actor public void a2(IIResult r){ r.r2 = y; x = r.r2; } }
x 的值來自於 y,y 的值來自於 x,而兩者的初始值都是 0,所以沒有可能有其餘結果
若要安全構造對象,並將其共享使用,須要用 final 或 volatile 修飾其成員變量,並避免 this 溢出狀況(靜態成員變量能夠安全地發佈)
例如
class Holder{ int x1; volatile int x2; public Holder(int x) { x1=x; x2=x; } }
須要將它做爲全局使用
Holder f;
兩個線程,一個建立,一個使用
Holder holder; @Actor public void a1(){ holder = new Holder(1); } @Actor public void a2(IIResult r){ Holder holder = this.holder; if (holder != null){ r.r1 = holder.x1 +holder.x2; }else { r.r1 = -1; } }
可能看見未構造完整的對象
前面沒有詳細展開從規則 2 以後的講解,是由於要理解規則,還需理解底層原理,即內存屏障
防止 y 的 Load 重排到 x 的 Load 以前
if(x) { LoadLoad return y }
意義:x == true 時,再去獲取 y,不然可能會因爲重排致使 y 的值相對於 x 是過時的
防止 A 的 Store 被重排到 B 的 Store 以後
A = x StoreStore B = true
意義:在 B 修改成 true 以前,其它線程別想看到 A 的修改
事實上對 volatile 而言 Store-Load 屏障最爲有用,簡化起見之後的分析省略部分其餘屏障
凡是須要cas操做的地方
好比AtomicInteger的源碼
public class AtomicInteger extends Number implements java.io.Serializable { private static final Unsafe U = Unsafe.getUnsafe(); private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value"); private volatile int value; // ... public final boolean compareAndSet(int expectedVal, int newVal) { return U.compareAndSetInt(this, VALUE, expectedVal, newVal); } // ... }
AbstractQueuedSynchronizer的源碼
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { private transient volatile Node head; private transient volatile Node tail; private volatile int state; protected final int getState() { return state; } protected final boolean compareAndSetState(int e, int n) { return U.compareAndSetInt(this, STATE, e, n); } final void enqueue(Node node) { if (node != null) { for (; ; ) { Node t = tail; node.setPrevRelaxed(t); if (t == null) tryInitializeHead(); else if (casTail(t, node)) { t.next = node; if (t.status < 0) LockSupport.unpark(node.waiter); break; } } } } private void tryInitializeHead() { Node h = new ExclusiveNode(); // 頭 if (U.compareAndSetReference(this, HEAD, null, h)) tail = h; } private boolean casTail(Node c, Node v) { return U.compareAndSetReference(this, TAIL, c, v); } }
ConcurrentHashMap源碼
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable { /** * Table initialization and resizing control. When negative, the * table is being initialized or resized: -1 for initialization, * else -(1 + the number of active resizing threads). Otherwise, * when table is null, holds the initial table size to use upon * reation, or 0 for default. After initialization, holds the * next element count value upon which to resize the table. */ private transient volatile int sizeCtl; /** * The array of bins. Lazily initialized upon first insertion. * Size is always a power of two. Accessed directly by iterators. */ transient volatile Node<K, V>[] table; private final Node<K, V>[] initTable() { Node<K, V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; } // ... }
volatile負責保證可見性,cas來保證原子
本質
起始synchronized本質就是通兩個JVM指令:monitorenter和monitorexit來實現了,咱們能夠經過下面一段代碼的來研究下,其原理
package com; public class SynchronizedTest { static int i = 0; public static void main(String[] args) { synchronized (SynchronizedTest.class){ i++; } } }
經過反編譯看下
#...... public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: ldc #2 // class com/SynchronizedTest 2: dup 3: astore_1 4: monitorenter 5: getstatic #3 // Field i:I 8: iconst_1 9: iadd 10: putstatic #3 // Field i:I 13: aload_1 14: monitorexit 15: goto 23 18: astore_2 19: aload_1 20: monitorexit 21: aload_2 22: athrow 23: return #......
能夠看到就是經過jvm指令monitorenter、monitorexit實現的,結合上圖,具體步驟以下:
咱們知道synchronized是通加對象鎖來實現的,可是這個對象是否做爲鎖而存在呢?
相關內存屏障
synchronized 更爲重量,申請鎖、鎖重入都要發起系統調用,頻繁調用性能會受影響
synchronized 若是沒法獲取鎖時,線程會陷入阻塞,引發的線程上下文切換成本高
雖然作了一系列優化,但輕量級鎖、偏向鎖都是針對無數據競爭場景的
若是數據的原子操做時間較長,仍應該讓線程阻塞,無鎖適合的是短頻快的共享數據修改操做主要用於計數器、中止標記、或是阻塞前的有限嘗試
目前Java 中的無鎖技術主要體如今以AtomicInteger 爲表明的的原子操做類,它的底層使用Unsafe 實現,而Unsafe 的問題在於安全性和可移植性
此外,volatile 主要使用了Store-Load 屏障來控制順序,這個屏障仍是太強了,有沒有更輕量級的解決方法呢?
在Java9 中引入了VarHandle,來提供更細粒度的內存屏障,保證共享變量讀寫可見性、有序性、原子性。提供了更好的安全性和可移植性,替代Unsafe 的部分功能
建立
public class TestVarHandle { int x; static VarHandle X; static { try { X = MethodHandles.lookup() .findVarHandle(TestVarHandle.class, "x", int.class); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); } } }
讀寫
方法名 | 做用 | 說明 |
---|---|---|
get | 獲取值 | 與普通變量取值同樣,會重排、有不可見現象 |
set | 設置值 | |
getOpaque | 獲取值 | 對其保護的變量,保證其不重排和可見性,但不使用屏障,不阻礙其它變量 |
setOpaque | 設置值 | |
getAcquire | 獲取值 | 至關於get 以後加LoadLoad + LoadStore |
setRelease | 設置值 | 至關於set 以前加LoadStore + StoreStore |
getVolatile | 獲取值 | 語義同volatile,至關於獲取以後加LoadLoad + LoadStore |
setVolatile | 設置值 | 語義同volatile,至關於設置以前加LoadStore + StoreStore,設置以後加StoreLoad |
compareAndSet | 原子賦值 | 原子賦值,成功返回true,失敗返回false |
64 位系統vs 32 位系統
若是須要保證long 和double 在32 位系統中原子性,須要用volatile 修飾
JMM9 以前
JMM9 32 位系統下double 和long 的問題,double 沒有問題,long 在-server -XX:+UnlockExperimentalVMOptions -XX:-AlwaysAtomicAccesses 纔有問題
你或許據說過對象對齊,它的一個主要目的就是爲了單個變量讀寫的原子性,可使用jol 工具查看java 對象的內存佈局
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.10</version> </dependency>
測試類
public class TestJol { public static void main(String[] args) { String layout = ClassLayout.parseClass(Test.class).toPrintable(); System.out.println(layout); } public static class Test { private byte a; private byte b; private byte c; private long e; } }
開啓對象頭壓縮(默認)輸出
com.itheima.test.TestJol$Test object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 1 byte Test.a N/A 13 1 byte Test.b N/A 14 1 byte Test.c N/A 15 1 (alignment/padding gap) 16 8 long Test.e N/A Instance size: 24 bytes Space losses: 1 bytes internal + 0 bytes external = 1 bytes total
不開啓對象頭壓縮 -XX:-UseCompressedOops 輸出
com.itheima.test.TestJol$Test object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 16 (object header) N/A 16 8 long Test.e N/A 24 1 byte Test.a N/A 25 1 byte Test.b N/A 26 1 byte Test.c N/A 27 5 (loss due to the next object alignment) Instance size: 32 bytes Space losses: 0 bytes internal + 5 bytes external = 5 bytes total
前面也看到了,Java 可以保證單個共享變量讀寫是原子的,相似的數組元素的讀寫,也會提供這樣的保證
byte[8] [0][1][2][3] [0][1][2][3]
若是上述效果不能保證,則稱之爲發生了字分裂現象,java 中沒有字分裂,但Java 中某些實現會有相似字分裂現象,例如BitSet、Unsafe 讀寫等
@JCStressTest @Outcome(id = {"0", "-1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE") @Outcome(expect = Expect.FORBIDDEN, desc = "FORBIDDEN") @State public static class Case4 { byte[] b = new byte[256]; int off = ThreadLocalRandom.current().nextInt(256); @Actor public void actor1() { b[off] = (byte) 0xFF; } @Actor public void actor2(I_Result r) { r.r1 = b[off]; } }
@JCStressTest @Outcome(id = "true, true", expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE") @Outcome(expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING") @State public static class Case6 { BitSet b = new BitSet(); @Actor public void a() { b.set(0); } @Actor public void b() { b.set(1); } @Arbiter public void c(ZZ_Result r) { r.r1 = b.get(0); r.r2 = b.get(1); } }
public class TestUnsafe { public static final long ARRAY_BASE_OFFSET = UnsafeHolder.U.arrayBaseOffset(byte[].class); static byte[] ss = new byte[8]; public static void main(String[] args) { System.out.println(ARRAY_BASE_OFFSET); UnsafeHolder.U.putInt(ss, ARRAY_BASE_OFFSET, 0xFFFFFFFF); System.out.println(Arrays.toString(ss)); } }
輸出
16 [-1, -1, -1, -1, 0, 0, 0, 0]
來個壓測
@JCStressTest @Outcome(id = "0", expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE") @Outcome(id = "-1", expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE") @Outcome(expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING") @State public static class Case5 { byte[] ss = new byte[256]; long base = UnsafeHolder.U.arrayBaseOffset(byte[].class); long off = base + ThreadLocalRandom.current().nextInt(256 - 4); @Actor public void writer() { UnsafeHolder.U.putInt(ss, off, 0xFFFF_FFFF); } @Actor public void reader(I_Result r) { r.r1 = UnsafeHolder.U.getInt(ss, off); } }
結果:
Observed state Occurrences Expectation Interpretation -1 25,591,098 ACCEPTABLE ACCEPTABLE -16777216 877 ACCEPTABLE_INTERESTING INTERESTING -256 923 ACCEPTABLE_INTERESTING INTERESTING -65536 925 ACCEPTABLE_INTERESTING INTERESTING 0 5,093,890 ACCEPTABLE ACCEPTABLE 16777215 1,673 ACCEPTABLE_INTERESTING INTERESTING 255 1,758 ACCEPTABLE_INTERESTING INTERESTING 65535 1,707 ACCEPTABLE_INTERESTING INTERESTING
@JCStressTest @Outcome(id = {"16", "-1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE") @Outcome(expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING") @State public static class Case1 { Holder f; int v = 1; @Actor public void a1() { f = new Holder(v); } @Actor void a2(I_Result r) { Holder o = this.f; if (o != null) { r.r1 = o.x8 + o.x7 + o.x6 + o.x5 + o.x4 + o.x3 + o.x2 + o.x1; r.r1 += o.y8 + o.y7 + o.y6 + o.y5 + o.y4 + o.y3 + o.y2 + o.y1; } else { r.r1 = -1; } } static class Holder { int x1, x2, x3, x4; int x5, x6, x7, x8; int y1, y2, y3, y4; int y5, y6, y7, y8; public Holder(int v) { x1 = v; x2 = v; x3 = v; x4 = v; x5 = v; x6 = v; x7 = v; x8 = v; y1 = v; y2 = v; y3 = v; y4 = v; y5 = v; y6 = v; y7 = v; y8 = v; } } }
緣由分析
好比有個Student類代碼以下:
public class Student{ final String name; int age; public Student(name,age){ this.name =name; this.age = age; } }
Student stu爲共享變量 stu = new Student("zhangsan",18);
name若是沒有final修飾
t =new Student(name,age) stu = t this.name = name this.age =age
name若是有final修飾,位置任意
t=new Student(name,age) this.name=name this.age=age >----StoreStore----< stu = t
name 有volatile 修飾,注意位置必須在最後
t=new Student(name,age) this.age=age this.name=name >----Store Load----< stu =t