併發編程中的Synchronized

第一章:併發編程中的三個問題

   1、可見性問題

     可見性概念
 
         可見性(Visibility):是指一個線程對共享變量進行修改,另一個先立即得到修改後的最新值。
 
     可見性演示
 
         案例演示:一個線程根據boolean類型的標記flflag while循環,另一個線程改變這個flag變量的值,另
 
一個線程並不會停止循環。
package com.itheima.demo01_concurent_problem;

/**
 * 演示可見性問題
 *  1、創建一個共享變量
 *  2、創建一個線程讀取共享變量
 *  3、創建一個線程修改共享變量
 *
 *
 *  總結:併發編程時,會出現可見性問題,當一個線程對共享變量進行了修改,另外的線程並沒有立即看到修改後的最新值
 */
public class Test01Visibility {

    public static boolean flag = true;

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


        new Thread(()->{

            while (flag)
            {

            }
        }).start();
        
        Thread.sleep(2000);

        new Thread(()->{
            flag = false;
            System.out.println("線程修改了變量的值爲false");
        }).start();

    }
}

  2、原子性問題 

     原子性概念
 
            原子性(Atomicity):在一次或多次操作中,要麼所有的操作都執行並且不會受其他因素干擾而中
 
斷,要麼所有的操作都不執行。
 
    原子性演示
 
           案例演示:5個線程各執行1000 i++;
package com.itheima.demo01_concurent_problem;


import java.util.ArrayList;
import java.util.List;

/**
 *    目標:演示原子性問題
 *        1、定義一個共享變量number
 *        2、對number進行1000次++操作
 *        3、使用5個線程來進行
 */
public class Test02Atomicity {

    public static int number = 0;

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

        Runnable increment = ()->{

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

        List<Thread> list = new ArrayList<>();

        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            list.add(t);
        }

        //保證5個線程執行完之後才輸出number的值
        for(Thread t :list)
        {
            t.join();
        }

        System.out.println("number="+number);

    }
}

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

其中,對於 number++ 而言(number 爲靜態變量),實際會產生如下的 JVM 字節碼指令:

9: getstatic #12 // Field number:I 
12: iconst_1 
13: iadd 
14: putstatic #12 // Field number:I

由此可見number++是由多條語句組成,以上多條指令在一個線程的情況下是不會出問題的,但是在多 線程情況下就可能會出現問題。比如一個線程在執行13: iadd時,另一個線程又執行9: getstatic。會導 致兩次number++,實際上只加了1

小結

併發編程時,會出現原子性問題,當一個線程對共享變量操作到一半時,另外的線程也有可能來操作共

享變量,干擾了前一個線程的操作。
 

3、有序性問題

有序性概念
 
            有序性(Ordering):是指程序中代碼的執行順序,Java在編譯時和運行時會對代碼進行優化,會導致
 
程序最終的執行順序不一定就是我們編寫代碼時的順序。
public static void main(String[] args)
 { 

    int a = 10; int b = 20; 

 }
有序性演示
 
jcstressjava併發壓測工具。https://wiki.openjdk.java.net/display/CodeTools/jcstress
 
修改pom文件,添加依賴:
<dependency> 
    <groupId>org.openjdk.jcstress</groupId> 
    <artifactId>jcstress-core</artifactId> 
    <version>${jcstress.version}</version> 
</dependency
代碼
package com.itheima.demo01_concurent_problem;


import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;

@JCStressTest
@Outcome(id = {"1","4"}, expect = Expect.ACCEPTABLE,desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING,desc = "danger")
@State
public class Test03Ordering {

    int num = 0;
    boolean ready =false;

    //線程1執行的代碼
    @Actor
    public void actor1(I_Result r)
    {
        if(ready)
        {
            r.r1 = num + num;
        }else {
            r.r1 = 1;
        }
    }

    //線程2執行的代碼
    @Actor
    public void actor2(I_Result r)
    {
       num = 2;
       ready = true;
    }
    
}
I_Result 是一個對象,有一個屬性 r1 用來保存結果,在多線程情況下可能出現幾種結果?
情況1:線程1先執行actor1,這時ready = false,所以進入else分支結果爲1
 
情況2:線程2執行到actor2,執行了num = 2;ready = true,線程1執行,這回進入 if 分支,結果爲 4。
 
