【併發編程基礎篇】Java併發編程的三大特性和Synchronized如何解決

目錄

前言

可見性問題及解決

概念描述

代碼展示

分析

synchronized如何解決可見性

原子性問題及解決 

概念描述

代碼展示

分析

synchronized如何解決原子性問題

有序性問題及解決

概念描述

代碼展示

synchronized如何解決有序性問題

synchronized的常見使用方式

修飾代碼塊(同步代碼塊)

修飾方法

synchronized不能繼承?(插曲)

修飾靜態方法

修飾類


前言

毫無疑問,synchronized是我們用過的第一個併發關鍵字,很多博文都在講解這個技術。不過大多數講解還停留在對synchronized的使用層面,其底層的很多原理和優化,很多人可能並不知曉。因此本文將通過對synchronized的大量C源碼分析,讓大家對他的瞭解更加透徹點。

本篇將從爲什麼要引入synchronized,常見的使用方式,存在的問題以及優化部分這四個方面描述,話不多說,開始表演。

可見性問題及解決

概念描述

指一個線程對共享變量進行修改,另一個能立刻獲取到修改後的最新值。

代碼展示

類:

public class Example1 {
    //1.創建共享變量
    private static boolean flag = true;

    public static void main(String[] args) throws Exception {
        //2.t1空循環,如果flag爲true,不退出
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if(!flag){
                        System.out.println("進入if");
                        break;
                    }
                }
            }
        });
        t1.start();

        Thread.sleep(2000L);
        //2.t2修改flag爲false
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                flag = false;
                System.out.println("修改了");
            }
        });

        t2.start();
    }
} 

 

運行結果:

分析

這邊先要了解下Java的內存模式,不明白的可點擊傳送門,todo。

下圖線程t1,t2從主內存分別獲取flag=true,t1空循環,直到flag爲false的時候退出循環。t2拿到flag的值,將其改爲false,並寫入到主內存。此時主內存和線程t2的工作內存中flag均爲false,但是線程t1工作內存中的flag還是true,所以一直退不了循環,程序將一直執行。

synchronized如何解決可見性

首先我們嘗試在t1線程中加一行打印語句,看看效果。

代碼:

public class Example1 {
    //1.創建共享變量
    private static boolean flag = true;

    public static void main(String[] args) throws Exception {
        //2.t1空循環,如果flag爲true,不退出
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    //新增的打印語句
                    System.out.println(flag);
                    if(!flag){
                        System.out.println("進入if");
                        break;
                    }
                }
            }
        });
        t1.start();

        Thread.sleep(2000L);
        //2.t2修改flag爲false
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                flag = false;
                System.out.println("修改了");
            }
        });

        t2.start();
    }
} 

運行結果:

我們發現if裏面的語句已經打印出來了,線程1已經感知到線程2對flag的修改,即這條打印語句已經影響了可見性。這是爲啥?

答案就是println中,我們看下源碼:

println有個上鎖的過程,即操作如下:

1.獲取同步鎖。

2.清空自己工作內存上的變量。

3.從主內存獲取最新值,並加載到工作內存中。

4.打印並輸出。

所以這裏解釋了爲什麼線程t1加了打印語句之後,t1立刻能感知t2對flag的修改。因爲每次打印的時候其都從主內存上獲取了最新值,當t2修改的時候,t1立刻從主內存獲取了值,所以進入了if語句,並最終能跳出循環。

synchronized的原理就是清空自己工作內存上的並,通過將主內存最新值刷新到工作內存中,讓各個線程能互相感知修改。

原子性問題及解決 

概念描述

在一次或多個操作中,要不所有操作都執行,要不所有操作都不執行。

代碼展示

類:

public class Example2 {
    //1.定義全局變量number
    private static int number = 0;

