Java 并發(fā)編程 — 并發(fā)編程基礎(chǔ)晉級篇

作者:追夢

借用 Java 并發(fā)編程實(shí)踐中的話;編寫正確的程序并不容易,而編寫正常的并發(fā)程序就更難了;相比于順序執(zhí)行的情況,多線程的線程安全問題是微妙而且出乎意料的,因?yàn)樵跊]有進(jìn)行適當(dāng)同步的情況下多線程中各個(gè)操作的順序是不可預(yù)期的。

并發(fā)編程相比 Java 中其他知識點(diǎn)學(xué)習(xí)起來門檻相對較高,學(xué)習(xí)起來比較費(fèi)勁,從而導(dǎo)致很多人望而卻步;而無論是職場面試和高并發(fā)高流量的系統(tǒng)的實(shí)現(xiàn)卻都還離不開并發(fā)編程,從而導(dǎo)致能夠真正掌握并發(fā)編程的人才成為市場比較迫切需求的。

本文作為 Java 并發(fā)編程之美系列的并發(fā)編程必備基礎(chǔ)晉級篇,通過通俗易懂的方式來和大家聊聊多線程并發(fā)編程中涉及到的高級基礎(chǔ)知識(建議先閱讀《Java 并發(fā)編程之美:線程相關(guān)的基礎(chǔ)知識》),具體內(nèi)容如下:

  • 什么是多線程并發(fā)和并行。
  • 什么是線程安全問題。
  • 什么是共享變量的內(nèi)存可見性問題。
  • 什么是 Java 中原子性操作。
  • 什么是 Java 中的 CAS 操作,AtomicLong 實(shí)現(xiàn)原理
  • 什么是 Java 指令重排序。
  • Java 中 Synchronized 關(guān)鍵字的內(nèi)存語義是什么。
  • Java 中 Volatile 關(guān)鍵字的內(nèi)存語義是什么。
  • 什么是偽共享,為何會出現(xiàn),以及如何避免。
  • 什么是可重入鎖、樂觀鎖、悲觀鎖、公平鎖、非公平鎖、獨(dú)占鎖、共享鎖。

多線程并發(fā)與并行

首先要澄清并發(fā)和并行的概念,并發(fā)是指同一個(gè)時(shí)間段內(nèi)多個(gè)任務(wù)同時(shí)都在執(zhí)行,并且都沒有執(zhí)行結(jié)束;而并行是說在單位時(shí)間內(nèi)多個(gè)任務(wù)同時(shí)在執(zhí)行;并發(fā)任務(wù)強(qiáng)調(diào)在一個(gè)時(shí)間段內(nèi)同時(shí)執(zhí)行,而一個(gè)時(shí)間段有多個(gè)單位時(shí)間累積而成,所以說并發(fā)的多個(gè)任務(wù)在單位時(shí)間內(nèi)不一定同時(shí)在執(zhí)行。

在單個(gè) CPU 的時(shí)代多個(gè)任務(wù)同時(shí)運(yùn)行都是并發(fā),這是因?yàn)?CPU 同時(shí)只能執(zhí)行一個(gè)任務(wù),單個(gè) CPU 時(shí)代多任務(wù)是共享一個(gè) CPU 的,當(dāng)一個(gè)任務(wù)占用 CPU 運(yùn)行時(shí)候,其它任務(wù)就會被掛起,當(dāng)占用 CPU 的任務(wù)時(shí)間片用完后,會把 CPU 讓給其它任務(wù)來使用,所以在單 CPU 時(shí)代多線程編程的意義不大,并且線程間頻繁的上下文切換還會帶來開銷。

如下圖單個(gè) CPU 上運(yùn)行兩個(gè)線程,可知線程 A 和 B 是輪流使用 CPU 進(jìn)行任務(wù)處理的,也就是同時(shí) CPU 只在執(zhí)行一個(gè)線程上面的任務(wù),當(dāng)前線程 A 的時(shí)間片用完后會進(jìn)行線程上下文切換,也就是保存當(dāng)前線程的執(zhí)行線程,然后切換線程 B 占用 CPU 運(yùn)行任務(wù)。

image.png

如下圖雙 CPU 時(shí)候,線程 A 和線程 B 在自己的 CPU 上執(zhí)行任務(wù),實(shí)現(xiàn)了真正的并行運(yùn)行。

image.png

而在多線程編程實(shí)踐中線程的個(gè)數(shù)往往多于 CPU 的個(gè)數(shù),所以平時(shí)都是稱多線程并發(fā)編程而不是多線程并行編程。

線程安全問題

談到線程安全問題不得不先說說什么是共享資源,所謂共享資源是說多個(gè)線程都可以去訪問的資源。

線程安全問題是指當(dāng)多個(gè)線程同時(shí)讀寫一個(gè)共享資源并且沒有任何同步措施的時(shí)候,導(dǎo)致臟數(shù)據(jù)或者其它不可預(yù)見的結(jié)果的問題。

image.png

如上圖,線程 A 和線程 B 可以同時(shí)去操作主內(nèi)存中的共享變量,是不是說多個(gè)線程共享了資源,都會產(chǎn)生線程安全問題呢?答案是否定的,如果多個(gè)線程都是只讀取共享資源,而不去修改,那么就不會存在線程安全問題。

只有當(dāng)至少一個(gè)線程修改共享資源時(shí)候才會存在線程安全問題。最典型的就是計(jì)數(shù)器類的實(shí)現(xiàn),計(jì)數(shù) count 本身是一個(gè)共享變量,多個(gè)線程可以對其進(jìn)行增加一,如果不使用同步的話,由于遞增操作是獲取 -> 加1 -> 保存三步操作,所以可能導(dǎo)致導(dǎo)致計(jì)數(shù)不準(zhǔn)確,如下表:

image.png

假如當(dāng)前 count=0,t1 時(shí)刻線程 A 讀取了 count 值到本地變量 countA。

然后 t2 時(shí)刻遞增 countA 值為1,同時(shí)線程 B 讀取 count 的值0放到本地變量 countB 值為0(因?yàn)?countA 還沒有寫入主內(nèi)存)。