情況3:線程2先執行actor2,只執行num = 2;但沒來得及執行 ready = true,線程1執行,還是進入 else分支,結果爲1
 
還有一種結果0
 
運行測試:
mvn clean install 
java -jar target/jcstress.jar
小結
 
程序代碼在執行過程中的先後順序,由於Java在編譯期以及運行期的優化,導致了代碼的執行順序未必就是開發者編寫代碼時的順序。
 

第二章、Java內存模型(JMM)

計算機結構簡介
 
馮諾依曼,提出計算機由五大組成部分,輸入設備,輸出設備存儲器,控制器,運算器
 
 
 
CPU
 

中央處理器,是計算機的控制和運算的核心,我們的程序最終都會變成指令讓CPU去執行,處理程序中的數據

內存

我們的程序都是在內存中運行的,內存會保存程序運行時的數據,供CPU處理。
 
緩存
 
CPU的運算速度和內存的訪問速度相差比較大。這就導致CPU每次操作內存都要耗費很多等待時間。內 存的讀寫速度成爲了計算
 
機運行的瓶頸。於是就有了在CPU和主內存之間增加緩存的設計。最靠近CPU 的緩存稱爲L1,然後依次是 L2L3和主內存,
 
CPU緩存模型如圖下圖所示
 
CPU Cache分成了三個級別: L1 L2 L3。級別越小越接近CPU,速度也更快,同時也代表着容量越
小。
1. L1是最接近CPU的,它容量最小,例如32K,速度最快,每個核上都有一個L1 Cache
2. L2 Cache 更大一些,例如256K,速度要慢一些,一般情況下每個核上都有一個獨立的L2 Cache
3. L3 Cache是三級緩存中最大的一級,例如12MB,同時也是緩存中最慢的一級,在同一個CPU插槽
之間的核共享一個L3 Cache
 
Cache的出現是爲了解決CPU直接訪問內存效率低下問題的,程序在運行的過程中,CPU接收到指令後,它會最先向CPU中的一級緩存(L1 Cache)去尋找相關的數據,如果命中緩存,CPU進行計算時就可以直接對CPU Cache中的數據進行讀取和寫入,當運算結束之後,再將CPUCache中的最新數據刷新到主內存當中,CPU通過直接訪問Cache的方式替代直接訪問主存的方式極大地提高了CPU 的吞吐能 力。但是由於一級緩存(L1 Cache)容量較小,所以不可能每次都命中。這時CPU會繼續向下一級的二 級緩存(L2 Cache)尋找,同樣的道理,當所需要的數據在二級緩存中也沒有的話,會繼續轉向L3 Cache、內存(主存)和硬盤。
 

 

 

Java內存模型

 
Java內存模型的概念Java Memory Molde (Java內存模型/JMM),千萬不要和Java內存結構混淆
 
關於“Java內存模型的權威解釋,請參考 https://download.oracle.com/otn-pub/jcp/memory_model-1.0-pfd-spec-oth-JSpec/memory_model-1_0-pfd-spec.pdf
 
Java內存模型,是Java虛擬機規範中所定義的一種內存模型,Java內存模型是標準化的,屏蔽掉了底層 不同計算機的區別。
 
