Android 多线程

Android中实现多线程,常见的方法有:

  • 继承Thread类
  • 实现Runnable接口
  • ThreadPoolExecutor
  • AsyncTask
  • Handler
  • ThreadLocal
  • HandlerThread
  • IntentService

Thread

具体使用

// 步骤1:创建线程类 (继承自Thread类)
   class MyThread extends Thread{

// 步骤2:复写run(),内容 = 定义线程行为
    @Override
    public void run(){
    ... // 定义的线程行为
    }
}

// 步骤3:创建线程对象,即 实例化线程类
  MyThread mt=new MyThread(“线程名称”);

// 步骤4:通过 线程对象 控制线程的状态,如 运行、睡眠、挂起  / 停止
// 此处采用 start()开启线程
  mt.start();

匿名类使用

// 步骤1:采用匿名类,直接 创建 线程类的实例
 new Thread("线程名称") {
                 // 步骤2:复写run(),内容 = 定义线程行为
                    @Override
                    public void run() {       
                  // 步骤3:通过 线程对象 控制线程的状态,如 运行、睡眠、挂起  / 停止   
                      }.start();

Runnable

具体使用

// 步骤1:创建线程辅助类,实现Runnable接口
 class MyThread implements Runnable{
    ....
    @Override
// 步骤2:复写run(),定义线程行为
    public void run(){

    }
}

// 步骤3:创建线程辅助对象,即 实例化 线程辅助类
  MyThread mt=new MyThread();

// 步骤4:创建线程对象,即 实例化线程类;线程类 = Thread类;
// 创建时通过Thread类的构造函数传入线程辅助类对象
// 原因:Runnable接口并没有任何对线程的支持,我们必须创建线程类    (Thread类)的实例,从Thread类的一个实例内部运行
  Thread td=new Thread(mt);

// 步骤5:通过 线程对象 控制线程的状态,如 运行、睡眠、挂起  / 停止
// 当调用start()方法时,线程对象会自动回调线程辅助类对象的run(),从而实现线程操作
  td.start();

匿名类使用

// 步骤1:通过匿名类 直接 创建线程辅助对象,即 实例化 线程辅助类
Runnable mt = new Runnable() {
                // 步骤2:复写run(),定义线程行为
                @Override
                public void run() {
                }
            };

            // 步骤3:创建线程对象,即 实例化线程类;线程类 = Thread类;
            Thread mt1 = new Thread(mt, "窗口1");
       
            // 步骤4:通过 线程对象 控制线程的状态,如 运行、睡眠、挂起  / 停止
            mt1.start();

synchronized相关问题
1、使用注意问题:锁对象不能为空,作用域不宜过大,避免死锁
2、Lock和synchronized如何选择:

ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize:线程池核心线程数(平时保留的线程数)
  • maximumPoolSize:线程池最大线程数(当workQueue都放不下时,启动新线程,最大线程数)
  • keepAliveTime:超出corePoolSize数量的线程的保留时间。
  • unit:keepAliveTime单位
  • workQueue:阻塞队列,存放来不及执行的线程
    • ArrayBlockingQueue:构造函数一定要传大小
    • LinkedBlockingQueue:构造函数不传大小会默认为(Integer.MAX_VALUE ),当大量请求任务时,容易造成 内存耗尽。
    • SynchronousQueue:同步队列,一个没有存储空间的阻塞队列 ,将任务同步交付给工作线程。
    • PriorityBlockingQueue : 优先队列
  • threadFactory:线程工厂
  • handler:饱和策略
    • AbortPolicy(默认):直接抛弃
    • CallerRunsPolicy:用调用者的线程执行任务
    • DiscardOldestPolicy:抛弃队列中最久的任务
    • DiscardPolicy:抛弃当前任务

阿里Java开发手册建议,不要手动创建线程(new Thread),不要使用官方推荐的线程池(FixedThreadPool, SingleThreadPool, CachedThreadPool, ScheduledThreadPool ).使用 自行创建 ThreadPoolExecutor 方式。

AsyncTask

AsyncTask 类属于抽象类,即使用时需 实现子类

public abstract class AsyncTask<Params, Progress, Result> { 
 ... 
}

// 类中参数为3种泛型类型
// 整体作用:控制AsyncTask子类执行线程任务时各个阶段的返回类型
// 具体说明:
// a. Params:开始异步任务执行时传入的参数类型,对应excute()中传递的参数
// b. Progress:异步任务执行过程中,返回下载进度值的类型
// c. Result:异步任务执行完成后,返回的结果类型,与doInBackground()的返回值类型保持一致
// 注:
// a. 使用时并不是所有类型都被使用
// b. 若无被使用,可用java.lang.Void类型代替
// c. 若有不同业务,需额外再写1个AsyncTask的子类
}

