java线程池那些事
1.线程池是什么?
线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
2、线程池有什么作用?
线程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队。
3.线程池中的几种重要的参数及流程说明
线程池参数:
参考jdk源码:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
corePoolSize :核心线程数
maximumPoolSize :最大线程数
workQueue :存储等待执行的任务的工作队列
keepAliveTime :线程存活时间
unit :线程存活时间的时间单位
threadFactory :用来创建线程的线程工厂
rejectHandler :拒绝处理任务时的策略
线程执行流程:
1. 请求任务进来时,小于corePoolSize,创建新线程.
2. 大于corePoolSize,请求任务交给workQuenue来处理.
3. 当workQuenue满了时,进程数大于corePoolSize但是小于maxMumPoolSize时,创建新线程.
4. 当maxMumPoolSize满了时,请求任务交给rejectHandler进行处理
5. 当超过corePoolSize空闲时间超过keepAliveTime时,自动关闭线程.
6. 当设置了allowCoreThread为true是,corePoolSize里面的线程超时也进行关闭
4.有哪几种常见的线程池及使用场景
(1) Executors.newSingleThreadExecutor 单线程池
定义:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
底层:
参考如下源码:
//以下为Executors源码
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// 线程池构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
#从源码我们可以看出
/**
1.返回的是FinalizableDelegatedExecutorService包装的ThreadPoolExecutor实例
2. corePoolSize=1,核心线程数为1
3. maximumPoolSize=1,最大线程数为1
4. keepAliveTime=0L,线程存活时间无限
5. unit=TimeUnit.MILLISECONDS,线程存活时间单位是毫秒
6. workQueue=new LinkedBlockingQueue<Runnable>() 无界阻塞队列
**/
执行单线程池示例如下,从执行结果来看,线程池在复用同一个线程
public class Test {
private static ExecutorService simpleExecutor = Executors.newSingleThreadExecutor();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
final int index = i;
Thread.sleep(1000);
simpleExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " " + index);
}
});
}
}
}
结果如下:只有一个线程在跑,说明在复用同一个线程
原理:创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列),复用同一个线程。
应用场景:用在单个线程顺序执行的场景
(2) Executors.newFixedThreadPool 固定数量的线程池
定义:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。固定数量的线程池是每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行.
底层:
//以下为Executors源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 线程池构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
#从源码我们可以看出
/**
1.返回的是ThreadPoolExecutor实例
2. corePoolSize=n,核心线程数为n,也是Executors.newFixedThreadPool(n)
3. maximumPoolSize=n,最大线程数为n
4. keepAliveTime=0L,线程存活时间无限
5. unit=TimeUnit.MILLISECONDS,线程存活时间单位是毫秒
6. workQueue=new LinkedBlockingQueue<Runnable>() 无界阻塞队列
**/
执行固定数量线程池代码如下:
public class Test {
private static ExecutorService simpleExecutor = Executors.newFixedThreadPool(2);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 6; i++) {
final int index = i;
Thread.sleep(1000);
simpleExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " " + index);
}
});
}
}
}
结果如下:newFixedThreadPool的线程数是可以进行控制的,因此我们可以通过控制最大线程来使我们的服务器达到最大的使用率,也可以保证即使流量突然增大也不会占用服务器过多的资源。
原理:创建可容纳固定数量线程池,每隔线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)
应用场景:执行长期异步的任务,性能好很多
(3) Executors.newCachedThreadPool 可缓存线程池
定义:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
底层:
//以下为Executors源码
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
// 线程池构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
#从源码我们可以看出
/**
1.返回的是ThreadPoolExecutor实例
2. corePoolSize=0,核心线程数为0
3. maximumPoolSize=Integer.MAX_VALUE,最大线程数为Integer.MAX_VALUE
4. keepAliveTime=60L,线程存活60s
5. unit=TimeUnit.SECONDS,线程存活时间单位是秒
6. workQueue=new SynchronousQueue<Runnable>() 同步队列
**/
代码示例如下:
public class Test {
private static ExecutorService simpleExecutor = Executors.newCachedThreadPool ();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 6; i++) {
final int index = i;
Thread.sleep(1000);
simpleExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " " + index);
}
});
}
}
}
执行结果:通过分析我看可以看到,至始至终都由一个线程执行,实现了线程的复用,并没有创建多余的线程。
原理:当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。
应用场景:执行很多短期异步的小程序或者负载较轻的服务器
(4)Executors.newScheduledThreadPool 大小无限制的线程池
定义:创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。
底层:
//以下为Executors源码
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
//从源码我们可以看出
/**
1.返回的是ThreadPoolExecutor实例
2. corePoolSize=n,核心线程数为n
3. maximumPoolSize=Integer.MAX_VALUE,最大线程数为Integer.MAX_VALUE
4. keepAliveTime=0L,线程存活时间无限
5. unit=TimeUnit.NANOSECONDS,线程存活时间单位是纳秒
6. workQueue=new DelayedWorkQueue() 按时间大小排序的队列.
**/
执行代码示例如下:
public class Test {
private static ExecutorService simpleExecutor = Executors.newScheduledThreadPool(2);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 6; i++) {
final int index = i;
Thread.sleep(1000);
simpleExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " " + index);
}
});
}
}
}
结果:
原理:创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构
应用场景:周期性执行任务的场景
线程执行顺序:
1. 请求任务进来时,小于corePoolSize,创建新线程.
2. 大于corePoolSize,请求任务交给workQuenue来处理.
3. 当workQuenue满了时,进程数大于corePoolSize但是小于maxMumPoolSize时,创建新线程.
4. 当maxMumPoolSize满了时,请求任务交给rejectHandler进行处理
5. 当超过corePoolSize空闲时间超过keepAliveTime时,自动关闭线程.
6. 当设置了allowCoreThread为true是,corePoolSize里面的线程超时也进行关闭
5.线程池都有哪几种工作队列?
(1)ArrayBlockingQueue基于数组结构的有界阻塞队列
源码:
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
//默认构造非公平锁的阻塞队列
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
// 构建初始容量数组
this.items = new Object[capacity];
// 初始化ReentrantLock重入锁,出队入队拥有这同一个锁
lock = new ReentrantLock(fair);
// 初始化非空等待队列
notEmpty = lock.newCondition();
// 初始化非满等待队列
notFull = lock.newCondition();
}
//put 方法
public void put(E e) throws InterruptedException {
checkNotNull(e);//检查插入元素是否为空
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//这里并没有调用lock方法,而是调用了可被中断的lockInterruptibly,该方法可被线程中断返回,lock不能被中断返回。
try {
while (count == items.length)
notFull.await();//当队列满时,使非满等待队列休眠
insert(e);//此时表示队列非满,故插入元素,同时在该方法里唤醒非空等待队列
} finally {
lock.unlock();
}
}
原理解析:从源码可以看到,ArrayBlockingQueue对象只有有参构造方法,要声明数组初始容量大小以及是否公平锁,通过这个公平锁来阻塞插入队列,当队列满时不会返回false,也不会抛出异常,而是一直阻塞等待,直到有空位可插入
综上所以说ArrayBlockingQueue是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
(2)LinkedBlockingQueue基于链表结构的双向阻塞队列
源码:
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
transient Node<E> head;
private transient Node<E> last;
// 无参构造方法,默认容量最大为int最大值
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
// 有参构造方法,需声明容量大小
public LinkedBlockingQueue(int capacity) {
// 容量小于等于0直接抛异常
if (capacity <= 0) throw new IllegalArgumentException();
// 容量赋值
this.capacity = capacity;
// 头指针和尾指针指向头节点(null)
last = head = new Node<E>(null);
}
}
// put方法
public void put(E e) throws InterruptedException {
if (e == null) throws new NullPointerException();
int c = -1;
Node<E> node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterrupted();//能被线程中断地获取锁
try {
while (count.get() == capacity) {//队列数据量等于队列容量
notFull.await();//休眠非满等待队列上的线程
}
enqueuer(node);//入队
c = count.getAndIncrement();//队列数据总数自增+1后返回
if (c + 1 < capacity)//还没有达到队列容量
notFull.signal();//唤醒非满等待队列上的线程
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();//唤醒非空等待队列上的线程
}
原理解析:从源码可以看到,LinkedBlockingQueue的构造方法内用到了链表,也就是说它基于链表结构,同时通过重入锁来阻塞插入队列。由于链表的插入更新效率要高于数组,所以这种队列吞吐量高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这。
这是一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
(3)SynchronousQueue不存储元素的同步阻塞队列
源码:
public class SynchronousQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
// 无参构造方法
public SynchronousQueue() {
this(false);
}
// 有参构造方法
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
// 转换队列
transient volatile QNode cleanMe;
TransferQueue() {
QNode h = new QNode(null, false); // initialize to dummy node.
head = h;
tail = h;
}
// put方法
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
if (transferer.transfer(e, false, 0) == null) {
Thread.interrupted();
throw new InterruptedException();
}
}
}
原理分析:从源码可以看出SynchronousQueue是一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
(4)PriorityBlockingQueue支持优先级的无界阻塞队列
源码:
public class PriorityBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
private static final int DEFAULT_INITIAL_CAPACITY = 11;
public PriorityBlockingQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.comparator = comparator;
this.queue = new Object[initialCapacity];
}
// put方法是非阻塞的,但是操作时需要获取独占锁,如果插入元素后超过了当前的容量,会调用tryGrow进行动态扩容,接着从插入元素位置进行向上调整,插入成功后,唤醒正在阻塞的读线程。
public void put(E e) {
offer(e); // never need to block
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock; // 独占锁
lock.lock();
int n, cap;
Object[] array;
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);
try {
Comparator<? super E> cmp = comparator;
if (cmp == null) // 未设置优先级比较条件,则使用默认规则插入元素
siftUpComparable(n, e, array);
else // 若设置了优先级,则使用对应的comparator规则插入元素
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
//将x插入到堆中,注意这里是不断和父节点比较,最终找到插入位置。并使用默认优先级
private static <T> void siftUpComparable(int k, T x, Object[] array) {
Comparable<? super T> key = (Comparable<? super T>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = array[parent];
if (key.compareTo((T) e) >= 0)
break;
array[k] = e;
k = parent;
}
array[k] = key;
}
//将x插入到堆中,注意这里是不断和父节点比较,最终找到插入位置。并使用优先级规则 cmp
private static <T> void siftUpUsingComparator(int k, T x, Object[] array,
Comparator<? super T> cmp) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = array[parent];
if (cmp.compare(x, (T) e) >= 0)
break;
array[k] = e;
k = parent;
}
array[k] = x;
}
}
原理分析:从源码可以看出是一个支持优先级的无界阻塞队列,基于数组的二叉堆,其实就是线程安全的PriorityQueue。内部使用一个独占锁来同时控制只有一个线程执行入队和出队操作,只是用notEmpty条件变量来控制读线程的阻塞,因为无界队列中入队操作是不会阻塞的。
指定排序规则有两种方式:
传入PriorityBlockingQueue中的元素实现Comparable接口,自定义compareTo方法。
初始化PriorityBlockingQueue时,指定构造参数Comparator,自定义compare方法来对元素进行排序。
底层数组是可动态扩容的:先释放锁,保证扩容操作和读操作可以同时进行,提高吞吐量,接着通过CAS自旋保证扩容操作的并发安全。
(5) DelayedWorkQueue任务定时周期的延迟队列
源码:
static class DelayedWorkQueue extends AbstractQueue<Runnable>
implements BlockingQueue<Runnable> {
public void put(Runnable e) {
offer(e);
}
public boolean offer(Runnable x) {
if (x == null)
throw new NullPointerException();
RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
final ReentrantLock lock = this.lock;
lock.lock();
try {
int i = size;
if (i >= queue.length)
grow();
size = i + 1;
if (i == 0) {
queue[0] = e;
setIndex(e, 0);
} else {
siftUp(i, e);
}
if (queue[0] == e) {
leader = null;
available.signal();
}
} finally {
lock.unlock();
}
return true;
}
}
原理分析:
DelayedWorkQueue是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。
6、怎么理解无界队列和有界队列?
有界队列:就是有固定大小的队列。比如:
- 设定固定大小的 ArrayBlockingQueue
- 大小为 0,只是在生产者和消费者中做中转用的 SynchronousQueue
- 设定了固定大小的 LinkedBlockingQueue
无界队列:指的是没有设置固定大小的队列。这些队列的特点是可以直接入列,直到溢出。当然现实几乎不会有到这么大的容量(超过 Integer.MAX_VALUE),所以从使用者的体验上,就相当于 “无界”。
比如:
- 没有设定固定大小的 LinkedBlockingQueue
- 支持优先级的无界阻塞队列PriorityBlockingQueue
7、非阻塞队列有哪些?
基于锁的算法会带来一些活跃度失败的风险。如果线程在持有锁的时候因为阻塞I/O、页面错误、或其他原因发生延迟,很可能所有的线程都不能工作了。一个线程的失败或挂起不应该影响其他线程的失败或挂起,这样的算法称为非阻塞算法;如果算法的每一个步骤中都有一些线程能够继续执行,那么这样的算法称为锁自由(lock-free)算法。在线程间使用CAS进行协调,这样的算法如果能构建正确的话,它既是非阻塞的,又是锁自由的。java中提供了基于CAS非阻塞算法实现的队列,比较有代表性的有ConcurrentLinkedQueue和LinkedTransferQueue,它们的性能一般比阻塞队列的好。
1.ConcurrentLinkedQueue
ConcurrentLinkedQueue是一个基于链表的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。ConcurrentLinkedQueue的线程安全是通过其插入、删除时采取CAS操作来保证的。由于使用CAS没有使用锁,所以获取size的时候有可能进行offer,poll或者remove操作,导致获取的元素个数不精确,所以在并发情况下size函数不是很有用。
2.LinkedTransferQueue
jdk7才提供这个类,这个类实现了TransferQueue接口,也是基于链表的,对于所有给定的生产者都是先入先出的。与其他阻塞队列的区别是:其他阻塞队列,生产者生产数据,如果队列没有满,放下数据就走,消费者获取数据,看到有数据获取数据就走。而LinkedTransferQueue生产者放数据的时候,如果此时消费者没有获取,则需阻塞等待直到有消费者过来获取数据。有点类似SynchronousQueue,但是LinkedTransferQueue是被设计有容量的。LinkedTransferQueue 通过使用CAS来实现并发控制,是一个无界的安全队列。其长度可以无限延伸,当然带来的问题也是显而易见的。