Java內存模型是一套規範,描述了Java程序中各種變量(線程共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取變量這樣的底層細節,具體如下。
 
主內存
 
主內存是所有線程都共享的,都能訪問的。所有的共享變量都存儲於主內存。
 
工作內存
 
每一個線程有自己的工作內存,工作內存只存儲該線程對共享變量的副本。線程對變量的所有的操作(讀,取)都必須在工作內存中完成,而不能直接讀寫主內存中的變量,不同線程之間也不能直接 訪問對方工作內存中的變量。
 
 
 
Java內存模型的作用
 
Java內存模型是一套在多線程讀寫共享數據時,對共享數據的可見性、有序性、和原子性的規則和保障。
synchronized,volatile
 
CPU緩存,內存與Java內存模型的關係
 
通過對前面的CPU硬件內存架構、Java內存模型以及Java多線程的實現原理的瞭解,我們應該已經意識
到,多線程的執行最終都會映射到硬件處理器上進行執行。
Java內存模型和硬件內存架構並不完全一致。對於硬件內存來說只有寄存器、緩存內存、主內存的概
念,並沒有工作內存和主內存之分,也就是說Java內存模型對內存的劃分對硬件內存並沒有任何影響,
因爲JMM只是一種抽象的概念,是一組規則,不管是工作內存的數據還是主內存的數據,對於計算機硬
件來說都會存儲在計算機主內存中,當然也有可能存儲到CPU緩存或者寄存器中,因此總體上來說,
Java內存模型和計算機硬件內存架構是一個相互交叉的關係,是一種抽象概念劃分與真實物理硬件的交
叉。
 
JMM內存模型與CPU硬件內存架構的關係
 
小結
 
Java內存模型是一套規範,描述了Java程序中各種變量(線程共享變量)的訪問規則,以及在JVM中將變量 存儲到內存和從內存中讀取變量這樣的底層細節,Java內存模型是對共享數據的可見性、有序性、和原子性的規則和保障。
 
 

主內存與工作內存之間的交互

Java內存模型中定義了以下8種操作來完成,主內存與工作內存之間具體的交互協議,即一個變量如何從主內存拷貝到工作內
存、如何從工作內存同步回主內存之類的實現細節,虛擬機實現時必須保證下面 提及的每一種操作都是原子的、不可再分的。

 對應如下的流程圖:

 

注意:
 
1. 如果對一個變量執行lock操作,將會清空工作內存中此變量的值
 
2. 對一個變量執行unlock操作之前,必須先把此變量同步到主內存中
 
小結
 
主內存與工作內存之間的數據交互過程
lock -> read -> load -> use -> assign -> store -> write -> unlock

第三章:synchronized保證三大特性  

 

synchronized能夠保證在同一時刻最多隻有一個線程執行該段代碼,以達到保證併發安全的效果。
synchronized (鎖對象) 
 { 

    // 受保護資源;

 }

 synchronized與原子性

        使用synchronized保證原子性

        案例演示:5個線程各執行1000 i++;
package com.itheima.demo02_concurrent_problem;

import java.util.ArrayList;
import java.util.List;

/*
    目標:演示原子性問題
        1.定義一個共享變量number
        2.對number進行1000的++操作
        3.使用5個線程來進行
 */
public class Test02Atomicity {
    // 1.定義一個共享變量number
    private static int number = 0;
    private static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        // 2.對number進行1000的++操作
        Runnable increment = () -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (obj) {
                    number++;
                }
            }
        };

        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("number = " + number);
    }
}
synchronized保證原子性的原理
 
number++;增加同步代碼塊後,保證同一時間只有一個線程操作number++;。就不會出現安全問題。如下圖:當進入同步代碼的時候獲取鎖,monitorenter  接下來獲取值 進行++操作 更新變量的值  最後釋放鎖。只有當前正在執行的線程釋放鎖之後,其他線程才能進入到同步代碼塊裏面
 
 

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

synchronized與可見性(使用volatile也可以解決可見性問題)

使用synchronized保證可見性

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

/*
    目標:演示可見性問題
        1.創建一個共享變量
        2.創建一條線程不斷讀取共享變量
        3.創建一條線程修改共享變量
 */
public class Test01Visibility {
    // 1.創建一個共享變量
    private static boolean flag = true;
    private static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        // 2.創建一條線程不斷讀取共享變量
        new Thread(() -> {
            while (flag) {
                //每次執行到synchronized,都會讓線程刷新工作內存裏面的值,從主存裏面重新獲取
                synchronized(){

                }
            }
        }).start();

        Thread.sleep(2000);

        // 3.創建一條線程修改共享變量
        new Thread(() -> {
            flag = false;
            System.out.println("線程修改了變量的值爲false");
        }).start();
    }
}

 synchronized保證可見性的原理

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

 

synchronized與有序性

爲什麼要重排序
 
爲了提高程序的執行效率,編譯器和CPU會對程序中代碼進行重排序。
 
as-if-serial語義
 
as-if-serial語義的意思是:不管編譯器和CPU如何重排序,必須保證在單線程情況下程序的結果是正確的。
以下數據有依賴關係,不能重排序。
 
寫後讀:
int a = 1; 
int b = a;
寫後寫:
int a = 1; 
int a = 2;
讀後寫:
int a = 1; 
int b = a; 
int a = 2;
編譯器和處理器不會對存在數據依賴關係的操作做重排序,因爲這種重排序會改變執行結果。但是,如
果操作之間不存在數據依賴關係,這些操作就可能被編譯器和處理器重排序。
int a = 1;
int b = 2; 
int c = a + b;
可以這樣: 
int a = 1;
int b = 2;
int c = a + b; 

