我们知道,java中实现多线程的方式有好几种,下面我们就来看看Callable接口
代码很简单:
// 实现Callable接口
class Demo implements Callable<Integer> {
@Override
public Integer call() {
System.out.println("come in Callable");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1024;
}
}
用法也很简单
// 声明一个FutureTask接口才可以执行实现Callable接口的类
// FutureTask可以复用,如果有多个线程调用了futureTask,只会执行一次,
// 如果要执行多次,那么就需要声明多个FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(new Demo());
Thread thread = new Thread(futureTask, "a");
thread.start();
我们要强调的重点是它和Runnable接口的区别
// 如果a线程没有运行完,那么就等着
while (!futureTask.isDone()) {
// Callable可以获取到返回值,但是Runnable没有办法获取到返回值
// 两个线程:一个main线程,一个a线程,
try {
int result01 = 123;
// 如无必要,建议放到最后,要求获得a线程的运行结果,
// 如果没有获取到,就会阻塞线程,直到获取到结果,所以放到最后,可以防止线程阻塞
System.out.println(futureTask.get() + result01);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
首先,我们要知道,我们的电脑是一个cpu对应多个核心数,具体怎么理解呢?可以参考下面的说明
核,你可以想象成人脑子,一个核就是一个脑子,四核就说明CPU有四个脑子,脑子越多解决问题的速度越快,Intel的核技术,可以把一个核模拟分成两个线程来用,充分的发挥了cpu的性能,线程8就代表核心数是4核的处理器可以模拟出8个线程来使用。线程我们可以理解每个大脑同时能处理多少件事。
// 查看当前电脑是几核的
System.out.println(Runtime.getRuntime().availableProcessors());
而我们日常生产环境中,基本不会单一的创建单个线程,而是通过线程池的方式帮助我们去创建和管理线程,那么线程池的作用和好处有哪些呢?
线程池的作用:控制运行的线程数量
线程池的特点:线程复用,控制最大并发数量,管理线程
线程池的处理过程:先将任务放入队列,在线程创建后启动这些任务,如果线程数量超过了最大数量,那么超出的数量线程排队等候,
等待其他线程执行完成后,再从队列中取出任务来执行
线程池的好处:
1、降低资源消耗。通过重复利用已经创建的线程降低线程创建和消耗造成的消耗
2、提高响应速度。当任务到达时,不需要等待线程创建就能立即执行
3、提高线程的可管理性,线程是稀缺资源,如果无限的创建,不仅会消耗系统资源,且还会降低系统的稳定性,使用线程池可以进行同一的分配,调优和监控
4、底层都是ThreadPoolExecutor
常见的线程池的种类:
// 线程池固定线程数量
ExecutorService service = Executors.newFixedThreadPool(3);
// 线程池内只有1个线程
ExecutorService service2 = Executors.newSingleThreadExecutor();
// 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
ExecutorService service3 = Executors.newCachedThreadPool();
try {
// execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。
// execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。
// execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService
// 实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。
for (int i = 0; i < 6; i++) {
service.execute(() -> System.out.println(Thread.currentThread().getName()));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
service.shutdown();
}
提交任务后,线程池会做如下判断:
1、如果正在运行的线程数量小于coreSize,那么会马上创建线程运行这个任务
2、如果正在运行的线程数量大于或等于coreSize,那么会将这个任务放入阻塞队列中
3、如果这个时候队列满了,且在运行的线程数量还小于maximumPoolSize,那么会创建非核心线程来立刻执行这个任务
4、如果队列满了,且在运行的线程数量大于等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
jdk内置的拒绝策略(4种):
第一种:AbortPolicy(默认),直接抛异常
第二种:CallerRunsPolicy,不抛弃任务,不抛出异常,而是将某些任务回退到调用方
第三种:DiscardOldestPolicy,抛弃队列中等待最久的任务,然后把当前任务加入到队列中再次尝试提交当前任务
第四种:DiscardPolicy,直接丢弃任务,不处理也不抛异常。如果允许任务丢失,那么这是一种最好的方案
int corePoolSize, 常驻核心数
int maximumPoolSize, 最大核心数
long keepAliveTime, 多余的空闲线程的存活时间,当线程中的线程数量超过corePoolSize时,当空闲时间到达keepAliveTime时,多余
空闲线程会被销毁直到只剩corePoolSize个线程为止
TimeUnit unit, keepAliveTime的单位
BlockingQueue workQueue, 阻塞队列,用于存储多余的任务
ThreadFactory threadFactory, 创建线程的线程工厂,一般默认就行
RejectedExecutionHandler handler, 拒绝策略
生产中我们会自定义线程池,而不用自定义的线程池,原因是会导致OOM(默认的线程池的阻塞队列大小都是Integer.MAX_VALUE,会导致OOM)
public LinkedBlockingDeque() {
this(Integer.MAX_VALUE);
}
自定义线程池:
ExecutorService executorService = new ThreadPoolExecutor(3, 5, 2, TimeUnit.SECONDS, new LinkedBlockingDeque<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
如何配置线程池的数量?
1、首先要调用Runtime.getRuntime().availableProcessors()知道当前服务器的核数
2、根据我们的业务确定我们的业务属于哪种业务类型:
cpu密集型:
意思是该任务需要大量的运算,而没有阻塞,cpu一直全速运行,cpu密集任务只有在真正的多核cpu上才可能得到加速(通过多线程),
而单核cpu上,无论你开几个模拟器的多线程任务都不可能得到加速,因为cpu的总运算能力就那些
cpu密集型任务配置尽可能少的线程数量:
一般公式:cpu核数+1个线程的线程池
IO密集型:
IO密集型,即该任务需要大量的IO,即大量的阻塞,在单线程上运行IO密集型的任务会浪费大量的cpu运算能力浪费在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核cpu上,这种加速主要就是利用了被浪费掉的阻塞时间
由于IO密集型任务并不是一直在执行任务,则应配置尽可能多的线程,如:cpu核数*2
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数 / (1 - 阻塞系数 )(阻塞系数在0.8~0.9之间)
比如8核cpu:8 / (1 - 0.9) = 80个线程
死锁编码和定位分析
死锁是指两个或者两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果没有外力干涉,那么它们都将无法推进下去,
如果系统资源充足,进程的资源请求都能得到满足,死锁出现的可能性就很低,否则就会因为争夺有限的资源而陷入死锁
简单的死锁代码:
class HoldThread implements Runnable {
private final String lock1;
private final String lock2;
public HoldThread(String lock1, String lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + "\t" + "持有" + lock1 + "尝试获得" + lock2);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + "\t" + "持有" + lock2 + "尝试获得" + lock1);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args) {
String lock1 = "lock1";
String lock2 = "lock2";
new Thread(new HoldThread(lock1, lock2), "a").start();
new Thread(new HoldThread(lock2, lock1), "b").start();
}
}
死锁的排查:
windows下的java运行程序也有类似ps的查看进行的命令,但是我们需要查看的只是java
jps = java版本的ps
一般来说,jps的进程号都会以文件的形式存储在Temp目录,比如我的电脑win10的路径就是C:\Users\qxf\AppData\Local\Temp\hsperfdata_qxf
我们可以通过 jps -l 查看类名,然后再用 jstack 进程号 查看堆栈信息
如果提示:Found 1 deadlock.
那么就是产生死锁了
Found one Java-level deadlock:
=============================
"b":
waiting to lock monitor 0x000001b724b29c78 (object 0x00000000d6202368, a java.lang.String),
which is held by "a"
"a":
waiting to lock monitor 0x000001b724b2c458 (object 0x00000000d62023a0, a java.lang.String),
which is held by "b"
Java stack information for the threads listed above:
===================================================
"b":
at thread3.HoldThread.run(DeadLockDemo.java:25)
- waiting to lock <0x00000000d6202368> (a java.lang.String)
- locked <0x00000000d62023a0> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"a":
at thread3.HoldThread.run(DeadLockDemo.java:25)
- waiting to lock <0x00000000d62023a0> (a java.lang.String)
- locked <0x00000000d6202368> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
找到原因后,重启系统,找到对应的业务逻辑修改后,重启就能解决死锁