个人成长博客

纸上得来终觉浅,绝知此事要躬行

0%

Java线程与线程池

概述

进程是资源分配的最小单位,线程是CPU调度的最小单位。进程是抢占处理机的调度单位,线程属于某个进程,共享其资源。多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。所谓多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程。

Java线程

线程的两种创建方式

  1. 对 Thread 类进行派生并覆盖 run方法,调用start方法会创建一个新的子线程并启动,直接调用run方法只是一个普通方法调用;
  2. 通过实现Runnable接口创建;

如何处理线程的返回值

  1. 主线程等待法(在主线程编写等待逻辑)

  2. 使用Thread类的join()阻塞当前线程以等待子线程处理完毕

  3. 通过Callable接口实现:通过Future Or 线程池获取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import java.util.concurrent.Callable;

    public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception{
    String value="test";
    System.out.println("Ready to work");
    Thread.currentThread().sleep(5000);
    System.out.println("task done");
    return value;
    }

    }

    Futrue实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;

    public class FutureTaskDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    FutureTask<String> task = new FutureTask<String>(new MyCallable());
    new Thread(task).start();
    if(!task.isDone()){
    System.out.println("task has not finished, please wait!");
    }
    System.out.println("task return: " + task.get());
    }
    }

    线程池实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;

    public class ThreadPoolDemo {

    public static void main(String[] args) {
    ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
    Future<String> future = newCachedThreadPool.submit(new MyCallable());
    if(!future.isDone()){
    System.out.println("task has not finished, please wait!");
    }
    try {
    System.out.println(future.get());
    } catch (InterruptedException e) {
    e.printStackTrace();
    } catch (ExecutionException e) {
    e.printStackTrace();
    } finally {
    newCachedThreadPool.shutdown();
    }
    }
    }

关键方法的区别

Sleep和Wait的区别

  1. 基本差别:sleep是Thread类的方法,wait是Object类中的方法。使用上sleep()可以在任意地方使用,wait()只能在synchrnized方法或者synchrnied块中使用
  2. 本质区别:Thread.sleep()只会让出CPU,不会导致锁行为的改变;Obkect.wait()不仅让出CPU,还会释放已经占有的同步资源锁

notify和notifyAll的区别

wait方法等待可以通过notify和notifyAll来唤醒。介绍两者的区别前,需要了解两个概念,一个是锁池一个是等待池。

锁池:假设线程A已经拥有某个对象(不是类)的锁,而其它线程B、C想要调用这个对象的某个synchrnized方法(或者块),由于B、C线程必须先获得该对象锁的拥有权,而恰巧该对象的锁正在被A线程占有。此时B、C线程会被阻塞,进入一个地方去等待锁的释放,这个地方就是该对象的锁池。

等待池:线程A调用某个对象的wait方法,线程A就会释放该对象的锁,同时线程A进入到了该对象的等待池中,进入等待池中的线程不会区竞争该对象的锁。

notifyAll,会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会。notify只会随机选取一个处于等待池中的线程进入锁池中竞争获取锁的机会。

yield

当调用Thread.yield()方法时,会给线程调度器一个当前线程愿意让出CPU使用得提示,但是线程调度器可能会忽略这个暗示。yield方法也不会导致锁行为的改变

interrupt

调用interrupt(),通知线程应该中断了

  1. 如果线程处于被阻塞状态,那么线程将立即退出阻塞状态,并且抛出一个InterruptedException异常
  2. 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。

所以用户需要及时中断线程可以通过判断中断标志位来自行终止线程的执行。

线程的状态

线程的几种状态,以及状态转换如下:

  1. 新建(New):创建尚未启动的线程状态
  2. 运行(Runable):包含操作系统线程状态中的Running和Ready,线程对象创建后,其他线程(比如 main 线程)调用了该对象 的 start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权之后进入Running状态
  3. 无限期等待(Waiting):不会被分配CPU执行时间,需要显示
    1. 没有设置Timeout参数的Object.wait()方法
    2. 没有设置Timeout参数的Thread.join()方法
    3. LockSupport.park()方法
  4. 限期等待(Timed Waiting):在一定时间后会由系统自动唤醒
  5. 阻塞(Blocked):等待获取排它锁
  6. 结束(Terminated):已终止线程的状态,线程已经结束执行

线程池

