评论

收藏

[Android] 多线程笔记

移动开发 移动开发 发布于:2022-03-18 13:20 | 阅读数:491 | 评论:0

Thread
线程状态:新建(new),就绪(start),运行(run),阻塞,死亡
start 方法内部调用了 run 方法,start 会开启线程,run 只是内部方法;
sleep 会占用锁,休眠时间到重新运行,wait 会释放锁;
stop 停止线程比较暴力,对锁的对象进行强制解锁,线程资源因此得不到正常释放;
interrupt 不会立马停止线程,只能中断阻塞状态的线程,可以捕获到一个异常来处理,加上标识判断是否中断;
join 等待该线程完成后,才能继续用下运行;
yield 线程让步,让自己或者其他线程运行,并不能保证其它线程就一定能获得执行权;
wait 进入阻塞状态,释放锁,需要在synchronized使用(获取锁后);
notify 唤起线程(随机),notifyAll唤起所有线程,释放锁,需要在synchronized使用(获取锁后),调用notify和wait的必须是作用同一个对象;
创建方式
//第一种
new Thread().start();
//第二种
new Thread(Runnable实现类).start();
ThreadLocal
线程局部变量,为线程提供变量副本,每个线程改变副本后不会对其它线程造成影响。
内部通过 ThreadLocalMap 来存储值,每个Thread类里面会有一个 ThreadLocalMap 内部变量,可以直接使用,而 ThreadLocalMap 内部使用数组来存储。
public class ThreadLocal<T> {
  static class ThreadLocalMap {
  //The table, resized as necessary. table.length MUST always be a power of two.
  private Enrty[] table;
  static class Entry extends WeakReference<ThreadLocal<?>> {
  /** The value associated with this ThreadLocal. */
  Object value;
  //key为ThreadLocal当前对象,value就是我们存入的值
  Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
  }
}
}
  ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
  }
}  
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
   * by the ThreadLocal class. */
  ThreadLocal.ThreadLocalMap threadLocals = null;
}
存储
想要存入的数据实际上并没有存到 ThreadLocal 中去,而是以这个 ThreadLocal 实例作为key存到了当前线程中的一个Map(没有链表结构)中去了;
如果发生hash冲突,会线性向后查找,一直找到 Entry 为 null 的时候添加,key相同则直接更新值;
过程碰到回收的过期数据,会进行探测清理操作(replaceStaleEntry()),执行方法遍历数组,直到碰到null停止探测,然后将过期数据清空,清空后把后面的数据往前移,提高后面查询效率。
查询
线性查询,判断hash值,如果存在相同hash值(发生过hash碰撞),则判断是否为相同key,如果不是继续往后迭代查找,如果一致直接返回value;
查询也会触发探测清理操作。
GC回收
ThreadLocalMap 内部使用 Entry extends ThreadLocal 弱引用的数组来存储,但是value依然会导致残留,根本解决方案有两种
1.需要remove这个Entry;
2.停止当前线程(map也会跟着gc);
第二种方案不太好控制,最好使用第一种。
使用弱引用的原因
ThreadLocalMap 自己有探测清理操作,所以只要当前线程在运行,调用查询增删方法也会把value删除,避免内存泄漏OOM
Handler- 线程通讯工具
Handler
Handler 通过 Looper 的 prepare 方法创建Looper对象,存放在 ThreadLocal 中,保证每个线程的Looper是独立的;
Looper 会持有当前线程的引用,并且创建一个 MessageQueue 队列存放消息 Message;
队列 MessageQueue  为单向链表(增删效率高),按 Message 中的 when(处理时间)字段排序;
Handler 负责发送和处理消息,在发送消息( enqueueMessage() )的时候会把自己赋值给 Message 中的 target 字段,在把 Message放入 MessageQueue 中。
Handler 将消息 message 发送到队列,Looper会调用loop方法开启一个死循环,读取队列消息,最后拿到 Handler 实例(target),然后通过 target 调用 dispatchMessage 分发到 Handler 所在线程中的 callback 中。
一个线程只有一个 Looper(包含消息队列),可以有多个 Handler。
public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
      handleCallback(msg);//post Runnable
    } else {
      if (mCallback != null) {
        if (mCallback.handleMessage(msg)) {
          return;
        }
      }
      handleMessage(msg);
    }
  }
内存泄漏
一般发生在延迟消息中,关闭Activity后延迟消息还未发出,MessageQueue就会持有这个Message的引用,而Message的target又持有Handler,Handler如果是内部类,会持有Activity,从而导致Activity无法被回收,引发OOM。
Handler 是内部类,持有 Activity 引用,Activity 被 finish 后 Handler 任务又没处理完,会导致 Activity 无法被回收,需要静态化处理,在调用 removeCallbacksAndMessages 清空消息队列。
Looper
Android启动时就会调用 prepare 方法绑定 Looper 对象,事件都在 Looper 的控制下,ActivityThread的main方法主要就是做消息循环,如果循环停止,应用也会停止。
public static void main(String[] args) {
  Looper.prepareMainLooper();
  ...
  Looper.loop();
  throw new RuntimeException("Main thread loop unexpectedly exited");
}

public static void loop() {
  final Looper me = myLooper();
  ...
  final MessageQueue queue = me.mQueue;
  ...
  for (;;) {
    Message msg = queue.next(); // 获取Message对象。
    ...
    try {
      msg.target.dispatchMessage(msg);
    } finally {
      ...
    }
    ...
  }
}
MessageQueue
单链表结构,时间顺序(when字段),没有消息时,会进入阻塞状态,此时主线程会释放CPU资源进入休眠状态,直到下个消息过来或者事务发生,才会唤醒主线程工作;
如果是带延迟的 message,根据时间顺序插入到 MessageQueue,然后入阻塞状态,如果队列前面有消息,会唤醒处理。
屏障消息
挡住普通消息以此来保证异步消息的优先处理,屏障消息和普通消息的区别在于屏障消息没有 tartget 字段,同样按时间 when 排序,挡住它后面的同步消息的分发;
postSyncBarrier 返回 int 类型,通过这个数值可以撤销屏障消息,并且是私有方法,无法唤醒队列工作。
message
一般通过 obtain 方法创建对象,会从消息池中获取对象,重新赋值然后返回,避免多次创建对象,并且 obtain 方法有同步锁,如果池子中没有对象则new新对象。
public static Message obtain() {
    synchronized (sPoolSync) {
      if (sPool != null) {
        Message m = sPool;
        sPool = m.next;
        m.next = null;
        m.flags = 0; // clear in-use flag
        sPoolSize--;
        return m;
      }
    }
    return new Message();
  }
AsyncTask(Android11将移除该库,推荐用线程池代替)
简化版异步任务,本质是对 Handler+Executors 的封装,内部维护一个长度MAX的队列;
在任务并不会立马将任务提交给线程池,而是等待上一个任务完成后在提交,达到串行的目的,任务完成后利用 Handler 发送消息。
AsyncTask 被声明为Activity的非静态内部类时,会持有引用,容易造成内存泄漏。
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() {
        public void run() {
          try {
            r.run();
          } finally {
            scheduleNext();
          }
        }
      });
      if (mActive == null) {
        scheduleNext();
      }
    }
    protected synchronized void scheduleNext() {
      if ((mActive = mTasks.poll()) != null) {
        THREAD_POOL_EXECUTOR.execute(mActive);
      }
    }
  }
线程池 - ExecutorService
Java提供了 Executors 工厂,方便开发者创建,所有的方法返回的都是ThreadPoolExecutor、ScheduledThreadPoolExecutor这两个类的实例。
newCachedThreadPool
可缓存线程池,如果超过线程池长度则回收空闲线程,若无可回收,则新建线程,因为允许创建的线程数量为MAX,线程过多可能会导致OOM。
newFixedThreadPool
固定长度线程池,可控制最大并发数,超出的线程会在队列中等待,但是 LinkedBlockingQueue 队列长度是MAX,任务多时会存在OOM。
newScheduledThreadPool
定时线程池,支持定时及周期性的任务执行,但是 LinkedBlockingQueue 队列长度是MAX,任务多时会存在OOM。
newSingleThreadExecutor
单线程的线程池,保证所有任务按照指定顺序执行,但是 LinkedBlockingQueue 队列长度是MAX,可能导致OOM。
阿里手册推荐:一般使用自定义线程池 ThreadPoolExecutor 创建,可以更加清楚的知道线程池的运行规则,精确控制池子粒度,工厂的创建也是走的这套流程。
线程个数不是越多越好,线程多了切换上下文时间变多,反而是负担。
上下文切换:线程调用CPU处理任务,但是CPU内核个数比较少,线程调用完会保存状态切换到下一个线程,很消耗时间。
CPU任务:一般内存数据计算型任务,主要消耗 CPU 资源,可以将线程数设置为 CPU 核心数+1,CPU任务上下文切换比较频繁,留出一个内核来协调其余因为频繁带来暂停中断等任务。
IO任务:网络,文件读取等,io读取比较耗时,处理io时CPU是不占用的,所以可以配置多一些,一般是两倍内核数线程。


   
   
   


关注下面的标签,发现更多相似文章