线程池的参数
ThreadPoolExecutor类参数:核心线程数,最大线程数(连核心数算在里面),非核心线程销毁之前等待任务的最长时间,前面时间的单位,等待队列大小,创建线程的工厂类,拒绝策略。
拒绝策略有哪几种?
- 丢弃任务,并抛出异常。
- 丢弃任务,不抛出异常。
- 丢弃等待队列等待时间最长的任务,把新任务入队列。
- 用调用任务的线程执行该任务。
任务执行顺序
提交优先级:核心线程>等待队列>非核心线程
执行优先级:核心线程>非核心线程>等待队列
如果核心线程10个,最大线程数20,等待队列10,在执行30个线程的时候。
会先执行任务1-10,然后执行任务21-30,最后执行任务11-20.
首先任务1-10会直接给核心线程执行,核心线程满了之后,任务会被放到等待队列;等待队列满了(11-20);就会把任务放到非核心线程处并创建非核心线程(21-30),这样放到非核心线程处的任务反而先执行了。如果连非核心线程处也满了,就执行拒绝策略。
线程池底层有一个workQueue(等待队列),一个HashSet结构的workers(线程池)。当一个任务提交到线程池的时候,会先判断当前线程池状态以及线程池运行的线程数量是否达到核心线程数,未达到则直接创建一个核心线程执行任务。达到了核心线程数就将线程放入等待队列,当等待队列满了之后。就会看当前线程数是否达到最大线程数,未达到就创建非核心线程执行任务。如果都不成功就执行拒绝策略。
两次创建线程执行任务调用的方法叫addWorker,通过一个布尔参数来区别是否是核心线程。
说说有几种线程池?区别?
四种,分别为newFixedThreadPool 定长线程池,newCachedThreadPool 可缓冲线程池,ScheduledThreadPool 周期线程池,newSingleThreadExecutor 单任务线程池。
- 定长线程池里面的线程都是核心线程,线程池的线程数未达到初始化线程数之前,任务到来就创建线程,一直到线程数达到初始值,就将任务访问任务队列,线程池里面的线程没有超时机制,会一直占用资源直到线程池关闭。优点是响应速度快。
- 而可缓冲线程池里面的线程都是非核心线程,最大线程数几乎没有限制(int.MAX_VALUE),任务到来就创建线程,当线程空闲时间超过60s就会回收线程,适合执行时间短并且数量多的任务场景。
- 周期线程池是定长线程池,支持定时的以及周期性的任务执行。
- 单任务线程池是只有一个线程的线程池,即只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行,每次任务到来后都会进入阻塞队列,然后按指定顺序执行。
创建线程池的方式
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
- Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
- Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
- ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置。
为什么阿里巴巴不推荐使用executors创建线程池
- 创建定长线程池,等待队列的大小默认是Integer最大值,可能会导致内存溢出。
- 创建可缓冲线程池,最大线程数是Integer最大值,可能导致内存溢出。
- 使用ThreadPoolExecutor创建线程池,让开发者使用参数进行创建,这样开发者可以更加明确线程池的运行规则,规避资源耗尽的风险。
实现线程的方式
实现Runnable接口,重新run方法,用新方法创建对象作为参数用Thread创建线程,然后调用start方法。这样可以很方便数据共享,并且因为java只支持单继承,所以实现线程这种小操作最好用接口来实现。
继承Thread类,重新run方法。不推荐,继承代价高。
实现callable接口,可以获取到线程执行的结果。不过效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。
设计线程池
线程安全的容器有哪些?
Vector,Hashtable,CurrentHashMap,CopyOnWriteArraylist,CopyOnWriteArraySet,BlockingQueue
锁升级过程
- JDK1.6以前,锁只有重量级锁,这时候锁的效率比较低下。为了提升效率,JDK1.6引入了偏向锁和轻量级锁。从此锁状态就包含无锁,偏向锁,轻量级锁,重量级锁。并且四种状态会随着竞争的激烈而逐步升级,这个过程是不可逆的,也就是说锁只能升级不能降级。
- 一开始资源是无锁状态,有线程执行到同步代码块,如果发现资源处于无锁状态,就将锁升级为偏向锁(也就是资源偏向第一个获取它的线程);在锁中记录当前线程的id,代码块执行完毕之后并不会释放偏向锁,如果下次到来的线程还是该线程就直接继续执行。如果大部分时候是单线程访问代码块,这样效率就大大提高,因为去掉了加锁释放锁的过程。
- 如果另一个线程到达同步代码块,发现锁标志是偏向锁,就会查看该线程id对应的线程是否死亡,如果已经死亡(或者对应的线程不在代码块内)就将偏向锁的线程id改成当前线程id并进行代码块运行;如果未死亡并且该线程正在执行代码块就将锁升级为轻量级锁,其他未获取锁的线程就会进行自旋(自旋相当于CPU一直原地消耗),一直查看是否可以获取锁。
- 如果自旋次数超过临界值(默认10),就会将所有自旋的线程放入阻塞队列。并且将锁升级为重量级锁,后续线程来到直接将线程挂起,等待被唤醒。
- 无锁和偏向锁的锁标志位是01,通过偏向锁标志位0,1来区分;轻量级锁标志位是00,重量级锁标志位是10,GC标记是11。
- 偏向锁适用于大部分时候是同一个线程访问同步代码块的情况;轻量级锁适用于同步代码块执行时间较短的情况;重量级锁适用于同步代码块执行时间较长的情况。