线程池

目标

1、线程池中线程的运行状态
2、worker的计算
3、线程池中ctr的作用
4、任务添加流程
5、任务处理的逻辑

总结

先写出流程结论, 后续再分析时通过代码对这些结论进行验证;
  1、添加Task时, 首先判断WorkerCount与corePoolSize的关系, 如果WorkerCount < corePoolSize, 此时会无条件创建一个Worker来执行Task, 同时将ctl的低位执行+1操作, 此时在创建Worker时, 会将Task与Worker进行关联, Worker执行完Task以后将Task进行释放;
  2、如果WorkerCount ≥ corePoolSize, 此时首先会将Task添加到任务队列中, 添加完成之后, 会再次判断WorkerCount, 此时只有当WorkerCount == 0时才会再次创建新的WorkerCount来执行任务, 但是此时通过addWorker创建Worker时, 没有将Worker与Task进行关联. 到这里有三个疑问: 1. WorkerCount ≥ corePoolSize与WorkerCount==0有没有冲突? 2. 如果后者WorkerCount > 0, 并不会调用addWorker创建新的线程, 那么此时提交的Task被谁执行? 3. 如果后者WorkerCount == 0, 那么addWorker创建的Worker又是如何执行Task的呢?
  3、如果WorkerCount ≥ corePoolSize && workQueue.offer(task) == false, 也就是说第二个条件任务队列已满的情况下, 此时会触发addWorker尝试为当前Task创建新的Worker, 此时为何说是尝试? 因为如果线程池中线程数量已经大于线程池运行的最大线程数时, 此时将不会再为新的Task创建Worker, 而是采取拒绝该Task的方式
  4、接下来是任务处理的流程, 同时也回答了第二步中的几个疑问, 当线程执行完Task之后, 尝试从任务队列中获取Task, 针对线程获取Task分两种情况: (1) 核心线程获取Task, 如果不允许核心线程超时的情况下, 如果workQueue.isNotEmpty, 此时核心线程将一直处于挂起状态, 直到任务队列中添加了元素. (2) 针对普通线程, 如果当前workQueue.isEmpty, 那么普通线程会挂起指定时间后进行释放操作, 同时执行ctr--操作.
  5、所以第二条的疑问也得到的解释, 任务被添加到任务队列之前, 线程池中的线程刚好全部被释放, 此时会重新创建线程执行workQueue中的Task. 任务被添加到任务队列之后, 如果WorkerCount != 0, 此时会直接唤醒等待挂起的线程, 避免线程的不必要创建.

一、关于Executor

1.1 线程池的顶层接口Executor
public interface Executor {
    void execute(Runnable command);
}
1.2 线程池的第二层接口ExecutorService
public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);
}

1、我们经常用到的几个线程池都是实现于ExecutorService接口, ExecutorService对Executor进行了扩展, 支持Callable和Runnable;
2、先不考虑ExecutorService每个方法到底什么作用, 待后边分析到线程池管理线程流程时自然明了;

1.3 线程池的提供者Executors:
public class Executors {
    public static ExecutorService newFixedThreadPool(int nThreads) {...}
    public static ExecutorService newWorkStealingPool() {...}
    public static ExecutorService newSingleThreadExecutor() {...}
    public static ExecutorService newCachedThreadPool() {...}
    public static ScheduledExecutorService newSingleThreadScheduledExecutor() {...}
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {...}
    private Executors() {}
}

  1、先忽略这些方法的内部实现, 以及传入的参数, 现在分析还为时过早, 属于强行记忆;
  2、这些方法主要返回两大类线程池ExecutorService和ScheduledExecutorService, 而ScheduledExecutorService又是继承于ExecutorService, 所以后续会重点关注这两个类;
  3、接下来开始分析ScheduledExecutorService和ExecutorService是如何管理内部维护的线程池;
  4、对Executors 方法展开, 发现ExecutorService和ScheduledExecutorService分别指向ThreadPoolExecutor和ScheduledThreadPoolExecutor, 而这两个类又都间接实现ExecutorService接口, 所以都支持execute(RunnableImpl)和submit(CallableImpl)方式;
先分析execute(RunnableImple)这种方式;
  5、关于四个方法的具体分析在模块三

在进行分析之前, 先列出线程池中线程的几个状态以及构建线程池时所需要的参数, 后续分析源码时遇到即回头来进行补充说明:
线程的几个状态值 :

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

构建线程池时所需参数 :

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
        ...
}

下面结合对ThreadPoolExecutor源码的分析, 对每个参数的作用进行总结描述:

其他参数 含义
corePoolSize 注意没有任务时, 核心线程的状态, 与allowCoreThreadTimeOut有关
maximumPoolSize 当有任务到来时, 通过该变量判断当前任务应该如何处理
keepAliveTime 如果元素队列为空时, 线程(包括核心线程和非核心线程)被挂起的时间
TimeUnit 如果元素队列为空时, 线程(包括核心线程和非核心线程)被挂起的时间单位
BlockingQueue 元素出入队列的方式
ThreadFactory 自定义线程创建的方式
RejectedExecutionHandler 任务被提交到线程池时, 如果当前线程池中线程数量大于最大工作线程数量时, 该任务会被拒绝, 拒绝策略取决于构造线程池时传入的具体的拒绝策略

二、分析线程池里面涉及到的位运算

public class ThreadPoolExecutor extends AbstractExecutorService {
    // 默认情况下ctl = 11100000 00000000 00000000 00000000;
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    // CAPACITY = 0001111 11111111 11111111 11111111
    // ~CAPACITY = 11100000 00000000 00000000 00000000
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
    // runState is stored in the high-order bits
    // 将线程池的运行状态记录在bits的高位
    // RUNNING = 11100000 00000000 00000000 00000000
    private static final int RUNNING    = -1 << COUNT_BITS;
    // SHUTDOWN = 00000000 00000000 00000000 00000000
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    // STOP = 00100000 00000000 00000000 00000000
    private static final int STOP       =  1 << COUNT_BITS;
    // TIDYING = 01000000 00000000 00000000 00000000
    private static final int TIDYING    =  2 << COUNT_BITS;
    // TERMINATED = 01100000 00000000 00000000 00000000
    private static final int TERMINATED =  3 << COUNT_BITS;
    // 通过计算获取高位三位的值, 进而判断当前线程池的运行状态
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    // CAPACITY的高三位是0, 所以线程池中Worker的数量存储在低位29位中.
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    //
    private static int ctlOf(int rs, int wc) { return rs | wc; }
    /*
     * Bit field accessors that don't require unpacking ctl.
     * These depend on the bit layout and on workerCount being never negative.
     */
    private static boolean runStateLessThan(int c, int s) {
        return c < s;
    }
    private static boolean runStateAtLeast(int c, int s) {
        return c >= s;
    }
    private static boolean isRunning(int c) {
        return c < SHUTDOWN;
    }
}

上面的运算结合下面实际操作进行分析.

三、任务提交

3.1 ThreadPoolExecutor.execute

注意Thread与Worker的关系

虽然代码少, 但是要表达的意思却是很多的;
public void execute(Runnable command) {
    // c默认 = RUNNING, 二进制 = 11100000 00000000 00000000 00000000
    int c = ctl.get();
    // 1. 通过workerCountOf内部与CAPACITY进行与运行得到当前线程池中Worker的数量;
    // 2. 然后将当前WorkerCount与corePoolSize进行比较, 如果WorkerCount < corePoolSize, 
    //    则进入if内部通过addWorker尝试为当前Runnable创建线程.
    // 3. 如果当前WorkerCount ≥ corePoolSize
    if (workerCountOf(c) < corePoolSize) {
        // 1. 注意此时传参command(command != null), true;
        // 2. 针对返回的结果, 有以下几种情况:
        //    true:   结合addWorker源码可知, 如果线程池处于运行状态, 返回true;
        //    false:  只有线程池处于非运行状态, 才可能返回false, 这种情况不考虑;
        if (addWorker(command, true))  
            return;
        // 再次获取当前线程池的一个状态
        c = ctl.get();
    }
    // 1. 因为我们一直假设线程池处于运行状态, 所以此时默认isRunning(c)=true, 直接分析第二个条件;
    // 2. 针对第二个条件, workQueue.offer:
    //    (1) true: 任务队列未满;
    //    (2) false: 任务队列已满;  
    // 3. 然后如果当WorkerCount ≥ corePoolSize时, 首先将任务进行入队操作.
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 只考虑线程池处于RUNNGING的场景, 跳过该if语句;
        if (!isRunning(recheck) && remove(command))
            reject(command);
        // 再次获取线程池中Worker的数量, 此时只有当WorkerCount == 0时, 才会进入到if语句中
        // 调用addWorker创建新的Worker实例, 而结合上下文可知, Worker与Thread是一个1:1对应
        // 的关系, 所以如果WorkerCount == 0, 也就是说当前线程池中线程数量为0;
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);     
    }
    // 1. 能执行到这里需要满足以下几个条件:
    //    (1) wc > corePoolSize即当前线程池中工作线程数量>核心线程数量;
    //    (2) workQueue.offer(command) = false, 即任务队列已满;
    // 2. 结合addWorker源码分析可知, 如果任务被成功执行, 返回true, 如果WorkerCount ≥ maximumPoolSize, 
    //    返回false, 此时对该任务才去拒绝策略;
    else if (!addWorker(command, false))
        reject(command);    
    }
}

