java线程池和中断总结

本系列文是对自己学习多线程和平时使用过程中的知识梳理,不适合基础比较差的阅读,适合看过java编程实战做整体回顾的,想到了会不断补充。

一、 线程池的使用

线程池其实在实际工作中有用到的话理解其实是非常简单的,合理的利用线程池能极大的提高效率。主要说明下程池的使用和参数的意义(暂时不考虑定时线程池):

1. corePoolSize 线程池的最小大小
2. maximumPoolSize 线程池的最大大小
3. keepAliveTime 大于线程池最小大小的空余线程的包活时间
4. workQueue 工作队列用于存放任务
5. threadFactory 创建线程的线程工厂
6. handler 用于处理任务被拒绝的情况
   public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

线程池的任务投递过程

  1. 向线程池投递一个任务时,首先看工作者线程有没有小于corePoolSize,如果是,利用threadFactory创建一个线程将任务投递
  2. 第一步如果大于corePoolSize,则将任务投递到workQueue,这里需要考虑workQueue是无界还是有界的情况,如果是无界肯定投递成功返回。如果是有界,投递成功则返回,否则看线程数有没有小于maximumPoolSize,如果是则再开一个线程返回,不是的话则调用handler的拒绝逻辑。

需要注意的点

  1. 如果放的workQueue是无界队列,那么maximumPoolSize这个参数其实就无效了,永远不会创建超过corePoolSize的线程数量,所以任务永远不会因为容量问题被拒绝,如果生产者速度一直大于消费者,很可能造成内存溢出
  2. 第1条说workQueue是无界队列那么任务永远不会因为容量问题被拒绝,但是handler还是有用的,当你关闭了线程池池,继续提交任务会用到这个来拒绝
  3. 用户代码在提交任务时底层使用的阻塞队列的offer方法,所有一般是不会阻塞的,要么成功,要么被阻绝。

关于线程池参数的设置

corePoolSize,maximumPoolSize,workQueue核心参数:

根据你的业务场景,如果是cpu密集型,可以设置线程池固定为ncpu+1,队列采用一个有界的,防止内存溢出。如果还是出现任务被拒绝是不是应该考虑加机器了。

如果是io密集型,需要根据io和cpu的比例来做相应的估算,这种也不是十分精确的,毕竟网络情况也会发生变化。这里推荐书中的公式: ncpuucpu(1+w/c) ncpu:cpu个数 ucpu:每个cpu利用率 w/c: io/cpu

另外不同类型的任务最好采用不同的线程池有利于线程池的设置,混杂的任务很难设置。

threadFactory:我主要用于设置线程的名字,这样在日志区分度更高

handler:拒绝执行器,这个得根据业务场景类

关于Executors的工具方法

alibaba编码规约中建议是手动创建线程池,这样根据自己的业务做更好的定制,而且自己也能更加的清理里面的逻辑。如果一时图快,可能在系统的负载不断升高时出现问题,反而更加不好排查。

关于线程池的优雅停机

在提高性能的同时不要忘记数据的安全性,因为线程池的特点,任务会被缓存在队列中,如果是不重要的应用可以直接将线程设置成守护线程,这样在应用停机的时候直接停机。

对于重要的应用,如果应用重启这些数据都是要考虑到的。这里就需要十分清楚中断机制,因为这里涉及任务取消的逻辑,这些逻辑是要对自己的任务代码自己进行相应的处理。线程池shutdown方法执行之后后续的任务都会被拒绝,已经有的任务会继续执行完,这个比较好理解。shutdownNow方法返回队列中的所有任务,然后发中断给正在执行的任务,这里返回的任务你可以进行持久化,主要就是正在执行的任务的处理,对于短任务你可以不响应中断,耗时任务必须得考虑进程退出时间过长被强杀。

实际应用场景举例

  1. 应用日志的记录:我们对于一些业务日志可能写到mysql中,如果每个操作插入一条日志必然会很耗时,这是我们可以单独开一个日志线程,将日志投递到日志线程对象的queue中,然后他定时扫,批量如库,既提高吞吐量,有降低延迟
  2. 第三方接口对接:在对接第三方时候,很多时候是http,这种操作相当耗时,可以利用线程池来进行异步化,如果需要得到返回接口可以利用Feature和CompletableFuture(后续讲解)
  3. 耗时的线程不安全操作:这种场景比较少,但公司确实遇到了,具体就是后台提交一个任务,这种任务可能会执行几十分钟,任务不能同时执行。这里思路就是采用单线程池,然后利用提交时返回的Future来实现任务的取消功能。

