Java多線程學(xué)習(xí)筆記(強烈建議收藏)

什么是程序,進程和線程?

  • 程序是計算機的可執(zhí)行文件
  • 進程是計算機資源分配的基本單位
  • 線程是資源調(diào)度執(zhí)行的基本單位
    • 一個程序里面不同的執(zhí)行路徑
    • 多個線程共享進程中的資源

線程和進程的關(guān)系

線程就是輕量級進程,是程序執(zhí)行的最小單位。

多進程的方式也可以實現(xiàn)并發(fā),為什么我們要使用多線程?

  1. 共享資源在線程間的通信比較容易。
  2. 線程開銷更小。

進程和線程的區(qū)別?

  • 進程是一個獨立的運行環(huán)境,而線程是在進程中執(zhí)行的一個任務(wù)。他們兩個本質(zhì)的區(qū)別是是否單獨占有內(nèi)存地址空間及其它系統(tǒng)資源(比如I/O)。
  • 進程單獨占有一定的內(nèi)存地址空間,所以進程間存在內(nèi)存隔離,數(shù)據(jù)是分開的,數(shù)據(jù)共享復(fù)雜但是同步簡單,各個進程之間互不干擾;而線程共享所屬進程占有的內(nèi)存地址空間和資源,數(shù)據(jù)共享簡單,但是同步復(fù)雜。
  • 進程單獨占有一定的內(nèi)存地址空間,一個進程出現(xiàn)問題不會影響其他進程,不影響主程序的穩(wěn)定性,可靠性高;一個線程崩潰可能影響整個程序的穩(wěn)定性,可靠性較低。
  • 進程單獨占有一定的內(nèi)存地址空間,進程的創(chuàng)建和銷毀不僅需要保存寄存器和棧信息,還需要資源的分配回收以及頁調(diào)度,開銷較大;線程只需要保存寄存器和棧信息,開銷較小。
  • 進程是操作系統(tǒng)進行資源分配的基本單位,而線程是操作系統(tǒng)進行調(diào)度的基本單位,即CPU分配時間的單位。

什么是線程切換?

從底層角度上看,CPU主要由如下三部分組成,分別是:

  • ALU: 計算單元
  • Registers: 寄存器組
  • PC:存儲到底執(zhí)行到哪條指令

T1線程在執(zhí)行的時候,將T1線程的指令放在PC,數(shù)據(jù)放在Registers,假設(shè)此時要切換成T2線程,T1線程的指令和數(shù)據(jù)放cache,然后把T2線程的指令放PC,數(shù)據(jù)放Registers,執(zhí)行T2線程即可。

以上的整個過程是通過操作系統(tǒng)來調(diào)度的,且線程的調(diào)度是要消耗資源的,所以,線程不是設(shè)置越多越好。

單核CPU設(shè)定多線程是否有意義?

有意義,因為線程的操作中可能有不消耗CPU的操作,比如:等待網(wǎng)絡(luò)的傳輸,或者線程sleep,此時就可以讓出CPU去執(zhí)行其他線程??梢猿浞掷肅PU資源。

  • CPU密集型
  • IO密集型

線程數(shù)量是不是設(shè)置地越大越好?

不是,因為線程切換要消耗資源。

示例:
單線程和多線程來累加1億個數(shù)。-> CountSum.java

工作線程數(shù)(線程池中線程數(shù)量)設(shè)多少合適?

  • 和CPU的核數(shù)有關(guān)

  • 最好是通過壓測來評估。通過profiler性能分析工具jProfiler,或者Arthas

  • 公式

N = Ncpu * Ucpu * (1 + W/C)

其中:

  • Ncpu是處理器的核的數(shù)目,可以通過Runtime.getRuntime().availableProcessors() 得到

  • Ucpu是期望的CPU利用率(該值應(yīng)該介于0和1之間)

  • W/C是等待時間和計算時間的比率。

