线程和线程池

1. 何为线程

简单的理解就是一个进程里的细致划分,我们正常的软件只会占用一个进程,但是为了更好的榨干cpu的性能,可以将进程分为多个线程,实际处理工作的是线程!

2. 线程的状态

image

java中线程的状态

  1. 新建状态:调用new,还没有启动开始代码执行!
  2. 就绪状态:对象已经准备好,但是还没有开始执行run
  3. 运行状态:执行run方法后,程序正常执行
  4. 阻塞状态:线程因为各种原因进入阻塞,而让出CPU资源的状态
    • sleep
      程序调用Thread.sleep(xx)后,线程释放CPU资源,但是没有释放锁资源
    • wait
      程序调用wait后,线程释放CPU资源和锁资源,注意wait只有在锁内才能调用,否则会抛出IllegalMonitorStateException异常!
    • IO or 调用其他同步方法
      其他原因就是程序在调用IO或者其他同步的方法时,调用者处于休眠状态时!
  5. 死亡状态:线程进入死亡状态
    正常执行完成后进入死亡状态或者在遇到异常后中断程序进入死亡状态

3. 线程池

池这技术,我们会经常遇到,比如jdbc会遇到连接池,目的都是差不多,主要是更好的管理这些单位的创建和死亡。线程池也是为了更好管理线程的创建与销毁动作!

3.1 ThreadPoolExecutor

java中自带线程池的线程池有四种:可缓存线程池定长线程池单线程池,还有一个支持简单定时调度的线程池。这些线程池不需要记。通过源码其实可以看到他们都是调用通一个构造函数。所以我们只需要理解这个构造函数的原理即可,很多团队也都是直接调用该构造函数来创建线程池,这样可以直观的理解创建线程池的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

实际的创建线程池的构造函数,简单说明一下每个参数的含义:

  • corePoolSize 核心线程数量
  • maximumPoolSize 最大线程数量
  • keepAliveTime 线程活跃保存时间,如果创建的线程大于核心线程数量的清除动作!
  • unit 上面这个参数的时间单位
  • workQueue 阻塞队列,存放等待的队列
  • threadFactory 线程工厂,用来创建线程
  • handler 驳回策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 1.如果当前活跃线程小于核心线程数量,直接执行线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2. 尝试将线程加入阻塞队列,如果加入成功!后面再次检查了一下是否加入成功!
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 3. 再次尝试启动队列,但是不能超过最大线程数
else if (!addWorker(command, false))
reject(command);
}

从上面的逻辑,已经可以了解核心参数的基本含义了!
源码y有更加深入的了解可以查看这篇文章了, 链接

参数的基本逻辑为:

  1. 判断核心线程数,小于则启动线程
  2. 判断阻塞队列,
  3. 判断最大线程数
  4. 驳回策略

3.2 阻塞队列

阻塞队列BlockingQueue,Java提供了几种实现!

  1. ArrayBlockingQueue 基于数组的有界阻塞队列,FIFO
  2. LinkedBlockingQueue 基于链式的有界阻塞队列,FIFO
  3. SynchronousQuene 不存储元素的阻塞队列,每次插入都要等上一个节点移除
  4. PriorityBlockingQuene 具有优先级的无界阻塞队列

LinkedBlockingQueue比ArrayBlockingQueue在插入删除节点性能方面更优,但是二者在put(), take()任务的时均需要加锁,SynchronousQueue使用无锁算法,根据节点的状态判断执行,而不需要用到锁,其核心是Transfer.transfer().

3.3 驳回策略

驳回策略,Java也提供了几种实现!

  1. AbortPolicy 直接抛出异常,默认
  2. CallerRunsPolicy 用调用者所有线程,执行任务
  3. DiscardOldestPolicy 丢弃阻塞队列中最靠前的任务,来执行该任务
  4. DiscardPolicy 不处理

我们也可以自己实现RejectedExecutionHandler接口,主要做一些记录日志操作。

3.4 自带线程池

其实有了上面的这些基础,在看看Java默认提供的几种线程池,就很容易理解了,也就是利用这些参数的使用来封窗了一些比较常用的线程池。

3.4.1 newFixedThreadPool

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

定长线程池,特点是核心线程数和最大线程数相等,采用链式阻塞队列,大小为Integer.MAX_VALUE
基本为无界队列,所以驳回策略基本失效,最大线程池理论也失效!

3.4.2 newSingleThreadExecutor

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

单线程池,特点是核心线程数和最大线程数都为1,采用链式阻塞队列和上面一样。所以驳回策略也失效,但是同时只能进行一个线程在运行中,所以叫单线程池。

3.4.3newCachedThreadPool

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

缓存线程池,核心线程数为0,最大线程数最大值。由于核心线程数为0以及采用了SynchronousQueue这种不存储元素的队列,所以实际上线程会一直无限量的在运行,吞吐量会比较大,但是高峰期会对性能有影响。但是由于核心线程数为0,所以线程池也会定时清理无用线程!该线程池是需要谨慎使用的一种。