AQS源碼分析

鎖的分類
悲觀鎖和樂觀鎖

在Java里使用的各種鎖,幾乎全都是悲觀鎖。synchronized從偏向鎖、輕量級鎖到重量級鎖,全是悲觀鎖。JDK提供的Lock實現類全是悲觀鎖。其實只要有“鎖對象”出現,那么就一定是悲觀鎖。因為樂觀鎖不是鎖,而是一個在循環里嘗試CAS的算法。

樂觀鎖是atomic包下的原子類。

公平鎖、非公平鎖

多個線程申請一把公平鎖,那么當鎖釋放的時候,先申請的先得到,非常公平。顯然如果是非公平鎖,后申請的線程可能先獲取到鎖,是隨機或者按照其他優先級排序的。

公平鎖與非公平鎖的區別在代碼中的區別為:

final void lock() {
    if (this.compareAndSetState(0, 1)) {
        this.setExclusiveOwnerThread(Thread.currentThread());
    } else {
        this.acquire(1);
    }
}

就是非公平鎖在調用lock()時,先嘗試插隊直接去拿鎖,更改state狀態為1,如果成功則把Owner線程設置為當前線程,則表示成功獲得鎖,沒獲得鎖則acquire,而公平鎖直接acquire。

偏向鎖 → 輕量級鎖 → 重量級鎖

初次執行到synchronized代碼塊的時候,鎖對象變成偏向鎖(通過CAS修改對象頭里的鎖標志位),字面意思是“偏向于第一個獲得它的線程”的鎖。執行完同步代碼塊后,線程并不會主動釋放偏向鎖。當第二次到達同步代碼塊時,線程會判斷此時持有鎖的線程是否就是自己(持有鎖的線程ID也在對象頭里),如果是則正常往下執行。由于之前沒有釋放鎖,這里也就不需要重新加鎖。如果自始至終使用鎖的線程只有一個,很明顯偏向鎖幾乎沒有額外開銷,性能極高。

一旦有第二個線程加入鎖競爭,偏向鎖就升級為輕量級鎖(自旋鎖)。在輕量級鎖狀態下繼續鎖競爭,沒有搶到鎖的線程將自旋,即不停地循環判斷鎖是否能夠被成功獲取。獲取鎖的操作,其實就是通過CAS修改對象頭里的鎖標志位。先比較當前鎖標志位是否為“釋放”,如果是則將其設置為“鎖定”,比較并設置是原子性發生的。這就算搶到鎖了,然后線程將當前鎖的持有者信息修改為自己。

長時間的自旋操作是非常消耗資源的,一個線程持有鎖,其他線程就只能在原地空耗CPU,執行不了任何有效的任務,這種現象叫做忙等(busy-waiting)。如果多個線程用一個鎖,但是沒有發生鎖競爭,或者發生了很輕微的鎖競爭,那么synchronized就用輕量級鎖,允許短時間的忙等現象。這是一種折衷的想法,短時間的忙等,換取線程在用戶態和內核態之間切換的開銷。

此忙等是有限度的(有個計數器記錄自旋次數,默認允許循環10次,可以通過虛擬機參數更改)。如果鎖競爭情況嚴重,某個達到最大自旋次數的線程,會將輕量級鎖升級為重量級鎖(依然是CAS修改鎖標志位,但不修改持有鎖的線程ID)。當后續線程嘗試獲取鎖時,發現被占用的鎖是重量級鎖,則直接將自己掛起(而不是忙等),等待將來被喚醒。

獨占鎖與共享鎖

獨占鎖:隊首持鎖,喚醒隊二后tryAcquire嘗試拿鎖,隊三及以后休眠

共享鎖:相較于獨占模式只喚醒隊二 ,共享模式還喚醒所有mode=shared節點

獨占和共享是對于加鎖而言(能否多線程同時獲鎖),釋放鎖時沒有獨占和共享的概念。

怎么實現同步

根據參考文章1,提出了4中方式實現同步:

1:自旋實現同步

缺點:耗費cpu資源。沒有競爭到鎖的線程會一直占用cpu資源進行cas操作

2:yield+自旋實現同步

優點:要解決自旋鎖的性能問題必須讓競爭鎖失敗的線程不空轉,而是在獲取不到鎖的時候能把cpu資源給讓出來,yield()方法就能讓出cpu資源,當線程競爭鎖失敗時,會調用yield方法讓出cpu。

缺點:自旋+yield的方式并沒有完全解決問題,當系統只有兩個線程競爭鎖時,yield是有效的。需要注意的是該方法只是當前讓出cpu,有可能操作系統下次還是選擇運行該線程

