小小建议——多线程与并发(1)线程的创建和使用

参考自编写高质量代码++改善Java程序的151个建议 提取码:ylv4

1. 不推荐在线程中重写start方法;

emmm,我想没人会试图重写Thread的start()方法

自定义代码

public class MultiThread extends Thread{
    //启动线程
    @Override
    public synchronized void start() {
        //super.start();
        //自己启动线程...代码逻辑
    }
    //线程执行逻辑
    @Override
    public void run() {
        super.run();
    }
}

注意:继承自Thread类的多线程类不必重写start方法。

因为Thread里面的start方法实际上会调用native方法,在底层实现启动线程、申请栈空间、运行run方法修改线程状态等职责,线程管理和栈内存管理都是JVM负责的,如果覆盖start方法,也就是撤销了线程管理和栈内存管理的能力。

2. 终止线程的方法

stop方法是一个弃用的方法(注意是弃用)...

1. 使用Thread的stop方法终止
线程启动完毕之后,在运行时可能需要终止,Java提供的终止方法只有一个stop,但是并不推荐使用这个方法。

  • stop方法是过时的方法。
  • stop是一种“恶意”的中断,一旦执行stop方法,即中断当前正在运行的线程,不管线程是否执行完毕。会导致代码逻辑的不完整。
  • stop方法会破坏原子逻辑。

2. 使用volatile修饰的标志位终止

volatile保证的是代码的可见性。线程每次在主存中获取volatile变量,即线程修改volatile变量之后,所有线程立即可见。注意,只能保证变量值到达操作栈顶是正确的,而后可能被其他线程修改,即只能保证其可见性,不能保证其原子性。

操作代码:

public class SafeStopThread extends Thread {

    //此变量必须加上volatile
    private volatile boolean stop = true;

    @Override
    public void run() {
        while (stop) {
           System.out.println("Running...");
        }
    }

    //线程终止
    public void terminate() {
        stop = false;
    }
}

3. interrupt中断方法

interrupt名字看上去是像是终止一个线程的方法,但是它不能终止一个正在执行着的线程,他只是修改中断标志而已。

//执行方法
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                while(true){
                    System.out.println("Running...");
                }
            }
        };
        //启动线程
        thread.start();
        //中断线程
        thread.interrupt();
    }

执行上面这段代码,会发现一直有Running在输出,永远不会停止,似乎执行了interrupt没有任何变化。那是因为interrupt方法不能终止一个线程状态,他只会改变中断标志位(如果在thread.interrupt()前后输出thread.isInterrupted()则会发现分别输出了falsetrue),如果需要终止该程序,还需要自行进行判断,例如我们可以使用interrupt编写出更加简洁、安全的终止线程代码。

interrupt正确终止线程

 //执行方法
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                while(!isInterrupted()){
                    System.out.println("Running...");
                }
            }
        };
        //启动线程
        thread.start();
        //中断线程
        thread.interrupt();
    }

4. 线程池关闭线程

如果我们使用线程池(比如ThreadPoolExecutor类),那么可以通过shutdown方法逐步关闭线程池中的线程,它采用的是比较温和、安全的关闭线程方法。

3. 线程优先级只使用三个等级

线程优先级(Priority)决定了线程获得CPU运行的机会,优先级越高的获取的运行机会越大。

线程优先级有2个特点:

  1. 并不是严格遵循线程优先级别来执行的。
  2. 但是优先级差别越大,运行机会差别越明显。

所以在Thread类中,默认设置了三个优先级,此意就是告诉开发者,建议使用优先级常量,而不是1到10随机数字。常量代码如下:

    //最低的优先级
    public final static int MIN_PRIORITY = 1;

   //默认的优先级
    public final static int NORM_PRIORITY = 5;

  //最高的优先级
    public final static int MAX_PRIORITY = 10;

