Android 多线程编程全面篇

一、线程的状态

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

二、创建线程

2.1、创建线程三种方法

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

public class TestThread extends Thread {
    @Override
    public void run() {
        Log.d("TAG", "Hello World");
    }
}
        Thread thread = new TestThread();
        thread.start();

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

public class TestThread extends Thread {
    @Override
    public void run() {
        Log.d("TAG", "Hello World");
    }
}
        TestRunnable runnable = new TestRunnable();
        Thread thread = new Thread(runnable);
        thread.start();

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

public class TestCallable implements Callable {
    @Override
    public Object call() throws Exception {
        return "Hello World";
    }
}
        TestCallable callable = new TestCallable();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future future = executorService.submit(callable);
        try {
            // 等待线程结束,并返回结果
            Log.d("TAG", (String) future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }

在上面 3 种方式中,一般推荐用实现 Runnable 接口的方式。

2.2、线程中断

参考博客

2.2.1、对于当前线程,即在线程内调用

  • Thread.currentThread().interrupt():将当前线程的标志位设置为 true。
  • Thread.currentThread().isInterrupted() :不会清除线程的中断标志位,即不会将标志位重新重新设置为 false,注意这里指的是当前线程的标志位。
  • Thread.interrupted():对于当前线程,得到中断标志位,如果发现中断标志位为 true,在返回当前标志位后会将标志位重新设置为 false。即如果发现中断标志位为 true,连续两次调用,第二次将返回 false。

2.2.2、通过线程对象调用,即在线程外调用

  • thread.isInterrupted() :会清除该线程的中断标志位,即将标志位重新设置为 false。应该使用上面的 Thread.currentThread().isInterrupted()
  • thread.interrupt():将该线程的标志位设置为 true。
    例:调用 thread.interrupt() 可以将标志位设置为 true,然后在线程中通过循环判断标志位来中断线程。
        TestRunnable runnable = new TestRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
        thread.interrupt();
public class TestRunnable implements Runnable {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            Log.d("TAG", "Hello World");
        }
    }
}

2.2.3、线程阻塞时不能中断线程

  • 如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait、1.5中的condition.await、以及可中断的通道上的 I/O 操作方法后可进入阻塞状态),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法(sleep、join、wait、1.5中的condition.await及可中断的通道上的 I/O 操作方法)调用处抛出InterruptedException异常,并且在抛出异常后立即将线程的中断标示位清除,即重新设置为false。
  • 例如。
public class TestRunnable implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TestRunnable runnable = new TestRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
        thread.interrupt();
    }
}

在这里插入图片描述

2.3、安全的终止线程

疑问:不知道为什么线程结束并不打印 Thread Stop,但是 debug 却会打印。

2.3.1、使用中断标志位终止线程

public class TestRunnable implements Runnable {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            Log.d("TAG", "Hello World");
        }
        Log.d("TAG", "Thread Stop");
    }
}
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TestRunnable runnable = new TestRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
        try {
            Thread.sleep(3000);
            thread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.3.2、采用 boolean 变量终止线程

volatile 关键字,在涉及多个线程对这个变量的访问时,当有其他线程改变其值时,所有的线程都会感知到它的变化。

public class TestRunnable implements Runnable {
    private volatile boolean on = true;

    @Override
    public void run() {
        while (on) {
            Log.d("TAG", "Hello World");
        }
        Log.d("TAG", "Thread stop");
    }

    public void cancel() {
        on = false;
    }
}
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TestRunnable runnable = new TestRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
        try {
            Thread.sleep(3000);
            runnable.cancel();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

三、线程同步

3.1、重入锁与条件对象

参考博客

一、先来看一看用 ReentrantLock 保护代码块的结构,如下。

        Lock mLock = new ReentrantLock();
        mLock.lock();
        try {
            // 临界区
        } finally {
            mLock.unlock();
        }
  • 一旦一个线程封锁了锁对象(// 临界区),其他任何线程都无法进入 Lock 语句。
  • 把解锁的操作放在 finally 中是十分必要的。如果在临界区发生了异常,琐是必须要释放的,否则其他线程将会永远被阻塞。

下面通过一个小例子展示了 ReentrantLock 的最基本使用。

public class MainActivity extends AppCompatActivity {

    private static final Lock lock = new ReentrantLock();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable() {
            @Override
            public void run() {
                test();
            }
        }, "线程A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test();
            }
        }, "线程B").start();
    }

    public static void test() {
        try {
            lock.lock();
            Log.d("TAG", Thread.currentThread().getName() + "获取了锁");
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            Log.d("TAG", Thread.currentThread().getName() + "释放了锁");
            lock.unlock();
        }
    }
}

