Go到底能不能實(shí)現(xiàn)安全的雙檢鎖恢口?

不安全的雙檢鎖

從其他語言轉(zhuǎn)入Go語言的同學(xué)經(jīng)常會(huì)陷入一個(gè)思考:如何創(chuàng)建一個(gè)單例狮斗?

有些同學(xué)可能會(huì)把其它語言中的雙檢鎖模式移植過來,雙檢鎖模式也稱為懶漢模式弧蝇,首次用到的時(shí)候才創(chuàng)建實(shí)例碳褒。大部分人首次用Golang寫出來的實(shí)例大概是這樣的:

type Conn struct {
    Addr  string
    State int
}

var c *Conn
var mu sync.Mutex

func GetInstance() *Conn {
    if c == nil {
        mu.Lock()
        defer mu.Unlock()
        if c == nil {
            c = &Conn{"127.0.0.1:8080", 1}
        }
    }
    return c
}

這里先解釋下這段代碼的執(zhí)行邏輯(已經(jīng)清楚的同學(xué)可以直接跳過):

GetInstance用于獲取結(jié)構(gòu)體Conn的一個(gè)實(shí)例,其中:先判斷c是否為空看疗,如果為空則加鎖沙峻,加鎖之后再判斷一次c是否為空,如果還為空两芳,則創(chuàng)建Conn的一個(gè)實(shí)例摔寨,并賦值給c。這里有兩次判空怖辆,所以稱為雙檢是复,需要第二次判空的原因是:加鎖之前可能有多個(gè)線程/協(xié)程都判斷為空,這些線程/協(xié)程都會(huì)在這里等著加鎖竖螃,它們最終也都會(huì)執(zhí)行加鎖操作淑廊,不過加鎖之后的代碼在多個(gè)線程/協(xié)程之間是串行執(zhí)行的,一個(gè)線程/協(xié)程判空之后創(chuàng)建了實(shí)例特咆,其它線程/協(xié)程在判斷c是否為空時(shí)必然得出false的結(jié)果季惩,這樣就能保證c僅創(chuàng)建一次。而且后續(xù)調(diào)用GetInstance時(shí)都會(huì)僅執(zhí)行第一次判空腻格,得出false的結(jié)果画拾,然后直接返回c。這樣每個(gè)線程/協(xié)程最多只執(zhí)行一次加鎖操作菜职,后續(xù)都只是簡(jiǎn)單的判斷下就能返回結(jié)果青抛,其性能必然不錯(cuò)。

了解Java的同學(xué)可能知道Java中的雙檢鎖是非線程安全的酬核,這是因?yàn)橘x值操作中的兩個(gè)步驟可能會(huì)出現(xiàn)亂序執(zhí)行問題蜜另。這兩個(gè)步驟是:對(duì)象內(nèi)存空間的初始化和將內(nèi)存地址設(shè)置給變量。因?yàn)榫幾g器或者CPU優(yōu)化愁茁,它們的執(zhí)行順序可能不確定蚕钦,先執(zhí)行第2步的話,鎖外邊的線程很有可能訪問到?jīng)]有初始化完畢的變量鹅很,從而引發(fā)某些異常嘶居。針對(duì)這個(gè)問題,Java以及其它一些語言中可以使用volatile來修飾變量,實(shí)際執(zhí)行時(shí)會(huì)通過插入內(nèi)存柵欄阻止指令重排邮屁,強(qiáng)制按照編碼的指令順序執(zhí)行整袁。

那么Go語言中的雙檢鎖是安全的嗎?

答案是也不安全佑吝。

先來看看指令重排問題:

