一、锁的种类

  • 偏向锁 / 轻量级锁 / 重量级锁
    1. 偏向锁
      如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好
    2. 轻量级锁
      JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞
    3. 重量级锁
      重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态
  • 可重入锁 / 非可重入锁
    1. 可重入锁
      指的是线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁
      最典型的就是 ReentrantLock 了 reentrant 的意思就是可重入,它也是 Lock 接口最主要的一个实现类
    2. 非可重入锁
      指的是虽然线程当前持有了这把锁,但是如果想再次获取这把锁,也必须要先释放锁后才能再次尝试获取
  • 共享锁 / 独占锁
    1. 共享锁
      指的是同一把锁,可以被多个线程同时获得,例如:读
    2. 独占锁
      指的是这把锁只能同时被一个线程获得,例如:写
  • 公平锁 / 非公平锁
    1. 公平锁
      公平的含义在于如果线程现在拿不到这把锁 那么线程就都会进入等待,开始排队 在等待队列里等待时间长的线程会优先拿到 这把锁,有先来先得的意思
    2. 非公平锁
      会在一定情况下忽略掉已经在排队的线程,发生插队现象
  • 悲观锁 / 乐观锁
    1. 悲观锁

      • 解释:在获取资源之前必须先拿到锁,以便达到“独占”的状态。当前线程在操作资源的时候,其他线程由于不能拿到锁,所以其他线程不能来影响我
      • 案例:
        1. synchronized 关键字和 Lock 接口
          Java 中悲观锁的实现包括 synchronized 关键字和 Lock 相关类等,例如 Lock 的实现类 ReentrantLock,类中的 lock() 等方法就是执行加锁,而 unlock() 方法是执行解锁处理资源之前必须要先加锁并拿到锁,等到处理完了之后再解开锁,这就是非常典型的悲观锁思想
        2. 在 MySQL 选择 select for update 语句
          在提交之前不允许第三方来修 改该数据,这当然会造成一定的性能损耗,在高并发的情况下是不可取的
    2. 乐观锁

      • 解释:并不要求在获取资源前拿到锁,也不会锁住资源,相反,乐观锁利用 CAS 理念,在不独占资源的情况下,完成了对资源的修改
      • 案例:
        1. 原子类
          乐观锁的典型案例就是原子类 例如 AtomicInteger 在更新数据时,就使用了乐观锁的思想,多个线程可以同时操作同一个原子变量
        2. 在 MySQL 获取及修改数据时都不需要加锁,但是我们在获取完数据并计算完毕,准备更新数据时,会检查版本号和获取数据时的版本号是否 一致,如果一致就直接更新,如果不一致,说明计算期间已经有其他线程修改过这个数据了,那 我就可以选择重新获取数据,重新计算,然后再次尝试更新数据
    3. 注意
      有一种说法认为:悲观锁由于它的操作比较重量级,不能多个线程并行执行,而且还会有上下文切换等动作,所以悲观锁的性能不如乐观锁好,应该尽量避免用悲观锁
      这种说法是不正确的,因为虽然悲观锁确实会让得不到锁的线程阻塞,但是这种开销是固定的,悲观锁的原始开销确实要高于乐观锁,但是特点是一劳永逸,就算一直拿不到锁,也不会对开销造成额外的影响。
      反观乐观锁虽然一开始的开销比悲观锁小,但是如果一直拿不到锁,或者并发量大,竞争激烈,导致不停重试,那么消耗的资源也会越来越多,甚至开销会超过悲观锁。所以,同样是悲观锁,在不同的场景下,效果可能完全不同

    4. 两种锁各自的使用场景

      • 悲观锁
        适用于并发写入多,临界区代码复杂,竞争激烈等场景。这种场景下悲观锁可以避免大量的无用的反复尝试等消耗
      • 乐观锁
        适用于大部分是读取,少部分是修改的场景。也适合虽然读写都很多 但是并发并不激烈的场景,在这些场景下,乐观锁不加锁的特点能让性能大幅提高
  • 自旋锁 / 非自旋锁
    1. 自旋锁
      如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为 “自旋”,就像是线程在“自我旋转”
    2. 非自旋锁
      没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等
  • 可中断锁 / 不可中断锁
    1. 可中断锁
      ReentrantLock 是一种典型的可中断锁,例如使用 lockInterruptibly 方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情,不需要一直傻等到获取到锁才离开
    2. 不可中断锁
      在 Java 中 synchronized 关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路,只能等到拿到锁以后才能进行其他的逻辑处理