Java中創(chuàng)建線程的方式

  1. 繼承Thread類,重寫run方法
  2. 實現(xiàn)Runnable接口,實現(xiàn)run方法,這比方式1更好,因為一個類實現(xiàn)了Runnable以后,還可以繼承其他類
  3. 使用lambda表達式
  4. 通過線程池創(chuàng)建
  5. 通過Callable/Future創(chuàng)建(需要返回值的時候)

具體示例可見:HelloThread.java

線程狀態(tài)

  • NEW

線程剛剛創(chuàng)建,還沒有啟動
即:剛剛New Thread的時候,還沒有調(diào)用start方法時候,就是這個狀態(tài)

  • RUNNABLE

可運行狀態(tài),由線程調(diào)度器可以安排執(zhí)行,包括以下兩種情況:

  • READY
  • RUNNING

READY和RUNNING通過yield來切換

  • WAITING

等待被喚醒

  • TIMED_WAITING

隔一段時間后自動喚醒

  • BLOCKED

被阻塞,正在等待鎖
只有在synchronized的時候在會進入BLOCKED狀態(tài)

  • TERMINATED

線程執(zhí)行完畢后,是這個狀態(tài)

線程狀態(tài)切換

線程基本操作

sleep

當(dāng)前線程睡一段時間

yield

這是一個靜態(tài)方法,一旦執(zhí)行,它會使當(dāng)前線程讓出一下CPU。但要注意,讓出CPU并不表示當(dāng)前線程不執(zhí)行了。當(dāng)前線程在讓出CPU后,還會進行CPU資源的爭奪,但是是否能夠再次被分配到就不一定了。

join

等待另外一個線程的結(jié)束,當(dāng)前線程才會運行

public class ThreadBasicOperation {
    static volatile int sum = 0;

    public static void main(String[] args) throws Exception {
        Thread t = new Thread(()->{
            for (int i = 1; i <= 100; i++) {
                sum += i;
            }
        });
        t.start();
        // join 方法表示主線程愿意等待子線程執(zhí)行完畢后才繼續(xù)執(zhí)行
        // 如果不使用join方法,那么sum輸出的可能是一個很小的值,因為還沒等子線程
        // 執(zhí)行完畢后,主線程就已經(jīng)執(zhí)行了打印sum的操作
        t.join();
        System.out.println(sum);
    }
}

示例代碼:ThreadBasicOperation.java

interrupt

  • interrupt()

打斷某個線程(設(shè)置標(biāo)志位)

  • isInterrupted()

查詢某線程是否被打斷過(查詢標(biāo)志位)

  • static interrupted

查詢當(dāng)前線程是否被打斷過,并重置打斷標(biāo)志位

示例代碼:ThreadInterrupt.java

如何結(jié)束一個線程#

不推薦的方式#

  • stop方法
  • suspend/resume方法

以上兩種方式都不建議使用, 因為會產(chǎn)生數(shù)據(jù)不一致的問題,因為會釋放所有的鎖。

優(yōu)雅的方式

如果不依賴循環(huán)的具體次數(shù)或者中間狀態(tài), 可以通過設(shè)置標(biāo)志位的方式來控制

public class ThreadFinished {
    private static volatile boolean flag = true;
    public static void main(String[] args) throws InterruptedException {

        // 推薦方式:設(shè)置標(biāo)志位
        Thread t3 = new Thread(() -> {
            long i = 0L;
            while (flag) {
                i++;
            }
            System.out.println("count sum i = " + i);
        });
        t3.start();
        TimeUnit.SECONDS.sleep(1);
        flag = false;
    }
}

如果要依賴循環(huán)的具體次數(shù)或者中間狀態(tài), 則可以用interrupt方式

public class ThreadFinished {

    public static void main(String[] args) throws InterruptedException {
        // 推薦方式:使用interrupt
        Thread t4 = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {

            }
            System.out.println("t4 end");
        });
        t4.start();
        TimeUnit.SECONDS.sleep(1);
        t4.interrupt();
    }
}

