介绍
多线程技术主要解决处理器单元多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
当同时并发多个网络线程的时候,为了防止内存过度消耗和并发线程过多,控制活动线程的数量,线程池可以限制创建线程的数目,以极大的提高App的性能。
假设在一个APP完成一项任务的时间为 T T ,为创建线程的时间; T2 T 2 为线程中执行任务的时间,包括线程间同步所需要的时间, T3 T 3 为线程销毁的时间。
显然, T T =+ T2 T 2 + T3 T 3 。这是一个简化的假设,可以看出 T1 T 1 和 T3 T 3 是多线程本身带来的开销,希望减少 T1 T 1 和 T3 T 3 ,从而减少 T T 。但是一些线程的使用者并没有注意到这一点,仍在程序中频繁地创建或者销毁线程,这就导致了和 T3 T 3 在 T T 中占用了相当大的比例。显然这就突出了线程的弱点(, T3 T 3 ),而不是优点(并发性)。
线程池的优点
- 避免线程的创建和销毁带来的性能开销。
- 能够很好的控制并发线程数,提高资源的利用率,避免大量的线程间因互相抢占系统资源导致的阻塞现象。
- 能够对线程进行简单的管理并提供定时执行、间隔执行、并发数量、并发模式,队列模式等定制功能。
线程池的基本组成
- 线程池管理器(ThreadPool):用于创建和管理线程池,包括创建线程池、销毁线程池、添加新任务。
- 工作线程(PoolWorker):线程池中的线程,在没有任务的时候处于等待的状态,可以循环执行任务。
- 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口、任务执行完之后的收尾工作、任务的执行状态等等。
- 任务队列(TaskQueue):用于存放没有处理的任务,提供一种缓冲机制。
线程池技术正式关注如何缩短或者调整 T1 T 1 和 T3 T 3 的技术,从而提高服务器程序性能。它把 T1 T 1 和 T3 T 3 分别安排在服务器程序的启动和结束的时间段和一些空闲的时间段,这样子服务器程序在处理客户请求的时候,就不会有 T1 T 1 和 T3 T 3 的开销了。
线程池不仅仅调整了 T1 T 1 和 T3 T 3 产生的时间段,而且还显著的减少了创建线程的数目。
假如一个服务器每天的要处理10W个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生的线程总数不会超过线程池中线程的数目;如果服务器不利用线程池来处理这些请求,则线程总数为10W个。一般的线程池是远小于10W个的,所以利用线程池的服务器程序是不会为了创建10W个线程而在处理请求的时候浪费时间,从而提高效率。
- CachedThreadPool:缓存型池子,先查看池中有没有以前创建的线程,如果有就重用,如果没有就建一个新的线程加入池子中。能重用的线程必须是timeout IDLE内的池中线程,默认timeout是60s(默认是60s),超过这个IDLE时长,线程实例将被终止并移出池。缓存型池子通常用于执行一些生存期很短的异步型任务。
- FixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小,线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。和CachedThreadPool不同的是,FixedThreadPool池线程数是固定的,但是0秒IDLE(无IDLE)。这也就意味着创建的线程会一直存在。所以FixedThreadPool多针对一些很稳定的、很固定的正规并发线程,多用于服务器。
- ScheduledThreadPool:调度型线程池。支持定时以及周期性的任务执行,类似与Timer,0秒IDLE(无IDLE)。
- SingleThreadExecutor:单例线程,任意时间池中只能有一个线程,并且所有的任务是串行执行的,保证所有任务按照制定顺序先进先出(FIFO)执行。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。
代码演示
public class MainActivity extends AppCompatActivity
implements View.OnClickListener {
/**
* 依据CPU的个数创建活动线程的个数
*/
private static final int COUNT = Runtime.getRuntime().availableProcessors()
* 3 + 2;
private static final String TAG = "Executor";
private static ExecutorService mCacheThreadExecutor;
private static ExecutorService mFixedThreadExecutor;
private static ScheduledExecutorService mScheduleThreadExecutor;
private static ExecutorService mSingleThreadExecutor;
private Button cached;
private Button fixed;
private Button scheduled;
private Button single;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initListener();
initExecutorService();
}
private void initListener() {
this.cached.setOnClickListener(this);
this.fixed.setOnClickListener(this);
this.scheduled.setOnClickListener(this);
this.single.setOnClickListener(this);
}
private void initView() {
this.cached = findViewById(R.id.cache);
this.fixed = findViewById(R.id.fix);
this.scheduled = findViewById(R.id.schedule);
this.single = findViewById(R.id.single);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.cache:
ExecutorServiceThread(mCacheThreadExecutor);
break;
case R.id.fix:
ExecutorServiceThread(mFixedThreadExecutor);
break;
case R.id.schedule:
ExecutorScheduleThread(mScheduleThreadExecutor);
break;
case R.id.single:
ExecutorServiceThread(mSingleThreadExecutor);
break;
default:
break;
}
}
private void initExecutorService() {
// 一个没有限制最大线程数的线程池,
// 由60的的生存周期,过了60s就会被移出线程池。
mCacheThreadExecutor = Executors.newCachedThreadPool();
// 限制线程池大小为count的线程池
mFixedThreadExecutor = Executors.newFixedThreadPool(COUNT);
// 一个按照制定时间周期性执行的线程池
mScheduleThreadExecutor = Executors.newScheduledThreadPool(COUNT);
// 每次只执行一个线程任务的线程池
mSingleThreadExecutor = Executors.newSingleThreadExecutor();
}
private void ExecutorServiceThread(ExecutorService executorService) {
for (int i = 0;i < 9; i++) {
final int index = i;
executorService.execute(() -> {
try {
// 暂停两秒钟
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i(TAG, "Thread:" + Thread.currentThread().getId() +
",activeCount:" + Thread.activeCount() +
",index:" + index);
});
}
}
private void ExecutorScheduleThread(ScheduledExecutorService executorService) {
for (int i = 0;i < 9; i++) {
final int index = i;
executorService.schedule(() -> {
try {
// 暂停两秒钟
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i(TAG, "Thread:" + Thread.currentThread().getId() +
",activeCount:" + Thread.activeCount() +
",index:" + index);
}, 2, TimeUnit.SECONDS);
}
}
}
CachedThreadPool
启动CachedThreadPool:
等待60秒:
从图中可以看出,191~199是最先创建的线程。过了60秒,重新点击创建线程的按钮的时候,由于当前的线程池中的线程的生存周期已过且都被移出线程池,因此新建了200~208号线程。
让我们直接在IDEA中直接演示我们写的代码的效果:
注意:图中使用的线程监控的软件是JDK自带的,位于JDK的BIN目录下的jconsole
。
FixedThreadPool
从两次中可以看出,FixedThreadPool创建的线程会一直存在。
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。并且创建的线程会一直的存在在线程池中。
ScheduledThreadPool
依次按照delay的时间执行或者周期性执行。
每隔两秒会执行一次。
SingleThreadExecutor
SingleThreadExecutor中线程池中等待的线程会等待正在的线程执行完之后才会开始执行。
Android网络请求使用示例
需要增加依赖库guava:
dependencies {
compile 'com.google.guava:guava:24.0-jre'
// or, for Android:
compile 'com.google.guava:guava:24.0-android'
}
public void startThread(Runnable runnable) {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("mdns-pool-%d").build();
ExecutorService singleThreadPool = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MICROSECONDS,
new LinkedBlockingDeque<>(1024),
threadFactory, new ThreadPoolExecutor.AbortPolicy()
);
singleThreadPool.execute(runnable);
singleThreadPool.shutdown();
}
说明:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
参数 | 说明 |
---|---|
corePoolSize | 线程池的核心线程数。一般情况下线程池中的线程在没有任务的时候会一直存在在线程池中,只有在只有在 ThreadPoolExecutor 中的方法 allowCoreThreadTimeOut(boolean value) 设置为 true 时,闲置的核心线程会存在超时机制,如果在指定时间没有新任务来时,核心线程也会被终止,而这个时间间隔由第3个属性 keepAliveTime 指定。 |
maximumPoolSize | 线程池所能容纳的最大线程数,当活动的线程数达到这个值后,后续的新任务将会被阻塞。 |
keepAliveTime | 控制线程闲置时的超时时长,超过则终止该线程。一般情况下用于非核心线程,只有在 ThreadPoolExecutor 中的方法 allowCoreThreadTimeOut(boolean value ) 设置为 true时,也作用于核心线程。 |
unit | 用于指定 keepAliveTime 参数的时间单位,TimeUnit 是个 enum 枚举类型,常用的有:TimeUnit.HOURS (小时)、TimeUnit.MINUTES (分钟)、TimeUnit.SECONDS (秒) 和 TimeUnit.MILLISECONDS (毫秒)等。 |
workQueue | 线程池的任务队列,通过线程池的 execute(Runnable command) 方法会将任务 Runnable 存储在队列中。 |
threadFactory | 线程工厂,它是一个接口,用来为线程池创建新线程的。 |
阻塞队列
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列中取元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里面拿元素。
阻塞队列的场景
场景 | 说明 |
---|---|
队列为空 | 获取元素的线程会等待队列由空状态转变为非空状态。 |
队列为满 | 存储元素的线程会等待队列由满状态转变为非满状态。 |
BlockingQueue方法
方法/处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除方法 | remove() | poll() | take() | poll(time, unit) |
检查方法 | element() | peek() | 不可用 | 不可用 |
- 抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出
IllegalStateException(“Queue full”)
异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException
异常 。 - 返回特殊值:插入方法会返回是否成功,成功则返回
true
。移除方法,则是从队列里拿出一个元素,如果没有则返回null
。 - 一直阻塞:当阻塞队列满时,如果生产者线程往队列里
put
元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take
元素,队列也会阻塞消费者线程,直到队列可用。 - 超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。
Java里的阻塞队列
阻塞队列 | 说明 |
---|---|
ArrayBlockingQueue | 一个由数组结构组成的有界阻塞队列。 |
LinkedBlockingQueue | 一个由链表结构组成的有界阻塞队列。 |
PriorityBlockingQueue | 一个支持优先级排序的无界阻塞队列。 |
DelayQueue | 一个使用优先级队列实现的无界阻塞队列。 |
SynchronousQueue | 一个不存储元素的阻塞队列。 |
LinkedTransferQueue | 一个由链表结构组成的无界阻塞队列。 |
LinkedBlockingDeque | 一个由链表结构组成的双向阻塞队列。 |
ArrayBlockingQueue
ArrayBlockingQueue
是一个用数组实现的有界阻塞队列,该队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列。所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列。即先阻塞的生产者线程,可以先往队列里插入元素;先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列:
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
LinkedBlockingQueue
LinkedBlockingQueue
是基于链表的阻塞队列,该队列按照先进先出(FIFO)的原则对元素进行排序。其内部也维持这一个数据缓冲队列(该队列由一个链表构成),此队列的默认最大长度为Integer.MAX_VALUE
。
注意:默认最大长度可能会影响系统性能,可以手动指定。
当生产者往队列里中放入一个数据的时候,队列会从生产者收做获取数据,并缓存在队列的内部。而生产者立即放回;只有当队列缓冲区达到缓存的容量的最大值(LinkedBlockingQueue可以通过构造方法制定该值),才会阻塞队列。直到消费者从队列中消费一份数据的时候,生产者才会被唤醒。
反之,消费者也是类似这样子处理的。
LinkedBlockingQueue
之所以能够高效地处理并发数据,还因为其对于生产者和消费者分别采用了独立的锁来控制同步。这也意味着在高并发的情况下生产者和消费者可以并行的操作队列中的数据,以此来提高队列的并发性能。
PriorityBlockingQueue
PriorityBlockingQueue
是一个支持优先级的无界队列。默认情况下元素采取自然顺序排列,也可以通过比较器comparator
来指定元素的排序规则。元素默认采用升序排列。
DelayQueue
DelayQueue
是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue
来实现。队列中的元素必须实现Delayed
接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
SynchronousQueue
SynchronousQueue
是一个不存储元素的阻塞队列。每一个put
操作必须等待一个take
操作,否则不能继续添加元素。
SynchronousQueue
可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue
的吞吐量高于LinkedBlockingQueue
和 ArrayBlockingQueue
。
LinkedTransferQueue
LinkedTransferQueue
是一个由链表结构组成的无界阻塞TransferQueue
队列。相对于其他阻塞队列LinkedTransferQueue
多了tryTransfer
和transfer
方法。
方法 | 说明 |
---|---|
transter(E e) | 如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll() 方法时),transfer 方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。 |
tryTransfer(E e) | 用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。 |
tryTransfer(E e, long timeout, TimeUnit unit) | 试图把生产者传入的元素直接传给消费者。如果没有消费者消费该元素则等待指定的时间再返回;如果超时还没消费元素,则返回false;如果在超时时间内消费了元素,则返回true。 |
LinkedBlockingDeque
LinkedBlockingDeque
是一个由链表结构组成的双向阻塞队列。
所谓双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
相比其他的阻塞队列,LinkedBlockingDeque
多了addFirst
,addLast
,offerFirst
,offerLast
,peekFirst
,peekLast
等方法。
附录
- 《Android物联网开发——基于Android Studio环境》
- 何福贵编著
- 《Android进阶之光》
- 刘望舒
- 线程、多线程与线程池总结
- 写得很清晰的一篇文章。
- JAVA进阶—-ThreadPoolExecutor机制
- 聊聊并发(七)——Java中的阻塞队列