二、获取和释放 monitor 锁的时机

线程在进入被 synchronized 保护的代码块之前,会自动获取锁,并且无论是正常路径退出,还是通过抛出异常退出,在退出的时候都会自动释放锁

看一组伪代码

public synchronized void method() {
    method body
}

等同于

public void method()  {
    this.intrinsicLock.lock();
    try {
        method body
    } finally {
        this.intrinsicLock.unlock();
    }
}

进入 method 方法后,立刻添加内置锁并 且用 try 代码块把方法保护起来,最后用 finally 释放这把锁

  • 用 javap 命令查看反汇编的结果
    public class SynTest { 
        public void synBlock()  { 
            synchronized (this) { 
                System.out.println("test"); 
            } 
        } 
    }
    
    1. 首先用 cd 命令切换到 SynTest.java 类所在的路径
    2. 然后执行 javac SynTest.java,于是就会产生一 个名为 SynTest.class 的字节码文件
    3. 然后我们执行 javap -verbose SynTest.class就 可以看到对应的反汇编内容
    4. 1个monitorenter,2个monitorexit
      因为JVM必须保证一个 monitorenter 必须对应一个 monitorexit
      这里的 monitorenter 插入的是方法的开始处,而 monitorexit 需要插入的是方法结束处以及异常处,这样才能保证程序即便抛了异常,也能释放锁
    5. monitorenter
      执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:
      • 如果该 monitor 的计数器为 0,则线程获得该 monitor 并将其计数设置为 1,然后该线程就是 这个 monitor 的所有者
      • 如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数
      • 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变 成为 0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor
    6. monitorexit
      monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止,代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁。所以,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权

三、synchronized 和 Lock 异同

  • 相同点
    1. synchronized 和 Lock 都是用来保护资源线程安全的
    2. 都可以保证可见性
    3. synchronized 和 ReentrantLock 都拥有可重入的特点
  • 不同点
    1. 用法不同
      • synchronized 关键字可以加在方法上,不需要指定锁对象(此时的锁对象为 this) 也可以新建一个同步代码块并且自定义 monitor 锁对象
      • Lock 接口必须显示用 Lock 锁对象开始加锁 lock() 和解锁 unlock(),并且一般会在 finally 块中确保用 unlock() 来解锁,以防发生死锁
    2. 解锁顺序不同
      • Lock 接口可以不按照加锁顺序,来反向解锁,比如
        先加锁1再加锁2,然后先解锁1再解锁2
         lock1.lock(); 
         lock2.lock(); 
         ... 
         lock1.unlock(); 
         lock2.unlock();
        
      • synchronized 解锁顺序必须和加锁顺序完全相反
        synchronized(obj1) { 
            synchronized(obj2) { 
                ... 
            } 
        }
        
    3. 类型不同
      • synchronized 是 【不可中断锁】【独占锁】
      • ReentrantLock 是 【可中断锁】【共享锁】
    4. 原理区别
      • synchronized 是内置锁,由 JVM 实现获取锁和释放锁的原理,还分为偏向锁、轻量级锁、重量级锁
      • Lock根据实现不同会有不同的原理,比如 ReentrantLock 是根据AQS来实现获取和释放锁的
    5. 是否可以设置公平/非公平
      ReentrantLock 等 Lock 实现类可以根据自己的需要来设置公平或非公平,synchronized 则不能设置