示例代碼: ThreadFinished.java

并發(fā)編程的三大特性

可見性

每個線程會保存一份拷貝到線程本地緩存,使用volatile,可以保持線程之間數(shù)據(jù)可見性。

如下示例: ThreadVisible.java

public class ThreadVisible {

    static volatile   boolean  flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(flag) {
                // 如果這里調(diào)用了System.out.println()
                // 會無論flag有沒有加volatile,數(shù)據(jù)都會同步
                // 因為System.out.println()背后調(diào)用的synchronized
                // System.out.println();
            }
            System.out.println("t end");
        });
        t.start();
        TimeUnit.SECONDS.sleep(3);
        flag = false;

        // volatile修飾引用變量
        new Thread(a::m,"t2").start();
        TimeUnit.SECONDS.sleep(2);
        a.flag = false;

        // 阻塞主線程,防止主線程直接執(zhí)行完畢,看不到效果
        new Scanner(System.in).next();
    }
    private static volatile A a = new A();
    static class A {
        boolean flag = true;
        void m() {
            System.out.println("m start");
            while(flag){}
            System.out.println("m end");
        }   
    }
}

代碼說明:

  • 如在上述代碼的死循環(huán)中增加了System.out.println(), 則會強制同步flag的值,無論flag本身有沒有加volatile。
  • 如果volatile修飾一個引用對象,如果對象的屬性(成員變量)發(fā)生了改變,volatile不能保證其他線程可以觀察到該變化。

關(guān)于三級緩存

如上圖,內(nèi)存讀出的數(shù)據(jù)會在L3,L2,L1上都存一份。所謂線程數(shù)據(jù)的可見性,指的就是內(nèi)存中的某個數(shù)據(jù),假如第一個CPU的一個核讀取到了,和其他的核讀取到這個數(shù)據(jù)之間的可見性。

在從內(nèi)存中讀取數(shù)據(jù)的時候,根據(jù)的是程序局部性的原理,按塊來讀取,這樣可以提高效率,充分發(fā)揮總線CPU針腳等一次性讀取更多數(shù)據(jù)的能力。

所以這里引入了一個緩存行的概念,目前一個緩存行多用64個字節(jié)來表示。

如何來驗證CPU讀取緩存行這件事,我們可以通過一個示例來說明:

public class CacheLinePadding {
    public static T[] arr = new T[2];

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

    public static void main(String[] args) throws Exception {
        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);
    }

    private static class Padding {
        public volatile long p1, p2, p3, p4, p5, p6, p7;
    }

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

說明:以上代碼,T這個類extends Padding與否,會影響整個流程的執(zhí)行時間,如果繼承了,會減少執(zhí)行時間,因為繼承Padding后,arr[0]和arr[1]一定不在同一個緩存行里面,所以不需要同步數(shù)據(jù),速度就更快一些了。

jdk1.8增加了一個注解:@Contended,標(biāo)注了以后,不會在同一緩存行, 僅適用于jdk1.8
還需要增加jvm參數(shù)

-XX:-RestrictContended

CPU為每個緩存行標(biāo)記四種狀態(tài)(使用兩位)

  • Exclusive
  • Invalid
  • Shared
  • Modified

有序性

為什么會出現(xiàn)亂序執(zhí)行呢?因為CPU為了提高效率,可能在執(zhí)行某些指令的時候,不按順序執(zhí)行(指令前后沒有依賴關(guān)系的時候)

亂序存在的條件是:不影響單線程的最終一致性(as - if - serial)

驗證亂序執(zhí)行的程序示例 DisOrder.java:

public class DisOrder {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    // 以下程序可能會執(zhí)行比較長的時間
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (;;) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread one = new Thread(() -> {
                // 由于線程one先啟動,下面這句話讓它等一等線程two. 讀著可根據(jù)自己電腦的實際性能適當(dāng)調(diào)整等待時間.
                shortWait(100000);
                a = 1;
                x = b;
            });

