线程的创建方式与线程池的使用


前言

前面几天忙着写论文好久没更新文章了,不能这么懈怠,还是得随时进行学习的反馈。虽然前面的坑还没填完,但我现在又准备开新坑了o( ̄︶ ̄)o。目前计划开个新专栏专门整理一下java.util.concurrent,Java并发包,也当做是复习,这一篇呢就是这个新系列的第一篇文章,话不多说,直接开冲!


一、创建线程有哪几种方式?

1、继承Thread类

  • 定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。
  • 创建Thread子类的实例,即创建了线程对象。
  • 调用线程对象的start()方法来启动该线程。
public class MyThread extends Thread{//继承Thread类
  public void run(){
  //重写run方法
  }
}
 
public class Main {
  public static void main(String[] args){
    new MyThread().start();//创建并启动线程
  }
}

2、实现Runnable接口

  • 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体。
  • 创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象为线
    程对象。
  • 调用线程对象的start()方法来启动该线程。
public class MyThread2 implements Runnable {//实现Runnable接口
  public void run(){
  //重写run方法
  }
}
 
public class Main {
  public static void main(String[] args){
    //创建并启动线程
    MyThread2 myThread=new MyThread2();
 
    Thread thread=new Thread(myThread);
 
    thread().start();
  }
}

3、使用Callable和Future创建线程

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的实例。
  • 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
  • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
public class CallableThreadTest implements Callable<String> {
    public static void main(String[] args) {
        CallableThreadTest mt = new CallableThreadTest();
        FutureTask<String> ft = new FutureTask<>(mt);
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + " ---- " + i);
            if (i == 10) {
                new Thread(ft, "子线程1").start();
            }
        }

        try {
            System.out.println("子线程返回值:" + ft.get());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @Override
    public String call() throws Exception {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + " ---- " + i);
        }
        return "This is a Test!";
    }

}

4、继承Thread类、实现Runnable、Callable接口的方式创建多线程的区别

通过继承Thread类、实现Runnable接口、实现Callable接口都可以实现多线程,不过实现Runnable接口或Callable接口的方式,还可以继承其他类,在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况;

但是如果需要访问当前线程,则必须使用Thread.currentThread()方法,相反采用继承Thread类的方式创建多线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。

实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常。

二、线程池

系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。

对于线程池,我的理解是,它是一个对线程进行管理的中介,在一些多线程背景下的操作,如果每来一个新的任务都去创建一个新的线程去进行处理,这样过多的线程会去占用系统的内存,另外也涉及线程创建和销毁带来的开销,这样的开销是我们不想看到的,所以我们需要线程池这样一个中介去对我们的线程进行一个管理。

线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个空闲的线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()或call()方法。

三、线程池构造函数的参数

前面刷牛客网面经的时候有一道题我记得就是说说线程池的创建有哪些参数,这里主要是下面表格记录的七个参数

参数名类型含义
corePoolSizeint线程池初始化时,默认的线程数是0,当有任务提交后,开始创建核心线程去执行任务
maximumPoolSizeint当线程数达到核心线程数时且任务队列满了后,开始创建非核心线程执行任务。线程池能够容纳同时执行的最大线程数
keepAliveTimelong多余的空闲线程存活时间,当空间时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止。
unit-keepAliveTime的单位
workQueue-任务队列,被提交但尚未被执行的任务
threadFactory-表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可
handler-拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示数(maxnumPoolSize)时如何来拒绝请求执行的runnable的策略

前面四个参数都很好理解,这里主要说一下后面三个参数。

workQueue

主要介绍一下5种阻塞队列。

ArrayBlockingQueue是基于数组的有界阻塞队列,按照FIFO排序,新来的队列会放到队列尾部,有界的数组可以防止资源被耗尽问题,当线程达到了核心线程数,再来任务的时候就放到队列的尾部,当队列满了的时候,则继续创建非核心线程,如果线程数量达到了maxPoolSize,则会执行拒绝策略。

LinkedBlockingQueue是基于链表的无界阻塞队列(最大容量是Integer.MAX),按照FIFO排序,当线程池中线程数量达到核心线程数时,继续来了新任务会一直存放到队列中,而不会创建新线程.因此使用此队列时,maxPoolSize是不起做的。

SynchronousQueue是一个不缓存任务的阻塞队列,当来了新任务的时候,不会缓存到队列中,而是直接被线程执行该任务,如果没有核心线程可用就创建新线程去执行任务,达到了maxPoolSize时,就执行拒绝策略。

PriorityBlockingQueue是一个具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

DelayedWorkQueu队列的特点是内部的任务并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”数据结构.而且它也是一个无界队列。

threadFactory

ThreadFactory是一个线程工厂,负责生产线程去执行任务,默认的线程工厂,创建的线程会在同一个线程组,并且拥有一样的优先级,且都不是守护线程,我们也可自定义线程工厂,以便给线程自定义名字。

handler

拒绝策略是当线程池中任务达到了队列最大容量,且线程数量也达到了最大maxPoolSize的时候,如果继续有新任务来了,则执行这个拒绝策略来处理新来的任务,系统默认的拒绝策略有以下4种。

AbortPolicy:为线程池默认的拒绝策略,该策略直接抛异常处理。

DiscardPolicy:直接抛弃不处理。

DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试把这次拒绝的任务放入队列。

CallerRunsPolicy:将任务分配给当前执行execute方法线程来处理,就是谁提交的任务,谁负责执行任务,这样任务不会丢失,而且执行任务比较费时,那么提交任务的线程也会被占用,就可以减缓任务提交速度。

四、线程池处理任务流程