四、Lock 中的常用方法

  • look()方法
    在线程获取锁时如果锁已被其他线程获取,则进行等待,是最初级的获取锁的方法,lock() 方法不能被中断,这会带来很大的隐患:一旦陷入死锁,lock() 就会陷入永久等 待,所以一般我们用 tryLock() 等其他更高级的方法来代替 lock()
    Lock lock = ...; 
    lock.lock(); 
    try {
        //获取到了被本锁保护的资源,处理任务 //捕获异常 
    } finally { 
        lock.unlock(); //释放锁 
    }
    
    1. 创建了一个 Lock,并且用 lock 方法加 锁
    2. 然后立刻在 try 代码块中进行相关业务 逻辑的处理
    3. 如果有需要还可以进行 catch 来捕获异常
    4. 最重要的是 finally,一定不要忘记在 finally 中添加 unlock() 方法,以便保 障锁的绝对释放
  • tryLock()
    tryLock() 用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回 true 否则返回 false,代表获取锁失败。相比于 lock(),这样的方法显然功能更强大,我们可以根据是否能获取到锁来决定后续程序的行为
    Lock lock = ...; 
    if (lock.tryLock()) { 
        try {
            //处理任务 
        } finally { 
            lock.unlock(); //释放锁 
        } 
    } else { 
        //如果不能获取锁,则做其他事情 
    }
    
    1. 创建 lock() 方法之后使用 tryLock() 方法并用 if 语句判断它的结果
    2. 如果 if 语句返回 true,就使用 try finally 完成相关业务逻辑的处理
    3. 如果 if 语句返回 false 就会进入 else 语句代表它暂时不能获取到锁,可以先去做一些其他事情
  • tryLock(long time, TimeUnit unit)
    tryLock() 的重载方法是 tryLock(long time, TimeUnit unit) ,和 tryLock() 很类似,区别在于 tryLock(long time, TimeUnit unit) 方法会有一个超时时间,在拿不到锁时会等待一定的时间,如果在时间期限结束后,还获取不到锁,就会返回 false 如果一开始就获取锁或者等待期间内获取到锁,则返回 true
  • lockInterruptibly()
    一句话总结:除非当前线程在获取锁期间被中断,否则便会一直尝试获取直到获取到为止
    lockInterruptibly() 是可以响应中断的,相比于不能响应中断的 synchronized 锁 lockInterruptibly() 可以让程序更灵活,可以在获取锁的同时,保持对中断的响应,可以把这个方法理解为超时时间是无穷长的 tryLock(long time, TimeUnit unit),因为 tryLock(long time, TimeUnit unit) 和 lockInterruptibly() 都能响应中断,只不过 lockInterruptibly() 永远不会超时。lockInterruptibly() 响应中断的之后,也会抛出InterruptedException异常
    public void lockInterruptibly() { 
        try { 
            lock.lockInterruptibly(); 
            try { 
                System.out.println("操作资源"); 
            } finally { 
                lock.unlock(); 
            } 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
    }
    
    1. 首先执行了 lockInterruptibly 方法, 并且对它进行了 try catch 包装
    2. 然后同样假设能够获取到这把锁,和之前 一样,就必须要使用 try finall 来保障 锁的绝对释放
  • unlock()
    用于解锁,方法比较简单 对于 ReentrantLock 而言,执行 unlock() 的时候,内部会把锁的“被持有计数器”减 1,直到减到 0,就代表当前这把锁已经完全释放了。如果减 1 后计数器不为 0,说明这把锁之前被“重入”了 那么锁并没有真正释放,仅仅是减少了持有的次数

五、为何要选择非公平锁

ReentrantLock默认就是非公平的,如果要让它变成公平锁,则在new构造器的时候需要传入参数true
注意:这里的非公平并不是指完全的随机,不是说线程可以任意插队,而是仅仅“在合适的时机”插队

  • 合适的时机
    • 假设当前线程在请求获取锁的时候,恰巧前一个持有锁的线程释放了这把锁 那么当前申请锁的线程就可以不顾已经等待的线程而选择立刻插队 但是如果当前线程请求的时候,前一个线程并没有在那一时刻释放锁 那么当前线程还是一样会进入等待队列。
    • 假设线程 A 持有一把锁,线程 B 请求这把锁,由于线程 A 已经持有这把锁了,所以线程 B 会陷入等待,在等待的时候线程 B 会被挂起,也就是进入阻塞状态,那么当线程 A 释放锁的时候,本该轮到线程 B 苏醒获取锁,但如果此时突然有一个线程 C 插队请求这 把锁,那么根据非公平的策略,会把这把锁给线程 C 这是因为唤醒线程 B 是需要很大开销的,很有可能在唤醒之前,线程 C 已经拿到了这把锁并且执行完 任务释放了这把锁。
    • 相比于等待唤醒线程 B 的漫长过程,插队的行为会让线程 C 本身跳过陷入阻塞的过程,如果在锁代码中执行的内容不多的话,线程 C 就可以很快完成任务,并且在线程 B 被完全唤醒之前就把这个锁交出去,这样是一个双赢的局面,对于线程 C 而言,不需要等待提高了它的效率 而对于线程 B 而言,它获得锁的时间并没有推迟,因为等它被唤醒的时候,线程 C 早就释放锁了,因为线程 C 的执行速度相比于线程 B 的唤醒速度,是很快的。所以 Java 设计者设计非公平锁,是为了提高整体的运行效率。
  • 对比公平和非公平的优缺点
    1. 公平锁
      • 优势:各线程公平平等 每个线程在等待一段时间后 总有执行的机会
      • 劣势:更慢,吞吐量更小
    2. 非公平锁
      • 优势:更快,吞吐量更大
      • 劣势:有可能产生线程饥饿 也就是某些线程在长时间内 始终得不到执行
  • 源码分析
    ReentrantLock 内部有个Sync继承自AQS,他自己又有两个子类分别是
    公平锁 FairSync 和非公平锁 NonfairSync,公平锁会去先判断等待队列里面是否有任务,而非公平锁不会
    public class ReentrantLock implements Lock, java.io.Serializable { 
        private static final long serialVersionUID = 7373984872572414699L; 
        /** Synchronizer providing all implementation mechanics */ 
        private final Sync sync;
    }
    
    abstract static class Sync extends AbstractQueuedSynchronizer { ... 
    }
    
    static final class NonfairSync extends Sync {...} 
    static final class FairSync extends Sync {...}
    
    protected final boolean tryAcquire(int acquires) { 
        final Thread current = Thread.currentThread(); 
        int c = getState(); 
        if (c == 0) { 
            if (!hasQueuedPredecessors() && //这里判断了 hasQueuedPredecessors() 
                   compareAndSetState(0, acquires)) { 
                setExclusiveOwnerThread(current); 
                return true; 
            } 
        } else if (current == getExclusiveOwnerThread()) { 
            int nextc = c + acquires; 
            if (nextc < 0) { 
                throw new Error("Maximum lock count exceeded"); 
            } 
            setState(nextc); 
            return true; 
        } 
        return false; 
     }
    
    final boolean nonfairTryAcquire(int acquires) { 
        final Thread current = Thread.currentThread(); 
        int c = getState(); 
        if (c == 0) { 
            //这里 没有判断 hasQueuedPredecessors()
            if (compareAndSetState(0, acquires)) {  
                setExclusiveOwnerThread(current); 
                return true; 
            } 
        } else if (current == getExclusiveOwnerThread()) { 
            int nextc = c + acquires;
            // overflow 
            if (nextc < 0) throw new Error("Maximum lock count exceeded"); 
            setState(nextc); 
            return true; 
        } 
        return false; 
    }
    
    有一个特例需要注意,针对 tryLock() 方法,它不遵守设定的公平原则 例如当有线程执行 tryLock() 方法的时候,一旦有线程释放了锁,那么这个正在 tryLock 的线程 就能获取到锁,即使设置的是公平锁模式,即使在它之前已经有其他正在等待队列中等待的线程,简单地说就是 tryLock 可以插队
    public boolean tryLock() { 
        return sync.nonfairTryAcquire(1); 
    }
    
    这里调用的就是 nonfairTryAcquire(),表明了是不公平的,和锁本身是否是公平锁无关

六、读写锁 ReadWriteLock

在没有读写锁之前,我们假设使用普通的 ReentrantLock,那么虽然我们保证了线程安全 但是也浪费了一定的资源,因为如果多个读操作同时进行,其实并没有线程安全问题 我们可以允许让多个读操作并行,以便提高程序效率。

  • 读写锁的获取规则
    1. 如果有一个线程已经占用了读锁,则此时其他线程如果要申请读锁,可以申请成功
    2. 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁,因为读写不能同时操作
    3. 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,都必须等待之前的线程释放写锁,同样也因为读写不能同时,并且两个线程不应该同时写
  • 读写锁适用场合
    相比于 ReentrantLock 适用于一般场合
    ReadWriteLock 适用于读多写少的情况,合理使用可以进一步提高并发效率
  • 读锁插队策略
    1. 公平锁
    ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
    final boolean writerShouldBlock() { return hasQueuedPredecessors(); }  
    final boolean readerShouldBlock() { return hasQueuedPredecessors(); }
    
    只要等待队列中有线程在等待,也就是 hasQueuedPredecessors() 返回 true 的时候,那么 writer 和 reader 都会 block,也就是一律不允许插队
    1. 非公平锁
    ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
    final boolean writerShouldBlock() { return false; } 
    final boolean readerShouldBlock() { return apparentlyFirstQueuedIsExclusive(); }
    
    在 writerShouldBlock() 这个方法中始终返回 false,可以看出对于想获取写锁的线程而言,由于返回值是 false,所以它是随时可以插队的。读锁插队的条件必须是等待队列头结点不是"写"任务。
  • 锁的升降级
    只能从写锁降级为读锁,不能从读锁升级为写锁
    1. 为什么不能升级
      假设线程 A 和 B 都想升级到写锁,那么对于线程 A 而言,它需要等待其他所有线程,包括线程 B 在内释放读锁,而线程 B 也需要等待所有的线程,包括线程 A 释放读锁,至此就造成了死锁
    2. 为什么需要降级
      我们对于写锁写任务之后的数据如果仅仅是读取,还一直使用写锁的话,就不能让多个线 程同时来读取了,持有写锁是浪费资源的,降低了整体的效率,所以这个时候利用锁的降级是很好的办法,可以提高整体性能

七、自旋锁

  • 自旋锁的好处
    自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。
  • AtomicLong 的实现
    public finallong getAndlncrement() { 
        return unsafe.getAndAddLong(this, valueOffset,iL);
    }
    
    可以看到它调用了一个 unsafe.getAndAddLong 所以我们再来看这个方法:
    public final long getAndAddLong(Objectvarl,longvar2,longvar4) {
        long var6; 
        do {
          Var6 = this.getLongVolatile(varl.var2);
        } while (!this.compareAndSwapLong(varl,var2,var6,var6+var4));
        return var6;
    }
    
  • 缺点
    1. 在避免线程切换开销的同时也带来了新的开销
    2. 随着时间的增加,后期甚至会超过线程切换的开销
  • 适用场景
    并发度不是特别高的场景;临界区比较短小的情况

八、JVM 对锁进行了哪些优化?

  • 自适应的自旋锁
    在 JDK 1.6 中引入了自适应的自旋锁来解决长时间自旋的问题 自适应意味着自旋的时间不再固定,而是会根据最近自旋尝试的成功率、失败率 以及当前锁的拥有者的状态等多种因素来共同决定 自旋的持续时间是变化的,自旋锁变“聪明”了
    比如:如果最近尝试自旋获取某一把锁成功了,那么下一次可能还会继续使用自旋,并且允许自旋更长的时间。但是如果最近自旋获取某一把锁失败了,那么可能会省略掉自旋的过程,以便减少无用的自旋,提高效率
  • 锁消除
    @Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }
    
    这个方法是被 synchronized 修饰的同步方法因为它可能会被多个线程同时使用,但是在大多数情况下,它只会在一个线程内被使用,如果编译器能确定这个StringBuffer 对象只会在一个线程内被使用就代表肯定是线程安全的,那么我们的编译器便会做出优化,把对应的 synchronized 给消除,省去加锁和解锁的操作,以便增加整体的效率
  • 锁粗化
    public void lockCoarsening() { 
        synchronized (this) { 
            //do something 
        }
        synchronized (this) { 
            //do something 
        }
        synchronized (this) { 
            //do something 
        }
    }
    
    释放了锁,紧接着什么都没做,又重新获取锁,这种情况下JVM会帮我们将锁扩大化,但是如果外面是一层for循环的话,则不会帮我们进行锁粗话,因为这就会导致其他线程长时间无法获得锁,所以这里的锁粗化不适用于循环的场景,仅 适用于非循环的场景
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容