在这里插入图片描述
二、通过条件对象来管理那些已经获得了锁但是却不能做有用工作的线程。通过一个例子说明。

疑问:signalAll 和 signal 的区别。

假设现在有 3 个账户,一个账户向另一个账户转账,发现钱不够时,如果有其他线程给这个转账方再转足够的钱,就可以转账成功了。但是这个线程现在已经获取了锁,具有排他性,别的线程无法获取锁进行存款,这时候就需要引入条件对象,一个锁拥有多个相关的条件对象,可以用 newCondition 方法来获得一个条件对象,调用 await 方法,一旦一个线程调用了这个方法,它就会进入该条件的等待集并处于阻塞状态,知道另一个线程调用了同一个条件的 signalAll 方法为止。

  1. 定义账户实体类
public class Account {
    public String name;
    public double money;

    public Account(String name, double money) {
        this.name = name;
        this.money = money;
    }
}
  1. 定义一个用于转账的类
public class TransferUtil {
    private Lock mLock;
    private Condition mCondition;

    public TransferUtil() {
        mLock = new ReentrantLock();
        // 获取条件对象
        mCondition = mLock.newCondition();
    }

    public void transfer(Account from, Account to, double money) throws InterruptedException {
        mLock.lock();
        try {
            while (from.money < money) {
                // 阻塞当前线程,并放弃锁
                Log.d("TAG", "阻塞中," + from.name + "向" + to.name + "钱不够,等待转账");
                mCondition.await();
            }
            // 转账的操作
            from.money = from.money - money;
            to.money = to.money + money;
            mCondition.signalAll();
            Log.d("TAG", from.name + "向" + to.name + ":" + money);
            Log.d("TAG", "转账后:" + from.name + "有" + from.money + "/" + to.name + "有" + to.money);
        } finally {
            mLock.unlock();
        }
    }
}
  1. 有 3 个账户各有金额 100,当 A 向 B 转发现金额不够,进入阻塞状态,等待 C 向 A 转入 100 后,A 再向 B 成功转账。
public class MainActivity extends AppCompatActivity {
    private TransferUtil transferTest = new TransferUtil();
    private Account accountA = new Account("A", 100);
    private Account accountB = new Account("B", 100);
    private Account accountC = new Account("C", 100);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    transferTest.transfer(accountA, accountB, 200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "线程A").start();

        // 延迟 3 秒后转账
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    transferTest.transfer(accountC, accountA, 100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "线程B").start();
    }
}
  1. 运行效果

在这里插入图片描述

3.2、同步方法

Java 中每一个对象都有一个内部锁,如果一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。跟 3.1 中的重入锁对比如下。

    public synchronized void method() {
        // 临界区
    }
        Lock mLock = new ReentrantLock();
        mLock.lock();
        try {
            // 临界区
        } finally {
            mLock.unlock();
        }

所以对于上面转账的方法还可以这样写,将方法声明为 synchronized,而不是使用一个显示的锁。内部对象锁只有一个相关条件,wait 方法将一个线程添加到等待集中,notifyAll 或 notify 方法解除等待线程的阻塞状态。也就是说,wait 相当于调用 mCondition.await(),notifyAll 等价于 mCondition.signalAll()。效果一样,并且要更简洁。

    public synchronized void transfer(Account from, Account to, double money) throws InterruptedException {
        while (from.money < money) {
            // 阻塞当前线程,并放弃锁
            Log.d("TAG", "阻塞中," + from.name + "向" + to.name + "钱不够,等待转账");
            wait();
        }
        // 转账的操作
        from.money = from.money - money;
        to.money = to.money + money;
        notifyAll();
        Log.d("TAG", from.name + "向" + to.name + ":" + money);
        Log.d("TAG", "转账后:" + from.name + "有" + from.money + "/" + to.name + "有" + to.money);
    }

