美文网首页
关于多线程你知道多少

关于多线程你知道多少

作者: bearPotMan | 来源:发表于2019-04-23 22:40 被阅读0次

我们就从线程的创建方式开始吧,那么常见的线程创建方式有哪几种呢?
1. 通过继承 Thread 类

public class ExtendsThreadTest {
    public static void main(String[] args) {
        MyThread t1 = new MyThread("t1");
        MyThread t2 = new MyThread("t2");
        t1.start();
        t2.start();
    }
}

class MyThread extends Thread {
    private String threadName;
    public MyThread(String threadName) {
        this.threadName = threadName;
    }
    @Override
    public void run() {
        System.out.println("extends Thread's " + threadName + " run");
    }
}

2. 通过实现 Runnable 接口

public class ImplementsRunnableTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable("t1"));
        Thread t2 = new Thread(new MyRunnable("t2"));
        t1.start();
        t2.start();
    }
}

class MyRunnable implements Runnable {
    private String threadName;
    public MyRunnable(String threadName) {
        this.threadName = threadName;
    }
    @Override
    public void run() {
        System.out.println("implements Runnable's " + threadName + " run");
    }
}

如果你只知道以上两种方式,如果是在几年前,很多面试官觉得你还是不错的,但是现在呢?已然不够了!

3. 通过实现 Callable 接口

  • 实现 Callable 接口,并重写 call 方法(call 方法是带有返回值的);
  • 使用 FutureTask 类包装 Callable 的实例;
  • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
public class ImplementsCallableTest {
    public static void main(String[] args) throws Exception {
        MyCallable myCallable = new MyCallable();
        FutureTask<AtomicInteger> futureTask_1 = new FutureTask<>(myCallable);
        FutureTask<AtomicInteger> futureTask_2 = new FutureTask<>(myCallable);
        // 第一个线程
        new Thread(futureTask_1, "futureTask_1").start();
        // main线程
        System.out.println(Thread.currentThread().getName() + " ----执行其他功能需要");
        while (!futureTask_1.isDone()) {
            // 阻塞直到 futureTask_1 的 call 方法执行完
        }
        System.out.println("futureTask_1 result " + futureTask_1.get());
        // 模拟故障
        myCallable.stop();
        // 第二个线程
        new Thread(futureTask_2, "futureTask_2").start();
        while (!futureTask_2.isDone()) {
            // 阻塞直到 futureTask_2 的 call 方法执行完
        }
        System.out.println("futureTask_2 result " + futureTask_2.get());
    }
}

class MyCallable implements Callable<AtomicInteger> {
    private AtomicInteger OK_CODE = new AtomicInteger(200);
    private AtomicInteger ERROR_CODE = new AtomicInteger(404);
    private volatile boolean FLAG = true;
    @Override
    public AtomicInteger call() throws Exception {
        System.out.println("implements Callable's call");
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (FLAG) {
            return OK_CODE;
        }
        return ERROR_CODE;
    }
    // 模拟故障
    public void stop() {
        this.FLAG = false;
    }
}

如果你还知道实现 Callable 接口,那你的档次就又上了一个层级。
那么请思考这样一个问题:既然已经有 Runnable 接口了,为什么还要提供一个 Callable 接口呢?而且 Thread 类不支持 Callable 接口的构造注入,为什么呢?
看了上面的代码,是不是有一点明白了呢?Callable 接口的 call 方法是可以有返回值的,而 Runnable 接口的 run 方法是没有返回值的,在一些业务场景中很可能就需要带有返回值,然后根据返回值做一些其他操作,所以 Callable 接口的出现也就不足为奇了。上面也提到说 Thread 类不支持 Callable 接口的构造注入,那么就需要一个“中间人”,这个中间人就是 FutureTask,通过查看这个实现类的源码就可以发现,它实现了 RunnableFuture 接口,这个接口又继承了 Runnable 接口,所以 FutureTask 就间接的实现了 Runnable 接口,是不是感觉大牛们的设计很 Nice,这不就是所谓的面向接口编程吗?好的设计一般都是这样的,当不确定传递的究竟是哪个类的时候,传递他们的共有接口就是一个很好的设计,JDK 中这样的设计可是有不少。
举个例子:比如说 Collections 工具类的一些同步实现,这里以 synchronizedList(List<T> list) 方法来说,通过判断参数是实现了哪个接口的实现类,然后实例化为具体的实现类,看源码:

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

好,关于这点暂时就说到这!
4. 使用线程池
谈及多线程,如果你没有想到 线程池 这个大 boss,那别人就会一眼看出你平时开发中几乎没有使用过多线程,光靠上面的三个你是很难去征服面试官的,如果你的 IDEA 中安装了 阿里巴巴开发规范手册 插件的话,你使用 new Thread() 的时候是会有提示的,它会提示你 “不要显示创建线程,请使用线程池”,线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
那么就来聊聊线程池的使用以及原理。
JDK 5 新增了一个 Executors 工厂类来产生线程池,常用以下几个静态工厂方法来创建线程池:

  • newFixedThreadPool(int nThreads):创建一个具有固定线程数的线程池,可控制最大线程数,超出的线程会在队列中等待。一般用于执行长期的任务;
