Android多线程编程

本文详细介绍了Android中多线程的使用,包括为何使用多线程、线程的状态转换、创建线程的三种方式(继承Thread、实现Runnable、实现Callable)以及中断线程的策略。接着讲解了同步机制,如重入锁、synchronized关键字和volatile的使用,强调了volatile不保证原子性但确保可见性和有序性。此外,还深入探讨了阻塞队列的概念、常见类型和实现原理,以及线程池的使用,包括ThreadPoolExecutor的工作流程、不同类型的线程池(FixedThreadPool、CachedThreadPool、SingleThreadExecutor、ScheduledThreadPool)以及AsyncTask的工作原理及其在Android 3.0及7.0版本的实现差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程基础

  进程(Process)是程序的一个运行实例,以区别于“程序”这一静态概念;进程就是程序的实体,是受操作系统管理的基本运行单元。
线程(Thread)是CPU调度的基本单位,线程有时也被成为轻量级的进程,他是程序执行的最小单元,一个进程可以拥有多个线程。

为何要使用多

  在操作系统级别上来看主要有以下几个方面:

  • 使用多线程可以减少程序的响应时间,如果某个操作和耗时,或者陷入长时间的等待,此时程序讲不会响应鼠标和键盘等的操作,使用多线程后可以把这个耗时的线程分配到一个单独的线程去执行,从而使程序具备了更好的交互性。
  • 与进程相比,线程创建和切换开销更小,同时多线程在数据共享方面效率非常高。
  • 多CPU或者多核计算机本身就具备执行多线程的能力,如果使用单个进程,将无法重复利用计算机资源,造成资源的巨大浪费。在多CPU计算机使用多线程能提高CPU的利用率。
  • 使用多线程能简化程序的结构,使程序便于理解和维护。

线程的状态

  Java线程在运行的声明周期中可能会处于6种不同的状态,这6种线程状态分别为如下所示。

  • New:新创建状态。线程被创建,还没有调用 start 方法,在线程运行之前还有一些基础工作要做。
  • Runnable:可运行状态。一旦调用start方法,线程就处于Runnable状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。
  • Blocked:阻塞状态。表示线程被锁阻塞,它暂时不活动。
  • Waiting:等待状态。线程暂时不活动,并且不运行任何代码,这消耗最少的资源,直到线程调度重新激活它。
  • Timed waiting:超时等待状态。和等待状态不同的是,它是可以在指定的时间自行返回的。
  • Terminated:终止状态。表示当前线程已经执行完毕。导致线程终止有两种情况:第一种就是run方法执行完毕正常退出;第二种就是因为一个没有捕获的异常而终止了run方法,导致线程进入终止状态。
    image
    线程的状态

  如上图所示,线程创建后,调用 Threadstart 方法,开始进入运行状态,当线程执行 wait 方法后,
线程进入等待状态,进入等待状态的线程需要其他线程通知才能返回运行状态。超时等待相当于在等待状态加上了时间限制,如果超过时间限制,则线程返回运行状态。当线程调用到同步方法时,如果线程没有获得锁则进入阻塞状态,当阻塞状态的线程获取到锁时则重新回到运行状态。当线程执行完毕或者遇到意外异常终止时,都会进入终止状态。

创建线程

多线程的实现一般有以下3种方法,其中前两种为最常用的方法。

继承Thread类,重写run()方法

  Thread本质上也是实现了Runnable接口的一个实例。需要注意的是调用start()方法后并不是立即地执行多线程的代码,而是使该线程变为可运行态,什么时候运行多线程代码是由操作系统决定的。以下是其主要步骤:

  1. 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。
    因此,run()方法被称为执行体。
  2. 创建Thread子类的实例,即创建了线程对象。
  3. 调用线程对象的start()方法来启动该线程。
public class TestThread extends Thread{ 
    public void run() {
            System.out.println("Hello World");
        }  
    public static void main(String[] args) {
        Thread mThread = new TestThread();
        mThread.start(); 
    } 
}

实现Runnable接口,并实现该接口的run()方法

  以下是主要步骤:

  1. 自定义类并实现Runnable接口,实现run()方法。
  2. 创建Thread子类的实例,用实现Runnable接口的对象作为参数实例化该Thread对象。
  3. 调用Threadstart()方法来启动该线程。
public class TestRunnable implements Runnable {
    public void run() { 
            System.out.println("Hello World");
        } 
}

public class TestRunnable {
    public static void main(String[] args) {
        TestRunnable mTestRunnable = new TestRunnable();      
        Thread mThread = new Thread(mTestRunnable);
        mThread.start(); 
    } 
}

实现Callable接口,重写call()方法

  Callable接口实际是属于Executor框架中的功能类,Callable接口与Runnable接口的功能类似,但提供了比Runnable更强大的功能,主要表现为以下的3点:

  1. Callable可以在任务接受后提供一个返回值,Runnable无法提供这个功能。
  2. Callable中的call()方法可以抛出异常,而Runnablerun()方法不能抛出异常。
  3. 运行Callable可以拿到一个Future对象,Future对象表示异步计算的结果,他提供了检查计算是否完成的方法。由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下就可以使用Future来监视目标线程调用call()方法的情况,但调用Futureget()方法以获取结果时,当前线程就会阻塞,直到call()方法的返回结果。
