ThreadLocal三兄弟

一、三兄弟

ThreadLocal:线程本地变量,维护当前线程内变量,不可以其他线程共享

InheritableThreadLocal(可继承的):维护当前线程以及子线程变量,可共享,一个线程修改对象引用所有线程的值都发生改变,有线程安全问题,通过Thread.init()方法初始化,不适用线程池模式

TransmittableThreadLocal(可传输的):维护变量可在多线程间共享,适用于线程池

二、原理分析

1.Thread

https://www.jianshu.com/p/b7d2958c5168

2.InheritableThreadLocal

使用

public class InheritableThreadLocalTest {

    private static ThreadLocal<Integer> tl = new InheritableThreadLocal<>();
    private static ThreadLocal<Hello> tl2 = new InheritableThreadLocal<>();

    public static void main(String[] args) throws Exception {
        tl.set(1);

        Hello hello = new Hello();
        hello.setName("init");
        tl2.set(hello);
        System.out.printf("当前线程名称: %s, main方法内获取线程内数据为: tl = %s,tl2.name = %s%n",
                Thread.currentThread().getName(), tl.get(), tl2.get().getName());
        fc();
        new Thread(() -> {
            Hello hello1 = tl2.get();
            hello1.setName("init2");
            fc();
        }).start();
        Thread.sleep(1000L); //保证下面fc执行一定在上面异步代码之后执行
        fc(); //继续在主线程内执行,验证上面那一步是否对主线程上下文内容造成影响
    }

    private static void fc() {
        System.out.printf("当前线程名称: %s, fc方法内获取线程内数据为: tl = %s,tl2.name = %s%n",
                Thread.currentThread().getName(), tl.get(), tl2.get().getName());
    }
}


@Data
class Hello {

    private String name;
    
}

输出

当前线程名称: main, main方法内获取线程内数据为: tl = 1,tl2.name = init
当前线程名称: main, fc方法内获取线程内数据为: tl = 1,tl2.name = init
当前线程名称: Thread-0, fc方法内获取线程内数据为: tl = 1,tl2.name = init2
当前线程名称: main, fc方法内获取线程内数据为: tl = 1,tl2.name = init2

说明主线程和子线程维护的引用变量指向同一个对象

原理
InheritableThreadLocal是ThreadLocal子类

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * Computes the child's initial value for this inheritable thread-local
     * variable as a function of the parent's value at the time the child
     * thread is created.  This method is called from within the parent
     * thread before the child is started.
     * <p>
     * This method merely returns its input argument, and should be overridden
     * if a different behavior is desired.
     *
     * @param parentValue the parent thread's value
     * @return the child thread's initial value
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

调用InheritableThreadLocal.set()方法是调用父类ThreadLocal.set()

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

调用getMap()方法,调用的是InheritableThreadLocal.getMap()方法。变量inheritableThreadLocals是Thread的一个属性

ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
 }