t3 時(shí)刻線程 A 才把 countA 為1的值寫入主內(nèi)存,至此線程 A 一次計(jì)數(shù)完畢,同時(shí)線程 B 遞增 CountB 值為1。

t4 時(shí)刻線程 B 把 countB 值1寫入內(nèi)存,至此線程 B 一次計(jì)數(shù)完畢。

先不考慮內(nèi)存可見性問題,明明是兩次計(jì)數(shù)哇,為啥最后結(jié)果還是1而不是2呢?其實(shí)這就是共享變量的線程安全問題。那么如何解決?這就需要在線程訪問共享變量時(shí)候進(jìn)行適當(dāng)?shù)耐剑琂ava 中首屈一指的是使用關(guān)鍵字 Synchronized 進(jìn)行同步,這個(gè)下面會有具體介紹。

共享變量的內(nèi)存可見性問題

要談內(nèi)存可見性首先需要介紹下 Java 中多線程下處理共享變量時(shí)候的內(nèi)存模型。

image.png

如上圖,Jav a內(nèi)存模型規(guī)定了所有的變量都存放在主內(nèi)存中,當(dāng)線程使用變量時(shí)候都是把主內(nèi)存里面的變量拷貝到了自己的工作空間或者叫做工作內(nèi)存。

Java 內(nèi)存模型是個(gè)抽象的概念,那么在實(shí)際實(shí)現(xiàn)中什么是線程的工作內(nèi)存呢?

image.png

如上圖是雙核 CPU 系統(tǒng)架構(gòu),每核有自己的控制器和運(yùn)算器,其中控制器包含一組寄存器和操作控制器,運(yùn)算器執(zhí)行算術(shù)邏輯運(yùn)算,并且有自己的一級緩存,并且有些架構(gòu)里面雙核還有個(gè)共享的二級緩存。

那么 對應(yīng) Java 內(nèi)存模型里面的工作內(nèi)存,在實(shí)現(xiàn)上這里是指 L1 或者 L2 緩存或者 CPU 的寄存器。

假如線程 A 和 B 同時(shí)去處理一個(gè)共享變量,會出現(xiàn)什么情況呢?

使用上圖 CPU 架構(gòu),假設(shè)線程 A和 B 使用不同 CPU 進(jìn)行去修改共享變量 X,假設(shè) X 的初始化為0,并且當(dāng)前兩級 Cache 都為空的情況,具體看下面分析:

  • 假設(shè)線程 A 首先獲取共享變量 X 的值,由于兩級 Cache 都沒有命中,所以到主內(nèi)存加載了 X=0,然后會把 X=0 的值緩存到兩級緩存,假設(shè)線程 A 修改 X 的值為1,然后寫入到兩級 Cache,并且刷新到主內(nèi)存(注:如果沒刷新會主內(nèi)存也會存在內(nèi)存不可見問題)。這時(shí)候線程 A 所在的 CPU 的兩級 Cache 內(nèi)和主內(nèi)存里面 X 的值都是1;
  • 然后假設(shè)線程 B 這時(shí)候獲取 X 的值,首先一級緩存沒有命中,然后看二級緩存,二級緩存命中了,所以返回 X=1;然后線程 B 修改 X 的值為2;然后存放到線程2所在的一級 Cache 和共享二級 Cache,最后更新主內(nèi)存值為2;
  • 然后假設(shè)線程 A 這次又需要修改 X 的值,獲取時(shí)候一級緩存命中獲取 X=1,到這里問題就出現(xiàn)了,明明線程 B 已經(jīng)把 X 的值修改為了2,為啥線程 A 獲取的還是1呢?這就是共享變量的內(nèi)存不可見問題,也就是線程 B 寫入的值對線程 A 不可見。

那么對于共享變量內(nèi)存不可見問題如何解決呢?Java 中首屈一指的 Synchronized 和 Volatile 關(guān)鍵字就可以解決這個(gè)問題,下面會有講解。

Java 中 Synchronized 關(guān)鍵字

Synchronized 塊是 Java 提供的一種原子性內(nèi)置鎖,Java 中每個(gè)對象都可以當(dāng)做一個(gè)同步鎖的功能來使用,這些 Java 內(nèi)置的使用者看不到的鎖被稱為內(nèi)部鎖,也叫做監(jiān)視器鎖。

線程在進(jìn)入 Synchronized 代碼塊前會自動(dòng)嘗試獲取內(nèi)部鎖,如果這時(shí)候內(nèi)部鎖沒有被其他線程占有,則當(dāng)前線程就獲取到了內(nèi)部鎖,這時(shí)候其它企圖訪問該代碼塊的線程會被阻塞掛起。

拿到內(nèi)部鎖的線程會在正常退出同步代碼塊或者異常拋出后或者同步塊內(nèi)調(diào)用了該內(nèi)置鎖資源的 wait 系列方法時(shí)候釋放該內(nèi)置鎖;內(nèi)置鎖是排它鎖,也就是當(dāng)一個(gè)線程獲取這個(gè)鎖后,其它線程必須等待該線程釋放鎖才能獲取該鎖。

上一節(jié)講了多線程并發(fā)修改共享變量時(shí)候會存在內(nèi)存不可見的問題,究其原因是因?yàn)?Java 內(nèi)存模型中線程操作共享變量時(shí)候會從自己的工作內(nèi)存中獲取而不是從主內(nèi)存獲取或者線程寫入到本地內(nèi)存的變量沒有被刷新會主內(nèi)存。

下面講解下 Synchronized 的一個(gè)內(nèi)存語義,這個(gè)內(nèi)存語義就可以解決共享變量內(nèi)存不可見性問題。

線程進(jìn)入 Synchronized 塊的語義是會把在 Synchronized 塊內(nèi)使用到的變量從線程的工作內(nèi)存中清除,在 Synchronized 塊內(nèi)使用該變量時(shí)候就不會從線程的工作內(nèi)存中獲取了,而是直接從主內(nèi)存中獲取;退出 Synchronized 塊的內(nèi)存語義是會把 Synchronized 塊內(nèi)對共享變量的修改刷新到主內(nèi)存。對應(yīng)上面一節(jié)講解的假如線程在 Synchronized 塊內(nèi)獲取變量 X 的值,那么線程首先會清空所在的 CPU 的緩存,然后從主內(nèi)存獲取變量 X 的值;當(dāng)線程修改了變量的值后會把修改的值刷新回主內(nèi)存。