public class TestCallable {  
    //创建线程类
    public static class MyTestCallable  implements Callable {  
        public String call() throws Exception {  
             retun "Hello World";
            }  
        }  
public static void main(String[] args) {  
        MyTestCallable mMyTestCallable= new MyTestCallable();  
        ExecutorService mExecutorService = Executors.newSingleThreadPool();  
        Future mfuture = mExecutorService.submit(mMyTestCallable);  
        try { 
        //等待线程结束,并返回结果
            System.out.println(mfuture.get());  
        } catch (Exception e) {  
           e.printStackTrace();
        } 
    }  
}

  在这三种方式中,一般推荐实现Runnable接口的方式,其原因是:首先,Thread类定义了多种方法可以被派生类使用重写,但是只有run()方法是必须被重写的,实现这个线程的主要功能,这也是实现Runnable接口需要的方法。其次,一个类应该在他们需要加强或者修改时才会被继承。因此如果没有必要重写Thread类的其他方法,那么在这种情况下最好是用实现Runnable接口的方式。

理解中断

  当线程的run()方法执行方法体中的最后一条语句后,并经由执行return语句返回时,或者出现在方法中没有捕获的异常时线程将终止。在java早期版本中有一个stop方法,其他线程可以调用它终止线程,但是这个方法现在已经被弃用了。interrupt方法可以用来请求终止线程,当一个线程调用interrupt方法时,线程的中断状态将被置位。这是每个线程都具有的boolean标志,每个线程都应该不时的检查这个标志,来判断线程是否被中断。要想弄清线程是否被置位,可以调用Thread.currentThread().isInterrupted()。还可以调用Thread.interrupted()来对中断标识位进行复位。但是如果一个线程被阻塞,就无法检测中断状态。如果一个线程处于阻塞状态,线程在检查中断标识位时如果发现中断标识位为true,则会在阻塞方法调用处抛出InterruptedException异常,并且在抛出异常前将线程的中断标识位复位,即重新设置为false。需要注意的是被中断的线程不一定会终止,中断线程是为了引起线程的注意,被中断的线程可以决定如何去响应中断。如果是比较重要的线程则不会理会中断,而大部分情况则是线程会将中断作为一个终止的请求。另外,不要在底层代码里捕获InterruptedException异常后不做处理,如下所示:

void myTask(){
    ...
    try{
        sleep(50)
    }catch(InterruptedException e){
        ...
    }
}

  如果你不知道抛出InterruptedException异常后如何处理,这里介绍两种合理的处理方式。

  1. 在catch子句中,调用Thread.currentThread.interrupt()来设置中断状态(因为抛出异常后中断标识位会复位),让外界通过判断Thread.currentThread().isInterrupted()来决定是否终止线程还是继续下去。
  2. 更好的选择用throw InterruptedException标记你的方法,不采用try语句块来捕获已成。这样调用者可以捕获这个异常:
void myTask()throw InterruptedException{
    sleep(50)
}

安全地终止线程

  前面我们讲到了中断,那么首先就用中断来终止线程,代码如下:

public class StopThread {
    public static void main(String[] args) {    
        Thread thread = new Thread(runnable,"StopThread");
        thread.start(); 
        TimeUnit.MILLISECONDS.sleep(10);//1
        thread.interrupt();
    } 
}

  在上面代码注释1处调用了sleep方法使得main线程睡眠10ms,这是为了留给MoonThread线程时间来感知中断从而结束。除了中断,还可以采用boolean变量来控制是否需要停止线程,代码如下:

public class StopThread {
    public static void main(String[] args) {
         
    }
    
    public static class MoonRunner implement Runnable{
        private long i;
        private volatile boolean on = true;//1
        
        @Override
        public void run() { 
            while(on){
                i++;
                System.out.printIn("i=" + i);
            }
            System.out.printIn("stop");
        } 
        
        public void cancel(){
            on = false;
        }
    }
}

同步

  在多线程应用中,两个或者两个以上的线程需要共享对同一个数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况通常被称为竞争条件。竞争条件最容易理解的例子如下:比如车站售卖火车票,火车票是一定
的,但卖火车票的窗口到处都有,每个窗口就相当于一个线程。这么多的线程共用所有的火车票资源,如果不使用同步是无法保证其原子性的。在一个时间点上,两个线程同时使用火车票资源,那其取出的火车票是一样的(座位号一样),这样就会给乘客造成麻烦。解决方法如下:当一个线程要使用火车票这个资源时,我们就交给它一把锁,等它把事情做完后再把锁给另一个要用这个资源的线程。这样就不会出现上述情况了。

重入锁与条件对象

  synchronized关键字自动提供了锁以及相关的条件,大多数需要显式锁的情况使用synchronized非常的方便,但是等我们了解ReentrantLock类和条件对象时,我们能更好的理解synchronized关键字。ReentrantLock是JAVA SE 5.0引入的, 用ReentrantLock保护代码块的结构如下:

Lock mLock = new ReentrantLock();
mLock.lock();
try{
    ...
}
finally{
    mLock.unlock();
}

  这一结构确保任何时刻只有一个线程进入临界区,一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们则被阻塞直到第一个线程释放锁对象。把解锁的操作放在finally中是十分必要的,如果在临界区发生了异常,锁是必须要释放的,否则其他线程将会永远阻塞。

同步方法

  Lock和Condition接口为程序设计人员提供了高度的锁定控制,然而大多数情况下,并不需要那样的控制,并且可以使用一种嵌入到java语言内部的机制。从Java1.0版开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。

public synchronized void method(){
    ...
}

等价于

public void method(){
    this.lock.lock();
    try{

    }finally{
        this.lock.unlock();
    }
}

  内部对象锁只有一个相关条件,wait方法添加到一个线程到等待集中,notifyAll或者notify方法解除等待线程的阻塞状态。也就是说wait相当于调用condition.await(),notifyAll等价于condition.signalAll();