            Thread other = new Thread(() -> {
                b = 1;
                y = a;
            });
            one.start();
            other.start();
            one.join();
            other.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                // 出現(xiàn)這個分支,說明指令出現(xiàn)了重排
                // 否則不可能 x和y同時都為0
                System.err.println(result);
                break;
            } else {
                // System.out.println(result);
            }
        }
    }

    public static void shortWait(long interval) {
        long start = System.nanoTime();
        long end;
        do {
            end = System.nanoTime();
        } while (start + interval >= end);
    }
}

如上示例,如果指令不出現(xiàn)亂序,那么x和y不可能同時為0,通過執(zhí)行這個程序可以驗證出來,在我本機測試的結(jié)果是:

執(zhí)行到第1425295次 出現(xiàn)了x和y同時為0的情況。

原子性

程序的原子性是指整個程序中的所有操作,要么全部完成,要么全部失敗,不可能滯留在中間某個環(huán)節(jié);在多個線程一起執(zhí)行的時候,一個操作一旦開始,就不會被其他線程所打斷。

一個示例:

class T {
    m = 9;
}

對象T在創(chuàng)建過程中,背后其實是包含了多條執(zhí)行語句的,由于有CPU亂序執(zhí)行的情況,所以極有可能會在初始化過程中生成以一個半初始化對象t,這個t的m等于0(還沒有來得及做賦值操作)

所以,不要在某個類的構(gòu)造方法中啟動一個線程,這樣會導(dǎo)致this對象逸出,因為這個類的對象可能還來不及執(zhí)行初始化操作,就啟動了一個線程,導(dǎo)致了異常情況。

volatile一方面可以保證線程數(shù)據(jù)之間的可見性,另外一方面,也可以防止類似這樣的指令重排,所以
所以,單例模式中,DCL方式的單例一定要加volatile修飾:

public class Singleton6 {
    private volatile static Singleton6 INSTANCE;

    private Singleton6() {
    }

    public static Singleton6 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton6.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton6();
                }
            }
        }
        return INSTANCE;
    }
}

具體可以參考設(shè)計模式學(xué)習(xí)筆記 中單例模式的說明。

CAS

比較與交換的意思

舉個例子:

內(nèi)存有個值是3,如果用Java通過多線程去訪問這個數(shù),每個線程都要把這個值+1。

之前是需要加鎖,即synchronized關(guān)鍵字來控制。但是JUC的包出現(xiàn)后,有了CAS操作,可以不需要加鎖來處理,流程是:

第一個線程:把3拿過來,線程本地區(qū)域做計算+1,然后把4寫回去,
第二個線程:也把3這個數(shù)拿過來,線程本地區(qū)域做計算+3后,在回寫回去的時候,會做一次比較,如果原來的值還是3,那么說明這個值之前沒有被打擾過,就可以把4寫回去,如果這個值變了,假設(shè)變?yōu)榱?,那么說明這個值已經(jīng)被其他線程修改過了,那么第二個線程需要重新執(zhí)行一次,即把最新的4拿過來繼續(xù)計算,回寫回去的時候,繼續(xù)做比較,如果內(nèi)存中的值依然是4,說明沒有其他線程處理過,第二個線程就可以把5回寫回去了。

流程圖如下:

ABA問題

CAS會出現(xiàn)一個ABA的問題,即在一個線程回寫值的時候,其他線程其實動過那個原始值,只不過其他線程操作后這個值依然是原始值。

如何來解決ABA問題呢?

我們可以通過版本號或者時間戳來控制,比如數(shù)據(jù)原始的版本是1.0,處理后,我們把這個數(shù)據(jù)的版本改成變成2.0版本, 時間戳來控制也一樣,

以Java為例,AtomicStampedReference這個類,它內(nèi)部不僅維護了對象值,還維護了一個時間戳。