AsyncTask原理=2个线程池 + Handler


Handler机制

  • Handler的处理过程运行在创建Handler的线程里
  • 一个Looper对应一个MessageQueue
  • 一个线程对应一个Looper
  • 一个Looper可以对应多个Handler
  • 线程是默认没有Looper的,线程需要通过Looper.prepare()、绑定Handler到Looper对象、Looper.loop()来建立消息循环
  • 主线程(UI线程),也就是ActivityThread,在被创建的时候就会初始化Looper,所以主线程中可以默认使用Handler
  • 可以通过Looper的quitSafely()或者quit()方法终结消息循环,quitSafely相比于quit方法安全之处在于清空消息之前会派发所有的非延迟消息。
  • 不确定当前线程时,更新UI时尽量调用post方法

重要的类 Handler Message MessageQueue Looper

总体流程是:Handler 发送 Message 到 MessageQueue 队列中,Looper 循环从 MessageQueue 中获取消息 发送给 Handler,最后Handler 的 handleMessage 处理消息。

流程
原理图
说明

几个注意点

  • 即 主线程的Looper对象自动生成,不需手动生成;而子线程的Looper对象则需手动通过Looper.prepare()创建
  • 在子线程若不手动创建Looper对象 则无法生成Handler对象

Handler 在 dispatchMessage时:

  • 若msg.callback属性不为空,则代表使用了post(Runnable r)发送消息,则直接回调Runnable对象里复写的run()
  • 若msg.callback属性为空,则代表使用了sendMessage(Message msg)发送消息,则回调复写的handleMessage(msg)

post 和 sendMessage 区别

  • post不需外部创建消息对象,而是内部根据传入的Runnable对象 封装消息对象
  • 回调的消息处理方法是:复写Runnable对象的run()

postDelay 原理

  • 调用路径:1.Handler.postDelayed(Runnable r, long delayMillis)
    2.Handler.sendMessageDelayed(getPostMessage(r), delayMillis)
    3.Handler.sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)
    4.Handler.enqueueMessage(queue, msg, uptimeMillis)
    5.MessageQueue.enqueueMessage(msg, uptimeMillis)
  • 原理流程:
    • postDelay()一个10秒钟的Runnable A、消息进队,MessageQueue调用 nativePollOnce() 阻塞,Looper阻塞;
    • 紧接着post()一个Runnable B、消息进队,判断现在A时间还没到、正在阻塞,把B插入消息队列的头部(A的前面),然后调用 nativeWake() 方法唤醒线程;
    • MessageQueue.next()方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给Looper;
    • Looper处理完这个消息再次调用next()方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩9秒)继续调用nativePollOnce()阻塞;
    • 直到阻塞时间到或者下一次有Message进队;

Handler内存泄漏
Handler 的用法:新建 内部类匿名内部类

泄漏原因:Java中,非静态内部类 匿名内部类 都默认持有 外部类的引用。主线程Looper对象的什么周期=该应用程序的生命周期。在Handler消息队列有未处理或正在处理的的消息时,默认持有外部类的引用销毁,由于引用关系,GC 无法回收。

解决方案:

  • 静态内部类+弱引用

      private static class FHandler extends Handler{
    
      // 定义 弱引用实例
      private WeakReference<Activity> reference;
    
      // 在构造方法中传入需持有的Activity实例
      public FHandler(Activity activity) {
          // 使用WeakReference弱引用持有Activity实例
          reference = new WeakReference<Activity>(activity); 
     }
    
      // 通过复写handlerMessage() 从而确定更新UI的操作
      @Override
      public void handleMessage(Message msg) {
          switch (msg.what) {
              case 1:
                  Log.d(TAG, "收到线程1的消息");
                  break;
              case 2:
                  Log.d(TAG, " 收到线程2的消息");
                  break;
          }
        }
     }
    
  • 当外部类结束生命周期时,清空Handler内消息队列

      @Override
      protected void onDestroy() {
      super.onDestroy();
      mHandler.removeCallbacksAndMessages(null);
      // 外部类Activity生命周期结束时,同时清空消息队列 & 结束Handler生命周期
      }
    