也可以重排序這樣: 
int b = 2; 
int a = 1; 
int c = a + b;

使用synchronized保證有序性

package com.itheima.demo02_concurrent_problem;

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;

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

    // 線程2執行的代碼
    @Actor
    public void actor2(I_Result r) {
            
			synchronized(obj){
			
			num = 2;
            ready = true;
		}
    }
}

synchronized保證有序性的原理

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

第四章:synchronized的特性

可重入特性

什麼是可重入
一個線程可以多次執行synchronized,重複獲取同一把鎖
package com.itheima.demo03_synchronized_nature;

/*
    目標:演示synchronized可重入
        1.自定義一個線程類
        2.在線程類的run方法中使用嵌套的同步代碼塊
        3.使用兩個線程來執行
 */
public class Demo01 {
    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");

            Demo01.test01();
        }
    }


}
可重入原理
synchronized的鎖對象中有一個計數器(recursions變量)會記錄線程獲得幾次鎖.
 
可重入的好處
1. 可以避免死鎖
2. 可以讓我們更好的來封裝代碼
小結
synchronized是可重入鎖,內部鎖對象中會有一個計數器記錄線程獲取幾次鎖啦,在執行完同步代碼塊
時,計數器的數量會-1,知道計數器的數量爲0,就釋放這個鎖。
 

不可中斷特性

什麼是不可中斷
一個線程獲得鎖後,另一個線程想要獲得鎖,必須處於阻塞或等待狀態,如果第一個線程不釋放鎖,第
二個線程會一直阻塞或等待,不可被中斷。
 
synchronized不可中斷演示
 
synchronized是不可中斷,處於阻塞狀態的線程會一直等待鎖
package com.itheima.demo03_synchronized_nature;
/*
    目標:演示synchronized不可中斷
        1.定義一個Runnable
        2.在Runnable定義同步代碼塊
        3.先開啓一個線程來執行同步代碼塊,保證不退出同步代碼塊
        4.後開啓一個線程來執行同步代碼塊(阻塞狀態)
        5.停止第二個線程
 */
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());
    }
}
ReentrantLock可中斷演示
package com.itheima.demo03_synchronized_nature;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/*
    目標:演示Lock不可中斷和可中斷
 */
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屬於不可被中斷
Locklock方法是不可中斷的
LocktryLock方法是可中斷的
 
 
 

第五章:synchronized原理

javap 反彙編

通過javap反彙編學習synchronized的原理

我們編寫一個簡單的synchronized代碼,如下:

