除了Thread类和实现runnable接口之外,多线程的另外两种写法是Callable接口和线程池。
Callable
一个futureTask只能启用一个线程,其get方法可以接受返回值,isDone方法可以判断是否完成
相比于Ruunable接口,它还会抛出异常
代码:
class MyCallableThread implements Callable {
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName()+"进入callable接口");
try{ TimeUnit.SECONDS.sleep(2); }catch (InterruptedException e){ e.printStackTrace(); }
return 1024;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask<>(new MyCallableThread());
Thread thread = new Thread(futureTask, "AA");
Thread thread2= new Thread(futureTask,"BB");//BB不会启用线程,一个futureTask只能启用一个线程
thread.start();
thread2.start();
/*
callable接口可以有返回值
*/
while (!futureTask.isDone()){
System.out.println("等等还没算完");
try{ TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); }
}//类似于自旋锁
System.out.println(futureTask.get());//1024 get方法建议放在最后,get要求获得callable线程的计算结果,如果没有计算完成,线程就会阻塞
}
}
运行结果:
等等还没算完
AA进入callable接口
等等还没算完
1024
线程池
为什么使用,优势在哪?
线程池的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后再线程创建后启动这些认为u,如通过线程数量超过了最大数量超出数量的线程排队等候,等其他线程指向完毕,再从队列中取出任务来执行。线程池的底层类是ThreadPoolExecutor
它的主要特点为:线程复用,控制最大并发数,管理线程
其优势在于:
- 可以降低资源消耗。通过重复利用已经创建的线程降低线程创建和销毁所造成的消耗
- 提高响应速度。当任务到达时,线程池不需要重新创建就可以立即执行
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创造,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
线程池的三个常用方式:
-
Executors.newFixedThreadPool(int) 固定数目线程
public class PoolDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(3);//一个池子三个线程
//其参数是Runnable接口 也可以使用submit来实现有返回值,其接口为callable
//模拟6个用户来办理业务,每个线程每次处理一个用户
try {
for (int i = 0; i < 6; i++) {
pool.execute(()->{
System.out.println(Thread.currentThread().getName()+"办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭线程池!!
pool.shutdown();
}
}
}
pool-1-thread-1办理业务
pool-1-thread-3办理业务
pool-1-thread-2办理业务
pool-1-thread-3办理业务
pool-1-thread-1办理业务
pool-1-thread-2办理业务
-
Executors.newSingleThreadExecutor() 一个池子一个线程
-
Executors.newCachedThreadPool() 一个池子多线程可以自动扩容,适用于执行很多短期异步的小程序或者负载较轻的服务器
如果能一个线程做完就一个线程做完
public class PoolDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
//其参数是Runnable接口 也可以使用submit来实现有返回值,其接口为callable
//模拟6个用户来办理业务,每个线程每次处理一个用户
try {
for (int i = 0; i < 6; i++) {
pool.execute(()->{
System.out.println(Thread.currentThread().getName()+"办理业务");
});
try{ TimeUnit.MICROSECONDS.sleep(300); }catch (InterruptedException e){ e.printStackTrace(); }
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭线程池!!
pool.shutdown();
}
}
}
pool-1-thread-1办理业务
pool-1-thread-1办理业务
pool-1-thread-1办理业务
pool-1-thread-1办理业务
pool-1-thread-1办理业务
pool-1-thread-1办理业务
注释掉睡眠300ms后打印结果:
pool-1-thread-1办理业务
pool-1-thread-6办理业务
pool-1-thread-5办理业务
pool-1-thread-3办理业务
pool-1-thread-4办理业务
pool-1-thread-2办理业务
这三个线程池的底层全部都是ThreadPoolExecutor。
ThreadPoolExecutor的七个参数。
- corePoolSize:线程池中的核心线程数
- maximumPoolSize:线程池能容纳同时执行的最大线程数(包含了core)所以能容纳的最大线程数就是max+queue的容量,多于这个数量就执行拒绝策略
- keepAliveTime:空闲线程的存活时间,在时间达到keepAliveTime之后,多余空闲线程会被销毁到只剩下corePoolSize个线程。
- unit:keepAliveTime的单位
- workQueue:任务队列,储存已经提交但是没有被执行的任务
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可
- handler:拒绝策略,当队列满了,并且工作线程大于等于线程池的最大线程数的时候,我们要怎么处理这些多于的线程
流程梳理,如果现在有大量任务涌入线程池:
- 核心线程---》阻塞队列---》最大线程数(新来的线程直接抢占最大线程数,不进入阻塞队列)---》拒绝策略
- 银行柜台今日当值--》候客区--》开放其他柜台--》给爷爬
总结起来就是候客区的人是笨比,宁可在外面站久一点等到最大线程开启,也不要进入阻塞队列
四种拒绝策略
- AbortPolicy(默认):直接抛出RejectedExecutionException异常组织系统正常运行
- CallerRunsPolicy:“调用者运行”一种调节机制,既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新的任务流量。比如main线程分配了任务,那么就会把这个任务返回给main线程去执行
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
- DiscardPolicy:直接丢弃任务,不与任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案
面试题:工作中三种线程池哪个用的最多?一个都不用,在生产上只能使用自定义的
FixedThreadPool和SingleThreadPool 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM,CachedThreadPool和ScheduledThreadPool可能会创建大量的线程,从而导致OOM。生产中应该手写ThreadPoolExecutors
面试题:如何合理配置线程池?
先调用这个方法直到自己的电脑是几核CPU
System.out.println(Runtime.getRuntime().availableProcessors());//我这里是12
CPU密集型:任务需要大量的运算,没有阻塞,CPU密集型任务配置尽可能的减少线程数量一般公式是CPU核数+1个线程的线程池
IO密集型(两个方案):任务需要大量的IO,就是大量的阻塞,在单线程上运行IO会导致浪费大量CPU的计算能力,所以在IO密集型任务中多线程可以大大加速程序的运行
由于IO密集型任务线程并不是一直在执行任务,应该配置经可能多的线程:CPU核心数*2
另一个参考公式是CPU/(1-阻塞系数) 阻塞系数在0.8-0.9之间,比如12/(1-0.9)=120个线程数
死锁编码
死锁:两个或者或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果没有外力干涉他们就会无法推进下去。先来看一个死锁范例:
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lock1";
String lockB = "lock2";
new Thread(new HoldLockThread(lockA, lockB), "A").start();
new Thread(new HoldLockThread(lockB, lockA), "B").start();
}
}
class HoldLockThread implements Runnable {
private String lockA;
private String lockB;
public HoldLockThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
//让线程拿到锁
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "持有" + lockA + "尝试获得" + lockB);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//让线程卡住
synchronized (lockB) {
System.out.println("爷根本没有被锁住,哈哈");
}
}
}
}
A持有lock1尝试获得lock2
B持有lock2尝试获得lock1
如何查看死锁位置?
jps -l命令查看定位进程号
jstack查看进程错误