多线程、Handler机制、ThreadLocal

Thread

线程状态:新建(new),就绪(start),运行(run),阻塞,死亡

start 方法内部调用了 run 方法,start 会开启线程,run 只是内部方法;

sleep 会占用锁(阻塞状态),会释放CPU,但是不会释放锁,休眠时间到在重新运行;

stop 停止线程比较暴力,对锁的对象进行强制解锁,线程资源因此得不到正常释放;

interrupt 不会立马停止线程,只能中断阻塞状态的线程,可以捕获到一个异常来处理,加上标识判断是否中断;

join 等待该线程完成后,才能继续往下执行;

yield 线程让步,相同优先级的线程让出 cpu 资源(如果没有将继续执行),并不能保证其它线程就一定能获得执行权;

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。

查询也会触发探测清理操作,在 key 被回收后,如果此时 value 存在其它强引用,不会被回收,此时虽然 key 依然可以通过断开前的连接地址去获取 value;

也就是说,引用的断开不会影响寻找地址,只会导致 key 对象会被回收处理。

GC回收

ThreadLocalMap 内部使用 Entry extends ThreadLocal 弱引用的数组来存储 key,但是 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 发送到队列,而 message 持有 Handler,此时 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 工厂,方便开发者创建,所有的方法返回的都是 ThreadPoolExecutorScheduledThreadPoolExecutor 这两个类的实例。

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 是不占用的,所以可以配置多一些,一般是两倍内核数线程。

 

热门相关:地球第一剑