public static ExecutorService newFixedThreadPool(int nThreads) {
  // 核心线程数 corePoolSize 和 最大线程数 maximumPoolSize 相等,使用 LinkedBlockingQueue
  return new ThreadPoolExecutor(nThreads, nThreads,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>());
}
  • newSingleThreadExecutor():创建只有一个线程的线程池,用唯一的工作线程来执行任务,保证任务按顺序执行。一般用于一个任务一个任务执行的场景;
public static ExecutorService newSingleThreadExecutor() {
  // 核心线程数 corePoolSize 和 最大线程数 maximumPoolSize 都是 1,使用 LinkedBlockingQueue
  return new FinalizableDelegatedExecutorService
      (new ThreadPoolExecutor(1, 1,
                              0L, TimeUnit.MILLISECONDS,
                              new LinkedBlockingQueue<Runnable>()));
}
  • newCachedThreadPool():创建一个根据需要创建新线程的线程池,但在它们可用时将重用先前构造的线程。一般用于执行短期的异步小程序或负载较轻的服务器;
// 当线程空闲超过 60秒就销毁线程
public static ExecutorService newCachedThreadPool() {
  return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                60L, TimeUnit.SECONDS,
                                new SynchronousQueue<Runnable>());
}

来个样例吧,可能看不出什么变化,但是理解原理就行。(因为在我的机器上 [2Core 4G] 就看不出什么变化)

public class ExecutorsTest {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        //ExecutorService threadPool = Executors.newSingleThreadExecutor();
        //ExecutorService threadPool = Executors.newCachedThreadPool();
        try {
            threadPool.execute(() -> {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + " 执行任务");
                }
            });
        } catch (Exception e) {
          e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

题外话:在JDK 1.8的时候,又提供了两个方法来创建线程池 - 有参的 newWorkStealingPool(int parallelism) 和无参的 newWorkStealingPool(),这两个方法可以充分利用多 CPU 并行的能力,有参和无参的区别就是设置并行级别的数量,无参的默认使用当前系统的CPU核数,即如果当前系统有2个CPU,则目标并行级别就设置为4,相当于有参方法传入4。这是目前我大概了解到的,还没有去深入学习,有兴趣的可以深入研究一下。
通过上面的3个常用创建线程池的方法,我们发现源代码中都是 new ThreadPoolExecutor(...)来创建线程池的,所以说只要掌握了这个类的构造方法的实质,线程池你掌握的也就算可以了,毕竟不能光是会用,得造火箭啊!哦,不对,是得了解底层原理,学习设计思想。
线程池各个参数的意义:

ThreadPoolExecutor(
                   // 核心线程数,指当前线程池中活跃的线程数
                   int corePoolSize, 
                   // 线程池所允许的最大线程数
                   int maximumPoolSize, 
                   // 线程池中除去工作线程之外其他线程的存活时间
                   long keepAliveTime,
                   // 存活时间的单位
                   TimeUnit unit, 
                   // 阻塞队列,已提交但尚未被执行的任务将在阻塞队列等待被执行
                   BlockingQueue<Runnable> workQueue, 
                   // 表示生成线程池中工作线程的线程工厂,用于创建线程,一般使用默认即可
                   ThreadFactory threadFactory,
                   // 拒绝策略,当工作线程等于线程池的最大线程数并且队列也满了的时候采用何种拒绝策略
                   RejectedExecutionHandler handler)

线程池底层原理(请结合 execute(Runnable command) 方法源代码):
我们使用下面这样一组参数来分析
new ThreadPoolExecutor(2, 5, 100L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(4), Executors.defaultThreadFactory(), new AbortPolicy());

ThreadPoolExecutor.png
  1. 在创建了线程池后,等待提交过来的任务请求;
  2. 当调用 execute() 方法添加一个请求任务时,线程池会做出如下判断:
    2.1 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    2.2 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入阻塞队列,等待被调度;
    2.3 如果这时候队列满了并且正在运行的线程数量还小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    2.4 如果队列满了并且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行;
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行;
  4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉,简而言之即线程池执行完所有的任务后它的大小会收缩到 corePoolSize 大小。

然后简单说一下4中拒绝策略。

  • AbortPolicy:直接抛出 RejectedExecutionException 异常来阻止系统正常运行;
  • CallerRunsPolicy:该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量;
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中再次提交当前任务;
  • DiscardPolicy:直接丢弃任务,不予任何处理也不抛任何异常,如果允许任务丢失,这是最好的一种方案。

写在最后: 以下内容来自阿里巴巴编程规范
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

这篇文章的分享到此结束,可能写的有点乱,说的也不是很清晰,但也是本人学习到的一点知识,做一简要记录,方便实时翻阅,查漏补缺嘛!

相关文章

网友评论

      本文标题:关于多线程你知道多少

      本文链接:https://www.haomeiwen.com/subject/glchgqtx.html