当提交任务后,线程池首先会检查当前线程数,如果当前线程数小于核心线程数,则新建核心线程数量的线程并执行任务。随着任务不断增加,线程数达到了核心线程数的数量,此时任务依然在增加,那么新来的任务将会放到workQueue等待队列中,等核心线程执行完任务后重新从队列中提取出等待被执行的任务。如果已经达到了核心线程数,且任务队列也满了,则线程池就会继续创建线程来执行任务,如果任务不断提交,线程池会持续创建线程直到达到maximumPoolSize最大线程数,当达到了最大线程数后,任务仍不断提交,那么此时就超过了线程池的最大处理能力,这个时候线程池就会拒绝处理这些任务,处理策略就是handler。
在这里插入图片描述

五、如何自定义合适的线程池

首先,如果我们直接使用JDK封装好的构造函数自动创建线程会发生什么,答案是可能由于使用无界工作队列导致任务堆积或者直接创建新的线程转发任务导致过多线程资源,发生内存溢出的问题。

如果自定义合适的线程池呢?首先我们要调整线程池中的线程数量以便充分并合理的使用CPU和内存资源,从而最大限度的提高性能。

CPU密集型任务

如果任务是一些列比较消耗CPU资源的任务,比如加密、解密、压缩、计算等,那么最佳线程数是CPU核心数的1~2倍,过多很导致占用大量CPU资源,这时每个CPU的核心工作基本都是满负荷,设置过多的线程会造成不必要的上下文切换,而导致性能下降,而且在同一台机器上,我们还要考虑到其他会占用较多CPU资源的程序运行,然后做整体平衡。

耗时IO任务

例如数据库、文件的读写,网络通信等任务,这种任务的特点是不会消耗很多CPU资源,但是IO操作很费时.这个时候可以设置最大线程数一般会大于CPU核心线程数很多倍,因为IO速度相比于CPU速度比较慢,我们设置较少的线程数,就会浪费CPU资源,如果设置更多的线程数,那么一部分线程正在等待IO的时候,他们此时不需要CPU计算,就能有更多线程去执行IO操作,任务队列中的等待任务就会减少,更合理的利用了资源。

java并发编程实战中有推荐:线程数 = CPU核心数 *(1+平均等待时间/平均工作时间),我们可以通过这个式子计算出一个合理的线程数量,同时也可以根据进行压测、监控jvm的线程情况等方式,确定线程数,更合理的利用资源。

总结以上特点可以得出以下几点:

线程的平均工作时间所占比例越高,就需要越少线程

线程的平均等待时间所占比例越高,就需要越多的线程

六、如何正确关闭线程池

先介绍线程池的五种状态, 分别是:

  1. RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务。
  2. SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。
  3. STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态。
  4. TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
  5. TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。
    在这里插入图片描述

采用shutdown()关闭线程:

void shutdown()

它可以安全的关闭一个线程池,调用shutdown()方法后,线程池不会立刻关闭,而是等执行完正在执行的任务和队列中等待的任务后才彻底关闭,而且调用shutdown()方法后,如果还有新的任务继续到来,那么线程池会根据拒绝策略直接拒绝后面来的新任务.

boolean isShutdown()

这个方法可以返回ture或者false来判断是否已经开始了关闭工作,也就是是否执行了shutdown或者shutdownNow方法,调用isShutdown()方法后如果返回true,并不代表线程池已经彻底关闭了,仅仅代表开始了关闭流程,仍然可能有线程正在执行任务,队列里也可能有任务等待被执行.

boolean isTerminated()

这个方法可以检测是否真正关闭了,不仅代表线程池是否已经关闭,同时也代表线程池中的所有任务是否已经都执行完毕,比如已经调用了shutdown()方法,但是有一个线程正在执行任务,则此时调用isShutdown方法返回true,而调用isTerminated方法便返回false,因为线程池中还有任务再执行,线程池没有真正关闭,直到所有线程都执行完毕,任务都执行完毕,再调用isTermainted就返回ture.

七、线程池复用原理,源码分析

线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中,不停地检查是否还有任务等待被执行,如果有则直接去执行这个任务,也就是调用任务的 run 方法。

public void execute(Runnable command) { 
    //如果传入的Runnable的空,就抛出异常
    if (command == null) 
        throw new NullPointerException();
    int c = ctl.get();
    /**
    * addWorker 方法的主要作用是在线程池中创建一个线程并执行
    * 第一个参数是传入的任务,它的第二个参数是个布尔值,这里的布尔值的含义是以核心线程数为界限还是以最大线程数为界限进行是否新增线程的判断
    * addWorker() 方法如果返回 true 代表添加成功,如果返回 false 代表添加失败
    */
    if (workerCountOf(c) < corePoolSize) { 
        if (addWorker(command, true)) 
            return;
        c = ctl.get();
    } 
    /**
    * 当前线程数大于或等于核心线程数或者 addWorker 失败了
    * 检查线程池状态是否为 Running
    * 如果线程池状态是 Running 就把任务放入任务队列中;
    * 如果线程池已经不处于 Running 状态,说明线程池被关闭,那么就移除刚刚添加到任务队列中的任务,并执行拒绝策略。
    */
    if (isRunning(c) && workQueue.offer(command)) { 
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command)) 
            reject(command);
    /**
     当任务被添加进来之后就需要防止没有可执行线程的情况发生(比如之前的线程被回收了或意外终止了),所以此时如果检查当前线程数是否为0
     */
        else if (workerCountOf(recheck) == 0) 
            addWorker(null, false);
    } 
    /**
    线程池不是 Running 状态或线程数大于或等于核心线程数并且任务队列已经满了,所以此时需要添加新线程,直到线程数达到“最大线程数”
    */
    else if (!addWorker(command, false)) 
        reject(command);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值