前面提到了创建线程的两种方式,如果并发请求的数量特别多,每个线程执行的时间特别短,所以会出现频繁创建并销毁线程,如此一来会大大降低系统的效率,就会出现服务器为每个线程创建和销毁的时间和消耗的系统资源,比处理实际用户所花的时间和资源要多。因此重复利用线程,是非常有必要的。通过线程池降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗,同时可以增加对于线程的管理

线程池API

接口定义和实现类

类型 名称 描述
Executors 创建线程池的工厂类
接口 Executor 最上层接口,定义了执行任务的方法execute
接口 ExecutorService 继承了Executor接口,拓展了Callable、Future、关闭方法,具备管理执行器和任务生命周期的方法,提交任务机制更完善
接口 ScheduledExecutorService 继承了ExecutorService,支持Future并增加了定时任务相关的方法
实现类 TheadPoolExcutor 基础、标准的线程池实现
实现类 ScheduledThreadPoolExecutor 继承了TheadPoolExcutor,实现了ScheduledExecutorService中相关定时任务的方法

类的继承关系

核心参数

TheadPoolExcutor是最基础、标准的线程池实现,主要参数如下:

  1. corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程,也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程)
  2. maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数
  3. keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数
  4. workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列
  5. threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)
  6. handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略,主要策略有:
    • AbortPolicy:直接抛出异常,这是默认策略
    • CallerRunsPolicy:用调用者所在的线程来执行任务
    • DiscardOldestPolicy:丢弃队列中靠前的任务,并执行当前任务
    • DiscardPolicy:直接丢弃任务
    • 实现RejectedExecutionHandler接口的自定义handler

执行过程

TheadPoolExcutor执行executor之后如下图,首先会进入workqueue,然后创建或者使用已有的workThread工作线程来执行。

其中:

  1. 如果当前池里运行的线程数量小于corePoolSize,则创建新线程(需要获取全局锁)
  2. 如果当前的线程数量大于corePoolSize,则将任务加入BlockQueue中
  3. 如果队列已满,但是线程数小于最大线程数量,则继续添加线程(需要再次获取全局锁)
  4. 已达到最大线程数量,任务队列也已经满了,任务将被拒绝,执行拒绝策略

如何配置线程池大小

  1. CPU密集型任务:尽量将线程池设置较小一点,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换
  2. IO密集型任务:可以将线程池设置稍大,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间
  3. 混合型任务:可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失

线程池的状态

  1. RUNNING: 可以接受新任务,并且可以执行阻塞队列中的任务。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0
  2. SHUTDOWN: 不可以接受新任务,但是可以执行阻塞队列中的任务。调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN
  3. STOP: 不可以接受新任务,不执行阻塞队列中的任务,并且还能打断正在执行的任务。调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP
  4. TIDYING: 所有的任务都被终止,线程池中的任务数是0的时候线程的状态将改变成TIDYING,并且经会执行terminated()。当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING;当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING
  5. TERMINATED:线程池彻底终止,就变成TERMINATED状态。线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED

线程池的状态转换图如下:

五种类型的线程池

Executors类目前提供了5种不同的线程池:

  1. newFixedThreadPool:通过TheadPoolExcutor构建,创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。workQueue为LinkedBlockingQueue(保证线程数可控,不会造成线程过多,导致系统负载更为严重)

  2. newCachedThreadPool:通过TheadPoolExcutor构建,用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。workQueue为SynchronousBlockingQueue(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)有以下特点:

    • 试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程
    • 如果线程闲置的时间超过阈值,则会被终止移出缓存
    • 系统长时间闲置的时候,不会消耗资源
  3. newSingleThreadExecutor:通过TheadPoolExcutor构建,创建一个单线程的线程池,适用于需要保证顺序执行各个任务。如果线程异常结束,会有另一个线程取代它。workQueue为LinkedBlockingQueue

  4. newSingleThreadScheduledExecutor和newScheduledThreadPool:通过ScheduledThreadPoolExecutor构建,适用于执行延时或者周期性任务。区别在于一个是否单一线程。workQueue为DelayedWorkQueue

  5. newWorkStealingPool: JDK8中提供,通过ForkJoinPool构建,自定义workqueue,利用working-stealing算法,并行处理任务,不保证处理顺序。Fork/Join框架:把大任务分割成若干小任务并行执行,最终汇总每个小任务结果后得到大任务结果框架。当一个队列中任务完成并且另一个队列任务未完成是,会根据working-stealing算法,已完成的线程会窃取未完成队列中的任务执行。这里的任务队列一般设置为双端队列。窃取任务的线程从队尾窃取。