• Android并发编程与多线程


       一、Android线程基础

    1.线程和进程

    • 一个进程最少一个线程,进程可以包含多个线程
    • 进程在执行过程中拥有独立的内存空间,而线程运行在进程内

    2.线程的创建方式

    • new Thread:

            缺点:缺乏统一管理,可能无限制创建线程,相互之间竞争,可能占用过多的系统资源导致死机或OOM

    1. new Thread(new Runnable() {
    2. @Override
    3. public void run() {
    4. }
    5. }).start();
    6. class MyThread extends Thread{
    7. @Override
    8. public void run() {
    9. super.run();
    10. }
    11. }
    12. new MyThread().start();
    • AysncTask:轻量级异步任务工具类,提供任务执行的进度回调给UI线程

            使用场景:需要知道任务执行的进度,多个任务串行执行

            缺点:生命周期和宿主的生命周期不同步,可能发生内存泄漏,默认情况下任务串行执行

            作为用来替代Thread + Handler的辅助类,AsyncTask可以很轻松地执行异步任务并更新ui,但由于context泄露,回调遗漏,configuration变化导致崩溃,平台差异性等原因,在api 30(Android 11)中AsyncTask被正式废弃:

            使用execute方法,串行执行,即先来后到,如果其中有一条任务休眠,或者执行时间过长,后面的任务都将被阻塞

            使用内置THREAD_POOL_EXECUTOR线程池,并发执行

    • HandlerThread:适用于主线程需要和工作线程通信,或者持续性任务,比如轮询,所有任务串行执行

            缺点:不会像普通线程一样主动销毁资源,会一直运行,可能造成内存泄漏(可以定义成静态,防止内存泄漏)

    • IntentService:适用于任务需要跨界面读取任务执行的进、结果。比如:后台上传图片、批量操作数据库等。任务完成后,会自我结束,不需要手动stopService
    • ThreadPoolExecutor:适用快读出来大量耗时较短的任务场景
      • Executors.newCacheThreadPool();//线程可复用线程池
      • Executors.newFixedThreadPool();//固定线程数量线程池
      • Executors.newScheduleThreadPool();//可指定定时任务线程池
      • Executors.newSingleExecutor();//线程数量为1的线程池

    3.线程的优先级

    • 线程的优先级具有继承性
    • 设置优先级的方法
      • java api:java.lang.Thread.setPriority(int newPriority);优先级必须为[1~10],优先级的值越高,获取CPU时间片的概率越高,UI线程的优先级为5  效果不是很明显
      • Android api:android.os.Process.setThreadPriority(int newPriority);优先级可设置[-20~19],优先级的值越低,获取时间片的概率越高,UI线程的优先级为-10 效果明显

    4.线程的状态

    • NEW:初始状态,线程被新建,还没有调用start方法
    • RUNNABLE:运行状态,包括运行中和就绪
    • BLOCKED:阻塞状态,表示线程阻塞于锁
    • WAITING:等待状态,需要其他线程通知唤醒
    • TIME_WAITING:超时等待状态,表示可以在指定的时间超过后自行返回
    • TERMINATED:终止状态,表示当前线程已经执行完毕

    关键方法:

    • wait:等待线程池,释放资源对象锁,可使用notify,notifyAll,或等待超时时间来唤醒
    • join:等待目标线程执行完后再执行此线程
    • yield:暂停当前正在执行的线程对象,不会释放当前线程持有的任何锁资源,使用优先级或更高优先级的线程有执行的机会,这个方法机会用不到
    • sleep:使调用线程进入休眠状态,一般情况下会释放线程锁对象,但如果在一个synchronized块中执行sleep,线程虽会休眠,但不会释放资源对象锁

    5.线程间消息通讯

    主线程向子线程发送消息

    二、多线程开发核心知识点

    1.线程并发安全

    本质:能够让并发线程有序的运行(可能是先来后到排队,也可能被插队,同一时刻只能一个线程有权访问同步资源),线程执行的结果,能够对其它线程可见

    2.线程安全的分类

    • 根据线程要不要锁住同步资源
      • 锁住:悲观锁  synchronized ReentrantLock
      • 不锁住:乐观锁  AtomicInteger  AtomicBoolean
    • 锁住同步资源失败要不要阻塞?
      • 阻塞:阻塞锁  synchronized ReentrantLock
      • 不阻塞:自旋锁  AtomicInteger  AtomicBoolean 
    • 获取资源锁时,要不要插队
      • 插队:公平锁  ReentrantLock
      • 不插队:非公平锁 synchronized ReentrantLock
    • 一个线程中的多个流程能不能获取同一把锁
      • 可重入锁: synchronized ReentrantLock
      • 不可重入锁
    • 多线程共享一把锁
      • 能:共享锁 RendLock
      • 不能:排他锁 WriteLock
    • 多线程竞争同步资源 synchronized
      • 不锁住资源,多个线程中只有一个能修改成功,其他会重试  无锁
      • 同一线程执行同步资源时自动获取锁   偏向锁
      • 多个线程竞争资源时,没有获取到资源的线程自选等待  轻量级锁
      • 多个线程竞争资源时,没有获取到资源的线程被阻塞等待唤醒  重量级锁

    3.如何线程安全

    AtomicInterger 原子包装类

    • CAS实现无锁数据更新,自旋的设计能够有效避免线程因阻塞-唤醒带来的系统资源开销
    • 适用场景:多线程计数,原子操作,并发数量小
    • 使用案例
    1. AtomicInteger atomicInteger = new AtomicInteger(1);
    2. atomicInteger.getAndIncrement();
    3. atomicInteger.getAndAdd(2);
    4. atomicInteger.getAndDecrement();
    5. atomicInteger.getAndAdd(-2);

    volatile 可见性修饰

    • volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存重新读取该成员的值,而且,当成员变量值发生变化时,强迫将变化的值重新写入共享内存
    • 缺点:不能保证原子性,不能解决非原子操作的线程安全性,性能不及原子类高
    • 使用案例
    1. volatile int count = 0;
    2. public void test() {
    3. // 赋值操作是原子性操作,对其他线程可见
    4. count = 1;
    5. //非原子操作,其他线程不可见
    6. count = count + 1;
    7. count++;
    8. }

    synchronized

    • 锁方法。加在方法上,未获取到对象锁的其他线程都不可以访问该对象
    • 锁Class对象。加在static方法上相当于给Class对象加锁,哪怕是不同的Java对象实例,也需要排队执行
    • 锁代码块。未获取到对象锁的其他线程可以执行同步代码块之外的代码

    优势:哪怕一个同步方法中出现了异常,那么JVM也能够为我们自动释放锁,能主动从而规避死锁,不需要开发者主动释放锁

    劣势:

    • 必须要等到获取锁对象的线程执行完成,或出现异常,才能释放掉,不能中途释放锁,不能中断一个正在试图获取锁的线程
    • 多线程竞争的时候,不知道获取锁成功与否,不够灵活
    • 每个锁仅有单一的条件不能设定超时

    ReentrantLock

    悲观锁,可重入锁,公平锁,非公平锁

    • 基础用法:
    1. ReentrantLock lock = new ReentrantLock();
    2. try {
    3. lock.lock();
    4. //……
    5. } finally {
    6. lock.unlock();
    7. }
    8. //获取锁,获取不到会阻塞
    9. lock.lock();
    10. //尝试获取锁,成功返回true
    11. lock.tryLock();
    12. //在一定的时间内去不断尝试获取锁
    13. lock.tryLock(3000, TimeUnit.MICROSECONDS);
    14. //可使用Thread.interrupt()打断阻塞,退出竞争,让给其他线程
    15. lock.lockInterruptibly();
    • 可重入,避免死锁
    1. ReentrantLock lock = new ReentrantLock();
    2. public void doWork() {
    3. try {
    4. lock.lock();
    5. doWork();//递归调用,使得统一线程多次获得锁
    6. } finally {
    7. lock.unlock();
    8. }
    9. }
    • 公平锁:所有进入阻塞的线程排队依次均有机会执行

    使用场景:交易

    1. //传入true 就是公平锁,传入false 或者不传就是非公平锁
    2. ReentrantLock lock = new ReentrantLock(true);
    • 非公平锁:默认,允许线程插队,避免每一个线程都进入阻塞,在唤醒带来的性能开销。性能高,因为线程可以插队,但是会导致队列中可能存在线程饿死的情况,一直得不到锁,一直得不到执行

    使用场景:synchronized,很多场景都是非公平锁

    • 进阶用法 -- Condition条件对象
    1. Condition worker = lock.newCondition();
    2. //进入阻塞,等待唤醒
    3. worker.await();
    4. //唤醒指定线程
    5. worker1.signal();

    共享锁,排他锁

    • 共享锁:所有线程都可以同时获得,并发量高。如:读操作
    • 排他锁:同一时刻只能一个线程有权修改资源。如:写操作
    1. ReentrantReadWriteLock.ReadLock
    2. ReentrantReadWriteLock.WriteLock

    三、正确使用锁和原子类

    1.减少持锁时间

    尽管锁在同一时间只能允许一个线程持有,其他想要占用锁的线程都得在临界区外等待锁的释放,这个等待的时间要尽可能的短

    2.锁分离

    读写锁分离,写锁才需要同步处理。对于大多数应用来说,读的场景更多一些,读写锁分离,可以提高系统性能

    3.锁粗化

    多次加锁,释放锁合并成一次

    对于一些不需要同步的代码,但能很快执行完毕,前后都有锁,这种情况可以进行锁粗化,整合成一次锁请求,释放。(锁请求、释放是需要性能开销的)

    四、线程池

    1.优势

    • 减低资源消耗。通过重复利用已创建的线程减低线程创建和销毁造成的消耗;
    • 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行;
    • 提高线程的可管理性。线程是稀有资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

    2.Java中默认的线程池

    线程池的默认构建

    1. ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2. 5, 20, 5, TimeUnit.SECONDS,new PriorityBlockingQueue<>());
    3. executor.execute(() -> {
    4. });
    5. // 源码
    6. public ThreadPoolExecutor(int corePoolSize,
    7. int maximumPoolSize,
    8. long keepAliveTime,
    9. TimeUnit unit,
    10. BlockingQueue workQueue,
    11. ThreadFactory threadFactory,
    12. RejectedExecutionHandler handler) {
    13. }

    参数

    • corePoolSize:线程池中的核心线程数量
    • maximumPoolSize:最大能创建的线程数量
    • keepAliveTime:非核心线程最大的存活时间
    • unit:keepAliveTime的时间单位
    • workQueue:等待队列,当任务提交时,如果线程中线程数量大于等于corePoolSize的时候,把任务放入等待队列
    • threadFactory:线程创建工厂,默认使用Executors.defaultThreadFactory()来创建线程,线程具有相同的NORM_PRIORITY优先级并且是非守护线程
    • handler:线程池的饱和就决策略。如果阻塞队列满了并且没有空闲的线程,这时如果继续提交任务,就需要采取一种策略处理该任务

    JUC包下提供的集中线程池

    1. // 单一线程数,同时只有一个线程存活,但是线程等待队列无界
    2. Executors.newSingleThreadExecutor();
    3. // 线程可复用线程池,核心线程数为0,最大可创建的线程数为Interger.max,线程复用存活时间为60s
    4. Executors.newCachedThreadPool();
    5. // 固定线程数量的线程池
    6. Executors.newFixedThreadPool(5);
    7. // 可执行定时任务,延迟任务的线程池
    8. Executors.newScheduledThreadPool(5);

    线程池的重要方法

    1. //提交任务,交给线程池调度
    2. void execute(Runnable command)
    3. //关闭线程池,等待执行任务完成,不接受新的任务,但可以继续执行池子中已添加到等待队列的任务
    4. void shutdown()
    5. //关闭线程池,不等待执行任务完成,不接受新的任务,也不再处理等待队列中的任务打断正在执行的任务
    6. void shutdownNow()
    7. //返回线程池中所有任务的数据
    8. long getTaskCount()
    9. //返回线程池中已执行完毕的任务数量
    10. long getCompletedTaskCount()
    11. //返回线程池中已创建的线程数量
    12. int getPoolSize()
    13. //返回当前正在运行的线程数量
    14. int getActiveCount()

    execute提交任务流程

    addWorker的工作任务

    1. 检查线程池状态,能否继续创建线程
    2. 把runnable封装成worker,添加到工作队列
    3. 启动新建的线程
    4. runWorker方法中开启whille循环,执行本次任务,本次任务结束后,去检查等待队列中,是否有任务,拿来继续执行,达到复用目的

    retry:双层for循环流程控制,使用retry可以退出外层循环

    1. int count = 0;
    2. retry:
    3. for (int i = 0; i < 10; i++) {
    4. for (int j = 0; j < 10; j++) {
    5. count++;
    6. if (count == 3) {
    7. break;
    8. }
    9. if (count == 4){
    10. break retry;
    11. }
    12. }
    13. }

    五、协程

  • 相关阅读:
    常见的数学物理方程
    opencv(14):error: expected type-specifier operator cv::_InputOutputArray()
    渗透测试 | 域名信息收集
    dB family cheat sheet: dB, dBW, dBm, dBi, dBc, etc
    境电商为什么要做独立站?API一键对接秒上架瞬间拥有全平台几十亿商品和用户!
    GBASE 8A v953报错集锦27--企业管理器执行投影列中含有重复列名的 sql 语句 报错
    5、Docker安装mysql主从复制与redis集群
    Z检验|T检验|样本标准差S代替总体标准差 σ
    如何让 JS 代码不可断点
    【构建并发程序】2-线程池-的注意事项与缺点
  • 原文地址:https://blog.csdn.net/weixin_42277946/article/details/134174281