當(dāng)AtomicStampedReference對應(yīng)的數(shù)值被修改時,除了更新數(shù)據(jù)本身外,還必須要更新時間戳。

當(dāng)AtomicStampedReference設(shè)置對象值時,對象值以及時間戳都必須滿足期望值,寫入才會成功。

因此,即使對象值被反復(fù)讀寫,寫回原值,只要時間戳發(fā)生變化,就能防止不恰當(dāng)?shù)膶懭搿?/p>

CAS的底層實現(xiàn)

Unsafe.cpp-->Atom::cmpxchg-->Atomic_linux_x86_inline.hpp-->調(diào)用了匯編的LOCK_IF_MP方法

Multiple_processor

lock cmpxchg

雖然cmpxchg指令不是原子的,但是加了lock指令后,則cmpxhg被上鎖,不允許被打斷。 在單核CPU中,無須加lock,在多核CPU中,必須加lock,可以參考stackoverflow上的這個回答:

is-x86-cmpxchg-atomic-if-so-why-does-it-need-lock

使用CAS好處

jdk早期是重量級別鎖 ,通過0x80中斷 進行用戶態(tài)和內(nèi)核態(tài)轉(zhuǎn)換,所以效率比較低,有了CAS操作,大大提升了效率。

對象的內(nèi)存布局(Hotspot實現(xiàn))

使用jol查看一個對象的內(nèi)存布局

我們可以通過jol包來查看一下某個對象的內(nèi)存布局

引入jol依賴

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.15</version>
</dependency>

示例代碼(ObjectModel.java)

public class ObjectModel {
    public static void main(String[] args) {
        T o = new T();
        String s = ClassLayout.parseInstance(o).toPrintable();
        System.out.println(s);
    }
}
class  T{

}

配置VM參數(shù),開啟指針壓縮

-XX:+UseCompressedClassPointers

運行結(jié)果如下:

OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0x00067248
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

其中8個字節(jié)的markword

4個字節(jié)的類型指針,可以找到T.class

這里一共是12個字節(jié), 由于字節(jié)數(shù)務(wù)必是8的整數(shù)倍,所以補上4個字節(jié),共16個字節(jié)

我們修改一下T這個類

class  T{
    public int a = 3;
    public long b = 3l;
}

再次執(zhí)行,可以看到結(jié)果是

OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0x00067248
 12   4    int T.a                       3
 16   8   long T.b                       3
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

其中多了4位表示int這個成員變量,多了8位表示long這個成員變量, 相加等于24,正好是8的整數(shù)倍,不需要補齊。

內(nèi)存布局詳細說明

使用synchronized就是修改了對象的markword信息,markword中還記錄了GC信息,Hashcode信息

鎖升級過程

偏向鎖

synchronized代碼段多數(shù)時間是一個線程在運行,誰先來,這個就偏向誰,用當(dāng)前線程標(biāo)記一下。

輕量級鎖(自旋鎖,無鎖)

偏向鎖撤銷,然后競爭,每個線程在自己線程棧中存一個LR(lock record)鎖記錄

偏向鎖和輕量級鎖都是用戶空間完成的,重量級鎖需要向操作系統(tǒng)申請。
兩個線程爭搶的方式將lock record的指針,指針指向哪個線程的LR,哪個線程就拿到鎖,另外的線程用CAS的方式繼續(xù)競爭

重量級鎖

JVM的ObjectMonitor去操作系統(tǒng)申請。

如果發(fā)生異常,synchronized會自動釋放鎖

interpreteRuntime.cpp --> monitorenter

鎖重入

synchronized是可重入鎖
可重入次數(shù)必須記錄,因為解鎖需要對應(yīng)可重入次數(shù)的記錄
偏向鎖:記錄在線程棧中,每重入一次,LR+1,備份原來的markword
輕量級鎖:類似偏向鎖
重量級鎖:記錄在ObjectMonitor的一個字段中