public synchronized void transfer(int from,int to,int amount)throws InterruptedException{
    while (accounts[from]<amount) {
        wait();
    }
    ...
    notifyAll();   
}

同步阻塞

  上面我们说过,每一个Java对象都有一个锁,线程可以调用同步方法来获得锁,还有另一种机制可以获得锁,通过进入一个同步阻塞,当线程进入如下形式的阻塞:

synchronized(obj){

}

于是他获得了obj的锁。再来看看Bank类

public class Bank {
private double[] accounts;
private Object lock=new Object();
   public Bank(int n,double initialBalance){
        accounts=new double[n];
        for (int i=0;i<accounts.length;i++){
            accounts[i]=initialBalance;
        }
    }
    public void transfer(int from,int to,int amount){
        synchronized(lock){
          //转账的操作
            ...
        }
    }
}

  在这里创建了一个名为Lock的Object类,为的是使用Object类所持有的锁。同步代码块是非常脆弱的,通常不推荐使用。一般实现同步最好用java.util.concurrent包下提供的类,比如阻塞队列。如果同步方法适合你的程序,那么请尽量使用同步方法,这样可以减少编写代码的数量,减少出错的概率。如果特别需要使用Lock/Condition结构提供的独有特性时,才使用Lock/Condition。

volatile

  有时仅仅为了读写一个或者两个实例域就使用同步的话,显得开销过大;而volatile关键字为实例域的同步访问提供了免锁的机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。再讲到volatile关键字之前,我们需要了解一下内存模型的相关概念以及并发编程中的3个特性:原子性、可见性和有序性。

Java内存模型

  Java中的堆内存用来存储对象实例,堆内存是被所有线程共享的运行时内存区域,因此,它存在内存可见性的问题。而局部变量、方法定义的参数则不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。Java内存模型定义了线程和主存之间的抽象关系:线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的副本。需要注意的是本地内存是Java内存模型的一个抽象概念,其并不真实存在,它涵盖了缓存、写缓冲区、寄存器等区域。Java内存模型控制线程之间的通信,它决定一个线程对主存共享变量的写入何时对另一个线程可见。Java 内存模型的抽象示意图如下图所示:
Java内存模型的抽象示意图

Java内存模型的抽象示意图

  线程A与线程B之间若要通信的话,必须要经历下面两个步骤:

  1. 线程A把线程A本地内存中更新过的共享变量刷新到主存中去。
  2. 线程B到主存中去读取线程A之前已更新过的共享变量。

由此可见,如果我们执行下面的语句:

int i=3;

执行线程必须先在自己的工作线程中对变量 i 所在的缓存行进行赋值操作,然后再写入主存当中,而不是直接将数值3写入主存当中。

原子性、可见性和有序性

原子性

对基本数据类型变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行完毕,要么就不执行。现在看一下下面的代码,如下所示:

x = 3;//语句1
y = x;//语句2
x++;//语句3

  在上面3个语句中,只有语句1是原子性操作,其他两个语句都不是原子性操作。语句2虽说很短,但它包含了两个操作,它先读取x的值,再将x的值写入工作内存。读取x的值以及将x的值写入工作内存这两个操作单拿出来都是原子性操作,但是合起来就不是原子性操作了。语句3包括3个操作:读取x的值、对x的值进行加1、向工作内存写入新值。通过这3个语句我们得知,一个语句含有多个操作时,就不是原子性操作,只有简单地读取和赋值(将数字赋值给某个变量)才是原子性操作。java.util.concurrent.atomic 包中有
很多类使用了很高效的机器级指令(而不是使用锁)来保证其他操作的原子性。例如 AtomicInteger类提供了方法incrementAndGetdecrementAndGet,它们分别以原子方式将一个整数自增和自减。可以安全地使用AtomicInteger类作为共享计数器而无须同步。另外这个包还包含AtomicBooleanAtomicLongAtomicReference这些原子类,这仅供开发并发工具的系统程序员使用,应用程序员不应该使用这些类。

可见性

  可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。当一个共享变量被volatile修饰时,它会保证修改的值立即被更新到主存,所以对其他线程是可见的。当有其他线程需要读取该值时,其他线程会去主存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,并不会立即被写入主存,何时被写入主存也是不确定的。当其他线程去读取该值时,此时主存中可能还是原来的旧值,这样就无法保证可见性。

有序性

  Java内存模型中允许编译器和处理器对指令进行重排序,虽然重排序过程不会影响到单线程执行的正确性,但是会影响到多线程并发执行的正确性。这时可以通过volatile来保证有序性,除了volatile,也可以通过synchronizedLock来保证有序性。我们知道,synchronizedLock保证每个时刻只有一个线程执行同步代码,这相当于是让线程顺序执行同步代码,从而保证了有序性。

volatile关键字

  当一个共享变量被volatile修饰之后,其就具备了两个含义,一个是线程修改了变量的值时,变量的新值对其他线程是立即可见的。换句话说,就是不同线程对这个变量进行操作时具有可见性。另一个含义是
禁止使用指令重排序。

  • volatile不保证原子性
  • volatile保证有序性

正确使用volatile关键字

  synchronized关键字可防止多个线程同时执行一段代码,那么这就会很影响程序执行效率。而volatile关键字在某些情况下的性能要优于synchronized。但是要注意volatile关键字是无法替代synchronized关键字的,
因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下两个条件:

  1. 对变量的写操作不会依赖于当前值。不能是自增、自减等操作,上文已经提到volatile不保证原子性。
  2. 该变量没有包含在具有其他变量的不变式中。

  使用volatile有很多种场景,这里介绍其中的两种。

(1)状态标志

volatile boolean shutdownRequested;
...
public void shutdown()
 { 
 shutdownRequested = true;
  }
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

  如果在另一个线程中调用shutdown方法,就需要执行某种同步来确保正确实现 shutdownRequested 变量的可见性。但是,使用synchronized 块编写循环要比使用 volatile 状态标志编写麻烦很多。在这里推荐使用volatile,状态标志shutdownRequested并不依赖于程序内的任何其他状态,并且还能简化代码。因此,此处适合使用volatile

(2)双重检查模式(DCL)

public class Singleton {  
    private volatile static Singleton instance = null;  
    public static Singleton getInstance() {  
        if (instance == null) {  
            synchronized(this) {  
                if (instance == null) {  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
}

在这里使用volatile会或多或少的影响性能,但考虑到程序的正确性,牺牲这点性能还是值得的。DCL优点是资源利用率高,第一次执行getInstance时单例对象才被实例化,效率高。缺点是第一次加载时反应稍慢一些,在高并发环境下也有一定的缺陷,虽然发生的概率很小。DCL虽然在一定程度解决了资源的消耗和多余的同步,线程安全等问题,但是他还是在某些情况会出现失效的问题,也就是DCL失效,在《java并发编程实践》一书建议用以下的代码(静态内部类单例模式)来替代DCL:

public class Singleton { 
    private Singleton(){
    }
      public static Singleton getInstance(){  
        return SingletonHolder.sInstance;  
    }  
    private static class SingletonHolder {  
        private static final Singleton sInstance = new Singleton();  
    }  
}

小结

  与锁相比,volatile变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循volatile的使用条件,即变量真正独立于其他变量和自己以前的值,在某些情况下可以使用volatile代替synchronized来简化代码。然而,使用volatile的代码往往比使用锁的代码更加容易出错。在前面介绍了可以使用volatile代替synchronized的最常见的两种用例,在其他情况下我们最好还是使用synchronized

阻塞队列

阻塞队列简介

  阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

1.常见阻塞场景

阻塞队列有两个常见的阻塞场景,它们分别是:

  1. 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
  2. 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。

支持以上两种阻塞场景的队列被称为阻塞队列。

BlockingQueue的核心方法

放入数据:

  • offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里。即如果BlockingQueue可以容纳,则返回true,否则返回false。(本方法不阻塞当前执行方法的线程。)
  • offer(E o,long timeout,TimeUnit unit):可以设定等待的时间。如果在指定的时间内还不能往队列
    中加入BlockingQueue,则返回失败。
  • put(anObject):将anObject加到BlockingQueue里。如果BlockQueue没有空间,则调用此方法的线程被阻断,直到BlockingQueue里面有空间再继续。

获取数据:

  • poll(time):取走 BlockingQueue 里排在首位的对象。若不能立即取出,则可以等 time参数规定的时间。取不到时返回null。
  • poll(long timeout,TimeUnit unit):从BlockingQueue中取出一个队首的对象。如果在指定时间内,
    队列一旦有数据可取,则立即返回队列中的数据;否则直到时间超时还没有数据可取,返回失败。
  • take():取走BlockingQueue里排在首位的对象。若BlockingQueue为空,则阻断进入等待状态,直到 BlockingQueue有新的数据被加入。
  • drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数)。通过该方法,可以提升获取数据的效率;无须多次分批加锁或释放锁。

Java中的阻塞队列

ArrayBlockingQueue

  它是用数组实现的有界阻塞队列,并按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证线程公平地访问队列。公平访问队列就是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列。即先阻塞的生产者线程,可以先往队列里插入元素;先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列,如下所示:

ArrayBlockingQueue fairQueue=new ArrayBlockingQueue(2000,true);
LinkedBlockingQueue

  它是基于链表的阻塞队列,同ArrayListBlockingQueue类似,此队列按照先进先出(FIFO)的原则对元素进行排序,其内部也维持着一个数据缓冲队列(该队列由一个链表构成)。当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到缓存容量的最大值时(LinkedBlockingQueue可以通过构造方法指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒。反之,对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效地处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步。这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE)。这样一来,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。ArrayBlockingQueue和LinkedBlockingQueue是两个最普通也是最常用的阻塞队列。一般情况下,在处理多线程间的生产者-消费者问题时,使用这两个类足已。

PriorityBlockingQueue

  它是一个支持优先级的无界队列。默认情况下元素采取自然顺序升序排列。这里可以自定义实现compareTo()方法来指定元素进行排序规则;或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。但其不能保证同优先级元素的顺序。

DelayQueue

  它是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口。创建元素时,可以指定元素到期的时间,只有在元素到期时才能从队列中取走。

SynchronousQueue

  它是一个不存储元素的阻塞队列。每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。因此此队列内部其实没有任何一个元素,或者说容量是0,严格来说它并不是一种容器。由于队列没有容量,因此不能调用peek操作(返回队列的头元素)。

LinkedTransferQueue

  它是一个由链表结构组成的无界阻塞TransferQueue队列。LinkedTransferQueue实现了一个重要的接口TransferQueue。该接口含有5个方法,其中有3个重要的方法,它们分别如下所示。

  1. transfer(E e):若当前存在一个正在等待获取的消费者线程,则立刻将元素传递给消费者;如果没有消费者在等待接收数据,就会将元素插入到队列尾部,并且等待进入阻塞状态,直到有消费者线程取走该元素。
  2. tryTransfer(E e):若当前存在一个正在等待获取的消费者线程,则立刻将元素传递给消费者;
    若不存在,则返回 false,并且不进入队列,这是一个不阻塞的操作。与 transfer 方法不同的是,tryTransfer 方法无论消费者是否接收,其都会立即返回;而transfer方法则是消费者接收了才返回。
  3. tryTransfer(E e,long timeout,TimeUnit unit):若当前存在一个正在等待获取的消费者线程,则立刻将元素传递给消费者;若不存在则将元素插入到队列尾部,并且等待消费者线程取走该元素。若在指定的超时时间内元素未被消费者线程获取,则返回false;若在指定的超时时间内其被消费者线程获取,则返回true。
LinkedBlockingDeque

  它是一个由链表结构组成的双向阻塞队列。双向队列可以从队列的两端插入和移出元素,因此在多线程同时入队时,也就减少了一半的竞争。由于是双向的,因此LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst、peekLast等方法。其中,以First单词结尾的方法,表示插入、获取或移除双端队列的第一个元素;以Last单词结尾的方法,表示插入、获取或移除双端队列的最后一个元素。

阻塞队列的实现原理

以ArrayBlockingQueue为例,我们先来看看代码,如下所示:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    private static final long serialVersionUID = -817911632652898426L;
    /** The queued items */
    final Object[] items;
    /** items index for next take, poll, peek or remove */
    int takeIndex;
    /** items index for next put, offer, or add */
    int putIndex;
    /** Number of elements in the queue */
    int count;
    final ReentrantLock lock;
    /** Condition for waiting takes */
    private final Condition notEmpty;
    /** Condition for waiting puts */
    private final Condition notFull;
 ...省略
 }

  从上面的代码可以看出 ArrayBlockingQueue 是维护一个 Object类型的数组,takeIndex和putIndex分别表示队首元素和队尾元素的下标,count表示队列中元素的个数,lock则是一个可重入锁,notEmpty和notFull是等待条件。接下来我们看看关键方法put,代码如下所示:

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

  从put方法的实现可以看出,它先获取了锁,并且获取的是可中断锁,然后判断当前元素个数是否等于数组的长度,如果相等,则调用notFull.await()进行等待,当被其他线程唤醒时,通过enqueue(e)方法插入元素,最后解锁。

/**
  * Inserts element at current put position, advances, and signals.
  * Call only when holding lock.
  */
 private void enqueue(E x) {
     // assert lock.getHoldCount() == 1;
     // assert items[putIndex] == null;
     final Object[] items = this.items;
     items[putIndex] = x;
     if (++putIndex == items.length) putIndex = 0;
     count++;
     notEmpty.signal();
 }

  插入成功后,通过notEmpty唤醒正在等待取元素的线程。再来看看take方法:

public E take() throws InterruptedException {
     final ReentrantLock lock = this.lock;
     lock.lockInterruptibly();
     try {
         while (count == 0)
             notEmpty.await();
         return dequeue();
     } finally {
         lock.unlock();
     }
 }

  跟put方法实现类似,put方法等待的是notFull信号,而take方法等待的是notEmpty信号。在take方法中,如果可以取元素,则通过dequeue方法取得元素,下面是dequeue方法的实现:

private E dequeue() {
       // assert lock.getHoldCount() == 1;
       // assert items[takeIndex] != null;
       final Object[] items = this.items;
       @SuppressWarnings("unchecked")
       E x = (E) items[takeIndex];
       items[takeIndex] = null;
       if (++takeIndex == items.length) takeIndex = 0;
       count--;
       if (itrs != null)
           itrs.elementDequeued();
       notFull.signal();
       return x;
   }

线程池

  在编程中经常会使用线程来异步处理任务,但是每个线程的创建和销毁都需要一定的开销。如果每次执行一个任务都需要开一个新线程去执行,则这些线程的创建和销毁将消耗大量的资源;并且线程都是“各自为政”的,很难对其进行控制,更何况有一堆的线程在执行。这时就需要线程池来对线程进行管理。这时就需要线程池来对线程进行管理。在Java1.5中提供了Executor框架用于把任务的提交和执行解耦,任务的提交交给Runnable或者Callable,而Executor框架用来处理任务。Executor框架中最核心的成员就是 ThreadPoolExecutor,它是线程池的核心实现类。

ThreadPoolExecutor

  可以通过ThreadPoolExecutor来创建一个线程池,ThreadPoolExecutor类一共有4个构造方法。其中,拥有最多参数的构造方法如下所示:

  public ThreadPoolExecutor(int corePoolSize,
                        int maximumPoolSize,
                        long keepAliveTime,
                        TimeUnit unit,
                        BlockingQueue<Runnable> workQueue,
                        ThreadFactory threadFactory,
                        RejectedExecutionHandler handler) {
    
    }

  这些参数的作用如下所示。

  • corePoolSize:核心线程数。默认情况下线程池是空的,只有任务提交时才会创建线程。如果当前运行的线程数少于corePoolSize,则创建新线程来处理任务;如果等于或者多于corePoolSize,则不再创建。如果调用线程池的prestartAllcoreThread方法,线程池会提前创建并启动所有的核心线程来等待任务。
  • maximumPoolSize:线程池允许创建的最大线程数。如果任务队列满了并且线程数小于maximumPoolSize时,则线程池仍旧会创建新的线程来处理任务。
  • keepAliveTime:非核心线程闲置的超时时间。超过这个时间则回收。如果任务很多,并且每个任务的执行事件很短,则可以调大keepAliveTime来提高线程的利用率。另外,如果设置allowCoreThreadTimeOut属性为true时,keepAliveTime也会应用到核心线程上。
  • TimeUnit:keepAliveTime参数的时间单位。可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、秒(SECONDS)、毫秒(MILLISECONDS)等。
  • workQueue:任务队列。如果当前线程数大于corePoolSize,则将任务添加到此任务队列中。该任务队列是BlockingQueue类型的,也就是阻塞队列。这在前面已经介绍过了,这里就不赘述了。
  • ThreadFactory:线程工厂。可以用线程工厂给每个创建出来的线程设置名字。一般情况下无须设置该参数。
  • RejectedExecutionHandler:饱和策略。这是当任务队列和线程池都满了时所采取的应对策略,默认是AbordPolicy,表示无法处理新任务,并抛出RejectedExecutionException异常。此外还有3种策略,它们分别如下:
    • CallerRunsPolicy:用调用者所在的线程来处理任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
    • DiscardPolicy:不能执行的任务,并将该任务删除。
    • DiscardOldestPolicy:丢弃队列最近的任务,并执行当前的任务。

线程池的处理流程和原理

当提交一个新的任务到线程池时,线程池的处理流程如下图:
image

线程池的处理流程

  线程的处理流程主要分为3个步骤,如下所示。

  1. 提交任务后,线程池先判断线程数是否达到了核心线程数(corePoolSize)。如果未达到核心线程数,则创建核心线程处理任务;否则,就执行下一步操作。
  2. 接着线程池判断任务队列是否满了。如果没满,则将任务添加到任务队列中;否则,就执行下一步操作。
  3. 接着因为任务队列满了,线程池就判断线程数是否达到了最大线程数。如果未达到,则创建非核心线程处理任务;否则,就执行饱和策略,默认会抛出 RejectedExecutionException异常。

  上面介绍了线程池的处理流程,但还不是很直观。下面结合图4-7,我们就能更好地了解线程池的原理了。image

线程池执行示意图

  如果我们执行ThreadPoolExecutor的execute方法,会遇到各种情况:

  1. 如果线程池中的线程数未达到核心线程数,则创建核心线程处理任务。
  2. 如果线程数大于或者等于核心线程数,则将任务加入任务队列,线程池中的空闲线程会不断地从任务队列中取出任务进行处理。
  3. 如果任务队列满了,并且线程数没有达到最大线程数,则创建非核心线程去处理任务。
  4. 如果线程数超过了最大线程数,则执行饱和策略。

线程池的种类

FixedThreadPool

FixedThreadPool 是可重用固定线程数的线程池。在 Executors 类中提供了创建FixedThreadPool的方法,如下所示:

public static ExecutorService newFixedThreadPool(int nThreads,
                                ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                              0L, TimeUnit.MILLISECONDS,
                              new LinkedBlockingQueue<Runnable>(),
                              threadFactory);
}

  FixedThreadPool的corePoolSize和maximumPoolSize都设置为创建FixedThreadPool指定的参数nThreads,也就意味着FixedThreadPool只有核心线程,并且数量是固定的,没有非核心线程。keepAliveTime设置为0L意味着多余的线程会被立即终止。因为不会产生多余的线程,所以keepAliveTime是无效的参数。另外,任务队列采用了无界的阻塞队列LinkedBlockingQueue。
image

FixedThreadPool的执行示意图

  当执行execute方法时,如果当前运行的线程未达到corePoolSize(核心线程数)时就创建核心线程来处理任务,如果达到了核心线程数则将任务添加到LinkedBlockingQueue中。FixedThreadPool就是一个有固定数量核心线程的线程池,并且这些核心线程不会被回收。当线程数超过corePoolSize时,就将任务存储在任务队列中;当线程池有空闲线程时,则从任务队列中去取任务执行。

CachedThreadPool

  CachedThreadPool是一个根据需要创建线程的线程池,创建CachedThreadPool的代码如下所示:

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

  CachedThreadPool的corePoolSize为0,maximumPoolSize设置为Integer.MAX_VALUE,这意味着CachedThreadPool没有核心线程,非核心线程是无界的。keepAliveTime设置为60L,则空闲线程等待新任务的最长时间为60s。在此用了阻塞队列SynchronousQueue,它是一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。
image

CachedThreadPool的执行示意图

  当执行execute方法时,首先会执行SynchronousQueue的offer方法来提交任务,并且查询线程池中是否有空闲的线程执行SynchronousQueue的poll方法来移除任务。如果有则配对成功,将任务交给这个空闲的线程处理;如果没有则配对失败,创建新的线程去处理任务。当线程池中的线程空闲时,它会执行SynchronousQueue的poll方法,等待SynchronousQueue中新提交的任务。如果超过60s没有新任务提交到SynchronousQueue,则这个空闲线程将终止。因为maximumPoolSize是无界的,所以如果提交的任务大于线程池中线程处理任务的速度就会不断地创建新线程。另外,每次提交任务都会立即有线程去处理。所以,CachedThreadPool比较适于大量的需要立即处理并且耗时较少的任务。

SingleThreadExecutor

SingleThreadExecutor是使用单个工作线程的线程池,其创建源码如下所示:

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

  corePoolSize和maximumPoolSize都为1,意味着SingleThreadExecutor只有一个核心线程,其他的参数都和FixedThreadPool一样,这里就不赘述了。
image

SingleThreadExecutor的执行示意图

  当执行execute方法时,如果当前运行的线程数未达到核心线程数,也就是当前没有运行的线程,则创建一个新线程来处理任务。如果当前有运行的线程,则将任务添加到阻塞队列LinkedBlockingQueue中。因此,SingleThreadExecutor能确保所有的任务在一个线程中按照顺序逐一执行。

ScheduledThreadPool