其實(shí)這也是加鎖和釋放鎖的語義,當(dāng)獲取鎖后會清空本地內(nèi)存中后面將會用到的共享變量,在使用這些共享變量的時(shí)候會從主內(nèi)存進(jìn)行加載;在釋放鎖時(shí)候會刷新本地內(nèi)存中修改的共享變量到主內(nèi)存。

除了可以解決共享變量內(nèi)存可見性問題外,Synchronized 經(jīng)常被用來實(shí)現(xiàn)原子性操作,另外注意,Synchronized 關(guān)鍵字會引起線程上下文切換和線程調(diào)度的開銷。

Java 中 Volatile 關(guān)鍵字

上面介紹了使用鎖的方式可以解決共享變量內(nèi)存可見性問題,但是使用鎖太重,因?yàn)樗鼤鹁€程上下文的切換開銷,對于解決內(nèi)存可見性問題,Java 還提供了一種弱形式的同步,也就是使用了 volatile 關(guān)鍵字。

一旦一個(gè)變量被 volatile 修飾了,當(dāng)線程獲取這個(gè)變量值的時(shí)候會首先清空線程工作內(nèi)存中該變量的值,然后從主內(nèi)存獲取該變量的值;當(dāng)線程寫入被 volatile 修飾的變量的值的時(shí)候,首先會把修改后的值寫入工作內(nèi)存,然后會刷新到主內(nèi)存。這就保證了對一個(gè)變量的更新對其它線程馬上可見。

下面看一個(gè)使用 volatile 關(guān)鍵字解決內(nèi)存不可見性的一個(gè)例子,如下代碼的共享變量 value 是線程不安全的,因?yàn)樗鼪]有進(jìn)行適當(dāng)同步措施。

    public class ThreadNotSafeInteger {

        private int value;

        public int get() {
            return value;
        }

        public void set(int value) {
            this.value = value;
        }
    }

首先看下使用 synchronized 關(guān)鍵字進(jìn)行同步方式如下:

    public class ThreadSafeInteger {

        private int value;

        public synchronized int get() {
            return value;
        }

        public synchronized  void set(int value) {
            this.value = value;
        }
    }

然后看下使用 volatile 進(jìn)行同步如下:

    public class ThreadSafeInteger {

        private volatile int value;

        public int get() {
            return value;
        }

        public void set(int value) {
            this.value = value;
        }
    }

這里使用 synchronized 和使用 volatile 是等價(jià)的,都解決了共享變量 value 的內(nèi)存不可見性問題;但是前者是獨(dú)占鎖,同時(shí)只能有一個(gè)線程調(diào)用 get() 方法,其它調(diào)用線程會被阻塞;并且會存在線程上下文切換和線程重新調(diào)度的開銷;而后者是非阻塞算法,不會造成線程上下文切換的開銷。

這里使用 synchronized 和使用 volatile 是等價(jià)的,但是并不是所有情況下都是等價(jià)的,這是因?yàn)?volatile 雖然提供了可見性保證,但是并沒有保證操作的原子性。

那么一般什么時(shí)候才使用 volatile 關(guān)鍵字修飾變量呢?

  • 當(dāng)寫入變量值時(shí)候不依賴變量的當(dāng)前值。因?yàn)槿绻蕾嚠?dāng)前值則是獲取 -> 計(jì)算 -> 寫入操作,而這三步操作不是原子性的,而 volatile 不保證原子性。
  • 讀寫變量值時(shí)候沒有進(jìn)行加鎖。因?yàn)榧渔i本身已經(jīng)保證了內(nèi)存可見性,這時(shí)候不需要把變量聲明為 volatile。

另外變量被聲明為 volatile 還可以避免重排序的發(fā)生,這個(gè)后面會講到。

Java 中原子性操作

所謂原子性操作是指當(dāng)執(zhí)行一系列操作時(shí)候,這些操作那么全部被執(zhí)行,那么全部不被執(zhí)行,不存在只執(zhí)行其中一部分的情況。

在設(shè)計(jì)計(jì)數(shù)器時(shí)候一般都是先讀取當(dāng)前值,然后+1,然后更新,這個(gè)過程是讀 -> 改 -> 寫的過程,如果不能保證這個(gè)過程是原子性,那么就會出現(xiàn)線程安全問題。如下代碼是線程不安全的,因?yàn)椴荒鼙WC ++value 是原子性操作。

    public class ThreadNotSafeCount {

        private  Long value;

        public Long getCount() {
            return value;
        }

        public void inc() {
            ++value;
        }
    }

通過使用 Javap -c 查看匯編代碼如下:

 public void inc();
    Code:
       0: aload_0       
       1: dup           
       2: getfield      #2                  // Field value:J
       5: lconst_1      
       6: ladd          
       7: putfield      #2                  // Field value:J
      10: return        

可知簡單的 ++value有 2,5,6,7 組成,其中2是獲取當(dāng)前 value 的值并放入棧頂,5是把常量1放入棧頂,6是把當(dāng)前棧頂中2個(gè)值相加并把結(jié)果放入棧頂,7則是把棧頂結(jié)果賦值會 value 變量,可知 Java 中簡單的一句 ++value 轉(zhuǎn)換為匯編后就不具有原子性了。

那么如何才能保證多個(gè)操作完成原子性呢,最簡單的是使用 Synchronized 進(jìn)行同步,修改代碼如下:

    public class ThreadSafeCount {

        private  Long value;

        public synchronized Long getCount() {
            return value;
        }

        public synchronized void inc() {
            ++value;
        }
    }