关于传值的问题, 这里用一张表进行总结: 接下表中数据对execute代码进行分析

对上述表中的以及代码中的注释再次解释一下:
1、添加任务时, 如果WorkerCount < corePoolSize, 那么此时会直接创建一个线程来执行该任务;
2、添加任务时, 如果WorkerCount ≥ corePoolSize, 那么此时会先将当前线程进行入队操作, 然后再次判断WorkerCount, 只有当WorkerCount == 0 时, 才会触发addWorker创建Thread去执行任务队列中的任务. 现在思考一个问题, 如果WorkerCount != 0, 提交的任务被谁处理了呢?
3、如果WorkerCount ≥ corePoolSize并且任务队列已满, 此时会尝试调用addWorker创建新的线程来执行提交的任务.

3.2 ThreadPoolExecutor.addWorker
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        // 再次获取当前线程池的状态: 高位存储线程池的运行状态.
        int c = ctl.get();
        // 通过高位获取当前线程池的运行状态;
        int rs = runStateOf(c);
        // 不考虑线程池中断的情况, 所以继续向下执行;
        if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))
            return false;
        for (;;) {
            // 通过低位29位的值获取当前线程池中WorkerCount;
            int wc = workerCountOf(c);
            // 忽略第一个条件, 直接看第二个条件, 第二个条件重点在于core;
            // 而core的传值又依赖于execute里面的几种情况:
            // 1. core = true:
            //    当wc < corePoolSize, 触发addWorker会传入core = true, 所以此时第二个条件为false;
            // 2. core = false:
            //    (1) wc ≥ corePoolSize && workQueue.offer == true && WorkerCount == 0
            //    (2) wc ≥ corePoolSize && workQueue.offer == false
            // 3. 不管core为true还是false, 只要线程池正常运行, 第二个条件就是始终为false;
            if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // 只要程序能够正常运行, 都会在这里执行ctl+1操作, 而ctl低29位标志的是线程池中线程数量
            // 对ctl执行+1操作
            if (compareAndIncrementWorkerCount(c))
                break retry;
        }
    }
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        // 关于firstTask取值情况上文已经用表进行了说明, 此处再简短的进行说明:
        // 1. firstTask != null:
        //    (1) wc < corePoolSize;
        //    (2) wc ≥ corePoolSize && workQueue.offer() == flase;
        // 2. firstTask == null:
        //    wc ≥ corePoolSize && workQueue.offer() == true && workerCount == 0;
        w = new Worker(firstTask); 
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                int rs = runStateOf(ctl.get());
                // 假设线程池是运行状态, 因此第一个条件为true;
                if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    // 标志位, 通过该变量判断是否返回true/false;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                // 触发其内部Thread.run的执行;
                t.start();    
                workerStarted = true;
            }
        }
    } finally {
        if (!workerStarted)
            // 如果执行失败, 将ctl进行-1操作;
            addWorkerFailed(w);
    }
    return workerStarted;
}

对addWorker进行总结:
  1、线程池处于正常运行状态, 只要触发了addWorker方法, 不论当前线程池中线程数量多少, 都会重新创建一个新的Worker与Thread, 然后通过该Worker与Thread来执行新的任务;
  2、有一个问题是当wc ≥ corePoolSize时, firstTask需要先被添加到workQueue中, 然后再次判断只有当wc == 0时, 才会触发addWorker创建新的Worker进行任务的执行, 那么问题来了, 此时调用addWorker传入的firstTask = null, 那么线程从哪里获取的任务进行执行呢?
  3、接着第二个条件如果wc != 0, 此时是不会触发addWorker的, 那么此时提交的任务又是被谁执行呢?
上面第二个和第三个问题在Worker.run中会得到答案

3.3 ThreadPoolExecutor.compareAndIncrementWorkerCount执行ctl+1操作
private boolean compareAndIncrementWorkerCount(int expect) {
    //ctl执行+1操作, 执行完这个操作之后, 开始创建Worker实例, 然后结合上下文
    //可知, ctl高三位记录的是当前线程池的状态, 低29位记录的是当前线程池中Worker数量.
    return ctl.compareAndSet(expect, expect + 1);
}
3.4 Worker结构
// Worker与Thread、Runnable是1:1:1对应的一个关系, 但是firstTask可能存在为null的情况.
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    final Thread thread;
    Runnable firstTask;
    volatile long completedTasks;
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }
}