    public static void main(String[] args) throws Exception {
        Runnable runnable = () -> {
            for (int i = 0; i < 10000; i++) {
                    number++;
            }
        };
        //2.t1讓其自增10000
        Thread t1 = new Thread(runnable);
        t1.start();

        //3.t2讓其自增10000
        Thread t2 = new Thread(runnable);
        t2.start();

        //4.等待t1,t2運行結束
        t1.join();
        t2.join();
        System.out.println("number=" + number);
    }
} 

運行結果:

分析

每個線程執行的邏輯是循環1萬次,每次加1,那我們希望的結果是2萬,但是實際上結果是不足2萬的。我們先用javap命令反彙編,我們看到很多代碼,但是number++涉及的指令有四句,具體看第二張圖。

如果有多條線程執行這段number++代碼,當前number爲0,線程1先執行到iconst_1指令,即將執行iadd操作,而線程2執行到getstatic指令,這個時候number值還沒有改變,所以線程2獲取到的靜態字段是0,線程1執行完iadd操作,number變爲1,線程2執行完iadd操作,number還是1。這個時候就發現問題了,做了兩次number++操作,但是number只增加了1。

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

synchronized如何解決原子性問題

在上面的分析中,我們已經知道發生問題的原因,number++是由四條指令組成,沒有保證原子操作。所以,我們只要將number++作爲一個整體就行,即保證他的原子性。具體代碼如下:

public class Example2 {
    //1.定義全局變量number
    private static int number = 0;
    //新增一個靜態變量object
    private static Object object = new Object();

    public static void main(String[] args) throws Exception {
        Runnable runnable = () -> {
            for (int i = 0; i < 10000; i++) {
                //將number++的操作用object對象鎖住
                synchronized (object) {
                    number++;
                }
            }
        };
        //2.t1讓其自增10000
        Thread t1 = new Thread(runnable);
        t1.start();

        //3.t2讓其自增10000
        Thread t2 = new Thread(runnable);
        t2.start();

        //4.等待t1,t2運行結束
        t1.join();
        t2.join();
        System.out.println("number=" + number);
    }
} 

 

我們看到最終number爲20000,那爲什麼要加上synchronized,結果就正確了?我們再反編譯下Example2,可以看到在四行指令前後分別有monitorenter和monitorexist,線程1在執行中間指令時,其他線程不可以進入monitorenter,需要等線程1執行完monitorexist,其他進程才能繼續monitorenter,進行自增操作。

有序性問題及解決

概念描述

代碼中程序執行的順序,Java在編譯和運行時會對代碼進行優化,這樣會導致我們最終的執行順序並不是我們編寫代碼的書寫順序。

代碼展示

咱先來看一個概念,重排序,也就是語句的執行順序會被重新安排。其主要分爲三種:

1.編譯器優化的重排序:可以重新安排語句的執行順序。

2.指令級並行的重排序:現代處理器採用指令級並行技術,將多條指令重疊執行。

3.內存系統的重排序:由於處理器使用緩存和讀寫緩衝區,所以看上去可能是亂序的。

上面代碼中的a = new A();可能被被JVM分解成如下代碼:

// 可以分解爲以下三個步驟
1 memory=allocate();// 分配內存 相當於c的malloc
2 ctorInstanc(memory) //初始化對象
3 s=memory //設置s指向剛分配的地址複製代碼

 

 // 上述三個步驟可能會被重排序爲 1-3-2,也就是:
1 memory=allocate();// 分配內存 相當於c的malloc
3 s=memory //設置s指向剛分配的地址
2 ctorInstanc(memory) //初始化對象  複製代碼

一旦假設發生了這樣的重排序,比如線程A在執行了步驟1和步驟3,但是步驟2還沒有執行完。這個時候線程B進入了第一個語句,它會判斷a不爲空,即直接返回了a。其實這是一個未初始化完成的a,即會出現問題。

synchronized如何解決有序性問題

給上面的三個步驟加上一個synchronized關鍵字,即使發生重排序也不會出現問題。線程A在執行步驟1和步驟3時,線程B因爲沒法獲取到鎖,所以也不能進入第一個語句。只有線程A都執行完,釋放鎖,線程B才能重新獲取鎖,再執行相關操作。

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