在Go語言規(guī)范中坐昙,賦值操作分為兩個(gè)階段:第一階段對(duì)賦值操作左右兩側(cè)的表達(dá)式進(jìn)行求值,第二階段賦值按照從左至右的順序執(zhí)行芋忿。(參考:https://golang.google.cn/ref/spec#Assignments

說的有點(diǎn)抽象炸客,但沒有提到賦值存在指令重排的問題,隱約感覺不會(huì)有這個(gè)問題戈钢。為了驗(yàn)證痹仙,讓我們看一下上邊那段代碼中賦值操作的偽匯編代碼:

golang賦值的匯編代碼

紅框圈出來的部分對(duì)應(yīng)的代碼是: c = &Conn{"127.0.0.1:8080", 1}

其中有一行:CMPL $0x0, runtime.writeBarrier(SB) ,這個(gè)指令就是插入一個(gè)內(nèi)存柵欄殉了。前邊是要賦值數(shù)據(jù)的初始化开仰,后邊是賦值操作。如此看薪铜,賦值操作不存在指令重排的問題众弓。

既然賦值操作沒有指令重排的問題,那這個(gè)雙檢鎖怎么還是不安全的呢隔箍?

在Golang中谓娃,對(duì)于大于單個(gè)機(jī)器字的值,讀寫它的時(shí)候是以一種不確定的順序多次執(zhí)行單機(jī)器字的操作來完成的鞍恢。機(jī)器字大小就是我們通常說的32位傻粘、64位,即CPU完成一次無定點(diǎn)整數(shù)運(yùn)算可以處理的二進(jìn)制位數(shù)帮掉,也可以認(rèn)為是CPU數(shù)據(jù)通道的大小。比如在32位的機(jī)器上讀寫一個(gè)int64類型的值就需要兩次操作窒典。(參考:https://golang.google.cn/ref/mem#tmp_2

因?yàn)?strong>Golang中對(duì)變量的讀和寫都沒有原子性的保證蟆炊,所以很可能出現(xiàn)這種情況:鎖里邊變量賦值只處理了一半,鎖外邊的另一個(gè)goroutine就讀到了未完全賦值的變量瀑志。所以這個(gè)雙檢鎖的實(shí)現(xiàn)是不安全的涩搓。

Golang中將這種問題稱為data race,說的是對(duì)某個(gè)數(shù)據(jù)產(chǎn)生了并發(fā)讀寫劈猪,讀到的數(shù)據(jù)不可預(yù)測(cè)昧甘,可能產(chǎn)生問題,甚至導(dǎo)致程序崩潰战得〕浔撸可以在構(gòu)建或者運(yùn)行時(shí)檢查是否會(huì)發(fā)生這種情況:

$ go test -race mypkg    // to test the package
$ go run -race mysrc.go  // to run the source file
$ go build -race mycmd   // to build the command
$ go install -race mypkg // to install the package

另外上邊說單條賦值操作沒有重排序的問題,但是重排序問題在Golang中還是存在的,稍不注意就可能寫出BUG來浇冰。比如下邊這段代碼:

a=1
b=1
c=a+b

在執(zhí)行這段程序的goroutine中并不會(huì)出現(xiàn)問題贬媒,但是另一個(gè)goroutine讀取到b==1時(shí)并不代表此時(shí)a==1,因?yàn)閍=1和b=1的執(zhí)行順序可能會(huì)被改變肘习。針對(duì)重排序問題际乘,Golang并沒有暴露類似volatile的關(guān)鍵字,因?yàn)槔斫夂驼_使用這類能力進(jìn)行并發(fā)編程的門檻比較高漂佩,所以Golang只是在一些自己認(rèn)為比較適合的地方插入了內(nèi)存柵欄脖含,盡量保持語言的簡(jiǎn)單。對(duì)于goroutine之間的數(shù)據(jù)同步投蝉,Go提供了更好的方式养葵,那就是Channel,不過這不是本文的重點(diǎn)墓拜,這里就不介紹了港柜。

sync.Once的啟示

還是回到最開始的問題,如何在Golang中創(chuàng)建一個(gè)單例咳榜?

很多人應(yīng)該會(huì)被推薦使用 sync.Once 夏醉,這里看下如何使用:

type Conn struct {
    Addr  string
    State int
}

var c *Conn
var once sync.Once

func setInstance() {
    fmt.Println("setup")
    c = &Conn{"127.0.0.1:8080", 1}
}

func doPrint() {
    once.Do(setInstance)
    fmt.Println(c)
}

func loopPrint() {
    for i := 0; i < 10; i++ {
        go doprint()
    }
}

這里重用上文的結(jié)構(gòu)體Conn,設(shè)置Conn單例的方法是setInstance涌韩,這個(gè)方法在doPrint中被once.Do調(diào)用畔柔,這里的once就是sync.Once的一個(gè)實(shí)例,然后我們?cè)趌oopPrint方法中創(chuàng)建10個(gè)goroutine來調(diào)用doPrint方法臣樱。