使用 Synchronized 的確可以實(shí)現(xiàn)線程安全,即實(shí)現(xiàn)內(nèi)存可見性和同步,但是 Synchronized 是獨(dú)占鎖,同時(shí)只有一個(gè)線程可以調(diào)用 getCount 方法,其他沒有獲取內(nèi)部鎖的線程會被阻塞掉;而這里 getCount 方法只是讀操作,多個(gè)線程同時(shí)調(diào)用不會存在線程安全問題,但是加了關(guān)鍵字 Synchronized 后同時(shí)就只能有一個(gè)線程可以調(diào)用了,這顯然大大降低了并發(fā)性。

也許你會問既然是只讀操作那么為何不去掉 getCount 方法上的 Synchronized 關(guān)鍵字呢?其實(shí)是不能去掉的,別忘了這里要靠 Synchronized 的內(nèi)存語義來實(shí)現(xiàn) value 的內(nèi)存可見性。

那么有沒有更好的實(shí)現(xiàn)呢?答案是肯定的,下面會講到的內(nèi)部使用非阻塞 CAS 算法實(shí)現(xiàn)的原子性操作類 AtomicLong 就是不錯(cuò)選擇。

Java 中的 CAS 操作和 AtomicLong 實(shí)現(xiàn)原理

CAS 來源

在 Java 中鎖在并發(fā)處理中占據(jù)了一席之地,但是使用鎖不好的地方是當(dāng)一個(gè)線程沒有獲取到鎖后會被阻塞掛起,這會導(dǎo)致線程上下文的切換和重新調(diào)度的開銷。

Java 中提供了非阻塞的 volatile 關(guān)鍵字來解決共享變量的可見性問題,這在一定程度上彌補(bǔ)了鎖所在帶來的開銷,但是 volatile 只能保證共享變量的可見性問題,但是還是不能解決例如讀 -> 改 -> 寫等的原子性問題。

CAS 即 Compare And Swap,是 JDK 提供的非阻塞原子性操作,它通過硬件保證了比較-更新操作的原子性,JDK 里面的 Unsafe 類提供了一些列的 compareAndSwap* 方法,下面以 compareAndSwapLong 為例進(jìn)行簡單介紹。

  • boolean compareAndSwapLong(Object obj,long valueOffset,long expect, long update)方法。

compareAndSwap 的意思也就是比較并交換,CAS 有四個(gè)操作數(shù)分別為:對象內(nèi)存位置,對象中的變量的偏移量,變量預(yù)期值 expect,新的值 update。

操作含義是如果對象 obj 中內(nèi)存偏移量為 valueOffset 位置的變量值為 expect 則使用新的值 update 替換舊的值 expect。這個(gè)是處理器提供的一個(gè)原子性指令。

AtomicLong 的原理

并發(fā)包中原子性操作類都有 AtomicInteger,AtomicLong,AtomicBoolean,原理類似,本節(jié)講解下 AtomicLong 類。AtomicLong 是原子性遞增或者遞減類,其內(nèi)部使用 Unsafe 來實(shí)現(xiàn),下面看下代碼:

public class AtomicLong extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 1927816293512124184L;

    // (1)獲取Unsafe實(shí)例
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    //(2)存放變量value的偏移量
    private static final long valueOffset;

    
     //(3)判斷JVM是否支持Long類型無鎖CAS
    static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();
    private static native boolean VMSupportsCS8();

    static {
        try {
            //(4)獲取value在AtomicLong中偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    //(5)實(shí)際變量值
    private volatile long value;

    public AtomicLong(long initialValue) {
        value = initialValue;
    }
    ....
}
  • 代碼(1)創(chuàng)建了通過 Unsafe.getUnsafe()方式獲取到 Unsafe 類實(shí)例,這里你可能會疑問為何這里能通過 Unsafe.getUnsafe() 方式獲取到 Unsafe 類實(shí)例?其實(shí)這是因?yàn)?AtomicLong 類也是在 rt.jar 包里面,AtomicLong 類的加載就是通過 BootStarp 類加載器進(jìn)行加載的(關(guān)于 Unsafe 后面高級篇會具體講解,這里先了解)
  • 代碼(5)中 value 聲明為 volatile 是為了多線程下保證內(nèi)存可見性,value 是具體存放計(jì)數(shù)的變量。
  • 代碼(2)(4)獲取 value 變量在 AtomicLong 類中偏移量。

下面重點(diǎn)看下 AtomicLong 中主要函數(shù):

  • 遞增和遞減操作代碼。
//(6)調(diào)用unsafe方法,原子性設(shè)置value值為原始值+1,返回值為遞增后的值
public final long incrementAndGet() {
    return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}

//(7)調(diào)用unsafe方法,原子性設(shè)置value值為原始值-1,返回值為遞減之后的值
public final long decrementAndGet() {
    return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
}

//(8)調(diào)用unsafe方法,原子性設(shè)置value值為原始值+1,返回值為原始值
public final long getAndIncrement() {
    return unsafe.getAndAddLong(this, valueOffset, 1L);
}


//(9)調(diào)用unsafe方法,原子性設(shè)置value值為原始值-1,返回值為原始值
public final long getAndDecrement() {
    return unsafe.getAndAddLong(this, valueOffset, -1L);
}

如上代碼內(nèi)部都是調(diào)用 Unsafe 的 getAndAddLong 方法實(shí)現(xiàn),這個(gè)函數(shù)是個(gè)原子性操作,這里第一個(gè)參數(shù)是 AtomicLong 實(shí)例的引用,第二個(gè)參數(shù)是 value 變量在 AtomicLong 中的偏移值,第三個(gè)參數(shù)是要設(shè)置第二個(gè)變量的值。

其中 getAndIncrement 方法在 JDK 7 的實(shí)現(xiàn)邏輯為:

public final long getAndIncrement() {
        while (true) {
            long current = get();
            long next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }

如上代碼可知每個(gè)線程是先拿到變量的當(dāng)前值(由于是 value 是 volatile 變量所以這里拿到的是最新的值),然后在工作內(nèi)存對其進(jìn)行增一操作,然后使用 CAS 修改變量的值,如果設(shè)置失敗,則循環(huán)繼續(xù)嘗試,直到設(shè)置成功。

而 JDK 8 邏輯為:

public final long getAndIncrement() {
    return unsafe.getAndAddLong(this, valueOffset, 1L);
}

其中JDK8中unsafe.getAndAddLong代碼為:

public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2)
  {
    long l;
    do
    {
      l = getLongVolatile(paramObject, paramLong1);
    } while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));
    return l;
  }
  