在编码时,直接使用这些常量,可以在大部分情况下MAX_PRIORITY的线程会比NORM_PRIOPITY的线程先运行,但也不是绝对。不能把这个优先级作为核心业务的必然条件。Java无法保证优先级高肯定会先执行,只能保证优先级高的有更多机会执行。故建议在开发时只使用此三类优先级

4. 使用线程异常处理器提升系统可靠性

如何实现线程异常的捕获或者处理呢?

线程Thread的run方法是不会抛出任何检查型异常(即不会throws Exception,但是可以try-catch),但是他自身却可能因为一个异常而中止,导致线程的终结。

在Java 1.5之后,在Thread类中增加了setUncaughtExceptionHandler方法,实现了线程异常的捕获和处理,

1. 主动方法解决运行时异常:

public class InitiativeCaught {
    void threadDeal(Runnable r,Throwable t){
        System.out.println("===Exception"+t.getMessage());
    }
    //成员内部类
    class InitialtiveThread implements Runnable{

        public void run() {
            Throwable thrown=null;
            try {
                System.out.println(3/2);
                System.out.println(3/0);
                System.out.println(3/1);
            }catch (Throwable e){
                thrown=e;
            }finally {
                threadDeal(this,thrown);
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService exec= Executors.newCachedThreadPool();
        exec.execute(new InitiativeCaught().new InitialtiveThread());
        exec.shutdown();
    }
}

上面介绍一种主动的方法来解决未检查异常。在Thread API中也可以使用UncaughtExceptionHandler,同样也能检测出多线程某个由于未捕获的异常而终结的情况。

这两种方法是互补的,通过将二者结合在一起,就能有效地防止线程泄露的情况。

2. UncaughtExceptionHandler的用法:

public class WitchCaughtThread {

    public static void main(String[] args) {
        Thread thread=new Thread(new WitchCaughtThread().new Task());
        thread.setUncaughtExceptionHandler(new WitchCaughtThread().new ExceptionHandler());
        thread.start();
    }

    //自定义异常handler
    class ExceptionHandler implements Thread.UncaughtExceptionHandler{
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("==Exception:"+e.getMessage());
        }
    }

    //实例内部类
   public class Task implements Runnable{
        public void run() {
            System.out.println(3/2);
            System.out.println(3/0);
            System.out.println(3/1);
        }
    }
}

注意,我们可以为所有的Thread类设置一个默认的UncaughtExceptionHandler

3. 设置Thread默认的UncaughtExceptionHandler

        Thread.setDefaultUncaughtExceptionHandler(new 
WitchCaughtThread().new ExceptionHandler());

4. 线程池如何使用UncaughtExceptionHandler:

只有通过execute提交的任务,才能将它抛出的异常交给UncaughtExceptionHandler,而通过submit提交的任务,无论是抛出Exception异常还是RuntimeException异常,都认为是任务返回状态的一部分。如果一个由submit提交的任务由于抛出异常而结束,那么这个异常将被Future.get封装在ExecutionException中重新抛出。

4.1 使用execute方法捕获异常:【异常捕获失败】

public class ExecuteCaught {
    public static void main(String[] args) {
        //创建线程池
        ExecutorService exec= Executors.newCachedThreadPool();
        //创建线程
        Thread thread=new Thread(new WitchCaughtThread().new Task());
        //设置线程异常处理机制
        thread.setUncaughtExceptionHandler(new WitchCaughtThread().new ExceptionHandler());
        //执行线程
        exec.execute(thread);
        //终止线程
        exec.shutdown();
    }
}

运行结果
可以看到,异常被抛出,但是并没有捕获异常。进行处理。

1
Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
    at com.bascis.ThreadDemo.WitchCaughtThread$Task.run(WitchCaughtThread.java:24)
    at java.lang.Thread.run(Thread.java:748)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

4.2 将异常的捕获封装到Runnable或Callable中:【异常捕获成功】

public class ExecuteCaught {
    public static void main(String[] args) {
        //创建线程池
        ExecutorService exec= Executors.newCachedThreadPool();
        //执行线程
        exec.execute(new ThreadPoolTask());
        //终止线程
        exec.shutdown();
    }
}
class ThreadPoolTask implements Runnable{