问题补充

面试常见问题

1、多个
2、有1个。通过 Looper 源码可知,使用 ThreadLocal 内的 ThreadLocalMap 保存 key->ThreadLocal,value->Looper。部分源码如下:

//Looper.java
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

//ThreadLoal.java
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

3、内部类持有外部类的引用 �MessageQueue - Message - Handler - Activity

4、主线程 ActivityThread 中的 main() 函数中已经启动了 Looper.prepareMainLooperLooper.loop
子线程要 prepare 和 loop,用完后要 quit

5、调用 Looper 的 quit() ;释放内存 释放线程

6、synchronized同步锁保证安全,所以 delaymsg 时间是不准确的

7、obtain()

8、首先主线程的Looper是不能quit的,会一直存在。所有的消息都会运行在其中。卡死和阻塞主线程会导致ANR,Looper的block只是睡眠

ThreadLocal (参考)

ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

synchronizedThreadLocal 对比:
对于多线程资源共享的问题,synchronized 仅提供一份变量,让不同的线程排队访问,而ThreadLocal为每一个线程都提供了一份变量,因此可以同时访问而互不影响。但是ThreadLocal却并不是为了解决并发或者多线程资源共享而设计的

所以ThreadLocal既不是为了解决共享多线程的访问问题,更不是为了解决线程同步问题,ThreadLocal的设计初衷就是为了提供线程内部的局部变量,方便在本线程内随时随地的读取,并且与其他线程隔离。

ThreadLocal的应用场景:

当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候
如:属性动画为每个线程设置AnimationHandler、Android的Handler消息机制中通过ThreadLocal实现Looper在线程中的存取、EventBus获取当前线程的PostingThreadState对象或者即将被分发的事件队列或者当前线程是否正在进行事件分发的布尔值

复杂逻辑下的对象传递
使用参数传递的话:当函数调用栈更深时,设计会很糟糕,为每一个线程定义一个静态变量监听器,如果是多线程的话,一个线程就需要定义一个静态变量,无法扩展,这时候使用ThreadLocal就可以解决问题。

public T get() {
    //1、首先获取当前线程
    Thread t = Thread.currentThread();
    //2、根据当前线程获取一个map
    ThreadLocalMap map = getMap(t);
     .....
}


 public void set(T value) {
    //1、首先获取当前线程
    Thread t = Thread.currentThread();
    //2、根据当前线程获取一个map
    ThreadLocalMap map = getMap(t);
    if (map != null)
    //3、map不为空,则把键值对保存到map中
        map.set(this, value);
    //4、如果map为空(第一次调用的时候map值为null),则去创建一个ThreadLocalMap对象并赋值给map,并把键值对保存到map中。
    else
        createMap(t, value);
}

get 和 set 方法里面都有一个 ThreadLocalMap。

Android早期版本,这部分的数据结构是通过Values实现的,Values中也有一个table的成员变量,table是一个Object数组,也是以类似map的方式来存储的。偶数单元存储的是key,key的下一个单元存储的是对应的value,所以每存储一个元素,需要两个单元,所以容量一定是2的倍数。这里的key存储的也是ThreadLocal实例的弱引用

如何保证线程安全

  • 每个线程拥有自己独立的ThreadLocals变量(指向ThreadLocalMap对象 )
  • 每当线程 访问 ThreadLocals变量时,访问的都是各自线程自己的ThreadLocalMap变量(键 - 值)
  • ThreadLocalMap变量的键 key = 唯一 = 当前ThreadLocal实例

HandlerThread

HandlerThread原理

Thread类 + Handler类机制

  • 通过继承Thread类,快速地创建1个带有Looper对象的新工作线程
  • 通过封装Handler类,快速创建Handler & 与其他线程进行通信
// 步骤1:创建HandlerThread实例对象
// 传入参数 = 线程名字,作用 = 标记该线程
 HandlerThread mHandlerThread = new HandlerThread("handlerThread");

// 步骤2:启动线程
   mHandlerThread.start();

// 步骤3:创建工作线程Handler & 复写handleMessage()
// 作用:关联HandlerThread的Looper对象、实现消息处理操作 & 与其他线程进行通信
// 注:消息处理操作(HandlerMessage())的执行线程 =         mHandlerThread所创建的工作线程中执行
  Handler workHandler = new Handler( handlerThread.getLooper() ) {
        @Override
        public boolean handleMessage(Message msg) {
            ...//消息处理
            return true;
        }
    });