3:sleep+自旋方式實現同步

缺點:sleep的時間怎設置

4:park+自旋方式實現同步

volatile int status=0;
Queue parkQueue;//集合 數組  list

void lock(){
    while(!compareAndSet(0,1)){
        //
        park();
    }
    //lock    10分鐘
   unlock()
}

void unlock(){
    lock_notify();
}

void park(){
    //將當期線程加入到等待隊列
    parkQueue.add(currentThread);
    //將當期線程釋放cpu  阻塞
    releaseCpu();
}
void lock_notify(){
    //得到要喚醒的線程頭部線程
    Thread t=parkQueue.header();
    //喚醒等待線程
    unpark(t);
}
ReentrantLock源碼分析

而我們的ReentrantLock就是采用第四種方法實現的。

首先來看一段代碼

public class MyRunnable implements Runnable {
    private int num = 0;
    private ReentrantLock lock = new ReentrantLock(true);
    @Override
    public void run() {
        while (num < 20){
            lock.lock();
            try{
                num++;
                Log.e("zzf",Thread.currentThread().getName() + "獲得鎖,num is"+ num);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
}

初始化鎖實例

首先來看構造函數,

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

有兩種方式,無參的默認new 一個非公平鎖NonfairSync。

在我們上面的例子中,調用了第二個構造函數,并且傳入的true,所以使用的是公平鎖。(后續都以公平鎖進行分析)

lock.lock()
public void lock() {
    sync.lock();
}

final void lock() {
    acquire(1);
}

public final void acquire(int arg) {
   if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
       selfInterrupt();
}

調用lock()時,首先調用acquire(),對于acquire()分兩部分來講。

tryAcquire

這個方法是嘗試去獲取鎖。

protected final boolean tryAcquire(int var1) {
    Thread var2 = Thread.currentThread();
    int var3 = this.getState();
    if (var3 == 0) {
        if (!this.hasQueuedPredecessors() && this.compareAndSetState(0, var1)) {
            this.setExclusiveOwnerThread(var2);
                return true;
        }
    } else if (var2 == this.getExclusiveOwnerThread()) {
        int var4 = var3 + var1;
        if (var4 < 0) {
            throw new Error("Maximum lock count exceeded");
         }

        this.setState(var4);
        return true;
    }
        return false;
    }
}

要想知道上面代碼的意思,我們需要先了解一下AQS的一個靜態內部類Node;

static final class Node {
    static final AbstractQueuedSynchronizer.Node SHARED = new AbstractQueuedSynchronizer.Node();//表示共享模式
    static final AbstractQueuedSynchronizer.Node EXCLUSIVE = null;//表示獨占模式
    static final int CANCELLED = 1;//因為超時或者中斷,結點會被設置為取消狀態,被取消狀態的結點不應該去競爭鎖,只能保持取消狀態不變,不能轉換為其他狀態。處于這種狀態的結點會被踢出隊列,被GC回收;
    static final int SIGNAL = -1;//表示這個結點的繼任結點被阻塞了,到時需要通知它
    static final int CONDITION = -2;//表示這個結點在條件隊列中,因為等待某個條件而被阻塞;
    static final int PROPAGATE = -3;//使用在共享模式頭結點有可能牌處于這種狀態,表示鎖的下一次獲取可以無條件傳播;
    volatile int waitStatus;//用來表示上面4種狀態
    volatile AbstractQueuedSynchronizer.Node prev;//前一個節點
    volatile AbstractQueuedSynchronizer.Node next;//后一個節點
    volatile Thread thread;//表示線程
    Node nextWaiter;//條件隊列才使用。
}

getState()得到的是一個int類型的volatile int state;被volatile修飾。默認為0,表示無鎖狀態,1表示上鎖狀態,>1表示重入。

當第一個線程進來,此時getState()為0,所以進入第一個if中。

public final boolean hasQueuedPredecessors() {
    AbstractQueuedSynchronizer.Node var1 = this.tail;
    AbstractQueuedSynchronizer.Node var2 = this.head;
    AbstractQueuedSynchronizer.Node var3;
    return var2 != var1 && ((var3 = var2.next) == null || var3.thread != Thread.currentThread());
}

該方法是判斷是否需要排隊。

在這需要分三種情況討論:

1:隊列沒有初始化

當隊列沒有初始化的時候,var1 = null,var2 = null.所以hasQueuedPredecessors()返回false。在第一個if里面進行取反,則表示不需要入隊列處理,直接CAS進行加鎖操作。并把當前線程設置成Node的thread。然后返回true,而此時在acquire()中又是取反,則不會走if里面的代碼。從這里可以看出,其實加鎖就是讓獲取到這個鎖的線程能夠順暢的往下執行邏輯。

2:隊列初始化且>1個節點

在隊列初始化的時候,此時會new一個Node,放在最前面,而在隊列的第二才是真正排隊的第一個,而new 出來的那個Node可以理解為第一個拿到鎖的那個的占位。當大于一個節點的時候var2 != var1為true,因為var2最開始為null,現在>1個節點,表示var2.next是有值的,所以(var3 = var2.next) == null為false。

var3.thread != Thread.currentThread())為true時表示前面已經有人在排隊,那我就必須要排隊。所以hasQueuedPredecessors()返回true。此時則會跳出第一個if。tryAcquire返回flase,則需在acquire()判斷acquireQueued(addWaiter(Node.EXCLUSIVE), arg)在后面講解。

var3.thread != Thread.currentThread()為flase時,則不需要排隊,最簡單的理解是前面排隊的是自己的女朋友,那她買票等價于我買票了,就不需要排隊了。

3:隊列初始化只有一個節點

當只有一個節點的時候,相當于就只有初始化隊列的時候new的那個Node。那此時隊首和隊尾就是它自己,但是它又是不算排隊的那個,只是算正在持有鎖的那個線程在隊列中的一個占位而已。

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

當處于上面講的第二種情況時,就會執行這段代碼。addWaiter()是將當前線程封裝成Node,然后添加到AQS隊列中。

private Node addWaiter(Node mode) {
    Node node = new Node(mode);

    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            U.putObject(node, Node.PREV, oldTail);
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                return node;
            }
        } else {
            initializeSyncQueue();
        }
    }
}