可知 JDK 7 的 AtomicLong 中的循環(huán)邏輯已經(jīng)被 JDK 8 的原子操作類 UNsafe 內(nèi)置了,之所以內(nèi)置應(yīng)該是考慮到這種函數(shù)會在其它地方也會用到,內(nèi)置可以提高復(fù)用性。

  • boolean compareAndSet(long expect, long update)方法
public final boolean compareAndSet(long expect, long update) {
    return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}

如上代碼可知道內(nèi)部還是調(diào)用了 unsafe.compareAndSwapLong 方法。如果原子變量中 value 的值等于 expect 則使用 update 值更新該值并返回 true,否者返回 false。

下面通過一個(gè)多線程使用 AtomicLong 統(tǒng)計(jì)0的個(gè)數(shù)的例子來加深對 AtomicLong 的理解:


/**
  統(tǒng)計(jì)0的個(gè)數(shù)
 */
public class Atomic 
{
   //(10)創(chuàng)建Long型原子計(jì)數(shù)器
   private static  AtomicLong atomicLong = new AtomicLong();
   //(11)創(chuàng)建數(shù)據(jù)源
   private static Integer[] arrayOne = new Integer[]{0,1,2,3,0,5,6,0,56,0};
   private static Integer[] arrayTwo = new Integer[]{10,1,2,3,0,5,6,0,56,0};
   
    public static void main( String[] args ) throws InterruptedException
    {
     //(12)線程one統(tǒng)計(jì)數(shù)組arrayOne中0的個(gè)數(shù)
        Thread threadOne = new Thread(new Runnable() {

            @Override
            public void run() {
              
             int size = arrayOne.length;
             for(int i=0;i<size;++i){
                 if(arrayOne[i].intValue() == 0){
                     
                     atomicLong.incrementAndGet();
                 }
             }

            }
        });
        //(13)線程two統(tǒng)計(jì)數(shù)組arrayTwo中0的個(gè)數(shù)
        Thread threadTwo = new Thread(new Runnable() {

            @Override
            public void run() {
              
                 int size = arrayTwo.length;
                 for(int i=0;i<size;++i){
                     if(arrayTwo[i].intValue() == 0){
                         
                         atomicLong.incrementAndGet();
                     }
                 }

            }
        });

        //(14)啟動(dòng)子線程
        threadOne.start();
        threadTwo.start();
        
        //(15)等待線程執(zhí)行完畢
        threadOne.join();
        threadTwo.join();
        
        System.out.println("count 0:" + atomicLong.get());
        
    }
}

輸出結(jié)果:count 0:7。

如上代碼兩個(gè)線程各自統(tǒng)計(jì)自己所在數(shù)據(jù)中0的個(gè)數(shù),每當(dāng)找到一個(gè)0就會調(diào)用 AtomicLong 的原子性遞增方法。

注:在沒有原子類的情況下例如最開始一節(jié)中自己做計(jì)數(shù)器的話,需要使用一定的同步措施,比如使用 Synchronized 關(guān)鍵字等,但是這些都是阻塞算法,對性能有一定損耗,而本節(jié)介紹的這些原子操作類都是使用 CAS 非阻塞算法,性能會更好。但是在高并發(fā)情況下 AtomicLong 還是會存在性能問題,后期高級篇會講到 JDK 8 中提供了一個(gè)在高并發(fā)下性能更好的 LongAdder 類。

偽共享

什么是偽共享

計(jì)算機(jī)系統(tǒng)中為了解決主內(nèi)存與 CPU 運(yùn)行速度的差距,在 CPU 與主內(nèi)存之間添加了一級或者多級高速緩沖存儲器(Cache),這個(gè) Cache 一般是集成到 CPU 內(nèi)部的,所以也叫 CPU Cache,如下圖是兩級 Cache 結(jié)構(gòu):

enter image description here

Cache 內(nèi)部是按行存儲的,其中每一行稱為一個(gè) Cache 行,Cache 行是 Cache 與主內(nèi)存進(jìn)行數(shù)據(jù)交換的單位,Cache 行的大小一般為2的冪次數(shù)字節(jié)。

enter image description here

當(dāng) CPU 訪問某一個(gè)變量時(shí)候,首先會去看 CPU Cache 內(nèi)是否有該變量,如果有則直接從中獲取,否者就去主內(nèi)存里面獲取該變量,然后把該變量所在內(nèi)存區(qū)域的一個(gè) Cache 行大小的內(nèi)存拷貝到 Cache(Cache 行是 Cache 與主內(nèi)存進(jìn)行數(shù)據(jù)交換的單位)。

由于存放到 Cache 行的的是內(nèi)存塊而不是單個(gè)變量,所以可能會把多個(gè)變量存放到了一個(gè) Cache 行。當(dāng)多個(gè)線程同時(shí)修改一個(gè)緩存行里面的多個(gè)變量時(shí)候,由于同時(shí)只能有一個(gè)線程操作緩存行,所以相比每個(gè)變量放到一個(gè)緩存行性能會有所下降,這就是偽共享。

enter image description here

如上圖變量 x,y 同時(shí)被放到了 CPU 的一級和二級緩存,當(dāng)線程1使用 CPU 1對變量 x 進(jìn)行更新時(shí)候,首先會修改 CPU 1 的一級緩存變量 x 所在緩存行,這時(shí)候緩存一致性協(xié)議會導(dǎo)致 CPU 2 中變量 x 對應(yīng)的緩存行失效。

那么線程2寫入變量 x 的時(shí)候就只能去二級緩存去查找,這就破壞了一級緩存,而一級緩存比二級緩存更快,這里也說明了多個(gè)線程不可能同時(shí)去修改自己所使用的 CPU 中緩存行中相同緩存行里面的變量。更壞的情況下如果 CPU 只有一級緩存,那么會導(dǎo)致頻繁的直接訪問主內(nèi)存。

