Android 多线程的那些事

909 阅读10分钟

我正在参加「掘金·启航计划」

并发就是同时做几件事情,比如网络数据获取,文件下载,数据库读写等这些耗时的业务逻辑,都需要我们使用子线程去处理,每个线程分工明确,主线程才能更好地处理其他事情,避免 ANR。

其实,我们的应用看似很多个线程,因为 CPU 计算速度太快,能迅速地进行线程间的创建与切换,把时间拉宽来看,就好像有多个线程在同时执行,但是,如果把时间拉短来看,它还是执行完该线程后才去执行另一个线程。

进程和线程的区别

进程:

  • 进程是系统资源分配的基本单位,每个应用程序在 Android 中都运行在自己的进程中。
  • 每个进程都有独立的内存空间,不同进程之间的数据不能直接共享,需要通过特定的方式进行通信。
  • 进程之间的切换开销较大,因为切换进程需要保存和恢复更多的状态信息。

线程:

  • 线程是进程内的执行单元,一个进程可以包含多个线程。
  • 同一进程内的线程共享内存空间,可以直接访问共享数据,因此线程之间的通信比进程之间的通信更方便高效。
  • 线程之间的切换开销较小,因为线程共享相同的地址空间,切换只需要保存和恢复少量状态信息。

线程按照特定的顺序执行

利用 join

join 可以处理一些需要等待任务完成后才能继续往下执行的场景。

Thread t1 = new Thread(() -> System.out.println("Executing thread 1"));
Thread t2 = new Thread(() -> System.out.println("Executing thread 2"));
Thread t3 = new Thread(() -> System.out.println("Executing thread 3"));
try {
    t1.start();
    t1.join();
    t2.start();
    t2.join();
    t3.start();
} catch (InterruptedException e) {
    e.printStackTrace();
}

利用单线程化的线程池

Thread t1 = new Thread(() -> System.out.println("Executing thread 1"));
Thread t2 = new Thread(() -> System.out.println("Executing thread 2"));
Thread t3 = new Thread(() -> System.out.println("Executing thread 3"));

ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(t1);
executorService.submit(t2);
executorService.submit(t3);

利用 wait/notify 等待通知机制

wait 是 Object 的方法,作用是让当前线程进入等待状态,让当前线程释放它所持有的锁,直到其他线程调用此对象的 notify 或 notifyAll 方法,当前线程被唤醒。

boolean run1, run2;
final Object lock1 = new Object();
final Object lock2 = new Object();
Thread t1 = new Thread(() -> {
    synchronized (lock1) {
        System.out.println("Executing thread 1");
        run1 = true;
        lock1.notify();
    }
});