將當前的線程封裝成Node并且mode為獨占鎖。

然后進行一個無限循環,第一次oldTail為null,則進入到else中,進行初始化。第二次進來的時候,oldTail則不為null,將當前線程Node的prev節點指向tail,然后通過cas將node加入AQS隊列,成功之后,把舊的tail的next指向新的tail。所以AQS并不是第一的時候就進行初始化。

final boolean acquireQueued(final Node node, int arg) {
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}

首先獲取前一個節點,只有當前節點的前一個節點是head,表示當前節點在隊列中是第二個元素。AQS是根據FIFO來的,所以當前一個釋放了鎖,只有隊列中的第二個才能去獲取鎖。如果索取鎖成功,則吧當前的節點設置為head。并把前一個head斷開。因為為head的那個node的node.head 和node.thread都是為null,只需要把node.next置為null時,就斷開了鏈表的鏈接。

如果獲取鎖失敗,則根據waitStatus決定是否需要掛起線程。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}

如果前一個節點的waitStatus是SIGNAL,表明當前的的線程需要被unpark。

如果waitStatus > 0,則表明是cancel狀態,則從前節點開始逐步循環找到下一個沒有被cancel的節點設置為當前節點的前節點。目的將cancel狀態的節點踢除掉。如果是其他的狀態,將前節點改成SIGNAL狀態。當返回true時,繼續走下面代碼。

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

意思就是通過park()將當前線程掛起到WATING狀態。

lock.unlock()
public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

執行unLock的時候,會執行release();

protected final boolean tryRelease(int var1) {
    int var2 = this.getState() - var1;
    if (Thread.currentThread() != this.getExclusiveOwnerThread()) {
        throw new IllegalMonitorStateException();
    } else {
        boolean var3 = false;
        if (var2 == 0) {
            var3 = true;
            this.setExclusiveOwnerThread((Thread)null);
        }

        this.setState(var2);
        return var3;
    }
}

getState() -1表示可能有重入的,getState()可能>1,只有當var2 ==0時,把當前線程設置為null,并釋放鎖,返回true。

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        node.compareAndSetWaitStatus(ws, 0);
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node p = tail; p != node && p != null; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
    }
        if (s != null)
            LockSupport.unpark(s.thread);
}

該方法是真正的釋放鎖,傳入的是head節點,當前線程被釋放后,需要喚醒下一個節點的線程。

這里是從隊列尾部向前遍歷找到最前面的一個waitStatus小于0的節點。如果從前面向后遍歷會導致死循環。

如果獲取到head的next節點不為空,則釋放許可喚醒線程。

Condition

在上面講的是AQS的同步隊列,除了這個同步隊列外,還有一個條件隊列。

加入條件隊列的前提是,當前線程已經拿到了鎖,并處于運行狀態,加入條件隊列后,就需要釋放鎖,進入阻塞狀態,同時喚醒同步隊列的隊二拿鎖。喚醒操作是將一個node從條件隊列,移動到同步隊列的隊尾,讓它返回同步隊列park,并不是隨機就喚醒一個線程。