為何會出現(xiàn)偽共享

偽共享的產(chǎn)生是因?yàn)槎鄠€(gè)變量被放入了一個(gè)緩存行,并且多個(gè)線程同時(shí)去寫入緩存行中不同變量。那么為何多個(gè)變量會被放入一個(gè)緩存行那。其實(shí)是因?yàn)?Cache 與內(nèi)存交換數(shù)據(jù)的單位就是 Cache 行,當(dāng) CPU 要訪問的變量沒有在 Cache 命中時(shí)候,根據(jù)程序運(yùn)行的局部性原理會把該變量在內(nèi)存中大小為 Cache 行的內(nèi)存放如緩存行。

long a;
long b;
long c;
long d;

如上代碼,聲明了四個(gè) long 變量,假設(shè) Cache 行的大小為32個(gè)字節(jié),那么當(dāng) CPU 訪問變量 a 時(shí)候發(fā)現(xiàn)該變量沒有在 Cache 命中,那么就會去主內(nèi)存把變量 a 以及內(nèi)存地址附近的 b、c、d 放入緩存行。

也就是地址連續(xù)的多個(gè)變量才有可能會被放到一個(gè)緩存行中,當(dāng)創(chuàng)建數(shù)組時(shí)候,數(shù)組里面的多個(gè)元素就會被放入到同一個(gè)緩存行。那么單線程下多個(gè)變量放入緩存行對性能有影響?其實(shí)正常情況下單線程訪問時(shí)候由于數(shù)組元素被放入到了一個(gè)或者多個(gè) Cache 行對代碼執(zhí)行是有利的,因?yàn)閿?shù)據(jù)都在緩存中,代碼執(zhí)行會更快,可以對比下面代碼執(zhí)行:

代碼(1):

public class TestForContent {

    static final int LINE_NUM = 1024;
    static final int COLUM_NUM = 1024;
    public static void main(String[] args) {
        
        long [][] array = new long[LINE_NUM][COLUM_NUM];
        
        long startTime = System.currentTimeMillis();
        for(int i =0;i<LINE_NUM;++i){
            for(int j=0;j<COLUM_NUM;++j){
                array[i][j] = i*2+j;
            }
        }
        long endTime = System.currentTimeMillis();
        long cacheTime = endTime - startTime;
        System.out.println("cache time:" + cacheTime);

    }
}

代碼(2):

public class TestForContent2 {

    static final int LINE_NUM = 1024;
    static final int COLUM_NUM = 1024;
    public static void main(String[] args) {
        
        long [][] array = new long[LINE_NUM][COLUM_NUM];

        long startTime = System.currentTimeMillis();
        for(int i =0;i<COLUM_NUM;++i){
            for(int j=0;j<LINE_NUM;++j){
                array[j][i] = i*2+j;
            }
        }
        long endTime = System.currentTimeMillis();
        
        System.out.println("no cache time:" + (endTime - startTime));

    }

}

我 Mac 電腦上執(zhí)行代碼(1)多次耗時(shí)均在10ms一下,執(zhí)行代碼(2)多次耗時(shí)均在10ms以上。

總的來說代碼(1)比代碼(2)執(zhí)行的快,這是因?yàn)閿?shù)組內(nèi)數(shù)組元素之間內(nèi)存地址是連續(xù)的,當(dāng)訪問數(shù)組第一個(gè)元素時(shí)候,會把第一個(gè)元素后續(xù)若干元素一塊放入到 Cache 行,這樣順序訪問數(shù)組元素時(shí)候會在 Cache 中直接命中,就不會去主內(nèi)存讀取,后續(xù)訪問也是這樣。

總結(jié)下也就是當(dāng)順序訪問數(shù)組里面元素時(shí)候,如果當(dāng)前元素在 Cache 沒有命中,那么會從主內(nèi)存一下子讀取后續(xù)若干個(gè)元素到 Cache,也就是一次訪問內(nèi)存可以讓后面多次直接在 Cache 命中。而代碼(2)是跳躍式訪問數(shù)組元素的,而不是順序的,這破壞了程序訪問的局部性原理,并且 Cache是有容量控制的,Cache 滿了會根據(jù)一定淘汰算法替換 Cache 行,會導(dǎo)致從內(nèi)存置換過來的 Cache 行的元素還沒等到讀取就被替換掉了。

所以單個(gè)線程下順序修改一個(gè) Cache 行中的多個(gè)變量,是充分利用了程序運(yùn)行局部性原理,會加速程序的運(yùn)行,而多線程下并發(fā)修改一個(gè) Cache 行中的多個(gè)變量而就會進(jìn)行競爭 Cache 行,降低程序運(yùn)行性能。

如何避免偽共享

JDK 8 之前一般都是通過字節(jié)填充的方式來避免,也就是創(chuàng)建一個(gè)變量的時(shí)候使用填充字段填充該變量所在的緩存行,這樣就避免了多個(gè)變量存在同一個(gè)緩存行,如下代碼:

      public final static class FilledLong {
            public volatile long value = 0L;
            public long p1, p2, p3, p4, p5, p6;     
        }

假如 Cache 行為64個(gè)字節(jié),那么我們在 FilledLong 類里面填充了6個(gè) long 類型變量,每個(gè) long 類型占用8個(gè)字節(jié),加上 value 變量的8個(gè)字節(jié)總共56個(gè)字節(jié),另外這里 FilledLong 是一個(gè)類對象,而類對象的字節(jié)碼的對象頭占用了8個(gè)字節(jié),所以當(dāng) new 一個(gè) FilledLong 對象時(shí)候?qū)嶋H會占用64個(gè)字節(jié)的內(nèi)存,這個(gè)正好可以放入 Cache 的一個(gè)行。

在 JDK 8 中提供了一個(gè) sun.misc.Contended 注解,用來解決偽共享問題,上面代碼可以修改為如下:

    @sun.misc.Contended 
      public final static class FilledLong {
            public volatile long value = 0L;
        }