3.3、同步代码块

在 3.2 中,我们知道了每一个 Java 都有一个锁,线程可以调用同步方法来获得锁。还有另一种机制可以获得锁,就是使用一个同步代码块,代码如下。

        synchronized (obj) {
            
        }

其获得了 obj 的锁,obj 指的是一个对象。改写转账的类来看效果。

public class TransferUtil2 {
    private Object mLock;

    public TransferUtil2() {
        mLock = new Object();
    }

    public void transfer(Account from, Account to, double money) {
        synchronized (mLock) {
            // 转账的操作
            from.money = from.money - money;
            to.money = to.money + money;
            Log.d("TAG", from.name + "向" + to.name + ":" + money);
            Log.d("TAG", "转账后:" + from.name + "有" + from.money + "/" + to.name + "有" + to.money);
        }
    }
}

在这里插入图片描述
在这里创建了一个名为 Lock 的 Object 类,为的是使用 Object 类所持有的锁。同步代码块是非常脆弱的,通常不推荐使用。如果 3.2 中的同步方法适合你的程序,那么尽量使用同步方法,这样可以减少编写代码的数量,如果特别需要使用 3.1 中的 Lock/Condition 结构提供特殊的独有特性,才使用 Lock/Condition。

3.4、volatile

有时仅仅为了读写一个或者两个实例域就使用同步的话,显得开销过大。而 volatile 关键字为实例域的同步访问提供了免锁的机制。如果声明一个域为 volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。

3.4.1、Java 内存模型

Java 中的堆内存用来存储对象实例,堆内存是被所有线程共享的运行时内存区域,因此,它存在内存可见性的问题。而局部变量、方法定义的参数则不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java 内存模型定义了线程和主存之间的抽象关系:线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的副本。需要注意的是本地内存是 Java 内存模型的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器等区域。

下面是 Java 内存模型的抽象示意图。
在这里插入图片描述
线程 A 与 线程 B 之间若要通信的话,必须要经历两个步骤:

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

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

 int i=3;

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

3.4.2、原子性、可见性和有序性

原子性

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

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

在上面 3 个语句中,只有语句 1 是原子性操作,其他两个语句都不是原子性操作。语句 2 虽说很短,但它包含了两个操作,它先读取 x 的值,再将 x 的值写入工作内存。读取 x 的值以及将 x 的值写入工作内存这两个操作单拿出来都是原子性操作,但是合起来就不是原子性操作了。语句 3 包括 3 个操作:读取 x 的值进行加 1,向工作内存写入新值,通过这 3 个语句我们得知,一个语句含有多个操作时,就不是原子性操作,只有简单的读取和赋值(将数字赋给某个变量)才是原子性操作。

可见性

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。当一个共享变量被 volatile 修饰时,它会保证修改的值立即被更新到主内存,所以对其他线程是可见的。当有其他线程需要读取该值时,其他线程会去主内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,并不会立即被写入主内存,何时被写入主内存也是不确定的。当其他线程去读取该值时,此时主内存中可能还是原来的旧值,这样就无法保证可见性。

有序性

参考博客

先看一个例子。

        int a = 2; //语句 1
        int b = 5; //语句 2
        a = a + 3; //语句 3
        b = a * b; //语句 4

这段代码有 4 个语句,那么执行顺序可能是:

语句1 -> 语句2 -> 语句3 -> 语句4
语句2 -> 语句1 -> 语句3 -> 语句4

在 Java 内存模型中允许编译器和处理器对指令进行重排序,所以语句 1 和语句 2 并不保证执行的顺序。

那么可不可能是:

语句2 -> 语句1 -> 语句4 -> 语句3

答案是不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令 2 必须用到 1 的结果,那么处理器会保证 1 会在 2 之前执行。

下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。但这只是在单线程中。

