小白哥帶你打通任督二脈:從JVM內(nèi)存模型談線程安全

作為一個三個多月沒有去工作的獨(dú)立開發(fā)者而言,今天去小米面試了一把.怎么說呢,無論你水平如何,請確保在面試之前要做準(zhǔn)備,就像其中一位面試官說的一樣,我知道你水平不錯,但是無論如何也是要準(zhǔn)備下的,不然你怎么會連這個方法也忘記了?

此刻,我突然覺得我是一個假程序員.為什么這么說呢,作為一個從12年就開始寫代碼的程序員來說,忘記某個方法太可恥了.等趕明寫一篇文章就叫做"我是個假程序員"來談?wù)勥@些有趣的事兒.

話不多說,今天要談的主題是相對較深,較廣,但我努力的讓他看起來清晰明了.


存儲器層次結(jié)構(gòu)

對于開發(fā)者來說,存儲器的層次結(jié)構(gòu)應(yīng)該是非常熟悉的,大體如下:


這里寫圖片描述

其中寄存器,L1,L2,L3都被封裝在CPU芯片中,作為應(yīng)用開發(fā)者而言我們很少去注意和使用它.之所以引入L1,L2,L3高速寄存器,其根本是為了解決訪問運(yùn)算器和內(nèi)存速度不匹配.但緩存的引入也帶來兩個問題:

  1. 緩存命中率:緩存的數(shù)據(jù)都是主存中數(shù)據(jù)的備份,如果指令所需要的數(shù)據(jù)恰好在緩存中,我們就說緩存命中,反之,需要從主存中獲取.一個好的緩存策略應(yīng)該盡可能的提高命中率,如何提高卻是一件非常困難的事情.
  2. 緩存一致性問題:我們知道緩存是主存數(shù)據(jù)的備份,但每個核心都有自己的緩存,當(dāng)緩存中的數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)不一致時,應(yīng)該以誰的數(shù)據(jù)為準(zhǔn)呢,這就是所謂緩存一致性問題.

上面只是展示存儲器的層次結(jié)構(gòu),現(xiàn)在我們來更形象的來看一下CPU芯片與內(nèi)存之間聯(lián)系,以Intel i5雙核處理器為例:


這里寫圖片描述

通過上圖我們能明顯的看出各個緩存之間的聯(lián)系,在隨后的JVM內(nèi)存模型剖析中,你同樣會發(fā)現(xiàn)類似的結(jié)構(gòu).關(guān)于存儲器層次結(jié)構(gòu)到這里已經(jīng)足夠,畢竟我們不是專門做操作系統(tǒng)的,下面我們來聊聊主存,更確切的說抽象的虛擬內(nèi)存.


虛擬內(nèi)存

談起內(nèi)存的時候,每個人的腦海中都會呈現(xiàn)出內(nèi)存條的形象,在很多時候,這種實(shí)物給我們對內(nèi)存最直觀的理解,對于非開發(fā)者這么理解是可以接受的,但是對于從事開發(fā)開發(fā)工作的工程師而言,我們還要加深一點(diǎn).


這里寫圖片描述

從硬件的角度來看,內(nèi)存就是一塊有固定容量的存儲體,與該硬件直接打交道的是我們的操作系統(tǒng).我們知道系統(tǒng)的進(jìn)程都是共享CPU和內(nèi)存資源的,現(xiàn)代操作系統(tǒng)為了更有效的管理內(nèi)存,提出了內(nèi)存的抽象概念,稱之為虛擬內(nèi)存.換言之,我們在操作系統(tǒng)中所提到的內(nèi)存管理談的都是虛擬內(nèi)存.虛擬內(nèi)存的提出帶來幾個好處:

  1. 虛擬內(nèi)存將主存看成是一個存儲在磁盤上的地址空間的告訴緩存.應(yīng)用在未運(yùn)行之前,只是存儲在磁盤上二進(jìn)制文件,運(yùn)行后,該應(yīng)用才被復(fù)制到主存中.
  2. 它為每個進(jìn)程提供了一致的地址空間,簡化了內(nèi)存管理機(jī)制.簡單點(diǎn)來看就是每個進(jìn)程都認(rèn)為自己獨(dú)占該主存.最簡單的例子就是一棟樓被分成許多個房間,每個房間都是獨(dú)立,是戶主專有,每個戶主都可以從零開始自助裝修.另外,在未征得其他戶主的同意之前,你是無法進(jìn)入其他房間的.

虛擬內(nèi)存的提出也改變了內(nèi)存訪問的方式.之前CPU訪問主存的方式如下:


這里寫圖片描述

上圖演示了CPU直接通過物理地址(假設(shè)是2)來訪問主存的過程,但如果有了虛擬內(nèi)存之后,整個訪問過程如下:


這里寫圖片描述

CPU給定一個虛擬地址,然后經(jīng)過MMU(內(nèi)存管理單元,硬件)將虛擬地址翻譯成真正的物理地址,再訪問主存.比如現(xiàn)在虛擬地址是4200經(jīng)過MMU的翻譯直接變成真正的物理地址2.

這里來解釋下什么是虛擬內(nèi)存地址.我們知道虛擬內(nèi)存為每個進(jìn)程提供了一個假象:每個進(jìn)程都在獨(dú)占地使用主存,每個進(jìn)程看到的內(nèi)存都是一樣的,這稱之為虛擬地址空間.舉個例子來說,比如我們內(nèi)存條是1G的,即最大地址空間$2{10}$,這時某個進(jìn)程需要4G的內(nèi)存,那么操作系統(tǒng)可以將其映射成更大的地址空間$2{32}$,這個地址空間就是所謂的虛擬內(nèi)存地址.關(guān)于如何映射,有興趣的可以自行學(xué)習(xí).用一張圖來抽象的表示:

這里寫圖片描述

到現(xiàn)在我們明白原來原來我們所談操作系統(tǒng)中談的內(nèi)存其實(shí)是虛擬內(nèi)存,如果你是C語言開發(fā)者,那對此的感受可能更深.既然每個進(jìn)程都擁有自己的虛擬地址空間,那么它的布局是如何的呢?以Linux系統(tǒng)為例,來看一下它的進(jìn)程空間地址的布局:


這里寫圖片描述

到現(xiàn)在為止,我們終于走到了進(jìn)程這一步.我們知道,每個JVM都運(yùn)行在一個單獨(dú)的進(jìn)程當(dāng)中,和普通應(yīng)用不同,JVM相當(dāng)于一個操作系統(tǒng),它有著自己的內(nèi)存模型.下面,就切入到JVM的內(nèi)存模型中.


并發(fā)模型(線程)

如果java沒有多線程的支持,沒有JIT的存在,那么也不會有現(xiàn)在JVM內(nèi)存模型.為什么這么說呢?首先我們從JIT說起,JIT會追蹤程序的運(yùn)行過程,并對其中可能的地方進(jìn)行優(yōu)化,其中有一項優(yōu)化和處理器的亂序執(zhí)行類似,不過這里叫做指令重排.如果沒有多線程,也就不會存在所謂的臨界資源,如果這個前置條件不存在當(dāng)然也就不會存在資源競爭這一說法了.這樣一來,可能Java早已經(jīng)被拋棄在歷史的長河中.

盡管Java語言不像C語言能夠直接操作內(nèi)存,但是掌握J(rèn)VM內(nèi)存模型仍然非常重要.對于為什么要掌握J(rèn)VM內(nèi)存模型得先從Java的并發(fā)編程模型說起.

在并發(fā)模型中需要處理兩個關(guān)鍵問題:線程之間如何通信以及線程之間如何同步.所謂的通信指的是線程之間如何交換消息,而同步則用于控制不同線程之間操作發(fā)生的相對順序.

從實(shí)現(xiàn)的角度來說,并發(fā)模型一般有兩種方式:基于共享內(nèi)存和基于消息傳遞.兩者實(shí)現(xiàn)的不同決定了通信和同步的行為的差異.在基于共享內(nèi)存的并發(fā)模型中,同步是顯示的,通信是隱式的;而在基于消息傳遞的并發(fā)模型中,通信是顯式的,同步是隱式的.我們來具體解釋一下.