按照sync.Once的語義靶擦,setInstance應(yīng)該近執(zhí)行一次」秃粒可以實(shí)際執(zhí)行下看看玄捕,我這里直接貼出結(jié)果:

setup
&{127.0.0.1:8080 1}
&{127.0.0.1:8080 1}
&{127.0.0.1:8080 1}
&{127.0.0.1:8080 1}
&{127.0.0.1:8080 1}
&{127.0.0.1:8080 1}
&{127.0.0.1:8080 1}
&{127.0.0.1:8080 1}
&{127.0.0.1:8080 1}
&{127.0.0.1:8080 1}

無論執(zhí)行多少遍,都是這個(gè)結(jié)果棚放。那么sync.Once是怎么做到的呢枚粘?源碼很短很清楚:

type Once struct {
    done uint32
    m    Mutex
}

func (o *Once) Do(f func()) {
    if atomic.LoadUint32(&o.done) == 0 {
        o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}

Once是一個(gè)結(jié)構(gòu)體,其中第一個(gè)字段標(biāo)識(shí)是否執(zhí)行過飘蚯,第二個(gè)字段是一個(gè)互斥量馍迄。Once僅公開了一個(gè)Do方法,用于執(zhí)行目標(biāo)函數(shù)f局骤。

這里重點(diǎn)看下目標(biāo)函數(shù)f是怎么被執(zhí)行的攀圈?

  1. Do方法中第一行是判斷字段done是否為0,為0則代表沒執(zhí)行過峦甩,為1則代表執(zhí)行過赘来。這里用了原子讀,寫的時(shí)候也要原子寫,這樣可以保證讀寫不會(huì)同時(shí)發(fā)生撕捍,能夠讀到當(dāng)前最新的值拿穴。
  2. 如果done為0,則調(diào)用doSLow方法忧风,從名字我們就可以體會(huì)到這個(gè)方法比較慢默色。
  3. doSlow中首先會(huì)加鎖,使用的是Once結(jié)構(gòu)體的第二個(gè)字段狮腿。
  4. 然后再判斷done是否為0腿宰,注意這里沒有使用原子讀,為什么呢缘厢?因?yàn)殒i中的方法是串行執(zhí)行的吃度,不會(huì)發(fā)生并發(fā)讀寫。
  5. 如果done為0贴硫,則調(diào)用目標(biāo)函數(shù)f椿每,執(zhí)行相關(guān)的業(yè)務(wù)邏輯。
  6. 在執(zhí)行目標(biāo)函數(shù)f前英遭,這里還聲明了一個(gè)defer:defer atomic.StoreUint32(&o.done, 1) 间护,使用原子寫改變done的值為1,代表目標(biāo)函數(shù)已經(jīng)執(zhí)行過挖诸。它會(huì)在目標(biāo)函數(shù)f執(zhí)行完畢汁尺,doSlow方法返回之前執(zhí)行。這個(gè)設(shè)計(jì)很精妙多律,精確控制了改寫done值的時(shí)機(jī)痴突。

可以看出,這里用的也是雙檢鎖的模式狼荞,只不過做了兩個(gè)增強(qiáng):一是使用原子讀寫辽装,避免了并發(fā)讀寫的內(nèi)存數(shù)據(jù)不一致問題;二是在defer中更改完成標(biāo)識(shí)相味,保證了代碼執(zhí)行順序如迟,不會(huì)出現(xiàn)完成標(biāo)識(shí)更改邏輯被編譯器或者CPU優(yōu)化提前執(zhí)行。

需要注意攻走,如果目標(biāo)函數(shù)f中發(fā)生了panic,目標(biāo)函數(shù)也僅執(zhí)行一次此再,不會(huì)執(zhí)行多次直到成功昔搂。

安全的雙檢鎖

有了對(duì)sync.Once的理解,我們可以改造之前寫的雙檢鎖邏輯输拇,讓它也能安全起來摘符。

type Conn struct {
    Addr  string
    State int
}

var c *Conn
var mu sync.Mutex
var done uint32