  dPool是一个能实现定时和周期性任务的线程池,它的创建源码如下所示:

public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}

  ThreadPoolExecutor,ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,它主要用于给定延时之后的运行任务或者定期处理任务。ScheduledThreadPoolExecutor 的构造方法如下所示:

public ScheduledThreadPoolExecutor(int corePoolSize,
                               ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE,
            DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
            new DelayedWorkQueue(), threadFactory);
    }

  从上面的代码可以看出,ScheduledThreadPoolExecutor 的构造方法最终调用的是ThreadPoolExecutor的构造方法。corePoolSize是传进来的固定数值,maximumPoolSize的值是Integer.MAX_VALUE。因为采用的DelayedWorkQueue是无界的,所以maximumPoolSize这个参数是无效的。image

ScheduledThreadPool的执行示意图

  当执行 ScheduledThreadPoolExecutor 的 scheduleAtFixedRate 或者 scheduleWithFixedDelay方法时,会向DelayedWorkQueue 添加一个 实现 RunnableScheduledFuture 接口的ScheduledFutureTask(任务的包装类),
并会检查运行的线程是否达到corePoolSize。如果没有则新建线程并启动它,但并不是立即去执行任务,而是去 DelayedWorkQueue 中取ScheduledFutureTask,然后去执行任务。如果运行的线程达到了corePoolSize
时,则将任务添加到DelayedWorkQueue中。DelayedWorkQueue会将任务进行排序,先要执行的任务放在队列的前面。其跟此前介绍的线程池不同的是,当执行完任务后,会将ScheduledFutureTask中的time变量改为下次要执行的时间并放回到DelayedWorkQueue中。

AsyncTask的原理

  AsyncTask是一个抽象的泛型类,它有3个泛型参数,分别为Params、Progress和Result,其中Params为参数类型,Progress为后台任务执行进度的类型,Result为返回结果的类型。如果不需要某个参数,可以将其设置为Void类型。AsyncTask中有4个核心方法,如下所示。

  1. onPreExecute():在主线程中执行。一般在任务执行前做准备工作,比如对 UI 做一些标记。
  2. doInBackground(Params…params):在线程池中执行。在 onPreExecute方法执行后运行,用来执
    行较为耗时的操作。在执行过程中可以调用publishProgress(Progress…values)来更新进度信息。
  3. onProgressUpdate(Progress…values):在主线程中执行。当调用
    publishProgress(Progress…values)时,此方法会将进度更新到UI组件上。
  4. onPostExecute(Result result):在主线程中执行。当后台任务执行完成后,它会被执行。
    doInBackground方法得到的结果就是返回的result的值。此方法一般做任务执行后的收尾工作,比如更新UI和数据。

Android 3.0版本之前的AsyncTask

下面是Android 2.3.7版本的AsyncTask的部分源码。

public abstract class AsyncTask<Params, Progress, Result> {
  private static final String LOG_TAG = "AsyncTask";
  private static final int CORE_POOL_SIZE = 5;
  private static final int MAXIMUM_POOL_SIZE = 128;
  private static final int KEEP_ALIVE = 1;
  private static final BlockingQueue<Runnable> sWorkQueue =
           new LinkedBlockingQueue<Runnable>(10);
  private static final ThreadFactory sThreadFactory = new ThreadFactory() {
      private final AtomicInteger mCount = new AtomicInteger(1);
      public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };
  private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
            MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory);

  ...         
}

  在这里又看到了ThreadPoolExecutor,它的核心线程数是5个,线程池允许创建的最大线程数为128,非核心线程空闲等待新任务的最长时间为1s。采用的阻塞队列是LinkedBlockingQueue,它的容量为10。3.0版本之前的AsyncTask有一个缺点,就是线程池最大的线程数为128,加上阻塞队列的10个任务,所以AsyncTask最多能同时容纳138个任务,当提交第139个任务时就会执行饱和策略,默认抛出RejectedExecutionException异常。

Android 7.0版本的AsyncTask

在这里采用Android 7.0版本的AsyncTask作为例子,首先来看AsyncTask的构造函数:

public AsyncTask() {
    mWorker = new WorkerRunnable<Params, Result>() {//1
        public Result call() throws Exception {
            mTaskInvoked.set(true);
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            Result result = doInBackground(mParams);
            Binder.flushPendingCommands();
            return postResult(result);
        }
    };

    mFuture = new FutureTask<Result>(mWorker) {//2
        @Override
        protected void done() {
            try {
                postResultIfNotInvoked(get());
            } catch (InterruptedException e) {
                android.util.Log.w(LOG_TAG, e);
            } catch (ExecutionException e) {
                throw new RuntimeException("An error occurred while executing doInBackground()",
                        e.getCause());
            } catch (CancellationException e) {
                postResultIfNotInvoked(null);
            }
        }
    };
}

  从注释1处看这个WorkerRunnable实现了Callable接口,并实现了它的call方法,在call方法中调用了doInBackground(mParams)来处理任务并得到结果,并最终调用postResult将结果投递出去。注释2处的FutureTask是一个可管理的异步任务,它实现了Runnable和Futrue这两个接口。因此它可以包装Runnable和Callable,并提供给Executor执行。也可以调用线程直接执行(FutureTask.run())。在这里WorkerRunnable作为参数传递给了FutureTask。这两个变量会暂时保存在内存中,稍后会用到它们。