package com.itheima.demo01_concurent_problem;

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是一個關鍵字,看不到源碼。我們可以將class文件 進行反彙編
 
 
JDK自帶的一個工具: javap ,對字節碼進行反彙編,查看字節碼指令。
DOS命令行輸入:
javap -p -v D:\workspace\Synchronized\target\classes\com\itheima\demo01_concurent_problem\Demo01.class
反彙編後的效果如下:
 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: getstatic     #2                  // Field obj:Ljava/lang/Object;
         3: dup
         4: astore_1
         5: monitorenter
         6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: ldc           #4                  // String 1
        11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             6    16    19   any
            19    22    19   any
      LineNumberTable:
        line 9: 0
        line 11: 6
        line 12: 14
        line 13: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      25     0  args   [Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public synchronized void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String a
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 17: 0
        line 18: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/itheima/demo01_concurent_problem/Demo01;

monitorenter
首先我們來看一下JVM規範中對於monitorenter的描述:
 
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter
 
Each object is associated with a monitor.
A monitor is locked if and only if it has anowner.The thread that executes monitorenter attempts to gain ownership of the monitorassociated with objectref, as follows: • If the entry count of the monitor associated withobjectref is zero, the thread enters the monitor and sets its entry count to one. The threadis then the owner of the monitor. •
 If the thread already owns the monitor associated withobjectref, it reenters the monitor, incrementing its entry count. • If anotherthreadalready owns the monitor associated with objectref, the thread blocks until the monitor'sentry count is zero, then tries again to gain ownership.
翻譯過來:
每一個對象都會和一個監視器monitor關聯。監視器被佔用時會被鎖住,其他線程無法來獲
取該monitor。 當JVM執行某個線程的某個方法內部的monitorenter時,它會嘗試去獲取當前對象對應
monitor的所有權。其過程如下:
1. monior的進入數爲0,線程可以進入monitor,並將monitor的進入數置爲1。當前線程成爲
monitorowner(所有者)
2. 若線程已擁有monitor的所有權,允許它重入monitor,則進入monitor的進入數加1
3. 若其他線程已經佔有monitor的所有權,那麼當前嘗試獲取monitor的所有權的線程會被阻塞,直
monitor的進入數變爲0,才能重新嘗試獲取monitor的所有權。
 
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
The thread that executes monitorexit must be the owner of the monitor associated with the
instance referenced by objectref. The thread decrements the entry count of the monitor
associated with objectref. If as a result the value of the entry count is zero, the thread
exits the monitor and is no longer its owner. Other threads that are blocking to enter the
monitor are allowed to attempt to do so
翻譯過來:
1. 能執行monitorexit指令的線程一定是擁有當前對象的monitor的所有權的線程。
2. 執行monitorexit時會將monitor的進入數減1。當monitor的進入數減爲0時,當前線程退出
monitor,不再擁有monitor的所有權,此時其他被這個monitor阻塞的線程可以嘗試去獲取這個
monitor的所有權
monitorexit釋放鎖。
monitorexit插入在方法結束處和異常處,JVM保證每個monitorenter必須有對應的monitorexit
 

面試題synchroznied出現異常會釋放鎖嗎?

會釋放鎖
 

同步方法

 
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10
 
可以看到同步方法在反彙編後,會增加 ACC_SYNCHRONIZED 修飾。會隱式調用monitorenter
monitorexit。在執行同步方法前會調用monitorenter,在執行完同步方法後會調用monitorexit
 
小結
通過javap反彙編我們看到synchronized使用編程了monitorentormonitorexit兩個指令.每個鎖對象
都會關聯一個monitor(監視器,它纔是真正的鎖對象),它內部有兩個重要的成員變量owner會保存獲得鎖
的線程,recursions會保存線程獲得鎖的次數,當執行到monitorexit,recursions-1,當計數器減到0
這個線程就會釋放鎖
 

面試題:synchronizedLock的區別

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

深入JVM源碼

目標

通過JVM源碼分析synchronized的原理
 
JVM源碼下載
 
http://openjdk.java.net/ --> Mercurial --> jdk8 --> hotspot --> zip
 
IDE(Clion )下載
 
https://www.jetbrains.com/

 

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又恢復爲NULLowner是一個臨界資源,JVM是通過CAS操作來保證其線程安全的。
2. _cxq:競爭隊列,所有請求鎖的線程首先會被放在這個隊列中(單向鏈接)。_cxq是一個臨界資源,JVM通過CAS原子指令來修改_cxq隊列。修改前_cxq的舊值填入了nodenext字段,_cxq指 向新值(新線程)。因此_cxq是一個後進先出stack(棧)。
3. _EntryList_cxq隊列中有資格成爲候選資源的線程會被移動到該隊列中。
4. _WaitSet:因爲調用wait方法而被阻塞的線程會被放在該隊列中。
 
 
每一個Java對象都可以與一個監視器monitor關聯,我們可以把它理解成爲一把鎖,當一個線程想要執 行一段被synchronized圈起來的同步方法或者代碼塊時,該線程得先獲取到synchronized修飾的對象對應的monitor
我們的Java代碼裏不會顯示地去創造這麼一個monitor對象,我們也無需創建,事實上可以這麼理解: monitor並不是隨着對象創建而創建的。我們是通過synchronized修飾符告訴JVM需要爲我們的某個對象創建關聯的monitor對象。每個線程都存在兩個ObjectMonitor對象列表,分別爲freeused列表。 同時JVM中也維護着global locklist。當線程需要ObjectMonitor對象時,首先從線程自身的free表中申 請,若存在則使用,若不存在則從global list中申請。
 
 
ObjectMonitor的數據結構中包含:_owner_WaitSet和_EntryList,它們之間的關係轉換可以用下圖表示:
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());
}
Handle 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);
}
assert(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();
   }
    Self->set_current_pending_monitor(NULL);
}

    


