多线程——《Android开发进阶从小工到专家》第3章读书笔记

3.1 Android中的消息机制

3.1.1 处理消息的手段——Handler、Looper与MessageQueue

  • 在Android应用启动时,会默认有一个主线程(UI线程),在这个线程中会关联一个消息队列,所有的操作都会被封装成消息然后交给主线程来处理。为了保证主线程不会主动退出,会将获取消息的操作放在一个死循环中,这样程序就相当于一直在执行死循环,因此不会退出。

3.2 Android中的多线程

3.2.1 多线程的实现——Thread和Runnable

  • Thread也是一个Runnable,它实现了Runnable接口,在Thread类中有一个Runnable类型的target字段,代表要被执行在这个子线程中的任务。

3.2.3 与多线程相关的方法——Callable、Future和FutureTask

  1. Callable:与Runnable的功能大致相似,不过它有一个泛型参数V,该接口有一个返回值(V)的call()函数。
  2. Future:为线程池制定了一个可管理的任务标准。提供了对Runnable或Callable任务的执行结果进行取消(cancel)、查询是否完成(isDone)、获取结果(get)、设置结果操作(set)。get方法会阻塞,直到任务返回结果。
  3. FutureTask:是Future的实现类。实现了RunnableFuture,而RunnableFuture实现了Runnable、Future这两个接口。FutureTask会像Thread包装Runnable那样对Runnable和Callable进行包装,Runnable与Callable由构造函数注入。总的来说,FutureTask既是Future、Runnable,又包装了Callable(如果是Runnable最终也会被转换为Callable),它是两者的合体。

3.2.4 构建服务器应用程序的有效方法——线程池

  • 线程池的使用准则
    线程池的最佳大小取决于可用处理器的数目以及工作队列中的任务的性质。若在一个具有N个处理器的系统上只有一个工作队列,其中全部是计算性质的任务,在线程池具有N或N+1个线程时一般会获得最大的CPU利用率;对于那些可能需要等待I/O完成的任务(例如,从套接字读取HTTP请求的任务),需要让线程池的大小超过可用处理器的数目,因为并不是所有线程都一直在工作。

3.2.5 同步集合

  1. 程序中的优化策略——CopyOnWriteArrayList

    Copy-On-Write是一种用于程序设计中的优化策略,基本思路:从多个线程共享一个列表,当某个线程想要修改这个列表的元素时,会把列表中的元素Copy一份,然后进行修改,修改完成后再将新的元素设置给这个列表。

    优点:可以对CopyOnWrite容器进行并发读,不需要加锁

    缺点:在添加、移除元素时占用的内存空间翻了一倍,以空间换时间。

    从JDK1.5开始提供了两个使用CopyOnWrite机制实现的并发容器:CopyOnWriteArrayList,CopyOnWriteArraySet。

  2. 提高并发效率——ConcurrentHashMap

    HashTable在竞争激烈的环境下效率低下的原因是:所有访问HashTable的线程都必须竞争同一把锁。假如容器里有多把锁每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,这就是ConcurrentHashMap所使用的锁分段技术。当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

  3. 有效的方法——BlockingQueue

    BlockingQueue在JDK中有多个实现:

    1. ArrayBlockingQueue:数组实现的、线程安全的、有界的阻塞队列。按FIFO原则对元素进行排序。
    2. LinkedBlockingQueue:单向链表实现的阻塞队列。FIFO。吞吐量通常高于基于数组的队列。
    3. LinkedBlockingDeque:双向链表实现的双向并发阻塞队列。支持FIFO、FILO。可以指定队列的容量,若不指定,默认容量大小为Integer.MAX_VALUE。

3.2.6 同步锁

3.2.6.1 同步机制关键字——synchronized

同步方法:public synchronized void method() {...}

同步块:public void method() { synchronized(this) {...} }

同步class对象:public void method() { synchronized(Demo.class) {...} }

同步静态方法:public synchronized static void method() {...}

前两种锁的是对象,后两种锁的是class对象。对于锁class对象,它的作用是防止多个线程同时访问添加了synchronized锁的代码块;对于锁对象,作用是防止其他线程访问同一个对象中的synchronized代码块或函数。

3.2.6.2 显示锁——ReetrantLock与Condition

与内置锁synchronized相比,(1)获取和释放的灵活性(2)轮循锁和定时锁(3)公平性

常用形式:

Lock lock = new ReetrantLock();

public void doSth() {
    lock.lock();
    try{
        //...
    } finally {
        lock.unlock();
    }
}

lock必须在finally块中释放。而synchronized,JVM将确保锁会获得自动释放。