虽然重排序不会影响到单线程执行的正确性,但是在多线程并发执行的情况下,结果就不一样了,这时我们可以通过 volatile 来保证有序性,除了 volatile,也可以通过 synchronized 和 Lock 来保证有序性。我们知道,synchronized 和 Lock 保证每个时刻只有一个线程执行同步代码,这相当于是让线程顺序执行同步代码,从而保证了有序性。

3.4.3、volatile 关键字

当一个共享变量被 volatile 修饰之后,其就具备了两个含义。

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

看一个例子。

        // 线程 1
        boolean stop = false;
        while (!stop) {
            // doSomething
        }
        // 线程 2
        stop = true;

像 2.3.2 中一样,采用 boolean 变量中断线程,但是这里有两个线程控制 stop 变量。在前面提到,每一个线程在运行时都有私有的工作内存,因此线程 1 在运行时会将 stop 变量复制一份放到私有的工作内存中。当线程 2 更改了 stop 变量的值之后,线程 2 突然需要去做其他的操作,这时就无法将更改的 stop 变量写入到主内存中,这样线程 1 就不会知道线程 2 对 stop 变量进行了更改,线程 1 就会一直循环下去,造成了死循环。但是当 stop 变量用 volatile 修饰后,当线程 2 进行修改是,会强制将修改的值立即写入主内存,并且会导致线程 1 的工作内存中 stop 变量的缓存行无效,这样线程 1 再次读取 stop 变量的值就会去主内存读取。

volatile 保证了操作的可见性,但是不保证原子性。前面提过,对于自增操作,是不具备原子性的,它包括读取变量的原始值、进行加 1、写入工作内存。也就是说,自增操作的 3 个子操作可能会分割开执行。假如现在有两个线程对一个用 volatile 修饰的变量进行自增操作。线程 1 对变量进行自增操作,先读取了变量的原始值,然后就被阻塞了。之后线程 2 对变量进行自增操作,也去读取变量的原始值,接着进行加 1 操作并写入到主内存中,这时因为线程 1 已经读取了变量的值,所以不会再去主内存读取最新的值,就导致了线程 1 对变量进行加 1 操作后的值就和线程 2 中对变量自增后的结果一样,都是原始值加 1,之后写入工作内存,再写入主内存。在两个线程分别对变量进行了一次自增操作后,变量的值也只是增加了 1,可以看出,自增操作不是原子性操作,volatile 也无法保证对变量的操作时原子性的。

volatile 保证有序性,volatile 关键字能禁止指令重排序,因此 volatile 能保证有序性。volatile 关键字禁止指令重排序有两个含义:一个是当程序执行到 volatile 变量的操作时,在其前面的操作已经全部执行完毕,并且结果会对后面的操作可见,在其后面的操作还没有进行;二是在指令优化时,在 volatile 变量之前的语句不能在 volatile 变量后面执行,同样,在 volatile 变量之后的语句也不能在 volatile 变量前面执行。

3.4.4、正确使用 volatile 关键字

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

  • 对变量的操作不会依赖于当前值。即不能是自增自减等操作。
  • 该变量没有包含在其他变量的不变式中。我的理解是不依赖于程序中的其他变量的状态等。

volatile 使用场景

1. 状态标志

    private volatile boolean shutdown;

    public void shutdown() {
        shutdown = true;
    }

    public void doWork() {
        while (!shutdown) {
            // do something
        }
    }

如果在另一个线程中调用 shutdown 方法来改变 shutdown 变量的值,就需要执行某种同步来确保 shutdown 变量的可见性。在这里,状态标志 shutdown 变量不依赖于程序的任何其他状态,相比于 synchronized,推荐使用 volatile,因为可以大大的简化代码。

2. 双重检查模式(DCL,Double Check Lock)

public class Singleton {
    private volatile static Singleton instance = null;

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

getInstance 方法中对 Singleton 进行了两次判空,第一次是为了不必要的 synchronized 同步,第二次是只有在 Singleton 等于 null 的情况下才创建实例。在这里用到了 volatile 关键字会或多或少地影响性能,但考虑到程序的正确性,牺牲这点还是值得的。DCL 的优点是资源利用率高,第一次执行 geInstance 方法时单例对象才被实例化,效率高。缺点是第一次加载时反应稍微慢一些,在高并发的环境下有一定的缺陷(虽然发生的概率很小)。

3.4.5、小结

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

四、阻塞队列

4.1、阻塞队列简介

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

常见阻塞场景