四、关于Worker

4.1 Worker.run执行任务
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    public void run() {
        // 触发线程池中的runWorker方法;
        runWorker(this);
    }
}

public class ThreadPoolExecutor {
    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            // 问题就在这里, 如果task == null的情况下, 当前线程通过getTask()会尝试从
            // 任务队列中获取任务进行执行;
            while (task != null || (task = getTask()) != null) {
                w.lock();
                task.run();
                task = null;
                w.completedTasks++;
                w.unlock();
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }
}
4.2 ThreadPoolExecutor.getTask线程从任务队列中获取任务
先写结论:
1. 当允许核心线程超时: 从元素队列中取出元素时, 如果元素队列为空, 当前线程会先挂起指定时间, 然后被结束;
2. 当不允许核心线程超时: 从元素队列中取出元素时, 如果当前元素队列为空, 当前线程为核心线程时, 线程被挂起,
   如果当前线程为非核心工作线程, 当前线程会挂起指定时间然后被结束;
private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        // 目前只考虑线程池正常运行的情况, 所以不考虑这段流程;
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
        // 再次获取WorkerCount;
        int wc = workerCountOf(c);
        // 对time取值有以下几种情况:
        // 1. timed = true:
        //    1.1 allowCoreThreadTimeOut = true: 通过allowCoreThreadTimeOut()进行赋值;
        //    1.2 wc > corePoolSize: 当前线程数量>核心工作线程数量;
        // 2. timed = false:
        //    2.1 wc ≤ corePoolSize: 当前线程数量 ≤ 核心工作线程数量;
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        // 第一个条件wc > maximumPoolSize认为是true, 所以重点在于后面两个条件:
        // (1) timed = true && timedOut == true
        //    针对第一个条件timed=true, 也就是说允许核心线程超时或者当前线程数量超过核心线程数量;
        //    第二个条件timedOut作为超时的标志, 结合下文如果r == null, 则认为当前线程超时成功;
        // (2) wc > 1 || workQueue.isEmpty()
        //    在当前线程超时成功的情况下, 如果任务队列此时还是为空的, 那么将会释放掉该线程, 通过
        //    进入if内执行ctl--操作
        // 通过对这段代码进行分析还可以发现一个结论, 对于超时的线程, 再进行释放之前还会再去尝试一下
        // 从任务队列中获取任务, 如果此时任务队列中还是没有任务, 然后释放该线程.
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        // 1. timed = true:
        //    此时allowCoreThreadTimeOut = true || wc > corePoolSize;
        // 2. timed = false: 
        //    结合上文可知, 此时allowCoreThreadTimeOut = false && wc ≤ corePoolSize;
        // 3. BlockingQueue的特性就是pool(...)当前线程在等待指定时间后进行元素出队操作;
        // 4. take()方法如果元素队列为空, 当前线程是会一直处于挂起状态;
        Runnable r = timed ?
            workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
            workQueue.take();
        if (r != null)
            return r;
        // 执行到这里需要满足的条件是r == null, 然后通过timedOut来认为获取任务失败是一种超时的情况.
        timedOut = true;
    }
}

五、Executors四个常见线程池

5.1 Executors.newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
5.2 Executors.newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}
5.3 Executors.newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}
5.4 Executors.newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

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

推荐阅读更多精彩内容

  • 前段时间遇到这样一个问题,有人问微信朋友圈的上传图片的功能怎么做才能让用户的等待时间较短,比如说一下上传9张图片,...
    加油码农阅读 1,190评论 0 2
  • 第一部分 来看一下线程池的框架图,如下: 1、Executor任务提交接口与Executors工具类 Execut...
    压抑的内心阅读 4,252评论 1 24
  • 一.Java中的ThreadPoolExecutor类 java.uitl.concurrent.ThreadPo...
    谁在烽烟彼岸阅读 643评论 0 0
  • 139、140days 昨天忘了? 真的忘了。 今天昨天连续两天腹泻。 真是心疼。 我还没反应过来。 希望妳一切安好!
    sueva阅读 98评论 0 0
  • 下火车坐汽车,又奔波了一天。浑身软得像面条,胳膊腿全不听使唤了。想睡觉,只想睡觉。 一进屋,她就丢下背包,踢掉鞋子...
    默默地中海阅读 234评论 2 4