流程

1、創建節點并添加到條件隊列的尾部。

2、釋放線程所持有的鎖。

3、判斷是否在同步隊列中,在的話,進行CAS拿鎖操作,不在的話,喚醒進入同步隊列的尾部。

public class ConditionDemo {

    private int queueSize = 10;
    private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);

    private Lock lock = new ReentrantLock();
    private Condition full = lock.newCondition();
    private Condition empty = lock.newCondition();

    class Consumer implements Runnable{

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

        private void consume() {
            while (true){
                lock.lock();
                try {
                    while (queue.size() == 0){
                        try {
                            System.out.println("隊列空,等待數據");
                            empty.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.poll();
                    full.signal();
                    System.out.println("從隊列取走一個元素,隊列剩余"+queue.size()+"個元素");
                }finally {
                    lock.unlock();
                }
            }
        }
    }

    class Producer implements Runnable{

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

        private void produce() {
            while (true){
                lock.lock();
                try {
                    while(queue.size()== queueSize){
                        try {
                            System.out.println("隊列滿,等待有空余空間");
                            full.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.offer(1);
                    empty.signal();
                }finally {
                    lock.unlock();
                }
            }

        }
    }
}

上面是利用Condition實現生產者消費者模式,當隊列的size為0的時候,empty調用await().

休眠
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
        if (node.nextWaiter != null) // clean up if cancelled
            unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
}

在ConditionObject中,包含了兩個成員對象,一個表示隊列的第一個node,一個表示隊列的最后一個node。

private transient Node firstWaiter;
private transient Node lastWaiter;


private Node addConditionWaiter() {
    Node t = lastWaiter;
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }

    Node node = new Node(Node.CONDITION);

    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

addConditionWaiter就是創建一CONDITION狀態的個node,如果隊列中沒有,則把這個新創建的賦值給首節點,如果有,則賦值給當前節點的后一個。并且這個也是最后一個節點。條件隊列也是先進先出的形式,只不過是單鏈表的形式。

final int fullyRelease(Node node) {
    try {
        int savedState = getState();
        if (release(savedState))
            return savedState;
        else    
            throw new IllegalMonitorStateException();
    } catch (Throwable t) {
        node.waitStatus = Node.CANCELLED;
        throw t;
    }
}

首先獲取當前的state,然后調用release方法進行釋放鎖。

在第三篇參考博客寫到,fullyRelease會一次性釋放所有的鎖,不管重入多少次。我理解的是重入的時候,還是同一個線程,同一把鎖。所以釋放的只是一把鎖吧。

經過上面的代碼之后,節點已經放到條件隊列并且釋放了所持有的鎖,而后需要掛起。但是在前面說了,掛起需要不在同步隊列,所以需要判斷是否在同步隊列。

final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    if (node.next != null) // If has successor, it must be on queue
        return true;
    return findNodeFromTail(node);
}

private boolean findNodeFromTail(Node node) {
    for (Node p = tail;;) {
        if (p == node)
            return true;
        if (p == null)
            return false;
        p = p.prev;
    }
}

當節點的狀態為Node.CONDITION或者node.prev == null時,則不在同步隊列,node.prev,node.next在條件隊列中沒有用到。如果含有這個,則表示是在同步隊列中。

private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
               trail.nextWaiter = next;
            if (next == null)
                lastWaiter = trail;
            }else
                trail = t;
                t = next;
        }
    }

如果狀態不是CONDITION,就會自動刪除。

喚醒
public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
        Node first = firstWaiter;
        if (first != null)
            doSignal(first);
}

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
            first.nextWaiter = null;
        } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
        }

首先判斷是不是同一個線程,不是則拋出異常。然后獲取等待隊列的頭結點,后續的操作都是基于該節點。

final boolean transferForSignal(Node node) {
    if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
        return false;

    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

private Node enq(Node node) {
    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            U.putObject(node, Node.PREV, oldTail);
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                return oldTail;
            }
        } else {
            initializeSyncQueue();
        }
    }
}

enq()是將該節點移入到同步隊列中去。

signal是只會對等待隊列的頭結點進行操作,而signalAll則是對每一個節點都移入到同步隊列中。

同步隊列與條件隊列完整配合如下圖所示:

1608883350(1).png
參考文獻

https://blog.csdn.net/java_lyvee/article/details/98966684

https://segmentfault.com/a/1190000017372067

https://segmentfault.com/a/1190000020345054

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

推薦閱讀更多精彩內容