关于异常

只有execute提交的任务才会将异常交给未捕获异常处理器,默认的未捕获异常处理器会打印异常。但是如果是submit会将异常封装到饭返回的Future中在get的时候才会抛出,可以通过改写线程池的afterExecute方法。

源码中的例子:

protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t == null && r instanceof Future<?>) {
try {
Object result = ((Future<?>) r).get();
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // ignore/reset
}
}
if (t != null)
System.out.println(t);
}

二、 java中断机制

中断机制对于javq多线程编程是一个十分基础的东西,很多时候都是在使用类库所以没有注意到,对于自己更好的使用类库和自己封装类库,中断是十分重要的。对于中断处理的好坏直接影响了编写的api合理性和使用类库时正确性。

在多线程中,很多时候会遇到需要停止一个线程中的任务这样的需求。实现这样的需求很容易想到在对象中放置一个标志位,然后线程在执行的过程中去不停的检测这个标志位,如果标志位被设置成false就退出。另外标志位需要采用volatile来修饰,可以保证内存的可见性。例子如下:

public class Main {
public static void main(String[] args) throws InterruptedException {
//创建一个任务
Task task = new Task();
new Thread(task).start();
Thread.sleep(3_000);
task.stop = true;
}
} @Slf4j
class Task implements Runnable {
/**
* 是否停止的标志位
*/
public volatile boolean stop = false;
/**
* 执行次数计数器
*/
AtomicInteger adder = new AtomicInteger(); @Override
public void run() {
while (!stop) {
log.info("运行次数:{}", adder.incrementAndGet());
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.warn("退出运行!");
}
}

上面的代码是很多人一开始就能想到的方案,但是,仔细想想就会发现问题,上面的代码中的sleep函数是一个类库封装好的,所以如果设置了停止标志位,那么每次检测运行都得等到while循环才行。这就是引入中断的意义,jdk中很多函数都能响应中断的操作。

先说一下java中断的含义:java中断是一种线程间协作机制,用来告诉一个线程可以停止了,但是具体那个线程是否响应以及采取什么样的动作,java语言没有做任何强制规定。这里就需要和操作系统的中的中断明确区别,这两种虽然中文名一样,但是实际的意义却差以千里。ava的中断是协作的,相当于只是告诉一个线程,但是那个线程可以选择不响应或者需要中断。

第二个版本的代码如下:

public class MainInterrupt {
public static void main(String[] args) throws InterruptedException {
//创建一个任务
Thread thread = new Thread(new TaskInterrupt());
thread.start();
Thread.sleep(3_000);
thread.interrupt();
}
} @Slf4j
class TaskInterrupt implements Runnable {
/**
* 执行次数计数器
*/
AtomicInteger adder = new AtomicInteger(); @Override
public void run() {
while (!Thread.interrupted()) {
log.info("运行次数:{}", adder.incrementAndGet());
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
log.warn("随眠过程打断退出!");
break;
}
}
log.warn("退出运行!");
}
}

中断的api:

  1. public void interrupt() 给一个线程发起中断
  2. public static boolean interrupted() 检测线程是否处于中断位,并清除标志位,这也是唯一清除标志位的 方法
  3. public boolean isInterrupted() 检测线程是否中断

中断的处理

封装的自己类库代码的时候一定要考虑清楚对于中断的处理,例如BlockingQueue的E poll(long timeout, TimeUnit unit)throws InterruptedException这个api,他的实现都是选择了将异常抛出,底层实现一般是Lock的lockInterruptibly()方法阻塞抛出。你站在用户代码的角度去想,如果你实现了这个方法是阻塞的,然后又把异常吃了,怎么去实现被取消后的逻辑,用户代码又怎么去针对取消做相应的动作。所以封装类库的时候最好还是重新抛出。

另外还有一种就是重置中断位,有些操作不能直接抛出,像Runnable接口,还有抛出异常前执行一些动作的情况。在处理完之后重置下中断位,这样就能让用户代码感知并做相应的处理。

三、 线程间通信机制总结

这里只是简单的罗列,当时面试的时候被问到线程间通信,当时没反应过来,其实都是知道的,这一块知道这么个东西比较简单,很多需要看源码的实现,看区别。以后会进行相关的源码分析

  1. synchronize关键字
  2. 对象的wait,notify,notifyall
  3. Thread的join
  4. Semaphore,ReentrantLock,CyclicBarrier,CountDownLatch,(这些都是基于AQS的工具类)BlockingQueue
  5. FutureTask相关的

