深入理解同步與鎖

一、CAS:(Compare and swap)

作用:
在沒有鎖的狀態下,保證多個線程對同一個值的更新

實現:
用戶態一直循環直到修改成功
do{}while(修改成功);

在這裏插入圖片描述

ABA問題:(你和女朋友分手,之後她和別的男的在一起,之後又和你複合了)

更新值的過程中,其他線程修改了。

解決方法:
-1.給這個值添加一個版本號,每次被修改時增加版本號,當你修改時判斷版本號是否和你當初讀取的一樣
-2.給這個值添加一個Boolean類型,標記是否被修改過。

本質是如何實現的(與synchronized,volatile底層實現一樣)

jdk中unsafe調用native方法。
底層有直接的指令支持CAS:lock cmpxchg(lock表示,當執行cmpxchg指令時,不能被打斷)

二、JOL(java object layout)

//pem.xml配置文件修改
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>
//打印對象佈局
public class ObjectTest {
    public static void main(String[] args) {

        Object o = new Object();

        //查看Object對象佈局
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

    }
}

對象在內存中的內存佈局

字段 大小
markword(對象頭) 8字節
class pointer(類型指針) 4字節
instance data(實例數據) 如果對象爲空,則爲0
padding(填充符) 4字節(補充爲8的整數倍)

java查看默認命令行參數

java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=132883072 -XX:MaxHeapSize=2126129152 -XX:+PrintCommandLineFl
ags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesInd
ividualAllocation -XX:+UseParallelGC
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

UseCompressedClassPointers: 會將8字節指針壓縮爲4字節(爲什麼8字節,因爲操作系統64位)

下面對象內存大小是多少?

計算大小時,要查看是否開啓指針壓縮(UseCompressedClassPointers)。

Object o = new Object();

o引用:4字節;object對象16字節;總共20字節

markword(對象頭) 8字節(鎖標誌(3),分代年齡(4bit),hashcode(31bit),unusedGC標記)
class pointer(類型指針) 4字節
instance data(實例數據) 如果對象爲空,則爲0
padding(填充符) 4字節(補充爲8的整數倍)

synchronized鎖升級

synchronized上鎖會優化。
對象new(無鎖) -》偏向鎖(默認打開,偏於第一個使用的線程) -》輕量級鎖(自旋鎖,無鎖) -》重量級鎖

這些變化記錄在markword。

查看對象頭

public class ObjectTest {
    public static void main(String[] args) {

        Object o = new Object();

        //查看Object對象佈局
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

        synchronized (o){ //鎖定對象
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

沒加鎖之前
在這裏插入圖片描述
加鎖之後
在這裏插入圖片描述

鎖的信息保存在對象頭中
對象頭

過程:

當一個線程對無鎖對象上鎖時,直接將自己的線程指針寫入對象頭(偏向鎖)。
當存在線程競爭鎖時,撤銷偏向鎖,線程內部生成lockrecord對象,並將這
個對象指針貼到對象頭中,並將hashcode保存到lockrecord(自旋鎖,CAS操作)。
解決自旋鎖等待問題,一定情況下(自旋時間長,JVM自己控制),升級爲重量
級鎖(將指向互斥量的指針寫入對象頭)。

鎖降級:

GC時候會出現,沒有意思(爲啥?因爲對象都要刪除了)。

鎖消除(lock eliminate):

public  void  add(String str1, String str2){
	/**
	 *StringBuilder與StringBuffer區別:
	 *StringBuffer線程安全,append方法是同步的
	 */
	StringBuffer str = new StringBuffer();
	str.append(str1).append(str2);  //str對象不是共享的資源,jvm會消除鎖
}

鎖粗化(lock coarsening):

public String test(String str){
	int i = 0;
	StringBuffer buffer = new StringBuffer();
	while(i < 10){
		buffer.append(str); //會將這裏的鎖添加到while之外
		i++;
	}

	return buffer.toString();
}

JIT(just in time compiler,即時編譯)

synchronized實現過程

1.java 代碼: synchronized
2.JVM指令: monitorenter monitorexit
3.執行過程中自動升級
4.機器指令: lock cmpxchg

三、超線程

一個ALU對應的多個PC寄存器的組合。當cpu執行線程2時,ALU直接去另一些寄存器中執行,不需要加載線程2的環境到線程1使用的寄存器中。

四、緩存

cpu有L1,L2緩存,如果多核,那麼多個核共享L3。

五、cache line(64字節)

cpu讀取內存的時候按塊讀取。
緩存行對齊,加速程序執行速度

沒有考慮cache line

public class Cache1 {
    private static class T{
        public volatile  long x = 0L;
    }

    public  static T[] arr = new T[2];

    static{
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(()->{
            for (long i = 0; i < 1000_0000L; i++){
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(()->{
            for (long i = 0; i < 1000_0000L; i++){
                arr[1].x = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }

}

考慮cache line

public class Cache2 {

    private static class Padding{
        public volatile  long a1,a2,a3,a4,a5,a6,a7;     //讓數據不在同一個cache line中
    }

    private static class T extends  Padding{
        public volatile  long x = 0L;
    }

    public  static T[] arr = new T[2];

    static{
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(()->{
            for (long i = 0; i < 1000_0000L; i++){
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(()->{
            for (long i = 0; i < 1000_0000L; i++){
                arr[1].x = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }
}

六、volatile

作用:

  • 保證變量在線程間的可見性
  • 禁止指令重排序(防止指令半初始化)
    創建對象時,jvm執行三步。1:堆上分配對象(並設爲默認值),2:調用構造函數初始化,3:將初始化好的對象和棧上的引用對應起來。如果不禁止重排序,會發生2,3重排序導致錯誤。

底層如何實現數據一致性:

  • (MESI(Modified,Exclusive,Shared,Invalid) Cache一致性協議) Inter CPU,其他也有對應的。(一個cache line中一個數據改變,通知其他數據)
  • MESI不行,鎖總線

系統底層如何保證有序性

  • 內存屏障(屏障兩邊的指令不可以重排序)
  • 鎖總線

volatile如何解決指令重排序
1.代碼中的volatile
2.字節碼,加ACC_VOLATILE標誌
3.JVM加內存屏障
4.hotspot直接把總線鎖了(方便移植)。

Java屏障規則:
讀讀,讀寫,寫讀,寫寫之間可以加屏障

JVM實現細節:
寫操作之前和之後加屏障,讀操作之前和之後加屏障

七、強軟,弱虛引用:

:棧上的對象引用和堆中內存對象對應,當棧上對象爲空時,堆中對象被回收

Object ob = new Object();
ob = null;

:和強引用類似,但是堆內存對象中又包含一個對象,他們之間是弱引用。

作用:
常常用於緩存中。

SoftReference<byte[]> m = new SoftReference<>(new byte[100102410]);
m.get(); //獲取對象地
System.gc(); //沒有釋放回收
如何釋放:當堆內存空間不足時,會自動釋放。

:類似軟引用,但是即使引用在也可以直接回收。一次性使用

作用:
防止內存泄漏。Thread Local使用這個技術。

WeakReference<byte[]> m = new WeakReference<>(new byte[100102410]);
System.gc(); //釋放回收

作用:
管理堆外內存,JVM使用堆外內存(zero copy)

當某個對象關聯到堆外內存的時候,這個對象被回收時,堆外內存也要被回收。
PhantomReference<byte[]> m = new PhantomReference<>(new byte[100102410],QUEUE);
m.get(); //獲取對象地,但是返回null

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章