/*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

inheritableThreadLocals属性在子线程初始化的时候默认会指向父线程的引用:this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            // 这一行代码将当前线程的引用指向父线程的引用
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

所以在子线程初始化的时候就会拿到父线程的变量。但是这样会有一个问题,如果变量是只读的还可以,如果有修改操作就会出现线程安全问题。所以就引出了InheritableThreadLocal的子类TransmittableThreadLocal

3.TransmittableThreadLocal

使用

public class TransmittableThreadLocalTest {


    // 需要注意的是,使用TTL的时候,要想传递的值不出问题,线程池必须得用TTL加一层代理(下面会讲这样做的目的)
    private static final ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));

    //这里采用TTL的实现
    private static final ThreadLocal<Integer> tl = new TransmittableThreadLocal<>();

    public static void main(String[] args) {

        new Thread(() -> {

            String mainThreadName = "main_01";

            tl.set(1);

            executorService.execute(() -> {
                sleep(1L);
                System.out.printf("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s%n", mainThreadName, Thread.currentThread().getName(), tl.get());
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.printf("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s%n", mainThreadName, Thread.currentThread().getName(), tl.get());
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.printf("本地变量改变之前(1), 父线程名称-%s, 子线程名称-%s, 变量值=%s%n", mainThreadName, Thread.currentThread().getName(), tl.get());
            });

            sleep(3L); //确保上面的会在tl.set执行之前执行
            tl.set(2); // 等上面的线程池第一次启用完了,父线程再给自己赋值

            executorService.execute(() -> {
                sleep(1L);
                System.out.printf("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s%n", mainThreadName, Thread.currentThread().getName(), tl.get());
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.printf("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s%n", mainThreadName, Thread.currentThread().getName(), tl.get());
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.printf("本地变量改变之后(2), 父线程名称-%s, 子线程名称-%s, 变量值=%s%n", mainThreadName, Thread.currentThread().getName(), tl.get());
            });

            System.out.printf("线程名称-%s, 变量值=%s%n", Thread.currentThread().getName(), tl.get());

        }).start();


        new Thread(() -> {

            String mainThreadName = "main_02";

            tl.set(3);

            executorService.execute(() -> {
                sleep(1L);
                System.out.printf("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s%n", mainThreadName, Thread.currentThread().getName(), tl.get());
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.printf("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s%n", mainThreadName, Thread.currentThread().getName(), tl.get());
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.printf("本地变量改变之前(3), 父线程名称-%s, 子线程名称-%s, 变量值=%s%n", mainThreadName, Thread.currentThread().getName(), tl.get());
            });

            sleep(1L); //确保上面的会在tl.set执行之前执行
            tl.set(4); // 等上面的线程池第一次启用完了,父线程再给自己赋值

            executorService.execute(() -> {
                sleep(1L);
                System.out.printf("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s%n", mainThreadName, Thread.currentThread().getName(), tl.get());
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.printf("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s%n", mainThreadName, Thread.currentThread().getName(), tl.get());
            });

            executorService.execute(() -> {
                sleep(1L);
                System.out.printf("本地变量改变之后(4), 父线程名称-%s, 子线程名称-%s, 变量值=%s%n", mainThreadName, Thread.currentThread().getName(), tl.get());
            });

            System.out.printf("线程名称-%s, 变量值=%s%n", Thread.currentThread().getName(), tl.get());

        }).start();
        
    }

    private static void sleep(long time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果

本地变量改变之前(3), 父线程名称-main_02, 子线程名称-pool-1-thread-2, 变量值=3
线程名称-Thread-1, 变量值=2
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-pool-1-thread-1, 变量值=1
线程名称-Thread-2, 变量值=4
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-pool-1-thread-2, 变量值=1
本地变量改变之前(3), 父线程名称-main_02, 子线程名称-pool-1-thread-1, 变量值=3
本地变量改变之前(1), 父线程名称-main_01, 子线程名称-pool-1-thread-2, 变量值=1
本地变量改变之前(3), 父线程名称-main_02, 子线程名称-pool-1-thread-1, 变量值=3
本地变量改变之后(4), 父线程名称-main_02, 子线程名称-pool-1-thread-2, 变量值=4
本地变量改变之后(4), 父线程名称-main_02, 子线程名称-pool-1-thread-1, 变量值=4
本地变量改变之后(4), 父线程名称-main_02, 子线程名称-pool-1-thread-2, 变量值=4
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-pool-1-thread-1, 变量值=2
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-pool-1-thread-2, 变量值=2
本地变量改变之后(2), 父线程名称-main_01, 子线程名称-pool-1-thread-1, 变量值=2

两个主线程里都使用线程池异步,而且值在主线程里还发生过改变,测试结果展示一切正常,由此可以知道TTL在使用线程池的情况下,也可以很好的完成传递,而且不会发生错乱。

原理分析
TransmittableThreadLocal的一个重要属性holder

/**
     * 是一个ITL类型的对象,持有一个全局的WeakMap(weakMap的key是弱引用,同TL一样,也是为了解决内存泄漏的问题),里面存放了TTL对象
     * 并且重写了initialValue和childValue方法,尤其是childValue,可以看到在即将异步时父线程的属性是直接作为初始化值赋值给子线程的本地变量对象(TTL)的
     */
private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder = new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
        protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
            return new WeakHashMap();
        }

        protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
            return new WeakHashMap(parentValue);
        }
    };

再看一下get()和set()