在共享內(nèi)存的并發(fā)模型中,任何線程都可以公共內(nèi)存進(jìn)行操作,如果不加以顯示同步,那么執(zhí)行順序?qū)⑹遣豢芍?也恰是因為哪個線程都可以對公共內(nèi)存操作,所以通信是隱式的.而在基于消息傳遞的并發(fā)模型中,由于消息的發(fā)送一定是在接受之前,因此同步是隱式的,但是線程之間必須通過明確的發(fā)送消息來進(jìn)行通信.

在最終并發(fā)模型選擇方案上,java選擇基于共享內(nèi)存的并發(fā)模型,也就是顯式同步,隱式通信.如果在編寫程序時,不處理好這兩個問題,那在多線程會出現(xiàn)各種奇怪的問題.因此,對任何Java程序員來說,熟悉JVM的內(nèi)存模型是非常重要的.


JVM內(nèi)存結(jié)構(gòu)

對于JVM內(nèi)存,主要包含兩方面:JVM內(nèi)存結(jié)構(gòu)和JVM內(nèi)存模型.兩者之間的區(qū)別在于模型是一種協(xié)議,規(guī)定對特定內(nèi)存或緩存的讀寫過程,千萬不要弄混了.

很多人往往對JVM內(nèi)存結(jié)構(gòu)和進(jìn)程的內(nèi)存結(jié)構(gòu)感到困惑,這里我將幫助你梳理一下.

JVM本質(zhì)上也是一個程序,只不過它又有著類似操作系統(tǒng)的特性.當(dāng)一個JVM實(shí)例開始運(yùn)行時,此時在Linux進(jìn)程中,其內(nèi)存布局如下:


這里寫圖片描述

JVM在進(jìn)程堆空間的基礎(chǔ)上再次進(jìn)行劃分,來簡單看一下.此時的永生代本質(zhì)上就是Java程序程序的代碼區(qū)和數(shù)據(jù)區(qū),而年輕代和老年代才是Java程序真正使用的堆區(qū),也就是我們經(jīng)常掛在嘴邊的.但是此時的堆區(qū)和進(jìn)程上的堆卻又很大的區(qū)別:在調(diào)用C程序的malloc函數(shù)時,會引起一次系統(tǒng)級的調(diào)用;在使用free函數(shù)釋放內(nèi)存時,同樣也會引起一次系統(tǒng)級的調(diào)用,但是JVM中堆區(qū)并非如此:JVM一次性向系統(tǒng)申請一塊連續(xù)的內(nèi)存區(qū)域,作為Java程序的堆,當(dāng)Java程序使用new申請內(nèi)存時,JVM會根據(jù)需要在這段內(nèi)存區(qū)域中為其分配,而不需要除非一次系統(tǒng)級別的調(diào)用.可以看出JVM其實(shí)自行實(shí)現(xiàn)了一條堆內(nèi)存的管理機(jī)制,這種管理方式有以下好處:

  1. 減少系統(tǒng)級別的調(diào)用.大部分內(nèi)存申請和回首不需要觸發(fā)系統(tǒng)函數(shù),僅僅只在Java堆大小發(fā)生變化時才會引起系統(tǒng)函數(shù)的調(diào)用.相比系統(tǒng)級別的調(diào)用,JVM實(shí)現(xiàn)內(nèi)存管理成本更低.
  2. 減少內(nèi)存泄漏情況的發(fā)生.通過JVM接管內(nèi)存管理過程,可以避免大多情況下的內(nèi)存泄漏問題.

現(xiàn)在已經(jīng)簡單介紹了JVM內(nèi)存結(jié)構(gòu),希望這樣能幫助你打通上下.當(dāng)然,為了好理解,我省略了其中一些相對不重要的點(diǎn),如有興趣可以自行學(xué)習(xí).講完了JVM內(nèi)存結(jié)構(gòu),下一步該是什么呢?


JVM內(nèi)存模型

