在讨论线程池的问题之前先说一个面试重点问题synchronized和Lock的区别:
1 synchronized是关键字加在方法和代码块上,Lock是接口,有自己的实现类;
2 synchronized不需要手动释放锁,当代码执行完毕的时候自动会将锁释放掉,而Lock需要有unLock的过程;
3 synchronized是非公平锁,Lock可以是公平锁也可以是非公平锁,默认是非公平的;
4 锁可以通过绑定多个condition,来分组唤醒线程;
文章目录
1 线程的几种实现方式
Java多线程实现方式主要有四种:继承Thread类、实现Runnable接口、实现Callable接口。
1.1 继承Thread类创建线程
public class MyThread extends Thread {
//重写run()
public void run() {
System.out.println("MyThread.run()");
}
}
//创建线程 调用start()方法启动
MyThread myThread1 = new MyThread(); myThread1.start();
MyThread myThread2 = new MyThread(); myThread2.start();
1.2 实现Runnable接口创建线程
public class MyThread extends OtherClass implements Runnable {
//重写run()方法
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
1.3 实现Callable接口通过FutureTask包装器来创建Thread线程
public interface Callable<V> {
V call() throws Exception;
}
public class SomeCallable<V> extends OtherClass implements Callable<V> {
@Override
public V call() throws Exception {
return null;
}
}
Callable<V> oneCallable = new SomeCallable<V>();
//使用Callable<Integer>创建一个FutureTask<Integer>对象:
FutureTask<V> oneTask = new FutureTask<V>(oneCallable);
//注释:FutureTask<Integer>是一个包装器,它通过接受Callable<Integer>来创建,它同时实现了Future和Runnable接口。
//由FutureTask<Integer>创建一个Thread对象:
Thread oneThread = new Thread(oneTask);
oneThread.start();
1.4 方法对比
实现Runnable接口比继承Thread类所具有的优势:
1 适合多个相同的程序代码的线程去处理同一个资源
2 可以避免java中的单继承的限制
3 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的
2 线程池
2.1 优势
与各种池技术的优势基本相同。
(1)、降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)、提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
2.2 线程池工作原理
2.3 3种实现方式
为啥不能使用Exectors创建线程池?首先Excutors会很耗系统资源,线程最大值比较大,或者堆积的请求队列对系统的系统资源的消耗,容易出现OOM,那么如何创建呢?使用ThreadPoolExecutor。
这部分认真学习一下《并发编程的艺术》第十章
2.3.1 FixedThreadPool
2.3.2 SingleThreadExecutor
2.3.3 CachedThreadPool
2.4 7 大参数与四种拒绝策略
1 corePoolSize 线程池核心线程大小
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会 被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
2 maximumPoolSize 线程池最大线程数量
一个任务被提交到线程池后,首先会缓存到工作队列(后面会介绍)中,如果工作队列满了,则会创建一个新线程,然后从工作队列中的取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize来指定。
3 keepAliveTime 空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
4 unit 空间线程存活时间单位
keepAliveTime的计量单位
5 workQueue 工作队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
6 threadFactory 线程工
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
7 handler 拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
①CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
②AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
③DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
④DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
2.5 合理地配置线程池
要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析:
-
任务的性质:CPU密集型任务、IO密集型任务、混合型任务。
-
对于不同性质的任务来说,CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程数,IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1,而对于混合型的任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。
-
任务的优先级:高、中、低。
优先级不同的任务可以采用优先级队列来处理 -
任务的执行时间:长、中、短。
执行时间不同的任务可以交给不同的线程池来处理,或者使用优先级队列,进行短作业优先 -
任务的依赖性:是否依赖其他系统资源,如数据库连接等。
若任务对其他系统资源有依赖,如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU。