【Monica的android学习之路】线程池那些事

1.创建线程池

1.1 线程池相关类

安卓线程池相关的接口与类的关系图如下:
在这里插入图片描述
其中:
(1)Executor、ExecutorService和ScheduledExecutorService为接口,之间是继承关系
(2)ThreadPoolExecutor 是最常用的实现类
(3)Executors是一个工厂工具类,提供了返回值类型为ExecutorService、ScheduledExecutorService的线程池对象,以及ThreadFactory类型的对象

最常用的线程池创建方法为:
(1)用Executors中的静态方法创建,例如:

        ExecutorService executorService = Executors.newFixedThreadPool(2);

(2)直接new一个线程池对象,例如:

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10L,
                TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(2));

1.2 Executors

该工具类中提供了如下创建线程池的方法,按返回值类型分为两大类:
1.ExecutorService:
(1) newFixedThreadPool() //线程池中线程数量在创建时指定
(2) newSingleThreadExecutor() //单个线程的线程池
(3) newCachedThreadPool() //缓存线程池,线程数量不固定,接到新任务且没有空闲线程时创建新线程执行任务,线程空闲60s时关闭。
(4) …

2.ScheduledExecutorService:
(1) newScheduledThreadPool() //创建固定大小的线程,可以延迟或定时的执行任务
(2) newSingleThreadScheduledExecutor() //单个线程的线程池,可以延迟或定时的执行任务
(3) …

1.3 ThreadPoolExecutor

该类包含三种创建线程池的构造方法:
(1)

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10L,
        TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(2));

(2)

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10L,
        TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(2), Executors.defaultThreadFactory());

(3)

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10L,
        TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(2),
        Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

他们综合起来一共有七个参数:

1.int corePoolSize, 核心线程数,当任务提交时,如果线程池没有空闲线程,且当前线程数小于核心线程数,就创建一个线程来执行该任务

2.int maximumPoolSize, 最大线程数,当阻塞队列已满,如果当前线程数小于最大线程数,则创建新线程执行任务

3.long keepAliveTime, 线程空闲时间,当前线程数大于核心线程数或 allowCoreThreadTimeOut(true)开启线程超时关闭开关,则空闲超出这个时间后关闭线程

4.TimeUnit unit, 时间单位

5.BlockingQueue workQueue, 阻塞队列,

6.ThreadFactory threadFactory, 生成线程的工厂

7.RejectedExecutionHandler handler 当线程数已达最大线程数,又收到新任务,就采取这个策略处理。这里的处理策略有四种:
(1)ThreadPoolExecutor.AbortPolicy 默认策略,直接丢弃任务并抛出 RejectedExecutionException 异常
(2)ThreadPoolExecutor.CallerRunsPolicy 由调用者线程处理该任务
(3)ThreadPoolExecutor.DiscardOldestPolicy 丢弃阻塞队列头的任务,然后重试执行该任务
(4)ThreadPoolExecutor.DiscardPolicy 只抛弃任务,不做任何操作

注意:使用该方法创建线程池后,线程池维护线程的策略为:
在这里插入图片描述

1.4 ThreadPoolTaskExecutor

这个类是spring包下提供的线程池类,不是原生安卓库。
该类将ThreadPoolExecutor进行了封装实现的,底层仍调用ThreadPoolExecutor的方法。
这个类可以用xml配置创建线程池所需的参数,参数的含义与ThreadPoolExecutor类似。创建线程的决定策略也和ThreadPoolExecutor相同,在此不再详述。

2.阻塞队列

在介绍阻塞队列之前,首先要了解ReentrantLock和Condition这两个类。
他们之间的关系图如下:
在这里插入图片描述

2.1 ReentrantLock

ReentrantLock重入锁,是实现Lock接口的一个类,在编程中经常用到。

他支持重入性,能够对共享资源重复加锁,即当前线程获取锁后,可以再次获取该锁,计数加一。释放该锁一次,计数减一,当计数等于0时,表示该线程释放了该锁,其他线程才能获取该锁。

java关键字synchronized隐式支持重入性,通过同样的计数方式实现重入。不同的是,ReentrantLock不仅支持重入,还支持公平锁和非公平锁两种方式。

ReentrantLock提供了两种构造方法:
(1)默认构造方法:
默认构造非公平锁

    public ReentrantLock() {
        sync = new NonfairSync();
    }

(2)传入true构造公平锁,传入false构造非公平锁

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

2.1.1 FairSync和NonfairSync

从类图可以看出,FairSync和NonfairSync均实现了Sync的lock()
并提供了tryAcquire()方法请求锁。其中NonfairSync的tryAcquire()通过调用Sync的nonfairTryAcquire方法实现。
均继承了Sync的tryRelease()来释放锁

ReentrantLock持有私有成员Sync,所以ReentrantLock对外的接口均通过调用sync对象的方法实现对锁的控制。通过构造公平或非公平的ReentrantLock对象,实现方法的多态。还能调用AbstractQueuedSynchronizer继承来的方法实现底层调用。

2.1.2 获取锁

下面分析一下两种锁获取时的源码:
(1)NonfairSync

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {     //状态等于0时其他线程才能获取锁
        if (compareAndSetState(0, acquires)) {//如果当前同步状态为0时,原子性地将状态设为acquires
            setExclusiveOwnerThread(current);//将运行线程设为当前线程,成功获取锁
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) { //如果持有该锁的线程还是当前线程,说明需要重入锁
        int nextc = c + acquires; //锁计数加一
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc); //更新锁计数
        return true;
    }
    return false;//获取锁失败
}

(2)FairSync

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() && //当任务链表中当前任务没有前置节点(即当前任务是链表头的任务时),才分配锁
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false; //当前任务不是链表头任务,则获取锁失败
}

hasQueuedPredecessors()的判断逻辑是是否公平获取锁的唯一区别

2.1.3 释放锁

不管是否公平获取锁,释放锁调用的方法相同

protected final boolean tryRelease(int releases) {
    int c = getState() - releases; //锁计数减一
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;   //计数为0,锁完全释放,返回true
        setExclusiveOwnerThread(null);
    }
    setState(c); //计数不为0,锁未被释放,更新计数,返回false
    return free;
}

2.1.4 公平锁和非公平锁的优缺点

(1)公平锁通过对当前任务是否为链表头任务的判断,保证了先请求的线程先获取到锁,保证线程执行顺序。
但判断需要时间,性能下降。
(2)非公平锁不需要做判断,提供了最大的吞吐量。
但有可能有的线程永远无法获取到锁,造成“饥饿”现象。

2.2 Condition

Condition是一个接口,抽象类AbstractQueuedSynchronizer的内部类ConditionObject实现了这个接口

Condition的实例化过程如下:
(1)由new ReentrantLock()获取ReentrantLock对象
(2)用ReentrantLock对象调用newCondition()方法,因为ReentrantLock对象持有Sync私有对象,通过该对象调用Sync的newCondition()方法
(3)由于Sync继承了AbstractQueuedSynchronizer抽象类,该类包含了ConditionObject内部类,所以new ConditionObject()返回一个ConditionObject对象,该对象可以向上自动类型转换为Condition对象
至此才得到Condition对象

2.3 阻塞队列

阻塞队列与普通队列的最大不同点,在于阻塞队列能够阻塞添加和阻塞删除:
(1)阻塞添加: 当阻塞队列元素已满时,队列会阻塞加入元素的线程,直队列元素不满时才重新唤醒线程执行元素加入操作。
(2)阻塞删除: 在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作

创建线程池时,需要的阻塞队列种类包括:

Queue底层结构容量阻塞策略ReentrantLock数Condition数
ArrayBlockingQueue数组容量构造时指定先进先出1,插入移除互斥2
LinkedBlockingDeque双向链表默认Integer.MAX_VALUE链表头尾均可插入和移除1,插入移除互斥2
LinkedBlockingQueue单链表默认Integer.MAX_VALUE先进先出2,插入和移除各一把锁,可同时执行2
DelayQueue优先队列-元素是有序的,必须实现Delayed接口(继承Comparable接口),最小先出队,出队前延迟getDelay()返回的时间1,插入移除互斥1
PriorityBlockingQueue数组默认11,最大Integer.MAX_VALUE - 8元素可排序,最小的元素先出队1,插入移除互斥1
SynchronousQueue无缓存结构默认1出队阻塞直至元素入队,入队阻塞到元素出队

注意:
阻塞队列通过ReentrantLock实现锁
利用Condition实现线程的阻塞与唤醒

采用数组作为底层结构时,插入或删除元素不会产生或销毁任何额外的对象
采用链表作为底层结构时,插入或删除元素会产生Node对象,大批量处理数据时对系统内存和GC有影响
采用两把锁可以让插入和移除同时执行,提供最大的吞吐量

2.4 阻塞队列的入队与出队

阻塞队列提供的方法有:

(1)入队:
add(E e) : 添加成功返回true,失败抛IllegalStateException异常
offer(E e) : 成功返回 true,如果此队列已满,则在到达指定的等待时间之前等待可用的空间,该方法可中断
put(E e) :将元素插入此队列的尾部,如果该队列已满,则阻塞

(2)出队
remove(Object o) :移除指定元素,成功返回true,失败返回false
poll() : 获取并移除此队列的头元素,若不能获取,在指定的等待时间前一直等到获取元素,超过时间后结束,该方法可中断
take():获取并移除此队列头元素,若没有元素则一直阻塞。直到有元素将唤醒等待线程执行移除操作

(3)获取元素
element():获取但不移除此队列的头元素,没有则抛出异常NoSuchElementException
peek():获取但不移除此队列的头;如果此队列为空,则返回 null。

可见,主要实现阻塞功能的方法为put(E e)方法和take()方法

3.调用线程池

创建线程池之后,向线程池提交异步任务的方法有两个:execute和submit,他们均能提交任务给线程池执行。提交的任务分为两种:Runnable和Callable。

3.1 Runnable和Callable

他们都是一个接口,在提交任务时,任务类需要实现这一接口。
Runnable任务需要覆写run()方法,Callable任务需要覆写call()方法

两者的区别在于:
run()无返回值,因此Runnable任务是无返回值的任务
call()有返回值,因此Callable任务是有返回值的,通常与Future配合使用。

3.2 execute()

execute()只能提交Runnable任务,无返回值。
由于run()无返回值,execute()也无返回值。
提交任务后,新的线程就和主线程脱离了关系,当然可以设置一些变量来获取到线程的运行结果。
当线程的执行过程中抛出了异常通常来说主线程也无法获取到异常,只有通过ThreadFactory主动设置线程的异常处理类才能感知到提交的线程中的异常。

例如:

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10L,
        TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(),
        Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());//初始化线程池
executor.execute(new Runnable() {
    @Override
    public void run() { //无返回值
        try {
            Log.i(TAG, "run; " + Thread.currentThread().getId());  //输出当前线程ID
            Thread.sleep(400);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});

3.3 submit()

submit()既可以提交Runnable任务,也可以提交Callable任务
(1)提交Runnable任务:
虽然run()无返回值,submit()仍然会返回一个Future对象,存储任务执行结果。
当任务执行成功时,该Future对象get()返回null
当任务出现异常时,get()返回异常信息。这是Runnable任务获取异常信息的一种方式。

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10L,
        TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(),
        Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

Future future = executor.submit(new Runnable() {
    @Override
    public void run() {
        try {
            Log.i(TAG, "run; " + Thread.currentThread().getId());
            Thread.sleep(400);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});

try {
    Log.i(TAG, "future.get():" + future.get() + "0");
} catch (ExecutionException | InterruptedException e) {
    e.printStackTrace();
}

输出为:
在这里插入图片描述
future.get()返回为null

(2)提交Callable任务:
Callable任务指定了String类型,所以call()的返回值为String,submit返回一个Future
任务执行成功时,future.get()获取到call()的返回结果,即"call"字符串
任务出现异常时,future.get()获取到异常信息

Future<String> future = executor.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
        Log.i(TAG, "call; " + Thread.currentThread().getId());
        Thread.sleep(400);
        return "call";
    }
});
try {
    Log.i(TAG, "future.get():" + future.get() + "0");
} catch (ExecutionException | InterruptedException e) {
    e.printStackTrace();
}

输出为:
在这里插入图片描述
在这里插入图片描述
总结来看:
(1)execute()只能提交Runnable任务,无返回值,不便于与主线程进行交互和捕获异常;
(2)submit()既可以提交Runnable任务,也可以提交Callable任务,均返回Future对象,Runnable任务的Future能捕获异常,不能获得返回值,Callable任务的Future能捕获异常,也能获得返回值。

3.4 Future和FutureTask

上面用 Future来接收任务的返回结果,实际上还可以用FutureTask。
他们的关系为:
在这里插入图片描述
Future是一个顶层接口,提供了如下方法:

  1. V get() :获取异步执行的结果,如果未执行完成,此方法会阻塞直到异步计算完成。
  2. V get(Long timeout , TimeUnit unit) :获取异步执行结果,如果未执行完成,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出TimeoutException异常。
  3. boolean isDone() :如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。
  4. boolean isCanceller() :如果任务完成前被取消,则返回true。
  5. boolean cancel(boolean mayInterruptRunning) :如果任务还没开始,执行cancel(…)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经完成,执行cancel(…)方法将返回false。mayInterruptRunning参数指定是否尝试中断执行中的线程。

RunnableFuture接口继承了Future和Runnable,FutureTask实现了RunnableFuture接口,因此FutureTask实际上是Future的实现类,它才能直接实例化对象。
下面是用FutureTask接收线程执行结果的例子:

FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
    @Override
    public String call() throws Exception {
        Log.i(TAG, "call; " + Thread.currentThread().getId());
        Thread.sleep(400);
        return "call";
    }
}); //实例化FutureTask对象
executor.submit(futureTask);//提交任务
try {
    Log.i(TAG, "futureTask.get():" + futureTask.get() + "0"); //获取执行结果
} catch (ExecutionException | InterruptedException e) {
    e.printStackTrace();
}

因为FutureTask也实现了Runnable的run()方法,所以它可以看作一个Runnable任务,可以用execute或者submit方法提交任务。这和上一小节的结论一致。

FutureTask和Future获取任务结果的用法相同,均能获取返回值和捕获异常。

FutureTask提供了两种构造方法,用Callable和Runnable都能构造他:
(1)

    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

(2)

    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

用Runnable 构造时,result表示任务执行结果。如果不需要获取Runnable任务的执行结果,可以这样构造:

Future<?> f = new FutureTask<Void>(new Runnable() {
            @Override
            public void run() {
                
            }
        }, null);

3.5 get()方法的超时机制

FutureTask和Future都能获取异步任务的执行结果,都用get()方法获取。
实际上get()方法有两种用法:
(1)futureTask.get(); //无超时机制,异步任务未完成时一直阻塞当前线程
(2)futureTask.get(1000, TimeUnit.MILLISECONDS); //有超时机制,如果阻塞时间超过设定的timeout时间,该方法将抛出TimeoutException异常。
完成的实例代码为:

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10L,
        TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(),
        Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());//初始化线程池
FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
    @Override
    public String call() throws Exception {
        Log.i(TAG, "call; " + Thread.currentThread().getId());
        Thread.sleep(4000); //该任务需要4s完成
        return "call";
    }
});
executor.submit(futureTask);
try {
    Thread.sleep(1000);//主线程任务需要1s完成
    futureTask.get(2000, TimeUnit.MILLISECONDS);//等待异步任务2s,超过2s时抛TimeoutException
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
} catch (TimeoutException e) {
    futureTask.cancel(true);//在超时时可以取消任务,如果不取消,即使超时,异步线程仍会继续执行到任务完成
}

这里异步任务和主任务并行执行1s,此时主任务进入等待状态,异步任务继续执行。等待2s后主线程捕获TimeoutException异常,主线程尝试取消异步任务后结束,异步任务收到取消命令结束。如果不取消,异步任务还需执行1s,才能结束。

4.线程池的关闭

shutdown()和shutdownNow()均能关闭线程池,他们之间的异同为:

功能如何阻止新线程返回值对已提交任务影响
shutdown()阻止新线程提交,正在执行的线程继续执行,空闲的线程进行interrupt(),阻塞队列中的任务继续往线程池中添加直至为空将状态设为SHUTDOWN,若提交将抛出RejectedExecutionExceptionvoid
shutdownNow()阻止新线程提交,试图对池中所有线程进行interrupt(),不再处理阻塞队列中的线程,而是将其作为返回值返回将状态设为STOP,若提交将抛出RejectedExecutionExceptionRunnable类型的List

4.1shutdown()

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN); //改变状态,阻止新任务提交
            interruptIdleWorkers(); //interrupt空闲线程
            onShutdown(); // 表示线程池将要关闭
        } finally {
            mainLock.unlock();
        }
        tryTerminate(); //关闭线程池
    }

4.2 shutdownNow()

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);//改变状态,阻止新任务提交
            interruptWorkers(); //interrupt所有线程
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();//关闭线程池
        return tasks; //返回阻塞队列中的任务列表
    }

4.线程安全

4.1 线程的状态

4.2 线程同步

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值