Java高并发系列——检视阅读(三)

Java高并发系列——ReentrantLock

ReentrantLock重入锁

synchronized的局限性

synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由jvm实现,用户不需要显示的释放锁,非常方便,然而synchronized也有一定的局限性,例如:

  1. 当线程尝试获取锁的时候,如果获取不到锁会一直阻塞,这个阻塞的过程,用户无法控制。(Synchronized不可中断的说法:只有获取到锁之后才能中断,等待锁时不可中断。
  2. 如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待。(synchronized不能响应中断?)

synchronized不能响应中断参考

ReentrantLock

ReentrantLock是Lock的默认实现,在聊ReentranLock之前,我们需要先弄清楚一些概念:

  1. 可重入锁:可重入锁是指同一个线程可以多次获得同一把锁;ReentrantLock和关键字Synchronized都是可重入锁
  2. 可中断锁:可中断锁是指线程在获取锁的过程中,是否可以响应线程中断操作。synchronized是不可中断的,ReentrantLock是可中断的
  3. 公平锁和非公平锁:公平锁是指多个线程尝试获取同一把锁的时候,获取锁的顺序按照线程到达的先后顺序获取,而不是随机插队的方式获取。synchronized是非公平锁,而ReentrantLock是两种都可以实现,不过默认是非公平锁。

ReentrantLock基本使用

ReentrantLock的使用过程:

  1. 创建锁:ReentrantLock lock = new ReentrantLock();
  2. 获取锁:lock.lock()
  3. 释放锁:lock.unlock();

对比上面的代码,与关键字synchronized相比,ReentrantLock锁有明显的操作过程,开发人员必须手动的指定何时加锁,何时释放锁,正是因为这样手动控制,ReentrantLock对逻辑控制的灵活度要远远胜于关键字synchronized,上面代码需要注意lock.unlock()一定要放在finally中,否则若程序出现了异常,锁没有释放,那么其他线程就再也没有机会获取这个锁了。

ReentrantLock是可重入锁

假如ReentrantLock是不可重入的锁,那么同一个线程第2次获取锁的时候由于前面的锁还未释放而导致死锁,程序是无法正常结束的。

  1. lock()方法和unlock()方法需要成对出现,锁了几次,也要释放几次,否则后面的线程无法获取锁了;可以将add中的unlock删除一个事实,上面代码运行将无法结束
  2. unlock()方法放在finally中执行,保证不管程序是否有异常,锁必定会释放

示例:

public class ReentrantLockTest {
    private static int num = 0;
    private static Lock lock = new ReentrantLock();
    public static void add() {
        lock.lock();
        lock.lock();
        try {
            num++;
        } finally {
            //lock()方法和unlock()方法需要成对出现,锁了几次,也要释放几次,否则后面的线程无法获取锁
            lock.unlock();
            lock.unlock();
        }
    }
    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                ReentrantLockTest.add();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        T t3 = new T("t3");
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println("get num =" + num);
    }
}
//输出: get num =3000

ReentrantLock实现公平锁

在大多数情况下,锁的申请都是非公平的。这就好比买票不排队,上厕所不排队。最终导致的结果是,有些人可能一直买不到票。而公平锁,它会按照到达的先后顺序获得资源。公平锁的一大特点是不会产生饥饿现象,只要你排队,最终还是可以等到资源的;synchronized关键字默认是有jvm内部实现控制的,是非公平锁。而ReentrantLock运行开发者自己设置锁的公平性,可以实现公平和非公平锁。

看一下jdk中ReentrantLock的源码,2个构造方法:

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

默认构造方法创建的是非公平锁

第2个构造方法,有个fair参数,当fair为true的时候创建的是公平锁,公平锁看起来很不错,不过要实现公平锁,系统内部肯定需要维护一个有序队列,因此公平锁的实现成本比较高,性能相对于非公平锁来说相对低一些。因此,在默认情况下,锁是非公平的,如果没有特别要求,则不建议使用公平锁。

示例:

public class ReentrantLockFairTest {
    private static int num = 0;
    //private static Lock lock = new ReentrantLock(false);
    private static Lock lock = new ReentrantLock(true);
    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName()+" got lock");
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        T t3 = new T("t3");
        t1.start();
        t2.start();
        t3.start();
    }
}