    public void run() {
        //在Runnable中处理异常
        Thread.currentThread().setUncaughtExceptionHandler(new WitchCaughtThread().new ExceptionHandler());
        System.out.println(3/2);
        System.out.println(3/0);
        System.out.println(3/1);
    }
}

执行结果

1
==Exception:/ by zero

但是我们线程池若是使用submit()方法提交...

submit提交线程:【异常捕获失败】

 public static void main(String[] args) {
        //创建线程池
        ExecutorService exec= Executors.newCachedThreadPool();
        //执行线程
        exec.submit(new ThreadPoolTask());
        //终止线程
        exec.shutdown();
    }
submit提交执行结果

下面为了证实submit提交的线程,异常信息保存到了Future的get方法中:

submit提交的完整代码:

public class ExecuteCaught {
    public static void main(String[] args) {
        //创建线程池
        ExecutorService exec= Executors.newCachedThreadPool();
        //执行线程
        Future<?> submit = exec.submit(new ThreadPoolTask());
        //终止线程
        exec.shutdown();
        try {
            Object o = submit.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

执行结果

1
java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
    at java.util.concurrent.FutureTask.report(FutureTask.java:122)
    at java.util.concurrent.FutureTask.get(FutureTask.java:192)
    at com.bascis.ThreadDemo.ExecuteCaught.main(ExecuteCaught.java:14)
Caused by: java.lang.ArithmeticException: / by zero
    at com.bascis.ThreadDemo.ThreadPoolTask.run(ExecuteCaught.java:28)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

4.2 使用UncaughtException注意事项

在实际开发中,需要注意以下三个方面

1. 共享资源锁定
如果线程异常产生的原因是资源被锁定,自行重启应用只会增加系统的负担,无法提供不间断服务。例如一个即时通信服务器出现信息不能写入的情况,即使再怎么重启服务,也是无法解决问题的。在此情况下最好的办法就是停止所有的线程,释放资源。

2. 脏数据引起系统逻辑异常
异常的产生中断了正在执行的业务逻辑,特别是如果正在执行一个原子操作,但如果此时抛出了RuntimeException就有可能会破坏正常的业务逻辑,在这种情况下重启应用服务器,虽然可以提供服务,但对部分用户产生了逻辑异常。

3. 内存溢出
线程异常了,但由该线程创建的对象不会马上回收,如果在重启新线程,在创建一批对象,特别是场景接管,就非常危险了。例如即时通讯服务,重新启动一个新线程必须保证原在线用户的透明性,即用户不会察觉服务器重启,在此种情况下,就需要线程初始化时加载大量对象以保证用户的状态信息,如果线程反复重启,很可能会造成OOM内存泄露问题 。

5. 异步运算考虑使用Callable接口

多线程应用有两种实现方式,一种是实现Runnable接口,另一种是继承Thread类。这两种方式都有缺陷:run方法没有返回值,并且不能抛出异常。

在JDK1.5 开始引入了一个新的接口:Callable,它类似与Runable接口,实现它就可以实现多线程任务。

Callable接口定义:

public interface Callable<V> {
  //具有返回值,并可抛出异常
    V call() throws Exception;
}

需要注意的是:实现Callable接口的类,只是表明它是一个可调用的任务,并不表示它具有多线程运算能力,还是需要执行器来执行的。我们先编写一个实现Callable接口的任务类:
任务类:

//收税计算器
public class TaxCalculator implements Callable<Integer> {
    //本金
    private int seedMoney;
    public TaxCalculator(int seedMoney) {
        this.seedMoney = seedMoney;
    }
    public Integer call() throws Exception {
        //模拟复杂的计算
        Thread.sleep(10000);
        return seedMoney/10;
    }
}

这里模拟了一个复杂的运算:税收计算器,该运算可能花费10s时间,但是不能让用户一直等待,需要给用户输出点什么,让用户知道系统还在运行,是系统友好性的体现:用户输入即有输出,若耗时较长,则显示运算进度。如果我们直接计算,就只有一个main线程,是不可能有友好提示的,如果税金不计算完毕,也不会有后续动作的,所有,此时最好的办法就是重启一个线程来计算,让main做进度提示,代码如下:

public static void main(String[] args) throws InterruptedException, ExecutionException {
        //创建一个线程
        ExecutorService es= Executors.newSingleThreadExecutor();
        //执行线程
        Future<Integer> future = es.submit(new TaxCalculator(200));
        //查看任务是否执行完毕
        while(!future.isDone()){
            //若是还没有运算完毕,等到200ms
            Thread.sleep(200);
            //输出进度符号
            System.out.print("#");
        }
        System.out.println("\n计算完毕,税金是:"+future.get()+"元");
        es.shutdown();
    }

执行结果

##################################################
计算完毕,税金是:20元

5.2 Callable总结

  • 尽可能多的占用系统资源,提供快速运算;
  • 尽可能监控线程执行的情况,比如是否执行完毕,是否有返回值,是否异常。
  • 可以为用户提供更好的支持,比如例子中的运算进度等。

6. 优先选择线程池

在java1.5之前,实现多线程编程比较麻烦,需要自己启动线程,并关注同步资源,防止出现死锁等问题,在JDK1.5之后引入了并行计算框架,大大简化了多线程开发。

线程一般有五个状态:新建状态(New)、可运行状态(Runnable)、阻塞状态(Blocked)、等待状态(Waiting)、结束状态(Terminated),线程的状态只能由新建转变为运行态才可能被阻塞或等待,最后终结,不可能出现本末倒置的情况。

线程池的实现涉及到三个名词:
1. 工作线程(Worker)
线程池中的线程,只有两个状态:可运行状态和等待状态,没有任务时他们就处于等待状态,运行时可以循环地执行任务。

2. 任务接口(Task)
这是每个任务必须实现的接口,以供工作线程调度器调度,他主要规定了任务的入口、任务执行完的场景处理,任务的执行状态等。这里有两种类型的任务:具有返回值(或异常)的Callable接口任务和无返回值并兼容旧版本的Runnable接口任务。

3. 任务队列(Work Queue)
也叫做工作队列,用于存放等待处理的任务,一般是BlockingQueue的实现类,用于实现任务的排队处理。

线程池的工作原理:创建一个阻塞队列以容纳任务,在第一次执行任务时创建足够多的线程(不超过许可线程数),并处理任务,之后每隔工作线程从任务队列中获取任务,直到任务队列中的任务数量为0为止。此时线程将处于等待状态,一旦有任务再加入到队列中,即唤醒工作线程进行处理,实现线程的可复用性。

7. 适时选择不同的线程池来实现

Java的线程池实现从最根本上来说只有两个:ThreadPoolExecutorScheduledThreadExecutor ['skedʒəld]类,这两个类还是父子类的关系,但是JAVA为了简化并行计算,还提供了一个Executors的静态类。

7.1 线程池的成员含义

ThreadPoolExecutor的构造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

这个是ThreadPoolExecutor最完整的构造方法,其他的构造函数都是引用该构造函数实现的,我们需要了解一下参数的含义:

  • corePoolSize:核心线程池大小。线程池启动后,在池中保持线程的最小数量。需要说明的是线程数量是逐步到达corePoolSize值的。

  • maximumPoolSize:最大线程池数量。池中最大能容纳的最大线程数量,如果超出,则使用RejectedExecutionHandler拒绝策略来处理。

  • keepAliveTime:线程最大生命期。这里的生命期有2个约束条件:一是该参数针对的是超过corePoolSize数量的线程;二是处于非运行状态的线程。这么说吧,如果corePoolSize为10,maximumPoolSize为20,此时线程池中有15个线程在运行,一段时间之后,其中有3个线程处于等待状态的时间超过了keepAliveTime指定的时间,则结束这3个线程,此时线程池还有12个线程正在运行。

  • unit:keepAliveTime的时间单位。

  • workQueue:任务队列。当线程池中的线程都处于运行状态,而此时任务数量继续增加,则需要一个容器来容纳这些任务,这就是任务工厂。

  • threadThread:线程工厂。定义如何启动一个线程,可以设置线程的名称,并且可以确定是否是后台线程等。

  • handler:拒绝任务处理器。由于超出线程数量和队列容量而对继续增加的任务进行处理的程序。

7.2 线程池的管理过程

  1. 首先创建线程池,然后根据任务的数量逐步将线程增大到corePoolSize数量。
  2. corePoolSizePool已满,则放置到workQueue中,直到workQueue爆满为止。
  3. 继续增加池中的线程数量(增强处理能力),最终达到maximumPoolSize
  4. 若还有任务进来,那么就采用拒绝策略handler来处理。

在任务队列和线程池都饱和的情况下,一旦有线程处于等待(任务处理完毕,没有新任务增加)状态时间超过keepAliveTime,则该线程终止。也就是说池中的线程数量会逐渐降低,直至为corePoolSize数量为止。

7.3 Executors的使用

  • newSingleThreadExecutor:单线程池。
    一个池中只有一个线程在运行,该线程永不超时。由于只有一个线程,当多个任务需要处理的时候,会将他们放置到一个无界阻塞队列中逐步处理。实现代码:
 public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

使用代码

 public static void main(String[] args) {
        //创建线程池
        ExecutorService es= Executors.newSingleThreadExecutor();
        //执行任务
        es.submit(new Callable<String>(){
            public String call() throws Exception {
                return null;
            }
        });
        //关闭执行器
        es.shutdown();
    }
  • newCachedThreadPool:缓冲功能的线程池
    建立一个线程池,而且线程的数量没有限制(当然不能超过Integer的最大值),新增一个任务即有一个线程处理,或复用之前空闲的线程,或新启用一个线程,但是一个线程在60s内一直是一个等待状态(也及时一分钟没有工作可做)就会被终止。
    源代码:
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

这里需要说明的是,任务队列使用了同步阻塞队列,这意味着想队列中新加入一个元素,即可唤醒一个线程(新创建的线程或复用线程池中空闲线程)来处理,这种队列已经没有队列深度的概念了。

  • newFixedThreadPool:固定线程数量的线程池
    在初始化时已经决定了线程的最大数量,若任务添加的能力超过了线程处理能力,则建立阻塞1队列容纳多余的任务。
    源代码:
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

我们可以看到,它的corePoolSize和maximumPoolSize是相等的。

总结:
我们可以看到,Executors中的阻塞队列均是使用了无限的阻塞队列,若是在生产环境中使用,可能造成内存溢出。故不推荐使用。

推荐阅读

JAVA多线程之UncaughtExceptionHandler——处理非正常的线程中止

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

推荐阅读更多精彩内容

  •   一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺...
    OmaiMoon阅读 1,668评论 0 12
  • 第5章 多线程编程 5.1 线程基础 5.1.1 如何创建线程 在java要创建线程,一般有==两种方式==:1)...
    AndroidMaster阅读 1,792评论 0 11
  • 一、线程的生命周期 线程状态转换图: 1、新建状态 用new关键字和Thread类或其子类建立一个线程对象后,该线...
    我是嘻哈大哥阅读 898评论 0 8
  • 第6章介绍了任务执行框架, 它不仅能简化任务与线程的生命周期管理, 而且还提供一 种简单灵活的方式将任务的提交与任...
    好好学习Sun阅读 1,169评论 0 2
  • 题记:Tim一家人最后决定把户外活动改成室内活动看电影。一家人来到电影院门口为在哪里停车犯了难,最后Tim还是停好...
    superAzalea阅读 443评论 0 0