  1. 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。

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

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

放入数据的方法

  • offer(E e):将元素放入阻塞队列,有返回值,如果队列可以容纳,返回 true,不可以容纳返回 false。
  • offer(E e, long timeout, TimeUnit unit):可以设置放入队列的等待时间,有返回值,如果在指定的时间内还不能加入元素,则返回 false。
  • put(E e):将元素放入阻塞队列,没有返回值,如果队列没有容纳空间,调用此方法的线程会被阻塞,知道队列中有空间再继续。

获取数据的方法

  • E poll(long timeout, TimeUnit unit):从队列中取出一个队首的对象,在设置时间内,如果队列中存在队首可取,则立即返回队列中的数据,否则返回失败。
  • E take():从队列中取出队首对象,如果队列为空,则阻塞线程,直到队列有新的数据加入。
  • int drainTo(Collection<? super E> c):一次性从队列中获取所有可用的数据对象。
  • int drainTo(Collection<? super E> c, int maxElements):一次性从队列中获取指定个数的可用的数据对象。

4.2、Java 中的阻塞队列

Java 中提供了 7个阻塞队列。

  • ArrayBlockingQueue:由数据结构组成的有界阻塞队列。

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

ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(2000, true);

  • LinkedBlockingQueue:由链表结构组成的有界阻塞队列。

    它是基于链表的阻塞队列,同 ArrayBlockingQueue 类似,此队列按照先进先出(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。

  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

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

4.3、阻塞队列的使用场景

首先使用 Object.wait()、Object.notify() 和非阻塞队列实现生产者-消费者模式。

public class QueueTest {
    private int queueSize = 3;
    private PriorityQueue<String> queue = new PriorityQueue<>(queueSize);

    public class Consumer extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.size() == 0) {
                        try {
                            Log.d("TAG", "队列空,等待数据");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify();
                        }
                    }
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 每次取出队首元素
                    String people = queue.poll();
                    Log.d("TAG", "取出元素:" + people);
                    queue.notify();
                }
            }
        }
    }

    public class Producer extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.size() == queueSize) {
                        try {
                            Log.d("TAG", "队列满,等待有空余空间");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify();
                        }
                    }
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 每次插入一个元素
                    queue.offer("Tom");
                    Log.d("TAG", "插入元素");
                    queue.notify();
                }
            }
        }
    }
}

再使用阻塞队列实现生产者-消费者模式。

public class QueueTest {
    private int queueSize = 3;
    private ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(queueSize);

    public class Consumer extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    String people = queue.take();
                    Log.d("TAG", "取出元素:" + people);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public class Producer extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    queue.put("Tom");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

使用方式如下。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        QueueTest test = new QueueTest();
        QueueTest.Producer producer = test.new Producer();
        QueueTest.Consumer consumer = test.new Consumer();
        producer.start();
        consumer.start();
    }
}

显然,使用阻塞队列实现无须单独考虑同步和线程间通信的问题,实现起来更加简单。

五、线程池

在编程中经常会使用线程来异步处理任务,但是每个线程的创建和销毁都需要一定的开销,如果每次执行一个任务都需要开一个新线程去执行,则这些线程的创建和销毁将消耗大量的资源;并且线程都是 “各自为政” 的,很难对其进行控制,跟何况有一堆线程在执行。这时就需要线程池来对线程进行管理。

5.1、ThreadPoolExecutor

线程基础:线程池(5)——基本使用(上)
Android线程池原理及使用

线程池的核心实现类,我们也可以通过此类来创建线程池,但是通过构造方法可以看出,这种方式创建线程池很复杂,官方也并不推荐。

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

在这里插入图片描述

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

使用示例:

        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 2000,
                TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(25));
        for (int i = 0; i < 30; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(2000);
                        Log.d("当前线程:", Thread.currentThread().getName());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            executor.execute(runnable);
        }