自旋鎖什么時候升級為重量級鎖?

  • 有線程超過十次自旋
  • -XX:PreBlockSpin(jdk1.6之前)
  • 自旋的線程超過CPU核數(shù)一半
  • jdk1.6 以后,JVM自己控制

為什么有偏向鎖啟動和偏向鎖未啟動?

未啟動:普通對象001
已啟動:匿名偏向101

為什么有自旋鎖還需要重量級鎖?

因為自旋會占用CPU時間,消耗CPU資源,如果自旋的線程多,CPU資源會被消耗,所以會升級成重量級鎖(隊列)例如:ObjectMonitor里面的WaitSet,重量級鎖會把線程都丟到WaitSet中凍結(jié), 不需要消耗CPU資源

偏向鎖是否一定比自旋鎖效率高?

明確知道多線程的情況下,不一定。
因為偏向鎖在多線程情況下,會涉及到鎖撤銷,這個時候直接使用自旋鎖,JVM啟動過程,會有很多線程競爭,比如啟動的時候,肯定是多線程的,所以默認情況,啟動時候不打開偏向鎖,過一段時間再打開。
有一個參數(shù)可以配置:BiasedLockingStartupDelay默認是4s鐘

偏向鎖狀態(tài)下,調(diào)用了wait方法,直接升級成重量級鎖

一個線程拿20個對象進行加鎖,批量鎖的重偏向(20個對象),批量鎖撤銷(變成輕量級鎖)(40個對象), 通過Epoch中的值和對應(yīng)的類對象里面記錄的值比較。

synchronized

鎖定對象