输出:

公平锁:
t1 got lock                          
t1 got lock
t2 got lock
t2 got lock
t3 got lock
t3 got lock
非公平锁:
t1 got lock
t3 got lock
t3 got lock
t2 got lock
t2 got lock
t1 got lock

ReentrantLock获取锁的过程是可中断的——使用lockInterruptibly()和tryLock(long time, TimeUnit unit)有参方法时。

对于synchronized关键字,如果一个线程在等待获取锁,最终只有2种结果:

  1. 要么获取到锁然后继续后面的操作
  2. 要么一直等待,直到其他线程释放锁为止

而ReentrantLock提供了另外一种可能,就是在等的获取锁的过程中(发起获取锁请求到还未获取到锁这段时间内)是可以被中断的,也就是说在等待锁的过程中,程序可以根据需要取消获取锁的请求。拿李云龙平安县围点打援来说,当平安县城被拿下后,鬼子救援的部队再尝试救援已经没有意义了,这时候要请求中断操作。

关于获取锁的过程中被中断,注意几点:

  1. ReentrankLock中必须使用实例方法 lockInterruptibly()获取锁时,在线程调用interrupt()方法之后,才会引发 InterruptedException异常
  2. 线程调用interrupt()之后,线程的中断标志会被置为true
  3. 触发InterruptedException异常之后,线程的中断标志有会被清空,即置为false
  4. 所以当线程调用interrupt()引发InterruptedException异常,中断标志的变化是:false->true->false

实例:

public class InterruptTest2 {
    private static ReentrantLock lock1 = new ReentrantLock();
    private static ReentrantLock lock2 = new ReentrantLock();

    public static class T1 extends Thread {
        int lock;

        public T1(String name, Integer lock) {
            super(name);
            this.lock = lock;
        }