Override
    public final void set(T value) {
        super.set(value);
        if (null == value) removeValue();
        else addValue();
    }

    @Override
    public final T get() {
        T value = super.get();
        if (null != value) addValue();
        return value;
    }
    
    private void removeValue() {
        holder.get().remove(this); //从holder持有的map对象中移除
    }

    private void addValue() {
        if (!holder.get().containsKey(this)) {
            holder.get().put(this, null); //从holder持有的map对象中添加
        }
    }

上边例子中使用TransmittableThreadLocal需要通过TtlExecutors.getTtlExecutorService包装一下线程池才可以,那么,下面就来看看在程序即将通过线程池异步的时候,TTL帮我们做了哪些操作(这一部分是TTL支持线程池传递的核心部分):

// 此方法属于线程池包装类ExecutorTtlWrapper
@Override
    public void execute(@Nonnull Runnable command) {
        //这里会把Rannable包装一层,这是关键,有些逻辑处理,需要在run之前执行
        executor.execute(TtlRunnable.get(command)); 
    }

    // 对应上面的get方法,返回一个TtlRunnable对象,属于TtLRannable包装类
    @Nullable
    public static TtlRunnable get(@Nullable Runnable runnable) {
        return get(runnable, false, false);
    }

    // 对应上面的get方法
    @Nullable
    public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) {
        if (null == runnable) return null;

        // 若发现已经是目标类型了(说明已经被包装过了)直接返回
        if (runnable instanceof TtlEnhanced) { 
            // avoid redundant decoration, and ensure idempotency
            if (idempotent) return (TtlRunnable) runnable;
            else throw new IllegalStateException("Already TtlRunnable!");
        }
        return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun); //最终初始化
    }

    // 对应上面的TtlRunnable方法
    private TtlRunnable(@Nonnull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        //这里将捕获后的父线程本地变量存储在当前对象的capturedRef里
        this.capturedRef = new AtomicReference<Object>(capture()); 
        this.runnable = runnable;
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }

    // 对应上面的capture方法,用于捕获当前线程(父线程)里的本地变量,此方法属于TTL的静态内部类Transmitter
    @Nonnull
    public static Object capture() {
        Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();
        for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) { // holder里目前存放的k-v里的key,就是需要传给子线程的TTL对象
            captured.put(threadLocal, threadLocal.copyValue());
        }
        return captured; // 这里返回的这个对象,就是当前将要使用线程池异步出来的子线程,所继承的本地变量合集
    }

    // 对应上面的copyValue,简单的将TTL对象里的值返回(结合之前的源码可以知道get方法其实就是获取当前线程(父线程)里的值,调用super.get方法)
    private T copyValue() {
        return copy(get());
    }
    protected T copy(T parentValue) {
        return parentValue;
    }

结合上述代码,其实就是把当前父线程里的本地变量取出来,然后赋值给Rannable包装类里的capturedRef属性,接下来大概率会在run方法里,将这些捕获到的值赋给子线程的holder赋对应的TTL值,那么我们继续往下看Rannable包装类里的run方法是怎么实现的:

//run方法属于Rannable的包装类TtlRunnable
@Override
    public void run() {
        Object captured = capturedRef.get(); // 获取由之前捕获到的父线程变量集
        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }

        /**
         * 重点方法replay,此方法用来给当前子线程赋本地变量,返回的backup是此子线程原来就有的本地变量值(原生本地变量,下面会详细讲),
         * backup用于恢复数据(如果任务执行完毕,意味着该子线程会归还线程池,那么需要将其原生本地变量属性恢复)
         */
        Object backup = replay(captured);
        try {
            runnable.run(); // 执行异步逻辑
        } finally {
            // 结合上面对于replay的解释,不难理解,这个方法就是用来恢复原有值的
            restore(backup); 
        }
    }

TTL在异步任务执行前,会先进行赋值操作(就是拿着异步发生时捕获到的父线程的本地变量,赋给自己),当任务执行完,就恢复原生的自己本身的线程变量值。

下面来具体看这俩方法:

//下面的方法均属于TTL的静态内部类Transmittable