Thread t2 = new Thread(() -> {
    synchronized (lock1) {
        try {
            if (!run1) {
                lock1.wait();
            }
            synchronized (lock2) {
                System.out.println("Executing thread 2");
                lock2.notify();
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});

Thread t3 = new Thread(() -> {
    synchronized (lock2) {
        try {
            if (!run2) {
                lock2.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    System.out.println("Executing thread 3");
});

t1.start();
t2.start();
t3.start();

Thread.sleep(0) 到底睡没睡

操作系统是在一个线程释放 CPU 资源以后,重新计算所有线程的优先级来重新分配 CPU 资源,所以 sleep 真正的意义不是暂停,而是在接下去的时间内不参与 CPU 的竞争,等到 CPU 重新分配完以后,如果优先级没变,那么继续执行,所以 sleep(0) 的真正含义是触发 CPU 资源重新分配。

sleep 和 wait 有什么区别

  • 所属类不同:sleep 方法是 Thread 类的静态方法。wait 方法是 Object 类的方法。
  • 释放锁的情况不同:调用 sleep 方法时,线程不会释放锁,其他线程无法进入同步代码块或同步方法。调用 wait 方法时,线程会释放对象的锁,其他线程可以获得该对象的锁并执行同步代码块或同步方法。
  • 使用场景不同:sleep 方法通常用于暂停线程执行一段时间,一般用于控制线程的执行节奏。wait 方法用于线程之间的协作和通信,通常在多线程同步中,当一个线程需要等待某个条件满足时使用。
  • 唤醒方式不同:sleep 方法在指定的时间过后,线程会自动恢复执行。wait 方法需要通过其他线程调用同一对象的 notify 方法或 notifyAll 方法来唤醒等待的线程。

synchronized 到底锁住了啥

实例方法

public synchronized void method() {
    // 锁住的是该类的实例对象
}

静态方法

public static synchronized void method() {
    // 锁住的是类对象
}

实例对象代码块

synchronized (this) {
    // 锁住的是该类的实例对象
}

类对象代码块

synchronized (Test.class) {
    // 锁住的是类对象
}

任意实例对象代码块

Object obj = new Object();

synchronized (obj) {
    // 锁住的是配置的实例对象
}

多线程的三个特性

  • 可见性:当一个线程修改了共享变量的值后,其他线程能够立即看到这个修改。
  • 原子性:一个操作是不可分割的,要么全部执行成功,要么全部不执行。在多线程环境下,如果多个线程同时访问同一个共享变量并进行写操作,可能会导致数据不一致的问题。
  • 有序性:程序执行的顺序按照代码的规定顺序进行。

volatile,synchronized 和 Lock

  • volatile:可以保证线程对变量的修改对其他线程可见,其作用是强制将修改后的值立即写入主存,并通知其他线程刷新缓存。volatile 可以保证变量在线程之间的可见性,但并不能保证线程安全,因为不具备原子性,多个线程可以同时修改 volatile 修饰的变量,仅保证修改后的值最终可见,因此无法替代锁解决多线程竞争问题,适用于仅一个线程修改变量,其他线程读取的场景。
  • synchronized:可以保证在同一时间只有一个线程访问一个共享资源,其他线程需要等待该线程访问结束才能继续执行,既具有原子性又具有可见性,可以保证多个线程访问共享资源时数据的一致性和正确性,但使用 synchronized 可能会使其他线程阻塞,导致性能下降。
  • Lock:是一个接口,用于更灵活地控制多线程对共享资源的同步访问,在使用 Lock 机制时,必须手动加锁和释放锁。
public class LockTest {

    public static ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) {
        new Thread(() -> {
            testSync();
        }, "t1").start();

        new Thread(() -> {
            testSync();
        }, "t2").start();

    }

    public static void testSync() {
        reentrantLock.lock();
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }
}

Atomic 包

Atomic 包(java.util.concurrent.atomic)提供了一系列用于处理原子操作的类和方法,以确保多线程环境下的线程安全和数据一致性,如 AtomicInteger,AtomicBoolean,AtomicLong 等。

public class AtomicTest {

    private static AtomicInteger count = new AtomicInteger(1);

    public static void main(String[] args) {

        new Thread(() -> {
            // 以原子方式将输入的数值与实例中的值相加,并返回结果。
            count.addAndGet(2);
        }).start();

        new Thread(() -> {
            System.out.println(count.get());
        }).start();
    }
}

线程池

  • 核心线程:有新任务提交时,先检查核心线程数,如果核心线程都在工作,而且数量也已经达到最大核心线程数,则不会继续新建核心线程,而会将任务放入等待队列。
  • 等待队列 :等待队列用于存储当核心线程都在忙时,继续新增的任务,核心线程在执行完当前任务后,会去等待队列拉取任务继续执行,这个队列一般是一个线程安全的阻塞队列,它的容量也可由开发者根据业务来定制。
  • 非核心线程:当等待队列满了,如果当前线程数没有超过最大线程数,则会新建线程执行任务,其实,核心线程和非核心线程本质上没有什么区别。
  • 饱和策略:当等待队列已满,线程数也达到最大线程数时,线程池会根据饱和策略来执行后续操作,默认的策略是抛弃要加入的任务。
  • 线程活动保持时间:线程空闲下来之后,保持存活的持续时间,超过这个时间还没有任务执行,该工作线程结束。

20201130142052367.png

线程池的好处:

  1. 降低资源消耗,通过重复利用已创建的线程来降低线程创建和销毁造成的消耗。
  2. 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性,使用线程池可以进行统一分配和监控。

线程池的构造

public ThreadPoolExecutor(int corePoolSize, //核心线程数量
                          int maximumPoolSize, //允许创建的最大线程数
                          long keepAliveTime, //工作线程空闲后保持存活的时间
                          TimeUnit unit, //保持存活的时间单位
                          BlockingQueue<Runnable> workQueue, //任务队列
                          //拒绝策略
                          RejectedExecutionHandler handler) { ... }

任务队列有几个可供选择:

  • ArrayBlockingQueue:一个基于数组结构的阻塞队列,按照先进先出的原则对元素进行排序。
  • LinkedBlockingQueue:一个基于链表结构的阻塞队列,按照先进先出的原则对元素进行排序,newFixedThreadPool 就是使用这个队列。
  • SynchronousQueue:一个不存储元素的阻塞队列,每插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,newCachedThreadPool 就是使用这个队列。
  • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
public class Task implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
            System.out.println("Perform Tasks");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
ArrayBlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(1);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 1, TimeUnit.MINUTES, blockingQueue);
for (int i = 0; i < 10; i++) {
    threadPoolExecutor.submit(new Task());
}

提交任务有两种方式:submit 和 execute。submit 会返回一个 Future 类型的对象,通过这个对象可以判断任务是否执行成功,并且可以通过 get 方法来获取返回值,而 execute 用于提交不需要返回值的任务。

关闭线程池有两种方式:shutdown 和 shutdownNow。shutdown 只是将线程池的状态设置为 SHUTWDOWN 状态,正在执行的任务会继续执行下去,没有被执行的则中断。而 shutdownNow 则是将线程池的状态设置为 STOP,正在执行的任务则被停止,没被执行任务的则返回。

线程池的种类:

newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。

ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(() -> {
    try {
        Thread.sleep(1000);
        System.out.println(" CurrentThread: " + Thread.currentThread().getName());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
executorService.shutdown();

newFixedThreadPool:创建一个定长的线程池,可控制最大并发数,超出的线程进入队列等待。

ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
    executorService.execute(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Current Thread: " + Thread.currentThread().getName());
    });
}
executorService.shutdown();

newCachedThreadPool:创建一个可以缓存的线程池,如果线程池长度超过处理需要,可以灵活回收空闲线程,没回收的话就新建线程。该线程池的最大核心线程为无限大,当执行第二个任务时第一个任务已经完成,则会复用执行第一个任务的线程,否则会新建一个线程。

ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(() -> {
    for (int i = 0; i < 5; i++) {
        try {
            Thread.sleep(1000);
            System.out.println("Current Thread: " + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});

newScheduledThreadPool:创建一个定长的线程池,支持定时或周期任务执行。

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);
scheduledThreadPool.scheduleAtFixedRate(() -> {
    System.out.println("Current Thread: " + Thread.currentThread().getName());
}, 2, 1, TimeUnit.SECONDS); // 周期任务:延迟2秒钟后每隔1秒执行一次任务

拒绝策略

  1. AbortPolicy(默认策略):默认的拒绝策略。当线程池无法接受新任务时,直接抛异常 RejectedExecutionException 通知调用者。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(2),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy()  // 默认就是这个,可以不写
);
  1. CallerRunsPolicy(调用者运行策略):不抛弃任务,也不抛异常,而是由提交任务的线程来执行这个任务。相当于让调用者帮忙消化任务,如果线程池很忙,那就让你自己慢慢跑。
  2. DiscardPolicy(丢弃策略):直接丢弃这个任务,不做任何处理,也不抛异常。
  3. DiscardOldestPolicy(丢弃最老任务策略):丢弃任务队列中最老(即最早进入队列)的那个任务,然后尝试重新提交当前新任务到线程池。