在ReetrantLock类中还有一个函数newCondition(),该函数用于获取Lock上的一个条件,也就是说Condition是和Lock绑定的。Condition用于实现线程间的通信,它是为了解决Object.wait()、notify()、notifyAll()难以使用的问题。

  • 通过ReetrantLock与Condition来实现一个简单的阻塞队列:p91
3.2.6.3 信号量Semaphore

semaphore是一个计数信号量,它的本质是一个“共享锁”。信号量维护了一个信号量许可集,线程可以通过调用aquire()来获取信号量的许可。当信号量中有可用的许可时,线程能获取该许可(semaphore.acquire());否则线程必须等待,直到有可用的许可为止。线程可以通过release()来释放它所持有的信号量许可。

示例:

public class SemaphoreTest {
    static int time = 0;

    public static void main(String[] args) {
        final ExecutorService executorService = Executors.newFixedThreadPool(3);
        final Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 5; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        semaphore.acquire();
                        System.out.println("剩余许可:" + semaphore.availablePermits());
                        Thread.sleep(2000);
                        semaphore.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

输出:

剩余许可:1
剩余许可:1
剩余许可:0
剩余许可:2
剩余许可:1
3.2.6.4 循环栅栏CyclicBarrier

CyclicBarrier是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点。因为该barrier在释放等待线程后可以重用,所以称它为循环的barrier。

示例:

public class CyclicBarrierTest {
    private static final int SIZE = 5;
    private static CyclicBarrier mCyclicBarrier;

    public static void main(String[] args) {
        mCyclicBarrier = new CyclicBarrier(SIZE, new Runnable() {
            @Override
            public void run() {
                System.out.println(" --->满足条件,执行特定操作。参与者:" + mCyclicBarrier.getParties());
            }
        });

        //新建5个任务
        for (int i = 0; i < SIZE; i++) {
            new WorkerThread().start();
        }
    }

    static class WorkerThread extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "等待CyclicBarrier");
            try {
                //将mCyclicBarrier的参与者数量加1
                mCyclicBarrier.await();
            } catch (BrokenBarrierException | InterruptedException e) {
                e.printStackTrace();
            }
            //mCyclicBarrier的参与者数量等于5时,才继续往后执行
            System.out.println(Thread.currentThread().getName() + "继续执行");
        }
    }
}

输出:

Thread-0等待CyclicBarrier
Thread-1等待CyclicBarrier
Thread-2等待CyclicBarrier
Thread-3等待CyclicBarrier
Thread-4等待CyclicBarrier
 --->满足条件,执行特定操作。参与者:5
Thread-4继续执行
Thread-0继续执行
Thread-1继续执行
Thread-2继续执行
Thread-3继续执行

只有当5个线程都调用了mCyclicBarrier函数之后,后续的代码才会执行。例子中在5个函数都就位后首先会执行一个Runnable,也就是CyclicBarrier构造函数的第二个参数。
由此可知,CyclicBarrier实际上相当于可以用于多个线程等待,直到某个条件被满足。对于上述示例来说,这里的条件就是有指定个数的线程调用了mCyclicBarrier.await()函数。

3.2.6.5 闭锁CountDownLatch

CountDownLatch也是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待,直到条件被满足。

示例:

public class CountDownLatchTest {
    private static int LATCH_SIZE = 5;

    public static void main(String[] args) {
        try {
            CountDownLatch latch = new CountDownLatch(LATCH_SIZE);
            //新建5个任务
            for (int i = 0; i < LATCH_SIZE; i++) {
                new WorkerThread(latch).start();
            }

            System.out.println("主线程等待");
            //主线程等待线程池中5个任务完成
            latch.await();
            System.out.println("主线程继续执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class WorkerThread extends Thread {
        CountDownLatch mLatch;

        public WorkerThread(CountDownLatch latch) {
            mLatch = latch;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + "执行操作");
                //将CountDownLatch的数值减1
                mLatch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出:

主线程等待
Thread-0执行操作
Thread-1执行操作
Thread-2执行操作
Thread-3执行操作
Thread-4执行操作
主线程继续执行

main函数中创建了一个数量为5的CountDownLatch对象,任务创建、启动5个WorkerThread对象,然后调用CountDownLactch对象的await函数使主线程进入等待状态。当5个WorkerThread都调用了CountDownLactch对象的countDown()后,主线程就会被唤醒。

CountDownLatch和CyclicBarrier的不同点

  1. CountDownLatch的作用是允许1个或N个线程等待其他线程完成执行,而CyclicBarrier则是允许N个线程相互等待。
  2. CountDownLatch的计数器无法被重置,CyclicBarrier的计数器可以被重置后使用,因此称为是循环的barrier。