上面是修飾類的,當(dāng)然也可以修飾變量,比如 Thread 類中的使用:

    /** The current seed for a ThreadLocalRandom */
    @sun.misc.Contended("tlr")
    long threadLocalRandomSeed;

    /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
    @sun.misc.Contended("tlr")
    int threadLocalRandomProbe;

    /** Secondary seed isolated from public ThreadLocalRandom sequence */
    @sun.misc.Contended("tlr")
    int threadLocalRandomSecondarySeed;

Thread 類里面這三個(gè)變量是在 ThreadLocalRandom(Chat:《Java 并發(fā)編程之美:并發(fā)編程高級篇之一》中對其進(jìn)行了講解)中為了實(shí)現(xiàn)高并發(fā)下高性能生成隨機(jī)數(shù)時(shí)候使用的,這三個(gè)變量默認(rèn)是初始化為0。

需要注意的是默認(rèn)情況下 @Contended 注解只用到 Java 核心類,比如 rt 包下的類,如果需要在用戶 classpath 下的類使用這個(gè)注解需要添加 JVM 參數(shù):-XX:-RestrictContended,另外默認(rèn)填充的寬度為128,如果你想要自定義寬度可以設(shè)置 -XX:ContendedPaddingWidth 參數(shù)。

注:本節(jié)講述了偽共享如何產(chǎn)生,以及如何避免,并證明多線程下訪問同一個(gè) Cache 行的多個(gè)的變量時(shí)候才會出現(xiàn)偽共享,當(dāng)單個(gè)線程訪問一個(gè) Cache 行里面的多個(gè)變量時(shí)候反而對程序運(yùn)行起到加速作用。這里為后面高級篇講解 LongAdder 的實(shí)現(xiàn)提供了基礎(chǔ)。

Java 中的指令重排序

Java 內(nèi)存模型允許編譯器和處理器對指令進(jìn)行重排序以提高運(yùn)行性能,并且重排序只會對不存在數(shù)據(jù)依賴性的指令進(jìn)行重排序;在單線程下重排序可以保證最終執(zhí)行的結(jié)果是與程序順序執(zhí)行的結(jié)果一致,但是在多線程下就會存在問題。

下面看一個(gè)例子


int a = 1;//(1)
int b = 2;//(2)
int c= a + b;//(3)

如上代碼變量 c 的值依賴 a 和 b 的值,所以重排序后能夠保證(3)的操作在(2)(1)之后,但是(1)(2)誰先執(zhí)行就不一定了,這在單線程下不會存在問題,因?yàn)椴⒉挥绊懽罱K結(jié)果。

下面看一個(gè)多線程的例子:

public static class ReadThread extends Thread {
        public void run() {

            while(!Thread.currentThread().isInterrupted()){
                if(ready){//(1)
                    System.out.println(num+num);//(2)
                }
                System.out.println("read thread....");
            }

        }
    }

public static class Writethread extends Thread {
   public void run() {
        num = 2;//(3)
        ready = true;//(4)
        System.out.println("writeThread set over...");
   }
}

private static int num =0;
private static boolean ready = false;

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

   ReadThread rt = new ReadThread();
   rt.start();

   Writethread  wt = new Writethread();
   wt.start();

   Thread.sleep(10);
   rt.interrupt();
   System.out.println("main exit");
}

首先這段代碼里面的變量沒有聲明為 volatile 也沒有使用任何同步措施,所以多線程下存在共享變量內(nèi)存可見性問題,這里先不談內(nèi)存可見性問題,因?yàn)橥ㄟ^把變量聲明為 volatile 本身就可以避免指令重排序問題。

這里先看看指令重排序會造成什么影響,如上代碼不考慮內(nèi)存可見性問題的情況下 程序一定會輸出4?答案是不一定,由于代碼(1)(2)(3)(4)之間不存在依賴,所以寫線程的代碼(3)(4)可能被重排序?yàn)橄葓?zhí)行(4)在執(zhí)行(3),那么執(zhí)行(4)后,讀線程可能已經(jīng)執(zhí)行了(1)操作,并且在(3)執(zhí)行前開始執(zhí)行(2)操作,這時(shí)候打印結(jié)果為0而不是4。

這就是重排序在多線程下導(dǎo)致程序執(zhí)行結(jié)果不是我們想要的了,這里使用 volatile 修飾 ready 可以避免重排序和內(nèi)存可見性問題。

當(dāng)寫 volatile 變量時(shí)候,可以確保 volatile 寫之前的操作不會被編譯器重排序到 volatile 寫之后。
當(dāng)讀 volatile 讀變量時(shí)候,可以確保 volatile 讀之后的操作不會被編譯器重排序到 volatile 讀之前。

鎖的概述

樂觀鎖與悲觀鎖

樂觀鎖和悲觀鎖是在數(shù)據(jù)庫中使用的名詞,本節(jié)這里也提下。

悲觀鎖

悲觀鎖指對數(shù)據(jù)被外界修改持保守態(tài)度,在整個(gè)數(shù)據(jù)處理過程中,將數(shù)據(jù)處于鎖定狀態(tài)。悲觀鎖的實(shí)現(xiàn),往往依靠數(shù)據(jù)庫提供的鎖機(jī)制,數(shù)據(jù)庫中實(shí)現(xiàn)是對數(shù)據(jù)記錄操作前給記錄加排它鎖。如果獲取鎖失敗,則說明數(shù)據(jù)正在被其它線程修改,則等待或者拋出異常。如果加鎖成功,則獲取記錄,對其修改,然后事務(wù)提交后釋放排它鎖。

使用悲觀鎖的一個(gè)常用的例子: select * from 表 where .. for update;

樂觀鎖

樂觀鎖是相對悲觀鎖來說的,它認(rèn)為數(shù)據(jù)一般情況下不會造成沖突,所以在訪問記錄前不會加排它鎖,而是在數(shù)據(jù)進(jìn)行提交更新的時(shí)候,才會正式對數(shù)據(jù)的沖突與否進(jìn)行檢測。具體說是根據(jù) update 返回的行數(shù)讓用戶決定如何去做。

例如: update 表 set comment='***',status='operator',version=version+1 where version = 1 and id = 1;