func getInstance() *Conn {
    if atomic.LoadUint32(&done) == 0 {
        mu.Lock()
        defer mu.Unlock()
        if done == 0 {
            defer atomic.StoreUint32(&done, 1)
            c = &Conn{"127.0.0.1:8080", 1}
        }
    }
    return c
}

改變的地方就是sync.Once做的兩個(gè)增強(qiáng);原子讀寫和defer中更改完成標(biāo)識(shí)。

當(dāng)然如果要做的工作僅限于此逛裤,還不如直接使用sync.Once瘩绒。

有時(shí)候我們需要的單例不是一成不變的,比如在ylog中需要每小時(shí)創(chuàng)建一個(gè)日志文件的實(shí)例带族,再比如需要為每一個(gè)用戶創(chuàng)建不同的單例锁荔;再比如創(chuàng)建實(shí)例的過程中發(fā)生了錯(cuò)誤,可能我們還會(huì)期望再執(zhí)行實(shí)例的創(chuàng)建過程蝙砌,直到成功阳堕。這兩個(gè)需求是sync.Once無法做到的。

處理panic

這里在創(chuàng)建Conn的時(shí)候模擬一個(gè)panic择克。

i:=0
func newConn() *Conn {
    fmt.Println("newConn")
    div := i
    i++
    k := 10 / div
    return &Conn{"127.0.0.1:8080", k}
}

第1次執(zhí)行newConn時(shí)會(huì)發(fā)生一個(gè)除零錯(cuò)誤恬总,并引發(fā) panic。再執(zhí)行時(shí)則可以正常創(chuàng)建肚邢。

panic可以通過recover進(jìn)行處理壹堰,因此可以在捕捉到panic時(shí)不更改完成標(biāo)識(shí),之前的getInstance方法可以修改為:

func getInstance() *Conn {
    if atomic.LoadUint32(&done) == 0 {
        mu.Lock()
        defer mu.Unlock()

        if done == 0 {
            defer func() {
                if r := recover(); r == nil {
                    defer atomic.StoreUint32(&done, 1)
                }
            }()

            c = newConn()
        }
    }
    return c
}

可以看到這里只是改了下defer函數(shù)骡湖,捕捉不到panic時(shí)才去更改完成標(biāo)識(shí)贱纠。注意此時(shí)c并沒有創(chuàng)建成功,會(huì)返回零值勺鸦,或許你還需要增加其它的錯(cuò)誤處理并巍。

處理error

如果業(yè)務(wù)代碼不是拋出panic,而是返回error换途,這時(shí)候怎么處理懊渡?

可以將error轉(zhuǎn)為panic,比如newConn是這樣實(shí)現(xiàn)的:

func newConn() (*Conn, error) {
    fmt.Println("newConn")
    div := i
    i++
    if div == 0 {
        return nil, errors.New("the divisor is zero")
    }
    k := 1 / div
    return &Conn{"127.0.0.1:8080", k}, nil
}

我們可以再把它包裝一層:

func mustNewConn() *Conn {
    conn, err := newConn()
    if err != nil {
        panic(err)
    }
    return conn
}

如果不使用panic军拟,還可以再引入一個(gè)變量剃执,有error時(shí)對(duì)它賦值,在defer函數(shù)中增加對(duì)這個(gè)變量的判斷懈息,如果有錯(cuò)誤值肾档,則不更新完成標(biāo)識(shí)位。代碼也比較容易實(shí)現(xiàn)辫继,不過還要增加變量怒见,感覺復(fù)雜了,這里就不測(cè)試這種方法了姑宽。

有范圍的單例

前文提到過有時(shí)單例不是一成不變的遣耍,我這里將這種單例稱為有范圍的單例。

這里還是復(fù)用前文的Conn結(jié)構(gòu)體炮车,不過需求修改為要為每個(gè)用戶創(chuàng)建一個(gè)Conn實(shí)例舵变。

看一下User的定義:

type User struct {
    done uint32
    Id   int64
    mu   sync.Mutex
    c    *Conn
}

其中包括一個(gè)用戶Id酣溃,其它三個(gè)字段還是用于獲取當(dāng)前用戶的Conn單例的。

再看看getInstance函數(shù)怎么改:

func getInstance(user *User) *Conn {
    if atomic.LoadUint32(&user.done) == 0 {
        user.mu.Lock()
        defer user.mu.Unlock()

        if user.done == 0 {
            defer func() {
                if r := recover(); r == nil {
                    defer atomic.StoreUint32(&user.done, 1)
                }
            }()

            user.c = newConn()
        }
    }
    return user.c
}

