作为面试时提问的半壁江山,多线程一直是一大考点,写篇详细的全解,等面试的时候吊打面试官!加油,奥利给!
多线程全解
一、线程的几种状态
- New 新建状态
- Runnable 运行状态
- Blocked 阻塞状态(.sleep 抱着锁睡;.await 放开锁等待;)
- Waiting 等待(死等,不见不散)
- Timed_Waiting 等待(等一会儿,过时不候)
- Terminated 结束
二、创建线程的几种方式
-
继承Thread类
-
实现Runnable接口,实现run()方法(最简单,最常用)
示例:new Thread(()->{}, String.valueOf(1)).start(); -
实现Callable接口,实现call()方法(使用一个FutureTask类做中间类来实现)
-
使用线程池获取线程(阿里规范推荐)
三、线程池获取线程
获取线程池
1.定长线程池,指定线程池中的线程个数
ExecutorService executorService = Executors.newFixedThreadPool(5);
缺点:队列最大值为Integer.MAX_VALUE。如果任务提交速度持续大余任务处理速度,会造成队列大量阻塞。因为队列很大,很有可能在拒绝策略前,内存溢出。
适用场景:可用于Web服务瞬时削峰,但需注意长时间持续高峰情况造成的队列阻塞。
2.单线程池,线程池中仅维护一个线程
ExecutorService executorService = Executors.newSingleThreadExecutor();
说明:单线程池其实就是长度为1的定长线程池!并在其代码中加了一层FinalizableDelegatedExecutorService包装,使他不能转型为ThreadPoolExecutor保证线程是‘单一’的
3.自适应线程池,线程池根据线程的使用量自动创建和回收线程
ExecutorService executorService = Executors.newCachedThreadPool();
缺点:maximumPoolSize = Integer.MAX_VALUE,即线程数量几乎无限制,容易造成内存溢出。
适用场景:快速处理大量耗时较短的任务,如Netty的NIO接受请求时,可使用CachedThreadPool。
4*.自定义线程池,使用5参构造或7参构造新建 ThreadPoolExecutor(),阿里规范推荐此方式创建线程。
ExecutorService executorService = new ThreadPoolExecutor(2, 5,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
说明:通过自定义线程池,我们可以更好的让线程池为我们所用,更加适应我的实际场景。
注意,无论是使用哪种线程池,用完都要关掉!
try{
多线程使用.....
}catch{
}finally{
executorService.shutdown();
}
通过线程池获取、使线程
1.execute()
executorService.execute(()->{System.out.println(Thread.currentThread().getName()); });
2.submit()
Future future = executorService.submit(()->{System.out.println(Thread.currentThread().getName()); });
3.区别
submit方法其实底层使用的还是execute方法,只不过在之上封装了返回值,可以通过future.get()获取线程执行结果【注意:如果future.get()==null,则说明线程正常结束】,由于需要自己调结果不会抛出异常,有时候会导致线程被挂起而不报错的情况;
execute是一个返回void方法,但是他在运行异常时会直接抛出异常;
创建线程池的参数分析
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
1、corePoolSize:线程池中的核心线程数;
2、maximumPoolSize:线程池能容纳的最大线程数,必须大于等于1;
3、keepAliveTime:多余线程的存活时间,当前线程池中的线程数大于核心线程数corePoolSize,且空闲时间达到存活时间keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止;
4、TimeUnit :keepAliveTime的单位;
5、BlockingQueue:阻塞队列,用来存放被提交但尚未执行的任务,在此队列中等待;
6、ThreadFactory :用于生成线程池中的工作线程的线程工厂,用来创建线程,一般默认即可;
7、RejectedExecutionHandler:拒绝策略,表示当前工作线程达到了最大线程数maximumPoolSize,且阻塞队列也满了,如何来拒绝请求执行的runnable的策略。
这里有的面试官会问:怎么设计这些参数?
一般,这些参数都是根据业务来设计。其中,最大线程数maximumPoolSize,一般设置为 当前计算机处理器个数+1,即
int maximumPoolSize= Runtime.getRuntime().availableProcessors+1;
线程池的处理流程:
一个任务提交到线程池,
- 如果当前在执行的线程,少于核心线程数,那么直接取一个线程去运行;
- 如果线程池中的核心线程个数都在被使用,那么就将当前任务放到阻塞队列中;
- 如果阻塞队列也满了,但是当前线程数小于最大线程数,那么就创建新线程来处理阻塞队列中的任务,并将当前任务放入阻塞队列;
- 如果阻塞队列满了,线程池也达到了最大线程数,那么就执行拒绝策略;
- 当一个线程完成当前任务,就会从队列中取下一个任务来执行;
- 当阻塞队列中的任务被处理完了,且当一个线程无事可做的时间超过keepAliveTime时,线程池判断当前线程是否大于核心线程数,如果大于,那么这个线程将会被销毁,直到恢复到核心线程数。
拒绝策略
1、AbortPolicy:直接抛出RejectedExecutionException异常,中断系统正常运行,默认使用该策略。
2、CallerRunsPolicy:“调用者运行”一种机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的溜了。
简单来说,谁指派任务给我的,我再回退给谁,让他自己处理,例如main方法调用线程池,线程池满了,那么就由main自己来处理该任务。
3、DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常,如果当前任务允许失败,这是最好的一种策略。
3、DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中,等待再次提交当前任务。
四、JUC对于多线程的三个辅助类
CountDownLatch(计数器递减,确保有一个线程必定在最后才执行)
举例:考试。
有5个学生考试,一个老师在监考,学生可以提前交卷,但是老师必须所有学生的试卷收齐后才能走。即,5个线程在运行,第6个线程必须在5个线程执行完之后才能执行。
CyclicBarrier(计数器递增,当计数器达到多少时才执行指定线程)
举例:开会。
开发小组有5个人,约定下午两点会议室开会,可能前几个人早点到了,但是必须等最后一个人到了之后才能开始正式开会,否则一直等着。即,每一个线程执行完计数器加一,直到5个线程都执行完,开启额外的第六个线程。
Semaphore(多线程竞争多资源,用于多线程的并发控制)
举例:抢车位。
公司楼下有3个车位,但是公司里有6辆车,所以先到先得,没抢到车位的就等着别人走了才能进去。即,6个线程竞争3个资源,控制线程的并发量。