java线程池和中断总结的更多相关文章

  1. Java 线程池框架核心代码分析--转

    原文地址:http://www.codeceo.com/article/java-thread-pool-kernal.html 前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和 ...

  2. 四种Java线程池用法解析

    本文为大家分析四种Java线程池用法,供大家参考,具体内容如下 http://www.jb51.net/article/81843.htm 1.new Thread的弊端 执行一个异步任务你还只是如下 ...

  3. Java线程池的几种实现 及 常见问题讲解

    工作中,经常会涉及到线程.比如有些任务,经常会交与线程去异步执行.抑或服务端程序为每个请求单独建立一个线程处理任务.线程之外的,比如我们用的数据库连接.这些创建销毁或者打开关闭的操作,非常影响系统性能 ...

  4. Java线程池应用

    Executors工具类用于创建Java线程池和定时器. newFixedThreadPool:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程.在任意点,在大多数 nThread ...

  5. Java线程池与java.util.concurrent

    Java(Android)线程池 介绍new Thread的弊端及Java四种线程池的使用,对Android同样适用.本文是基础篇,后面会分享下线程池一些高级功能. 1.new Thread的弊端执行 ...

  6. Java 线程池框架核心代码分析

    前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和资源消耗都是很高的.线程池应运而生,成为我们管理线程的利器.Java 通过Executor接口,提供了一种标准的方法将任务的提交过 ...

  7. java线程池的使用与详解

    java线程池的使用与详解 [转载]本文转载自两篇博文:  1.Java并发编程:线程池的使用:http://www.cnblogs.com/dolphin0520/p/3932921.html   ...

  8. java线程池分析和应用

    比较 在前面的一些文章里,我们已经讨论了手工创建和管理线程.在实际应用中我们有的时候也会经常听到线程池这个概念.在这里,我们可以先针对手工创建管理线程和通过线程池来管理做一个比较.通常,我们如果手工创 ...

  9. java 线程池 并行 执行

    https://github.com/donaldlee2008/JerryMultiThread/blob/master/src/com/jerry/threadpool/ThreadPoolTes ...

随机推荐

  1. 常见编码GBK、GB2312、UTF-8、ISO-8859-1的区别

    https://blog.csdn.net/shijing_0214/article/details/50908144 在项目开发中,会经常遇到不同的编码方式.不管什么编码,都是信息在计算机中的一种表 ...

  2. SEO高级技巧

    原文地址:http://www.it28.cn/sousuoyinqing/853115.html 现在提起SEO来最少能让一部人感到痛苦,为什么呢,因为他们看不到希望,他们追求的永远是排名,其实SE ...

  3. MySQL数据库导入错误:ERROR 1064 (42000) 和 ERROR at line xx: Unknown command '\Z'.

    使用mysqldump命令导出数据: D:\Program Files\MySQL\MySQL Server 5.6\bin>mysqldump -hlocalhost -uroot -proo ...

  4. javasscript基础

    一.使用JS完成注册表单数据校验 1.需求分析 用户在进行注册的时候会输入一些内容,但是有些用户会输入一些不合法的内容,这样会导致服务器的压力过大,此时我们需要对用户输入的内容进行一个校验(前端校验和 ...

  5. Kotlin入门(11)江湖绝技之特殊函数

    上一篇文章介绍了Kotlin对函数的输入参数所做的增强之处,其实函数这块Kotlin还有好些重大改进,集中体现在几类特殊函数,比如泛型函数.内联函数.扩展函数.尾递归函数.高阶函数等等,因此本篇文章就 ...

  6. 用Python实现数据结构之映射

    映射与字典 字典dict是Python中重要的数据结构,在字典中,每一个键都对应一个值,其中键与值的关系就叫做映射,也可以说是每一个键都映射到一个值上. 映射(map)是更具一般性的数据类型,具体到P ...

  7. Linux远程访问及控制(SSH)

    1.ssh协议:用于远程登录,端口号:22/tcp 配置文件: 1)服务器端口:/etc/ssh/sshd_config 2)客户端 :/etc/ssh/ssh_config 2.服务器监听选项: U ...

  8. mysql中case的一个例子

    最近遇到一个问题: year amount num 1991 1 1.1 1991 2 1.2 1991 3 1.3 1992 1 2.1 1992 2 2.2 1992 3 3.3 把上面表格的数据 ...

  9. NGINX 安装于配置

    just a simple example, for more information -> http://nginx.org/en/docs/.1.vi /etc/yum.repos.d/ng ...

  10. 控件布局_TableLayout

    <?xml version="1.0" encoding="utf-8"?> <TableLayout xmlns:android=" ...