此處省略鎖的自旋優化等操作,統一放在後面synchronzied優化中說。
以上代碼的具體流程概括如下:
1. 通過CAS嘗試把monitorowner字段設置爲當前線程。
2. 如果設置之前的owner指向當前線程,說明當前線程再次進入monitor,即重入鎖,執行
recursions ++ ,記錄重入的次數。
3. 如果當前線程是第一次進入該monitor,設置recursions1_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 ; 
	}
	if (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() ;
		}
		if (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循環中,通過CASnode節點push_cxq列表中,同一時刻可能有多個線程把自己的node 節點push_cxq列表中。
3. node節點push_cxq列表之後,通過自旋嘗試獲取鎖,如果還是沒有獲取到鎖,則通過park將當 前線程掛起,等待被喚醒。
4. 當該線程被喚醒時,會從掛起的點繼續執行,通過 ObjectMonitor::TryLock 嘗試獲取鎖
 
monitor釋放
 
當某個持有鎖的線程執行完同步代碼塊時,會進行鎖的釋放,給其它線程機會執行同步代碼,在 HotSpot中,通過退出monitor的方式實現鎖的釋放,並通知被阻塞的線程,具體實現位於 ObjectMonitor的exit方法中。(位於:src/share/vm/runtime/objectMonitor.cpp),源碼如下所 示:
 

1. 退出同步代碼塊時會讓_recursions1,當_recursions的值減爲0時,說明線程釋放了鎖。
2. 根據不同的策略(由QMode指定),從cxqEntryList中獲取頭節點,通過 ObjectMonitor::ExitEpilog 方法喚醒該節點封裝的線程,喚醒操作最終由unpark完成,實現 如下:
被喚醒的線程,會回到 void ATTR ObjectMonitor::EnterI (TRAPS) 的第600行,繼續執行monitor 的競爭。

 

 monitor是重量級鎖

可以看到ObjectMonitor的函數調用中會涉及到Atomic::cmpxchg_ptrAtomic::inc_ptr等內核函數,
執行同步代碼塊,沒有競爭到鎖的對象會park()被掛起,競爭到鎖的線程會unpark()喚醒。這個時候就
會存在操作系統用戶態和內核態的轉換,這種切換會消耗大量的系統資源。所以synchronizedJava
言中是一個重量級(Heavyweight)的操作。
用戶態和和內核態是什麼東西呢?要想了解用戶態和內核態還需要先了解一下Linux系統的體系架構:
從上圖可以看出,Linux操作系統的體系架構分爲:用戶空間(應用程序的活動空間)和內核。
內核:本質上可以理解爲一種軟件,控制計算機的硬件資源,並提供上層應用程序運行的環境。
用戶空間:上層應用程序活動的空間。應用程序的執行必須依託於內核提供的資源,包括CPU資源、存 儲資源、I/O資源等。
系統調用:爲了使上層應用能夠訪問到這些資源,內核必須爲上層應用提供訪問的接口:即系統調用。
所有進程初始都運行於用戶空間,此時即爲用戶運行狀態(簡稱:用戶態);但是當它調用系統調用執
行某些操作時,例如 I/O調用,此時需要陷入內核中運行,我們就稱進程處於內核運行態(或簡稱爲內 核態)。
系統調用的過程可以簡單理解爲:
1. 用戶態程序將一些數據值放在寄存器中, 或者使用參數創建一個堆棧, 以此表明需要操作系統提 供的服務。
2. 用戶態程序執行系統調用。
3. CPU切換到內核態,並跳到位於內存指定位置的指令。
4. 系統調用處理器(system call handler)會讀取程序放入內存的數據參數,並執行程序請求的服務。
5. 系統調用完成後,操作系統會重置CPU爲用戶態並返回系統調用的結果。
由此可見用戶態切換至內核態需要傳遞許多變量,同時內核還需要保護好用戶態在切換時的一些寄存器 值、變量等,以備內核態切換回用戶態。這種切換就帶來了大量的系統資源消耗,這就是在 synchronized未優化之前,效率低的原因。
 
 

第六章:JDK6 synchronized優化

CAS

目標
學習CAS的作用
學習CAS的原理

 

CAS概述和作用
CAS的全成是: Compare And Swap(比較相同再交換)。是現代CPU廣泛支持的一種對內存中的共享數據進行操作的一種特殊指令。
CAS的作用:CAS可以將比較和交換轉換爲原子操作,這個原子操作直接由CPU保證。CAS可以保證共享變量賦值時的原子操作。CAS操作依賴3個值:內存中的值V,舊的預估值X,要修改的新值B,如果舊的預估值X等於內存中的值V,就將新的值B保存到內存中。
 
CASvolatile實現無鎖併發
package com.itheima.demo05_cas;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/*
    目標:演示原子性問題
        1.定義一個共享變量number
        2.對number進行1000的++操作
        3.使用5個線程來進行
 */
public class Demo01 {
    // 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類提供了原子操作。
 
Unsafe類介紹
Unsafe類使Java擁有了像C語言的指針一樣操作內存空間的能力,同時也帶來了指針的問題。過度的使 用Unsafe類會使得出錯的機率變大,因此Java官方並不建議使用的,官方文檔也幾乎沒有。Unsafe對象不能直接調用,只能通過反射獲得。

 Unsafe實現CAS

 
 
樂觀鎖和悲觀鎖
悲觀鎖從悲觀的角度出發:
總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這
樣別人想拿這個數據就會阻塞。因此synchronized我們也將其稱之爲悲觀鎖。JDK中的ReentrantLock
也是一種悲觀鎖。性能較差!
樂觀鎖從樂觀的角度出發:
總是假設最好的情況,每次去拿數據的時候都認爲別人不會修改,就算改了也沒關係,再重試即可。所
以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去修改這個數據,如何沒有人修改則更
新,如果有人修改則重試。
CAS這種機制我們也可以將其稱之爲樂觀鎖。綜合性能較好!
CAS獲取共享變量時,爲了保證該變量的可見性,需要使用volatile修飾。結合CAS和volatile可以
實現無鎖併發,適用於競爭不激烈、多核 CPU 的場景下。
1. 因爲沒有使用 synchronized,所以線程不會陷入阻塞,這是效率提升的因素之一。
2. 但如果競爭激烈,可以想到重試必然頻繁發生,反而效率會受影響。
小結
CAS的作用? Compare And Swap,CAS可以將比較和交換轉換爲原子操作,這個原子操作直接由處理
器保證。
CAS的原理?CAS需要3個值:內存地址V,舊的預期值A,要修改的新值B,如果內存地址V和舊的預期值
A相等就修改內存地址值爲B
synchronized鎖升級過程
 
高效併發是從JDK 5JDK 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採用instanceOopDescarrayOopDesc來描述對象頭,arrayOopDesc對象用來描述數組類 型。instanceOopDesc的定義的在Hotspot源碼的 instanceOop.hpp 文件中,另外,arrayOopDesc 的定義對應 arrayOop.hpp
instanceOopDesc代碼中可以看到 instanceOopDesc繼承自oopDescoopDesc的定義載Hotspot源碼中的 oop.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大小的,其存儲結構如下:
32位虛擬機下,Mark Word是32bit大小的,其存儲結構如下:
klass pointer
這一部分用於存儲對象的類型指針,該指針指向它的類元數據,JVM通過這個指針確定對象是哪個類的
實例。該指針的位長度爲JVM的一個字大小,即32位的JVM32位,64位的JVM64位。 如果應用的對
象過多,使用64位的指針將浪費大量內存,統計而言,64位的JVM將會比32位的JVM多耗費50%的內
存。爲了節約內存可以使用選項 -XX:+UseCompressedOops 開啓指針壓縮,其中,oopordinary
object pointer普通對象指針。開啓該選項後,下列指針將壓縮至32位:
1. 每個Class的屬性指針(即靜態變量)
2. 每個對象的屬性指針(即對象變量)
3. 普通對象數組的每個元素指針
當然,也不是所有的指針都會壓縮,一些特殊類型的指針JVM不會優化,比如指向PermGenClass
象指針(JDK8中指向元空間的Class對象指針)、本地變量、堆棧元素、入參、返回值和NULL指針等。
對象頭 = Mark Word + 類型指針(未開啓指針壓縮的情況下)
32位系統中,Mark Word = 4 bytes,類型指針 = 4bytes,對象頭 = 8 bytes = 64 bits64位系統中,Mark Word = 8 bytes,類型指針 = 8bytes,對象頭 = 16 bytes = 128bits
 
實例數據
就是類中定義的成員變量。
 
對齊填充
對齊填充並不是必然存在的,也沒有什麼特別的意義,他僅僅起着佔位符的作用,由於HotSpot VM
自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的
整數倍。而對象頭正好是8字節的倍數,因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充
來補全。
 
小結
Java對象由3部分組成,對象頭,實例數據,對齊數據
對象頭分成兩部分:Mark World + Klass pointer
 
 
偏向鎖
 
目標
學習偏向鎖的原理和好處
 
什麼是偏向鎖
偏向鎖是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的傳統鎖而言的,
因此傳統的鎖機制就稱爲重量級鎖。首先需要強調一點的是,輕量級鎖並不是用來代替重量級鎖的。
引入輕量級鎖的目的:在多線程交替執行同步塊的情況下,儘量避免重量級鎖引起的性能消耗,但是如
果多個線程在同一時刻進入臨界區,會導致輕量級鎖膨脹升級重量級鎖,所以輕量級鎖的出現並非是要
替代重量級鎖。
 
 
輕量級鎖原理
當關閉偏向鎖功能或者多個線程競爭偏向鎖導致偏向鎖升級爲輕量級鎖,則會嘗試獲取輕量級鎖,其步
驟如下: 獲取鎖
當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標誌位設爲“01”,即偏向模式。同時使用CAS
作把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中 ,如果CAS操作成功,持有偏向鎖的線程以後每
次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作,偏向鎖的效率高。
偏向鎖是在只有一個線程執行同步塊時進一步提高性能,適用於一個線程反覆獲得同一鎖的情況。偏向鎖可以
提高帶有同步但無競爭的程序性能。1. 判斷當前對象是否處於無鎖狀態(hashcode001),如果是,則JVM首先將在當前線程的棧幀
中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝(官方
把這份拷貝加了一個Displaced前綴,即Displaced Mark Word),將對象的Mark Word複製到棧
幀中的Lock Record中,將Lock Reocrd中的owner指向當前對象。
2. JVM利用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record的指針,如果成功表示競爭到
鎖,則將鎖標誌位變成00,執行同步操作。
3. 如果失敗則判斷當前對象的Mark Word是否指向當前線程的棧幀,如果是則表示當前線程已經持
有當前對象的鎖,則直接執行同步代碼塊;否則只能說明該鎖對象已經被其他線程搶佔了,這時輕
量級鎖需要膨脹爲重量級鎖,鎖標誌位變成10,後面等待的線程將會進入阻塞狀態。
 

 

輕量級鎖的釋放
 
輕量級鎖的釋放也是通過CAS操作來進行的,主要步驟如下:
1. 取出在獲取輕量級鎖保存在Displaced Mark Word中的數據。
2. CAS操作將取出的數據替換當前對象的Mark Word中,如果成功,則說明釋放鎖成功。3. 如果CAS操作替換失敗,說明有其他線程嘗試獲取該鎖,則需要將輕量級鎖需要膨脹升級爲重量級
鎖。
對於輕量級鎖,其性能提升的依據是對於絕大部分的鎖,在整個生命週期內都是不會存在競爭的,如
果打破這個依據則除了互斥的開銷外,還有額外的CAS操作,因此在有多線程競爭的情況下,輕量級鎖
比重量級鎖更慢。

 

輕量級鎖好處
 
在多線程交替執行同步塊的情況下,可以避免重量級鎖引起的性能消耗。
 
小結
 
輕量級鎖的原理是什麼?
將對象的Mark Word複製到棧幀中的Lock Recod中。Mark Word更新爲指向Lock Record的指針。
輕量級鎖好處是什麼?
 
在多線程交替執行同步塊的情況下,可以避免重量級鎖引起的性能消耗。
自旋鎖
 
自旋鎖原理
synchronized (Demo01.class) { ... System.out.println("aaa"); }
前面我們討論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(); 
} 
}
StringBufffferappend ( ) 是一個同步方法,鎖就是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會探測到一連串細小的操作都使用同一個對象加鎖,將同步代碼塊的範圍放大,放
到這串操作的外面,這樣只需要加一次鎖即可。
 

平時寫代碼如何對synchronized優化

減少synchronized的範圍
 
同步代碼塊中儘量短,減少同步代碼塊中代碼的執行時間,減少鎖的競爭。
降低synchronized鎖的粒度
 
將一個鎖拆分爲多個鎖提高併發度

 

 

 

讀寫分離
 
讀取時不加鎖,寫入和刪除時加鎖
 
ConcurrentHashMapCopyOnWriteArrayListConyOnWriteSet

 

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