Java采用的是基于共享內(nèi)存的并發(fā)模型,使得JVM看起來非常類似現(xiàn)代多核處理器:在基于共享內(nèi)存的多核處理器體系架構(gòu)中,每個處理器都有自己的緩存,并且定期與主內(nèi)存進(jìn)行協(xié)調(diào).這里的線程同樣有自己的緩存(也叫工作內(nèi)存),此時,JVM內(nèi)存模型呈現(xiàn)出如下結(jié)構(gòu):

WX20170227-191046@2x.png

上圖展示JVM的內(nèi)存模型,也稱之為JMM.對于JMM有以下規(guī)定:

  1. 所有的變量都存儲在主內(nèi)存(Main Memory)
  2. 每個線程也有用自己的工作內(nèi)存(Work Memory)
  3. 工作內(nèi)存中的變量是主內(nèi)存變量的拷貝,線程不能直接讀寫主內(nèi)存的變量,而只能操作自己工作內(nèi)存中的變量
  4. 線程間不共享工作內(nèi)存,如果線程間需要通信必須借助主內(nèi)存來完成

共享變量所在的內(nèi)存區(qū)域也就是共享內(nèi)存,也稱之為堆內(nèi)存,該區(qū)域中的變量都可能被共享,即被多線程訪問.說的再通俗點(diǎn)就是在java當(dāng)中,堆內(nèi)存是在線程間共享的,而局部變量,形參和異常程序參數(shù)不在堆內(nèi)存,因此就不存在多線程共享的情況.

與JMM規(guī)定相對應(yīng),我們定義了以下四個原子性操作來實(shí)現(xiàn)變量從主內(nèi)存拷貝到工作內(nèi)存的過程:

  1. read:讀取主內(nèi)存的變量,并將其傳送到工作內(nèi)存
  2. load:把read操作從主內(nèi)存得到的變量值放入到工作內(nèi)存的拷貝中
  3. store:把工作內(nèi)存中的一個變量值傳送到主內(nèi)存當(dāng)中,以便用于后面的write操作
  4. write:把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中.

可以看出,從主內(nèi)存到工作內(nèi)存的過程其實(shí)是要經(jīng)過read和load兩個操作的,反之需要經(jīng)過store和write兩個操作.

現(xiàn)在我們來看一段代碼,并用結(jié)合上文談?wù)勏露嗑€程安全問題:

public class ThreadTest {


    public static void main(String[] args) throws InterruptedException {
        ShareVar ins = new ShareVar();
        List<Thread> threadList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            Thread thread;
            if (i % 2 == 0) {
                thread = new Thread(new AddThread(ins));

            } else {
                thread = new Thread(new SubThread(ins));

            }

            thread.start();
            threadList.add(thread);

        }

        for (Thread thread : threadList) {
            thread.join();
        }

        System.out.println(Thread.currentThread().getId() + "   " + ins.getCount());


    }


}

class ShareVar {
    private int count;

    public void add() {
        try {
            Thread.sleep(100);//此處為了更好的體現(xiàn)多線程安全問題
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;


    }

    public void sub() {
        count--;
    }

    public int getCount() {
        return count;
    }
}

class AddThread implements Runnable {
    private ShareVar shareVar;

    public AddThread(ShareVar shareVar) {
        this.shareVar = shareVar;
    }

    @Override
    public void run() {
        shareVar.add();
    }
}

class SubThread implements Runnable {
    private ShareVar shareVar;

    public SubThread(ShareVar shareVar) {
        this.shareVar = shareVar;
    }