@Nonnull
    public static Object replay(@Nonnull Object captured) {
        @SuppressWarnings("unchecked")
        Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map<TransmittableThreadLocal<?>, Object>) captured; //使用此线程异步时捕获到的父线程里的本地变量值
        Map<TransmittableThreadLocal<?>, Object> backup = new HashMap<TransmittableThreadLocal<?>, Object>(); //当前线程原生的本地变量,用于使用完线程后恢复用

        //注意:这里循环的是当前子线程原生的本地变量集合,与本方法相反,restore方法里循环这个holder是指:该线程运行期间产生的变量+父线程继承来的变量
        for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
             iterator.hasNext(); ) {
            Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
            TransmittableThreadLocal<?> threadLocal = next.getKey();

            backup.put(threadLocal, threadLocal.get()); // 所有原生的本地变量都暂时存储在backup里,用于之后恢复用

            /**
             * 检查,如果捕获到的线程变量里,不包含当前原生变量值,则从当前原生变量里清除掉,对应的线程本地变量也清掉
             * 这就是为什么会将原生变量保存在backup里的原因,为了恢复原生值使用
             * 那么,为什么这里要清除掉呢?因为从使用这个子线程做异步那里,捕获到的本地变量并不包含原生的变量,当前线程
             * 在做任务时的首要目标,是将父线程里的变量完全传递给任务,如果不清除这个子线程原生的本地变量,
             * 意味着很可能会影响到任务里取值的准确性。
             *
             * 打个比方,有ttl对象tl,这个tl在线程池的某个子线程里存在对应的值2,当某个主线程使用该子线程做异步任务时
             * tl这个对象在当前主线程里没有值,那么如果不进行下面这一步的操作,那么在使用该子线程做的任务里就可以通过
             * 该tl对象取到值2,不符合预期
             */
            if (!capturedMap.containsKey(threadLocal)) {
                iterator.remove();
                threadLocal.superRemove();
            }
        }

        // 这一步就是直接把父线程本地变量赋值给当前线程了(这一步起就刷新了holder里的值了,具体往下看该方法,在异步线程运行期间,还可能产生别的本地变量,比如在真正的run方法内的业务代码,再用一个tl对象设置一个值)
        setTtlValuesTo(capturedMap);

        // 这个方法属于扩展方法,ttl本身支持重写异步任务执行前后的操作,这里不再具体赘述
        doExecuteCallback(true);

        return backup;
    }

    // 结合之前Rannable包装类的run方法来看,这个方法就是使用上面replay记录下的原生线程变量做恢复用的
    public static void restore(@Nonnull Object backup) {
        @SuppressWarnings("unchecked")
        Map<TransmittableThreadLocal<?>, Object> backupMap = (Map<TransmittableThreadLocal<?>, Object>) backup;
        // call afterExecute callback
        doExecuteCallback(false);

        // 注意,这里的holder取出来的,实际上是replay方法设置进去的关于父线程里的所有变量(结合上面来看,就是:该线程运行期间产生的变量+父线程继承来的变量)
        for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();
             iterator.hasNext(); ) {
            Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
            TransmittableThreadLocal<?> threadLocal = next.getKey();

            /**
             * 同样的,如果子线程原生变量不包含某个父线程传来的对象,那么就删除,可以思考下,这里的清除跟上面replay里的有什么不同?
             * 这里会把不属于原生变量的对象给删除掉(这里被删除掉的可能是父线程继承下来的,也可能是异步任务在执行时产生的新值)
             */
            if (!backupMap.containsKey(threadLocal)) {
                iterator.remove();
                threadLocal.superRemove();
            }
        }

        // 同样调用这个方法,进行值的恢复
        setTtlValuesTo(backupMap);
    }

    // 真正给当前子线程赋值的方法,对应上面的setTtlValuesTo方法
    private static void setTtlValuesTo(@Nonnull Map<TransmittableThreadLocal<?>, Object> ttlValues) {
        for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : ttlValues.entrySet()) {
            @SuppressWarnings("unchecked")
            TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey();
            threadLocal.set(entry.getValue()); //赋值,注意,从这里开始,子线程的holder里的值会被重新赋值刷新,可以参照上面ttl的set方法的实现
        }
    }

所谓线程池内线程的本地原生变量,其实是第一次使用线程时被传递进去的值,TTL是继承至ITL的,线程池第一次启用时是会触发Thread的init方法的,也就是说,在第一次异步时拿到的主线程的变量会被传递给子线程,作为子线程的原生本地变量保存起来,后续是replay操作和restore操作也是围绕着这个原生变量(即原生holder里的值)来进行设置、恢复的,设置的是当前父线程捕获到的本地变量,恢复的是子线程原生本地变量。

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