樂觀鎖并不會使用數(shù)據(jù)庫提供的鎖機(jī)制,一般在表添加 version 字段或者使用業(yè)務(wù)狀態(tài)來做。樂觀鎖直到提交的時(shí)候才去鎖定,所以不會產(chǎn)生任何鎖和死鎖。

公平鎖與非公平鎖

根據(jù)線程獲取鎖的搶占機(jī)制鎖可以分為公平鎖和非公平鎖,公平鎖表示線程獲取鎖的順序是按照線程請求鎖的時(shí)間長短來分決定的的,也就是最早獲取鎖的線程將最早獲取到鎖,也就是先來先得的 FIFO 順序。而非公平鎖則運(yùn)行時(shí)候闖入,也就是先來不一定先得。

ReentrantLock 提供了公平和非公平鎖的實(shí)現(xiàn):

  • 公平鎖:ReentrantLock pairLock = new ReentrantLock(true);
  • 非公平鎖:ReentrantLock pairLock = new ReentrantLock(false);

如果構(gòu)造函數(shù)不傳遞參數(shù),則默認(rèn)是非公平鎖。

具體來說假設(shè)線程 A 已經(jīng)持有了鎖,這時(shí)候線程 B 請求該鎖將會被掛起,當(dāng)線程 A 釋放鎖后,假如當(dāng)前有線程 C 也需要獲取該鎖,如果采用非公平鎖方式,則根據(jù)線程調(diào)度策略線程 B 和 C 兩者之一可能獲取鎖,這時(shí)候不需要任何其它干涉,如果使用公平鎖則需要把 C 掛起,讓 B 獲取當(dāng)前鎖。

在沒有公平性需求的前提下盡量使用非公平鎖,因?yàn)楣芥i會帶來性能開銷。

獨(dú)占鎖與共享鎖

根據(jù)鎖只能被單個(gè)線程持有還是能被多個(gè)線程共同持有,鎖分為獨(dú)占鎖和共享鎖。

獨(dú)占鎖保證任何時(shí)候都只有一個(gè)線程能得到鎖,ReentrantLock 就是以獨(dú)占方式實(shí)現(xiàn)的。共享鎖則同時(shí)有多個(gè)線程可以持有,例如 ReadWriteLock 讀寫鎖,它允許一個(gè)資源可以被多線程同時(shí)進(jìn)行讀操作。

獨(dú)占鎖是一種悲觀鎖,每次訪問資源都先加上互斥鎖,這限制了并發(fā)性,因?yàn)樽x操作并不會影響數(shù)據(jù)一致性,而獨(dú)占鎖只允許同時(shí)一個(gè)線程讀取數(shù)據(jù),其它線程必須等待當(dāng)前線程釋放鎖才能進(jìn)行讀取。

共享鎖則是一種樂觀鎖,它放寬了加鎖的條件,允許多個(gè)線程同時(shí)進(jìn)行讀操作。

什么是可重入鎖

當(dāng)一個(gè)線程要獲取一個(gè)被其它線程持有的獨(dú)占鎖時(shí)候,該線程會被阻塞,那么當(dāng)一個(gè)線程再次獲取它自己已經(jīng)獲取的鎖時(shí)候是否會被阻塞那?如果不被阻塞,那么我們說該鎖是可重入的,也就是只要該線程獲取了該鎖,那么可以無限制次數(shù)(高級篇我們會知道嚴(yán)格來說是有限次數(shù))進(jìn)入被該鎖鎖住的代碼。

下面看一個(gè)例子看看什么情況下會用可重入鎖。

public class Hello{
     public Synchronized void helloA(){
        System.out.println("hello");
     }

     public Synchronized void helloB(){
        System.out.println("hello B");
        helloA();
     }

}

如上面代碼當(dāng)調(diào)用 helloB 函數(shù)前會先獲取內(nèi)置鎖,然后打印輸出,然后調(diào)用 helloA 方法,調(diào)用前會先去獲取內(nèi)置鎖,如果內(nèi)置鎖不是可重入的那么該調(diào)用就會導(dǎo)致死鎖了,因?yàn)榫€程持有并等待了鎖導(dǎo)致調(diào)用 helloA 時(shí)候永遠(yuǎn)不會獲取到鎖。

實(shí)際上 synchronized 內(nèi)部鎖是可重入鎖,可重入鎖的原理是在鎖內(nèi)部維護(hù)了一個(gè)線程標(biāo)示,用來標(biāo)示該鎖目前被那個(gè)線程占用,然后關(guān)聯(lián)一個(gè)計(jì)數(shù)器。一開始計(jì)數(shù)器值為0,說明該鎖沒有被任何線程占用,當(dāng)一個(gè)線程獲取了該鎖,計(jì)數(shù)器會變成1,其它線程在獲取該鎖時(shí)候發(fā)現(xiàn)鎖的所有者不是自己就會被阻塞掛起。

但是當(dāng)獲取該鎖的線程再次獲取鎖時(shí)候發(fā)現(xiàn)鎖擁有者是自己,就會把計(jì)數(shù)器值+1, 當(dāng)釋放鎖后計(jì)數(shù)器會-1,當(dāng)計(jì)數(shù)器為0時(shí)候,鎖里面的線程標(biāo)示重置為 null,這時(shí)候阻塞的線程會獲取被喚醒來競爭獲取該鎖。

總結(jié)

本章主要介紹了并發(fā)編程的基礎(chǔ)知識,為后面高級篇講解并發(fā)包源碼提供了基礎(chǔ),通過圖形結(jié)合講述了為什么要使用多線程編程,多線程編程存在的線程安全問題,以及什么是內(nèi)存可見性問題。然后講解了 synchronized 和 volatile 關(guān)鍵字,并且強(qiáng)調(diào)了前者既保證了內(nèi)存可見性同時(shí)也保證了原子性,而后者則主要做到了內(nèi)存可見性,但是它們的內(nèi)存語義還是很相似的,最后講解的什么是 CAS 和線程間同步以及各種鎖的概念,都為后面講解 JUC 包源碼奠定了基礎(chǔ)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,978評論 2 374