一共提交了 30 个线程,任务队列最大容量是 25,这时线程池中就会创建并同时执行 5 个线程,并达到了最大线程数,其中有 2 个核心线程,因此应该就有 3 个非核心线程。如果任务队列最大容量 25 加上最大线程数 5 的和小于所提交的线程数 30,就会执行饱和策略,默认会抛出 RejectedExecutionException 异常。以下执行效果可以看出一次会执行 5 个线程。
在这里插入图片描述

5.2、线程池的种类

通过直接或间接地配置 ThreadPoolExecutor 的参数可以创建不同类型的 ThreadPoolExecutor,Java 通过 Executors 提供四种线程池,它们分别是 FixedThreadPool、CachedThreadPool、SingleThreadExecutor 和 ScheduledThreadPool。

1、FixedThreadPool:可重用固定线程数的线程池。

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

FixedThreadPool 的 corePoolSize 和 maximumPoolSize 都设置为创建 FixedThreadPool 指定的参数 nThreads,也就意味着 FixedThreadPool 只有核心线程,并且数量是固定的,没有非核心线程。keepAliveTime 设置为 0L 意味着多余的线程会被立即终止,因为不会产生多余的线程,所以 keepAliveTime 是无效的参数。另外,任务队列采用了无界阻塞队列 LinkedBlockingQueue(容量默认为 Integer.MAX_VALUE)。FixedThreadPool 的 execute 方法的执行示意图如下。
在这里插入图片描述
当执行 execute 方法时,如果当前运行的线程未达到 corePoolSize(核心线程数)时就创建核心线程来处理任务,如果达到了核心线程数则将任务添加到 LinkedBlockingQueue 中。FixedThreadPool 就是一个有固定数量核心线程的线程池,并且这些线程不会被回收。当线程数超过 corePoolSize 时,就将任务存储在任务队列中;当线程池有空闲线程时,则从任务队列中去取任务执行。

使用示例:

        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 30; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(2000);
                        Log.d("当前线程:", Thread.currentThread().getName());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            executorService.execute(runnable);
        }

2、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,它是一个不存储元素的阻塞队列,每次插入操作必须伴随一个移除操作,一个移除操作也要伴随一个插入操作。CachedThreadPool 的 execute 方法的执行示意图如下。
在这里插入图片描述
当执行 execute 方法时,先用 SynchronousQueue 的 offer 方法提交任务,如果线程池中有线程空闲,则调用 SynchronousQueue 的 poll 方法来移除任务并交给线程处理;如果没有线程空闲,则开启一个新的非核心线程来处理任务。由于 maximumPoolSize 是无界的,所以如果线程处理任务速度小于提交任务的速度,则会不断地创建新的线程,这时需要注意不要过度创建,应采取措施调整双方速度,不然线程创建太多会影响性能。另外,每次提交任务都会立即有线程去处理。所以,CachedThreadPool 比较适用于大量的需要立即执行的耗时少的任务。

使用示例:

        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 30; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(2000);
                        Log.d("当前线程:", Thread.currentThread().getName());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            executorService.execute(runnable);
        }

3、SingleThreadExecutor :使用单个工作线程的线程池。

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

corePoolSize 和 maximumPoolSize 都为 1,意味着 SingleThreadExecutor 只有一个核心线程,其他的参数都和 FixedThreadPool 一样。SingleThreadExecutor 的 execute 方法的执行示意图如下。
在这里插入图片描述
当执行 execute 方法时,如果当前运行的线程数未达到核心线程数,也就是当前没有运行的线程,则创建一个新线程来处理任务。如果当前有运行的线程,则将任务添加到阻塞队列 LinkedBlockingQueue 中。因此,SingleThreadExecutor 能确保所有的任务在一个线程中按照顺序逐一执行。

使用示例:

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 30; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(2000);
                        Log.d("当前线程:", Thread.currentThread().getName());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            executorService.execute(runnable);
        }

4、ScheduledThreadPool:能实现定时和周期性任务的线程池。

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

这里创建了 ScheduledThreadPoolExecutor,继承自 ThreadPoolExecutor,主要用于给定延时之后的运行任务或者定期处理任务。构造方法如下。

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