        @Override
        public void run() {
            try {
                if (lock == 1) {
                    lock1.lockInterruptibly();
                    TimeUnit.SECONDS.sleep(1);
                    lock2.lockInterruptibly();
                } else {
                    lock2.lockInterruptibly();
                    TimeUnit.SECONDS.sleep(1);
                    lock1.lockInterruptibly();
                }
            } catch (InterruptedException e) {
                //线程发送中断信号触发InterruptedException异常之后,中断标志将被清空。
                System.out.println(this.getName() + "中断标志:" + this.isInterrupted());
                e.printStackTrace();
            } finally {
                //ReentrantLock自有的方法,多态实现的Lock不能用
                if (lock1.isHeldByCurrentThread()) {
                    lock1.unlock();
                }
                if (lock2.isHeldByCurrentThread()) {
                    lock2.unlock();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        T1 t1 = new T1("thread1", 1);
        T1 t2 = new T1("thread2", 2);
        t1.start();
        t2.start();
        TimeUnit.SECONDS.sleep(1000);
        //不加interrupt()通过jstack查看线程堆栈信息,发现2个线程死锁了
        //"thread2":
        //  waiting for ownable synchronizer 0x000000076b782028, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
        //  which is held by "thread1"
        //"thread1":
        //  waiting for ownable synchronizer 0x000000076b782058, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
        //  which is held by "thread2"
        t1.interrupt();
    }
}

ReentrantLock锁申请等待限时

ReentrantLock刚好提供了这样功能,给我们提供了获取锁限时等待的方法 tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。

tryLock无参方法——tryLock()是立即响应的,中间不会有阻塞。

看一下源码中tryLock方法:

public boolean tryLock()
tryLock有参方法

该方法在指定的时间内不管是否可以获取锁,都会返回结果,返回true,表示获取锁成功,返回false表示获取失败。 此方法在执行的过程中,如果调用了线程的中断interrupt()方法,会触发InterruptedException异常。

可以明确设置获取锁的超时时间:

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException

关于tryLock()方法和tryLock(long timeout, TimeUnit unit)方法,说明一下:

  1. 都会返回boolean值,结果表示获取锁是否成功。
  2. tryLock()方法,不管是否获取成功,都会立即返回;而有参的tryLock方法会尝试在指定的时间内去获取锁,中间会阻塞的现象,在指定的时间之后会不管是否能够获取锁都会返回结果。
  3. tryLock()方法不会响应线程的中断方法;而有参的tryLock方法会响应线程的中断方法,而出发 InterruptedException异常,这个从2个方法的声明上可以可以看出来。

ReentrantLock其他常用的方法

  1. isHeldByCurrentThread:实例方法,判断当前线程是否持有ReentrantLock的锁,上面代码中有使用过。
获取锁的4种方法对比
获取锁的方法 是否立即响应(不会阻塞) 是否响应中断
lock() × ×
lockInterruptibly() ×
tryLock() ×
tryLock(long timeout, TimeUnit unit) ×

实例:

public class ReentrantLockTest1 {
    private static ReentrantLock lock1 = new ReentrantLock();

    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            try {
                System.out.println(this.getName()+"尝试获取锁");
                if (lock1.tryLock(2,TimeUnit.SECONDS)){
                    System.out.println(this.getName()+"获取锁成功");
                    TimeUnit.SECONDS.sleep(3);
                }else {
                    System.out.println(this.getName()+"获取锁失败");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if(lock1.isHeldByCurrentThread()){
                    lock1.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        t1.start();
        t2.start();

    }
}

输出:

lock1.tryLock()
t1尝试获取锁
t1获取锁成功
t2尝试获取锁
t2获取锁失败
lock1.tryLock(2,TimeUnit.SECONDS)
t1尝试获取锁
t2尝试获取锁
t1获取锁成功
t2获取锁失败

总结

  1. ReentrantLock可以实现公平锁和非公平锁
  2. ReentrantLock默认实现的是非公平锁
  3. ReentrantLock的获取锁和释放锁必须成对出现,锁了几次,也要释放几次
  4. 释放锁的操作必须放在finally中执行
  5. lockInterruptibly()实例方法可以响应线程的中断方法,调用线程的interrupt()方法时,lockInterruptibly()方法会触发 InterruptedException异常
  6. 关于 InterruptedException异常说一下,看到方法声明上带有 throwsInterruptedException,表示该方法可以相应线程中断,调用线程的interrupt()方法时,这些方法会触发 InterruptedException异常,触发InterruptedException时,线程的中断中断状态会被清除。所以如果程序由于调用 interrupt()方法而触发 InterruptedException异常,线程的标志由默认的false变为ture,然后又变为false
  7. 实例方法tryLock()获会尝试获取锁,会立即返回,返回值表示是否获取成功
  8. 实例方法tryLock(long timeout, TimeUnit unit)会在指定的时间内尝试获取锁,指定的时间内是否能够获取锁,都会返回,返回值表示是否获取锁成功,该方法会响应线程的中断
疑问

Q:可中断锁:可中断锁时线程在获取锁的过程中,是否可以相应线程中断操作。为什么synchronized是不可中断的,ReentrantLock是可中断的?

首先,只有获取到锁之后才能中断,等待锁时不可中断。

查看Thread.interrupt()源码发现,这里面的操作只是做了修改一个中断状态值为true,并没有显式声明抛出InterruptedException异常。因此:

  • 若线程被中断前,如果该线程处于非阻塞状态(未调用过wait,sleep,join方法),那么该线程的中断状态将被设为true, 除此之外,不会发生任何事。
  • 若线程被中断前,该线程处于阻塞状态(调用了wait,sleep,join方法),那么该线程将会立即从阻塞状态中退出,并抛出一个InterruptedException异常,同时,该线程的中断状态被设为false, 除此之外,不会发生任何事。

所以说,Synchronized锁此时为轻量级锁或重量级锁,此时等待线程是在自旋运行或者已经是重量级锁导致的阻塞状态了(非调用了wait,sleep,join等方法的阻塞),只把中断状态设为true,没有抛出异常真正中断。

ReentrantLock.lockInterruptibly()首次尝试获取锁之前就会判断是否应该中断,如果没有获取到锁,在自旋等待的时候也会继续判断中断状态。(代码里会判断中断状态,所有会响应中断。)

synchronized不能响应中断参考

JUC中的Condition对象

Condition使用简介——实现等待/通知机制

注意:在使用使用Condition.await()方法时,需要先获取Condition对象关联的ReentrantLock的锁;就像使用Object.wait()时必须在synchronized同步代码块内。

从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同:

  1. Condition能够支持不响应中断,而通过使用Object方式不支持
  2. Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个
  3. Condition能够支持超时时间的设置,而Object不支持

Condition由ReentrantLock对象创建,并且可以同时创建多个,Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁,之后调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用Condition的signal()方法唤醒线程,使用方式和wait()、notify()类似。

需要注意的时,当一个线程被signal()方法唤醒线程时,它第一个动作是去获取同步锁,注意这一点,而这把锁目前在调用signal()方法唤醒他的线程上,必须等其释放锁后才能得到争抢锁的机会。

实例:

public class ConditionTest {

    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();

    public static void main(String[] args) {
        T1 t1 = new T1("TT1");
        T2 t2 = new T2("TT2");
        t1.start();
        t2.start();
    }

    static class T1 extends Thread {
        public T1(String name) {
            super(name);
        }

        @Override
        public void run() {
            lock.lock();
            System.out.println(this.getName() + " start");
            try {
                System.out.println(this.getName() + " wait");
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            System.out.println(this.getName() + " end");
        }
    }

    static class T2 extends Thread {
        public T2(String name) {
            super(name);
        }

        @Override
        public void run() {
            lock.lock();
            System.out.println(this.getName() + " start");
            System.out.println(this.getName() + " signal");
            condition.signal();
            System.out.println(this.getName() + " end");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.getName() + " end,2 second later");
        }
    }
}

输出:

TT1 start
TT1 wait
TT2 start
TT2 signal
TT2 end
TT2 end,2 second later

Condition常用方法

和Object中wait类似的方法

  1. void await() throws InterruptedException:当前线程进入等待状态,如果在等待状态中被中断会抛出被中断异常;
  2. long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时
  3. boolean await(long time, TimeUnit unit) throws InterruptedException:同第二种,支持自定义时间单位,false:表示方法超时之后自动返回的,true:表示等待还未超时时,await方法就返回了(超时之前,被其他线程唤醒了)
  4. boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间
  5. void awaitUninterruptibly();:当前线程进入等待状态,不会响应线程中断操作,只能通过唤醒的方式让线程继续

和Object的notify/notifyAll类似的方法

  1. void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。
  2. void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程

Condition.await()过程中被打断

调用condition.await()之后,线程进入阻塞中,调用t1.interrupt(),给t1线程发送中断信号,await()方法内部会检测到线程中断信号,然后触发 InterruptedException异常,线程中断标志被清除。从输出结果中可以看出,线程t1中断标志的变换过程:false->true->false

await(long time, TimeUnit unit)超时之后自动返回

t1线程等待2秒之后,自动返回继续执行,最后await方法返回false,await返回false表示超时之后自动返回

await(long time, TimeUnit unit)超时之前被唤醒

t1线程中调用 condition.await(5,TimeUnit.SECONDS);方法会释放锁,等待5秒,主线程休眠1秒,然后获取锁,之后调用signal()方法唤醒t1,输出结果中发现await后过了1秒(1、3行输出结果的时间差),await方法就返回了,并且返回值是true。true表示await方法超时之前被其他线程唤醒了。

long awaitNanos(long nanosTimeout)超时返回

t1调用await方法等待5秒超时返回,返回结果为负数,表示超时之后返回的

//awaitNanos参数为纳秒,可以调用TimeUnit中的一些方法将时间转换为纳秒。
long nanos = TimeUnit.SECONDS.toNanos(2);

waitNanos(long nanosTimeout)超时之前被唤醒

t1中调用await休眠5秒,主线程休眠1秒之后,调用signal()唤醒线程t1,await方法返回正数,表示返回时距离超时时间还有多久,将近4秒,返回正数表示,线程在超时之前被唤醒了。

其他几个有参的await方法和无参的await方法一样,线程调用interrupt()方法时,这些方法都会触发InterruptedException异常,并且线程的中断标志会被清除。

同一个锁支持创建多个Condition

使用两个Condition来实现一个阻塞队列的例子:

public class MyBlockingQueue<E> {

    //阻塞队列最大容量
    private int size;
    //队列底层实现
    private LinkedList<E> list = new LinkedList<>();
    private static Lock lock = new ReentrantLock();
    //队列满时的等待条件
    private static Condition fullFlag = lock.newCondition();
    //队列空时的等待条件
    private static Condition emptyFlag = lock.newCondition();

    public MyBlockingQueue(int size) {
        this.size = size;
    }

    public void enqueue(E e) throws InterruptedException {
        lock.lock();
        try {
            //队列已满,在fullFlag条件上等待
            while (list.size() == size) {
                fullFlag.await();
            }
            //入队:加入链表末尾
            list.add(e);
            System.out.println("生产了" + e);
            //通知在emptyFlag条件上等待的线程
            emptyFlag.signal();
        } finally {
            lock.unlock();
        }
    }


    public E dequeue() throws InterruptedException {
        lock.lock();
        try {
            while (list.size() == 0) {
                emptyFlag.await();
            }
            E e = list.removeFirst();
            System.out.println("消费了" + e);
            //通知在fullFlag条件上等待的线程
            fullFlag.signal();
            return e;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 创建了一个阻塞队列,大小为3,队列满的时候,会被阻塞,等待其他线程去消费,队列中的元素被消费之后,会唤醒生产者,生产数据进入队列。上面代码将队列大小置为1,可以实现同步阻塞队列,生产1个元素之后,生产者会被阻塞,待消费者消费队列中的元素之后,生产者才能继续工作。
     * @param args
     */
    public static void main(String[] args) {
        MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(1);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            Thread producer = new Thread(() -> {
                try {
                    queue.enqueue(finalI);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            producer.start();
        }
        for (int i = 0; i < 10; i++) {
            Thread consumer = new Thread(() -> {
                try {
                    queue.dequeue();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            consumer.start();
        }
    }
}

输出:

生产了0
消费了0
生产了1
消费了1
。。。。
生产了9
消费了9

Object的监视器方法与Condition接口的对比

注意同步队列和等待队列的区别,同步队列表示在竞争一把锁的队列中,是处于阻塞或运行状态的队列。

而等待队列是指被置为等待、超时等待状态的线程,这些是没有竞争锁的权限的,处于等待被唤醒的状态中。

对比项 Object 监视器方法 Condition
前置条件 获取对象的锁 调用Lock.lock获取锁,调用Lock.newCondition()获取Condition对象
调用方式 直接调用,如:object.wait() 直接调用,如:condition.await()
等待队列个数 一个 多个,使用多个condition实现
当前线程释放锁并进入等待状态 支持 支持
当前线程释放锁进入等待状态中不响应中断 不支持 支持
当前线程释放锁并进入超时等待状态 支持 支持
当前线程释放锁并进入等待状态到将来某个时间 不支持 支持
唤醒等待队列中的一个线程 支持 支持
唤醒等待队列中的全部线程 支持 支持

总结

  1. 使用condition的步骤:创建condition对象,获取锁,然后调用condition的方法
  2. 一个ReentrantLock支持创建多个condition对象
  3. void await() throws InterruptedException;方法会释放锁,让当前线程等待,支持唤醒,支持线程中断
  4. void awaitUninterruptibly();方法会释放锁,让当前线程等待,支持唤醒,不支持线程中断
  5. long awaitNanos(longnanosTimeout)throws InterruptedException;参数为纳秒,此方法会释放锁,让当前线程等待,支持唤醒,支持中断。超时之后返回的,结果为负数;超时之前被唤醒返回的,结果为正数(表示返回时距离超时时间相差的纳秒数)
  6. boolean await (longtime,TimeUnitunit)throws InterruptedException;方法会释放锁,让当前线程等待,支持唤醒,支持中断。超时之后返回的,结果为false;超时之前被唤醒返回的,结果为true
  7. boolean awaitUntil(Datedeadline)throws InterruptedException;参数表示超时的截止时间点,方法会释放锁,让当前线程等待,支持唤醒,支持中断。超时之后返回的,结果为false;超时之前被唤醒返回的,结果为true
  8. void signal();会唤醒一个等待中的线程,然后被唤醒的线程会被加入同步队列,去尝试获取锁
  9. void signalAll();会唤醒所有等待中的线程,将所有等待中的线程加入同步队列,然后去尝试获取锁
疑问:

Q:Condition能够支持超时时间的设置,而Object不支持。Object不是有wait(long timeout)超时时间设置么?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354