    @Override
    public void run() {
        shareVar.sub();
    }
}

理想情況下,最后應(yīng)該輸出0,但是多次運(yùn)行你會先可能輸出-1或者-2等.為什么呢?
在創(chuàng)建的這10個線程中,每個線程都有自己工作內(nèi)存,而這些線程又共享了ShareVar對象的count變量,當(dāng)線程啟動時,會經(jīng)過read-load操作從主內(nèi)存中拷貝該變量至自己的工作內(nèi)存中,隨后每個線程會在自己的工作內(nèi)存中操作該變量副本,最后會將該副本重新寫會到主內(nèi)存,替換原先變量的值.但在多個線程中,但由于線程間無法直接通信,這就導(dǎo)致變量的變化不能及時的反應(yīng)在線程當(dāng)中,這種細(xì)微的時間差最終導(dǎo)致每個線程當(dāng)前操作的變量值未必是最新的,這就是所謂的內(nèi)存不可見性.

現(xiàn)在我想你已經(jīng)完全明白了多線程安全問題的由來.那該怎么解決呢?最簡單的方法就是讓多個線程對共享對象的讀寫操作編程串行,也就是同一時刻只允許一個線程對共享對象進(jìn)行操作.我們將這種機(jī)制成為鎖機(jī)制,java中規(guī)定每個對象都有一把鎖,稱之為監(jiān)視器(monitor),有人也叫作對象鎖,同一時刻,該對象鎖只能服務(wù)一個線程.

有了鎖對象之后,它是怎么生效的呢?為此JMM中又定義了兩個原子操作:

  1. lock:將主內(nèi)存的變量標(biāo)識為一條線程獨(dú)占狀態(tài)
  2. unlock:解除主內(nèi)存中變量的線程獨(dú)占狀態(tài)

在鎖對象和這兩個原子操作共同作用下而成的鎖機(jī)制就可以實(shí)現(xiàn)同步了,體現(xiàn)在語言層面就是synchronized關(guān)鍵字.上面我們也說道Java采用的是基于共享內(nèi)存的并發(fā)模型,該模型典型的特征是要顯式同步,也就是說在要人為的使用synchronized關(guān)鍵字來做同步.現(xiàn)在我們來改進(jìn)上面的代碼,只需要為add()和sub()方法添加syhcronized關(guān)鍵字即可,但在這之前,先來看看這兩個方法對應(yīng)的字節(jié)碼文件:

  public void add();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=1
         0: ldc2_w        #2                  // long 100l
         3: invokestatic  #4                  // Method java/lang/Thread.sleep:(J)V
         6: goto          14
         9: astore_1
        10: aload_1
        11: invokevirtual #6                  // Method java/lang/InterruptedException.printStackTrace:()V
        14: aload_0
        15: dup
        16: getfield      #7                  // Field count:I
        19: iconst_1
        20: iadd
        21: putfield      #7                  // Field count:I
        24: return

  public void sub();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #7                  // Field count:I
         5: iconst_1
         6: isub
         7: putfield      #7                  // Field count:I
        10: return
      LineNumberTable:
        line 18: 0
        line 19: 10

現(xiàn)在我們使用synchronized來讓著兩個方法變得安全起來:

class ShareVar {
    private int count;

    public synchronized void add() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;


    }

    public synchronized void sub() {
        count--;
    }

    public int getCount() {
        return count;
    }
}

此時這段代碼在多線程中就會表現(xiàn)良好.再來看看它的字節(jié)碼文件發(fā)生了什么變化:

  public synchronized void add();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=2, args_size=1
         0: ldc2_w        #2                  // long 100l
         3: invokestatic  #4                  // Method java/lang/Thread.sleep:(J)V
         6: goto          14
         9: astore_1
        10: aload_1
        11: invokevirtual #6                  // Method java/lang/InterruptedException.printStackTrace:()V
        14: aload_0
        15: dup
        16: getfield      #7                  // Field count:I
        19: iconst_1
        20: iadd
        21: putfield      #7                  // Field count:I
        24: return
   

  public synchronized void sub();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #7                  // Field count:I
         5: iconst_1
         6: isub
         7: putfield      #7                  // Field count:I
        10: return
      LineNumberTable:
        line 18: 0
        line 19: 10

通過字節(jié)碼不難看出最大的變化在于方法的flags中增加了ACC_SYNCHRONIZED標(biāo)識,虛擬機(jī)在遇到該標(biāo)識時,會隱式的為方法添加monitorenter和monitorexit指令,這兩個指令就是在JMM的lock和unlock操作上實(shí)現(xiàn)的.