当要执行AsyncTask时,需要调用它的execute方法,代码如下所示。

public final AsyncTask<Params, Progress, Result> execute(Params... params) {
      return executeOnExecutor(sDefaultExecutor, params);
  }

  这里会首先调用onPreExecute方法,在注释1处将AsyncTask的参数传给WorkerRunnable,从前面我们知道WorkerRunnable会作为参数传递给了FutureTask,因此,参数被封装到FutureTask中。接下来会调用exec的execute方法,并将mFuture也就是前面讲到的FutureTask传进去。这里exec是传进来的参数sDefaultExecutor,它是一个串行的线程池,它的代码如下所示。

private static class SerialExecutor implements Executor {
      final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
      Runnable mActive;
      public synchronized void execute(final Runnable r) {
          mTasks.offer(new Runnable() {//1
              public void run() {
                  try {
                      r.run();//2
                  } finally {
                      scheduleNext();
                  }
              }
          });
          if (mActive == null) {
              scheduleNext();
          }
      }
      protected synchronized void scheduleNext() {
          if ((mActive = mTasks.poll()) != null) {
              THREAD_POOL_EXECUTOR.execute(mActive);
          }
      }
  }

  从注释1处可以看出,当调用SerialExecutor的execute方法时,会将FutureTask加入到mTasks中。当任务执行完或者当前没有活动的任务时都会执行scheduleNext方法,它会从mTasks取出FutureTask任务并交由THREAD_POOL_EXECUTOR处理。关于THREAD_POOL_EXECUTOR,后面会介绍。从这里看出SerialExecutor是串行执行的。在注释2处可以看到执行了FutureTask的run方法,它最终会调用WorkerRunnable的call方法。

前面我们提到call方法postResult方法将结果投递出去,postResult方法代码如下所示。

private Result postResult(Result result) {
       @SuppressWarnings("unchecked")
       Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
               new AsyncTaskResult<Result>(this, result));
       message.sendToTarget();
       return result;
   }

在postResult方法中会创建Message,并将结果赋值给这个Message,通过getHandler方法得到Handler,并通过这个Handler发送消息,getHandler方法如下所示。

private static Handler getHandler() {
    synchronized (AsyncTask.class) {
        if (sHandler == null) {
            sHandler = new InternalHandler();
        }
        return sHandler;
    }
}

在getHandler方法中创建了InternalHandler,InternalHandler的定义如下所示。

private static class InternalHandler extends Handler {
      public InternalHandler() {
          super(Looper.getMainLooper());
      }

      @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
      @Override
      public void handleMessage(Message msg) {
          AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
          switch (msg.what) {
              case MESSAGE_POST_RESULT:
                  // There is only one result
                  result.mTask.finish(result.mData[0]);
                  break;
              case MESSAGE_POST_PROGRESS:
                  result.mTask.onProgressUpdate(result.mData);
                  break;
          }
      }
  }

在接收到MESSAGE_POST_RESULT消息后会调用AsyncTask的finish方法:

private void finish(Result result) {
    if (isCancelled()) {
        onCancelled(result);
    } else {
        onPostExecute(result);
    }
    mStatus = Status.FINISHED;
}

如果AsyncTask任务被取消了则执行onCancelled方法,否则就调用onPostExecute方法。而正是通过onPostExecute方法我们才能够得到异步任务执行后的结果。
接着回头来看SerialExecutor,线程池SerialExecutor主要用来处理排队,将任务串行处理。 SerialExecutor中调用scheduleNext方法时,将任务交给THREAD_POOL_EXECUTOR。THREAD_POOL_EXECUTOR同样是一个线程池,用来执行任务。

private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE_SECONDS = 30;
private static final BlockingQueue<Runnable> sPoolWorkQueue =
        new LinkedBlockingQueue<Runnable>(128);
public static final Executor THREAD_POOL_EXECUTOR;
static {
    ThreadPoolExecutor threadPoolExecutor = new threadPoolExecutor (
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            sPoolWorkQueue, sThreadFactory);
    threadPoolExecutor.allowCoreThreadTimeOut(true);
    THREAD_POOL_EXECUTOR = threadPoolExecutor;
}

THREAD_POOL_EXECUTOR指的就是threadPoolExecutor,他的核心线程和线程池允许创建的最大线程数都是由CPU的核数来计算出来的。它采用的阻塞队列仍旧是LinkedBlockingQueue,容量为128。
到此, Android 7.0版本的AsyncTask的源码就分析完了,在AsyncTask中用到了线程池,线程池中运行线程并且又用到了阻塞队列,因此,本章前面介绍的知识在这一节中做了很好的铺垫。Android 3.0及以上版本用SerialExecutor作为默认的线程,它将任务串行的处理保证一个时间段只有一个任务执行,而3.0之前版本是并行处理的。关于3.0之前版本的缺点在3.0之后版本也不会出现,因为线程是一个接一个执行的,不会出现超过任务数而执行饱和策略。如果想要在3.0及以上版本使用并行的线程处理可以使用如下的代码:

asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,"");

其中asyncTask是我们自定义的AsyncTask,当然也可以传入Java提供的线程池,比如传入CachedThreadPool。

syncTask.executeOnExecutor(Executors.newCachedThreadPool(),"");

也可以传入自定义的线程池:

Executor exec =new ThreadPoolExecutor(0,  Integer.MAX_VALUE,
             0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
asyncTask.executeOnExecutor(exec,"");

以上内容摘自《Android进阶之光》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值