从上面的代码可以看出,ScheduledThreadPoolExecutor 的构造方法最终调用的是父类 ThreadPoolExecutor 的构造方法。corePoolSize 传进来的是固定数值,maximumPoolSize 的值是 Integer.MAX_VALUE。因为采用的 DelayedWorkQueue 是无界的,所以 maximumPoolSize 这个参数是无效的。ScheduledThreadPoolExecutor 的 execute 方法的执行示意图如下。
在这里插入图片描述
当执行 ScheduledThreadPoolExecutor 的 scheduleAtFixedRate 或者 scheduleWithFixedDelay 方法时,会向 DelayWorkQueue 添加一个实现 RunnableScheduledFuture 接口的 ScheduledFutureTask(任务的包装类),并会检查运行的线程是否达到了 corePoolSize。如果没有则创建新线程并启动它,但并不是立即去执行任务,而是去 DelayedWorkQueue 中取到 ScheduledFutureTask,然后去执行任务。如果运行的线程达到了 corePoolSize 时,则将任务添加到 DelayedWorkQueue 中。DelayedWorkQueue 会将任务进行排序,先要执行的任务放在队列的前面。跟前面介绍的几个线程池不同的是,当执行完任务后,会将 ScheduledFutureTask 中的 time 变量改为下次要执行的时间并放回到 DelayedWorkQueue 中。

使用示例:

        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
        for (int i = 0; i < 5; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    Log.d("当前线程:", Thread.currentThread().getName());
                }
            };
            executorService.scheduleAtFixedRate(runnable, 3, 5, TimeUnit.SECONDS);
        }

    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);

在这里核心线程数设为 3,提交了 5 个任务,scheduleAtFixedRate 方法的时间参数跟核心线程数和任务提交数并没有关系,在这里执行过程为,在第一次延迟3 秒(initialDelay ) 后开始执行 3 个任务(核心线程数),执行完马上执行剩余的 2 个任务(一共提交了 5 个任务),都执行完后间隔 5 秒(period)再以同样的方式重复执行。

上面的例子中要注意,间隔 5 秒(period)只是当任务执行时间少于间隔时间(period)时有用,如果任务执行时间大于间隔时间,那么此间隔时间会变成任务执行时间,如下任务执行中加 6 秒延迟,当 5 个任务执行完后,不再是间隔 5 秒(period),而是 6 秒了,由此也可以看出,所谓的间隔时间(period)是指每次最开始提交任务时到所有任务执行完毕再到开始下一次重复执行前的时间,在这里是当 i =0 提交任务时,到 5 个任务执行完毕,再到下一轮开始的间隔时间。

        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
        for (int i = 0; i < 5; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(6000);
                        Log.d("当前线程:", Thread.currentThread().getName());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            executorService.scheduleAtFixedRate(runnable, 3, 5, TimeUnit.SECONDS);
        }

对于 scheduleWithFixedDelay,间隔时间就有所不同,它所指的间隔时间就不受任务执行时间的影响,也不再值提交任务时到任务执行完毕再到下一轮的间隔时间了,就单纯的指每次任务执行完毕后到下一次执行之间的时间。参考博客

六、AsyncTask 的使用

参考博客

当我们通过线程去执行耗时任务,并在操作完成后可能还要更新 UI,通常会用到 Handler 来更新 UI 线程。虽然实现起来简单,但是如果有多个任务同时执行时则会显得代码臃肿。Android 提供了 AsyncTask,它使得异步任务实现起来更加简单,代码更简洁。

public abstract class AsyncTask<Params, Progress, Result>{
  
    ...
  
    @MainThread
    protected void onPreExecute() {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    @MainThread
    protected void onPostExecute(Result result) {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    @MainThread
    protected void onProgressUpdate(Progress... values) {
    }

    @SuppressWarnings({"UnusedParameters"})
    @MainThread
    protected void onCancelled(Result result) {
        onCancelled();
    }    

    @MainThread
    protected void onCancelled() {
    }

    @WorkerThread
    protected abstract Result doInBackground(Params... params);
    
    ...
}

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值