// 步骤4:使用工作线程Handler向工作线程的消息队列发送消息
// 在工作线程中,当消息循环时取出对应消息 & 在工作线程执行相关操作
  // a. 定义要发送的消息
  Message msg = Message.obtain();
  msg.what = 2; //消息的标识
  msg.obj = "B"; // 消息的存放
  // b. 通过Handler发送消息到其绑定的消息队列
  workHandler.sendMessage(msg);

// 步骤5:结束线程,即停止线程的消息循环
  mHandlerThread.quit();

IntentService

开启一个新的工作线程

@Override
public void onCreate() {
super.onCreate();

// 1. 通过实例化andlerThread新建线程 & 启动;故 使用IntentService时,不需额外新建线程
// HandlerThread继承自Thread,内部封装了 Looper
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

// 2. 获得工作线程的 Looper & 维护自己的工作队列
mServiceLooper = thread.getLooper();

// 3. 新建mServiceHandler & 绑定上述获得Looper
// 新建的Handler 属于工作线程 ->>分析1
mServiceHandler = new ServiceHandler(mServiceLooper); 
}


   /** 
 * 分析1:ServiceHandler源码分析
 **/ 
 private final class ServiceHandler extends Handler {

     // 构造函数
     public ServiceHandler(Looper looper) {
     super(looper);
   }

    // IntentService的handleMessage()把接收的消息交给onHandleIntent()处理
    @Override
     public void handleMessage(Message msg) {

      // onHandleIntent 方法在工作线程中执行
      // onHandleIntent() = 抽象方法,使用时需重写 ->>分析2
      onHandleIntent((Intent)msg.obj);
      // 执行完调用 stopSelf() 结束服务
      stopSelf(msg.arg1);

    }
}

   /** 
 * 分析2: onHandleIntent()源码分析
 * onHandleIntent() = 抽象方法,使用时需重写
 **/ 
  @WorkerThread
  protected abstract void onHandleIntent(Intent intent);

通过onStartCommand() 将Intent 传递给服务 & 依次插入到工作队列中

/** 
  * onStartCommand()源码分析
  * onHandleIntent() = 抽象方法,使用时需重写
  **/ 
  public int onStartCommand(Intent intent, int flags, int startId) {

// 调用onStart()->>分析1
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

/** 
  * 分析1:onStart(intent, startId)
  **/ 
  public void onStart(Intent intent, int startId) {

// 1. 获得ServiceHandler消息的引用
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;

// 2. 把 Intent参数 包装到 message 的 obj 发送消息中,
//这里的Intent  = 启动服务时startService(Intent) 里传入的 Intent
msg.obj = intent;

// 3. 发送消息,即 添加到消息队列里
mServiceHandler.sendMessage(msg);
  }

从上面源码可看出:IntentService本质 = Handler + HandlerThread:

  • 通过HandlerThread 单独开启1个工作线程:IntentService
  • 创建1个内部 Handler :ServiceHandler
  • 绑定 ServiceHandler 与 IntentService
  • 通过 onStartCommand() 传递服务intent 到ServiceHandler 、依次插入Intent到工作队列中 & 逐个发送给 onHandleIntent()
  • 通过onHandleIntent() 依次处理所有Intent对象所对应的任务

不建议通过 bindService() 启动 IntentService
原因:bind的生命周期为

onCreate() ->> onBind() ->> onunbind()->> onDestory()

并没有执行 onStart 或 onStartCommand ,故不会将消息发送到消息队列,那么onHandleIntent()将不会回调,即无法实现多线程的操作


多线程并发问题

  • 当只有一个线程写,其它线程都是读的时候,可以用volatile修饰变量
  • 当多个线程写,那么一般情况下并发不严重的话可以用Synchronized,不过Synchronized有局限性,比如不能设置锁超时,不能通过代码释放锁。
  • ReentranLock 可以通过代码释放锁,可以设置锁超时。
  • 高并发下,Synchronized、ReentranLock 效率低,因为同一时刻只有一个线程能进入同步代码块,如果同时有很多线程访问,那么其它线程就都在等待锁。这个时候可以使用并发包下的数据结构,例如ConcurrentHashMap,LinkBlockingQueue,以及原子性的数据结构如:AtomicInteger。

详见 说说多线程并发

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容