這里增加了一個(gè)參數(shù) user纪隙,方法內(nèi)的邏輯基本沒變僵朗,只不過操作的東西都變成user的字段叁鉴。這樣就可以為每個(gè)用戶創(chuàng)建一個(gè)Conn單例泊脐。

這個(gè)方法有點(diǎn)泛型的意思了恩尾,當(dāng)然不是泛型。

有范圍單例的另一個(gè)示例:在ylog中需要每小時(shí)創(chuàng)建一個(gè)日志文件用于記錄當(dāng)前小時(shí)的日志麸拄,在每個(gè)小時(shí)只需創(chuàng)建并打開這個(gè)文件一次派昧。

先看看Logger的定義(這里省略和創(chuàng)建單例無關(guān)的內(nèi)容。):

type FileLogger struct {
    lastHour int64
    file     *os.File
    mu       sync.Mutex
    ...
}

lastHour是記錄的小時(shí)數(shù)拢切,如果當(dāng)前小時(shí)數(shù)不等于記錄的小時(shí)數(shù)蒂萎,則說明應(yīng)該創(chuàng)建新的文件,這個(gè)變量類似于sync.Once中的done字段淮椰。

file是打開的文件實(shí)例五慈。

mu是創(chuàng)建文件實(shí)例時(shí)需要加的鎖。

下邊看一下打開文件的方法:

func (l *FileLogger) ensureFile() (err error) {
    curTime := time.Now()
    curHour := getTimeHour(curTime)
    if atomic.LoadInt64(&l.lastHour) != curHour {
        return l.ensureFileSlow(curTime, curHour)
    }

    return
}

func (l *FileLogger) ensureFileSlow(curTime time.Time, curHour int64) (err error) {
    l.mu.Lock()
    defer l.mu.Unlock()
    if l.lastHour != curHour {
        defer func() {
            if r := recover(); r == nil {
                atomic.StoreInt64(&l.lastHour, curHour)
            }
        }()
        l.createFile(curTime, curHour)
    }
    return
}

這里模仿sync.Once中的處理方法主穗,有兩點(diǎn)主要的不同:數(shù)值比較不再是0和1泻拦,而是每個(gè)小時(shí)都會(huì)變化的數(shù)字;增加了對(duì)panic的處理忽媒。如果打開文件失敗争拐,則還會(huì)再次嘗試打開文件。

要查看完整的代碼請(qǐng)?jiān)L問Github:https://github.com/bosima/ylog/tree/1.0

雙檢鎖的性能

從原理上分析晦雨,雙檢鎖的性能要好過互斥鎖架曹,因?yàn)榛コ怄i每次都要加鎖;不使用原子操作的雙檢鎖要比使用原子操作的雙檢鎖好一些闹瞧,畢竟原子操作也是有些成本的绑雄。那么實(shí)際差距是多少呢?

這里做一個(gè)Benchmark Test奥邮,還是處理上文的Conn結(jié)構(gòu)體万牺,為了方便測(cè)試,定義一個(gè)上下文:

type Context struct {
    done uint32
    c    *Conn
    mu   sync.Mutex
}

編寫三個(gè)用于測(cè)試的方法:

func ensure_unsafe_dcl(context *Context) {
    if context.done == 0 {
        context.mu.Lock()
        defer context.mu.Unlock()
        if context.done == 0 {
            defer func() { context.done = 1 }()
            context.c = newConn()
        }
    }
}

func ensure_dcl(context *Context) {
    if atomic.LoadUint32(&context.done) == 0 {
        context.mu.Lock()
        defer context.mu.Unlock()
        if context.done == 0 {
            defer atomic.StoreUint32(&context.done, 1)
            context.c = newConn()
        }
    }
}

func ensure_mutex(context *Context) {
    context.mu.Lock()
    defer context.mu.Unlock()
    if context.done == 0 {
    defer func() { context.done = 1 }()
        context.c = newConn()
    }
}