public class SynchronizedObject implements Runnable {
    static SynchronizedObject instance = new SynchronizedObject();
    final Object object = new Object();
    static volatile int i = 0;

    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            // 任何線程要執(zhí)行下面的代碼,必須先拿到object的鎖
            synchronized (object) {
                i++;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

鎖定方法

  • 鎖定靜態(tài)方法相當(dāng)于鎖定當(dāng)前類
public class SynchronizedStatic implements Runnable {
    static SynchronizedStatic instance = new SynchronizedStatic();
    static volatile int i = 0;

    @Override
    public void run() {
        increase();
    }

    // 相當(dāng)于synchronized(SynchronizedStatic.class)
    synchronized static void increase() {
        for (int j = 0; j < 1000000; j++) {
            i++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

  • 鎖定非靜態(tài)方法相當(dāng)于鎖定該對象的實例或synchronized(this)
public class SynchronizedMethod implements Runnable {
    static SynchronizedMethod instance = new SynchronizedMethod();
    static volatile int i = 0;

    @Override
    public void run() {
        increase();
    }
    void increase() {
        for (int j = 0; j < 1000000; j++) {
            // 任何線程要執(zhí)行下面的代碼,必須先拿到object的鎖
            synchronized (this) {
                i++;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

臟讀#

public class DirtyRead {
  String name;
  double balance;

  public static void main(String[] args) {
    DirtyRead a = new DirtyRead();
    Thread thread = new Thread(() -> a.set("zhangsan", 100.0));

    thread.start();
    try {
      TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println(a.getBalance("zhangsan"));
    try {
      TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println(a.getBalance("zhangsan"));
  }

  public synchronized void set(String name, double balance) {
    this.name = name;

    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }

    this.balance = balance;
  }

  // 如果get方法不加synchronized關(guān)鍵字,就會出現(xiàn)臟讀情況
  public /*synchronized*/ double getBalance(String name) {
    return this.balance;
  }
}

其中的getBalance方法,如果不加synchronized,就會產(chǎn)生臟讀的問題。

可重入鎖

一個同步方法可以調(diào)用另外一個同步方法,
一個線程已經(jīng)擁有某個對象的鎖,再次申請的時候仍然會得到該對象的鎖(可重入鎖)
子類synchronized,如果調(diào)用父類的synchronize方法:super.method(),如果不可重入,直接就會死鎖。

public class SynchronizedReentry implements Runnable {
    public static void main(String[] args) throws IOException {
        SynchronizedReentry myRun = new SynchronizedReentry();
        Thread thread = new Thread(myRun, "t1");
        Thread thread2 = new Thread(myRun, "t2");
        thread.start();
        thread2.start();
        System.in.read();

    }

    synchronized void m1(String content) {
        System.out.println(this);
        System.out.println("m1 get content is " + content);
        m2(content);
    }

    synchronized void m2(String content) {
        System.out.println(this);
        System.out.println("m2 get content is " + content);

    }

    @Override
    public void run() {
        m1(Thread.currentThread().getName());
    }
}

程序在執(zhí)行過程中,如果出現(xiàn)異常,默認情況鎖會被釋放 ,所以,在并發(fā)處理的過程中,有異常要多加小心,不然可能會發(fā)生不一致的情況。比如,在一個web app處理過程中,多個servlet線程共同訪問同一個資源,這時如果異常處理不合適, 在第一個線程中拋出異常,其他線程就會進入同步代碼區(qū),有可能會訪問到異常產(chǎn)生時的數(shù)據(jù)。因此要非常小心的處理同步業(yè)務(wù)邏輯中的異常。

示例見:

SynchronizedException.java

synchronized的底層實現(xiàn)

在早期的JDK使用的是OS的重量級鎖

后來的改進鎖升級的概念:

synchronized (Object)

  • markword 記錄這個線程ID (使用偏向鎖)
  • 如果線程爭用:升級為 自旋鎖
  • 10次自旋以后,升級為重量級鎖 - OS

所以:

  • 執(zhí)行時間短(加鎖代碼),線程數(shù)少,用自旋
  • 執(zhí)行時間長,線程數(shù)多,用系統(tǒng)鎖

synchronized不能鎖定String常量,Integer,Long等基礎(chǔ)類型

見示例:

SynchronizedBasicType.java

如何模擬死鎖

public class DeadLock implements Runnable {
  int flag = 1;
  static Object o1 = new Object();
  static Object o2 = new Object();

  public static void main(String[] args) {
    DeadLock lock = new DeadLock();
    DeadLock lock2 = new DeadLock();
    lock.flag = 1;
    lock2.flag = 0;
    Thread t1 = new Thread(lock);
    Thread t2 = new Thread(lock2);
    t1.start();
    t2.start();
  }

  @Override
  public void run() {
    System.out.println("flag = " + flag);
    if (flag == 1) {
      synchronized (o2) {
        try {
          Thread.sleep(500);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        synchronized (o1) {
          System.out.println("1");
        }
      }
    }
    if (flag == 0) {
      synchronized (o1) {
        try {
          Thread.sleep(500);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }

        synchronized (o2) {
          System.out.println("0");
        }
      }
    }
  }
}

volatile

  • 保持線程之間的可見性(不保證操作的原子性),依賴這個MESI協(xié)議
  • 防止指令重排序,CPU的load fence和store fence原語支持

CPU原來執(zhí)行指令一步一步執(zhí)行,現(xiàn)在是流水線執(zhí)行,編譯以后可能會產(chǎn)生指令的重排序,這樣可以提高性能

DCL為什么一定要加volatile?

DCL示例:

public class Singleton6 {
    private volatile static Singleton6 INSTANCE;

    private Singleton6() {
    }

    public static Singleton6 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton6.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton6();
                }
            }
        }
        return INSTANCE;
    }
}

在New對象的時候,編譯完實際上是分了三步

  1. 對象申請內(nèi)存,成員變量會被賦初始值
  2. 成員變量設(shè)為真實值
  3. 成員變量賦給對象

指令重排序可能會導(dǎo)致2和3進行指令重排,導(dǎo)致下一個線程拿到一個半初始化的對象,導(dǎo)致單例被破壞。所以DCL必須加Volitile

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容