其中monitorenter指令會獲取對象的占有權(quán),此時有以下三種可能:

  1. 如果該對象的monitor的值0,則該線程進(jìn)入該monitor,并將其值標(biāo)為1,表明對象被該線程獨(dú)占.
  2. 同一個線程,如果之前已經(jīng)占有該對象了,當(dāng)再次進(jìn)入時,需將該對象的monitor的值加1.
  3. 如果該對象的monitor值不為0,表明該對象被其他線程獨(dú)占了,此時該線程進(jìn)入阻塞狀態(tài),等到該對象的monitor的值為0時,在嘗試獲取該對象.

而monitorexit的指令則是已占有該對象的線程在離開時,將monitor的值減1,表明該線程已經(jīng)不再獨(dú)占該對象.

用synchronized修飾的方法叫做同步方法,除了這種方式之外,還可以使用同步代碼塊的形式:

package com.cd.app;

class ShareVar {
    private int count;

    public void add() {
        synchronized (this) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }


    }

    public void sub() {
        synchronized (this) {
            count--;
        }

    }

    public int getCount() {
        return count;
    }
}

接下來同樣是看一下他的字節(jié)碼,主要看add()和sub()方法:

 public void add();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: dup
         6: getfield      #2                  // Field count:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field count:I
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return



public void sub();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: dup
         6: getfield      #2                  // Field count:I
         9: iconst_1
        10: isub
        11: putfield      #2                  // Field count:I
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return

同步代碼塊和同步方法的實(shí)現(xiàn)原理是一致的,都是通過monitorenter/monitorexit指令,唯一的區(qū)別在于同步代碼塊中monitorenter/monitorexit是顯式的加載字節(jié)碼文件當(dāng)中的.

上面我們通過synchronized解決了內(nèi)存可見性問題,另外也可以認(rèn)為凡是被synchronized修飾的方法或代碼塊都是原子性的,即一個變量從主內(nèi)存到工作內(nèi)存,再從工作內(nèi)存到主內(nèi)存這個過程是不可分割的.

正如我們在 談亂序執(zhí)行和內(nèi)存屏障所提到的,javac編譯器和JVM為了提高性能會通過指令重排的方式來企圖提高性能,但是在某些情況下我們同樣需要阻止這過程,由于synchronized關(guān)鍵字保證了持有同一個鎖的的兩個同步方法/同步塊只能串行進(jìn)入,因此無形之中也就相當(dāng)阻止了指令重排.


總結(jié)

希望這么從下往上,再從上往下的解釋能讓各位同學(xué)對JVM內(nèi)存模型以及多線程安全問題有個更通透的理解.好了,今天就到這,歡迎關(guān)注訂閱"江湖人稱小白哥"

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嗡善,一起剝皮案震驚了整個濱河市掐暮,隨后出現(xiàn)的幾起案子折联,更是在濱河造成了極大的恐慌,老刑警劉巖芭届,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異膏孟,居然都是意外死亡窜醉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門铜邮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仪召,“玉大人寨蹋,你說我怎么就攤上這事松蒜。” “怎么了已旧?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵秸苗,是天一觀的道長。 經(jīng)常有香客問我运褪,道長惊楼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任秸讹,我火速辦了婚禮檀咙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘璃诀。我一直安慰自己弧可,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布劣欢。 她就那樣靜靜地躺著棕诵,像睡著了一般裁良。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上校套,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天价脾,我揣著相機(jī)與錄音,去河邊找鬼笛匙。 笑死侨把,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的妹孙。 我是一名探鬼主播座硕,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼涕蜂!你這毒婦竟也來了华匾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤机隙,失蹤者是張志新(化名)和其女友劉穎蜘拉,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體有鹿,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡旭旭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了葱跋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片持寄。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖娱俺,靈堂內(nèi)的尸體忽然破棺而出稍味,到底是詐尸還是另有隱情,我是刑警寧澤荠卷,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布模庐,位于F島的核電站,受9級特大地震影響油宜,放射性物質(zhì)發(fā)生泄漏掂碱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一慎冤、第九天 我趴在偏房一處隱蔽的房頂上張望疼燥。 院中可真熱鬧,春花似錦蚁堤、人聲如沸醉者。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽湃交。三九已至熟空,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間搞莺,已是汗流浹背息罗。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留才沧,地道東北人迈喉。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像温圆,于是被迫代替她去往敵國和親挨摸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容