這三個(gè)方法分別對(duì)應(yīng)不安全的雙檢鎖洽腺、使用原子操作的安全雙檢鎖和每次都加互斥鎖脚粟。它們的作用都是確保Conn結(jié)構(gòu)體的實(shí)例存在,如果不存在則創(chuàng)建蘸朋。

使用的測(cè)試方法都是下面這種寫法珊楼,按照計(jì)算機(jī)邏輯處理器的數(shù)量并行運(yùn)行測(cè)試方法:

func BenchmarkInfo_DCL(b *testing.B) {
    context := &Context{}
    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            ensure_dcl(context)
            processConn(context.c)
        }
    })
}

先看一下Benchmark Test的結(jié)果:

benchmark-test-for-double-checked-locking

可以看到使用雙檢鎖相比每次加鎖的提升是兩個(gè)數(shù)量級(jí),這是正常的度液。

而不安全的雙檢鎖和使用原子操作的安全雙檢鎖時(shí)間消耗相差無幾厕宗,為什么呢?

主要原因是這里寫只有1次堕担,剩下的全是讀已慢。即使使用了原子操作,絕大部分情況下CPU讀數(shù)據(jù)的時(shí)候也不用在多個(gè)核心之間同步(鎖總線霹购、鎖緩存等)佑惠,只需要讀緩存就可以了。這也從一個(gè)方面證明了雙檢鎖模式的意義齐疙。

另外上文提到過Go讀寫超過一個(gè)機(jī)器字的變量時(shí)是非原子的膜楷,那如果讀寫只有1個(gè)機(jī)器字呢?在64位機(jī)器上讀寫int64本身就是原子操作贞奋,也就是說讀寫應(yīng)該都只需1次操作赌厅,不管用不用atomic方法。這可以在編譯器文檔或者CPU手冊(cè)中驗(yàn)證轿塔。(Reference:https://preshing.com/20130618/atomic-vs-non-atomic-operations/

不過這兩個(gè)分析不是說我們使用原子操作沒有意義特愿,不安全雙檢鎖的執(zhí)行結(jié)果是沒有Go語言規(guī)范保證的,上邊的結(jié)果只是在特定編譯器勾缭、特定平臺(tái)下的基準(zhǔn)測(cè)試結(jié)果揍障,不同的編譯器、CPU俩由,甚至不同版本的Go都不知道會(huì)出什么幺蛾子毒嫡,運(yùn)行的效果也就無法保證。我們不得不考慮程序的可移植性幻梯。


以上就是本文主要內(nèi)容兜畸,如有問題歡迎反饋。完整代碼已經(jīng)上傳到Github礼旅,歡迎訪問:https://github.com/bosima/go-demo/tree/main/double-check-locking

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末膳叨,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子痘系,更是在濱河造成了極大的恐慌菲嘴,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汰翠,死亡現(xiàn)場(chǎng)離奇詭異龄坪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)复唤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門健田,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人佛纫,你說我怎么就攤上這事妓局∽芊牛” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵好爬,是天一觀的道長(zhǎng)局雄。 經(jīng)常有香客問我,道長(zhǎng)存炮,這世上最難降的妖魔是什么炬搭? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮穆桂,結(jié)果婚禮上宫盔,老公的妹妹穿的比我還像新娘。我一直安慰自己享完,他們只是感情好灼芭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著驼侠,像睡著了一般姿鸿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上倒源,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天苛预,我揣著相機(jī)與錄音,去河邊找鬼笋熬。 笑死热某,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的胳螟。 我是一名探鬼主播昔馋,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼糖耸!你這毒婦竟也來了秘遏?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤嘉竟,失蹤者是張志新(化名)和其女友劉穎邦危,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舍扰,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡倦蚪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了边苹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陵且。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖脓钾,靈堂內(nèi)的尸體忽然破棺而出售睹,到底是詐尸還是另有隱情,我是刑警寧澤可训,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站捶枢,受9級(jí)特大地震影響握截,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜烂叔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一蒜鸡、第九天 我趴在偏房一處隱蔽的房頂上張望胯努。 院中可真熱鬧,春花似錦逢防、人聲如沸叶沛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽溉箕。三九已至,卻和暖如春悦昵,著一層夾襖步出監(jiān)牢的瞬間肴茄,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工但指, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留寡痰,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓枚赡,卻偏偏與公主長(zhǎng)得像氓癌,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子贫橙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

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