Android多线程笔记

消息机制

处理消息的手段--Handler,Looper与MessageQueue

=tips:=

  1. 子线程无法更新UI,所以需要通过Handler将一个消息Post到UI线程中(该Handler必须在主线程中创建),
    为什么?
    每个Handler都会关联一个消息队列,消息队列被封装在Looper中,每个Looper又回关联一个线程(Looper通过ThreadLocal封装),最终就等于每个消息队列会关联一个线程。

  2. Handler就是一个消息处理器,将消息投递给消息队列,然后再由对应的线程从消息队列中逐个取出消息,并且执行。默认情况下,消息队列只有一个,即主线程的消息队列,这个消息队列是在ActivityThread.main方法中创建的,通过Looper.prepareMainLooper()来创建,最后执行Looper.loop()来启动消息循环。

  3. 那么Handler是如何关联消息队列以及线程的呢?
    Handler会在内部通过Looper.myLooper()来获取Looper对象,并且与之关联,最重要的就是消息队列

  4. 消息队列通过Looper与线程关联上,Handler与Looper关联;
    Handler要与主线程的消息队列关联上,这样handlerMessage才会执行在UI线程,此时更新UI才是线程安全的!

  5. 消息循环的建立是通过Looper.loop()这个方法。

Looper总结:
通过Looper.prepare()来创建Looper对象(消息队列封装在Looper对象中),并且保存在sThreadLoal中,然后通过Looper.loop()来执行消息循环。

Handler最终将消息追加到MessageQueue中,而Looper不断地从
MessageQueue中读取消息,并且调用Handler的dispatchMessage消息,
这样消息就源源不断地被产生,添加到MessageQueue,被handler处理,
这样Android引用就运转起来了

在子线程中创建Handler为何会抛出异常?

    new Thread(){
        Handler handler = null;
        public void run(){
            handler = new Handler();
        }
    }.start();

上述代码的问题?
在Handler源码中,Looper对象是ThreadLocal的,每个线程都有自己的Looper,Looper可以为空,但是当子线程中创建Handler对象时,如果Looper为空,那么就会抛出异常
Handler源码中有判断:

   public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

修改:

    new Thread(){
        Handler handler = null;
        public void run(){
            //1,为当前线程创建Looper,绑定到ThreadLocal中
            Looper.prepare();
            handler = new Handler();
            //2,启动消息循环
            Looper.loop();
        }
    }.start();

多线程

Android中的多线程就是JAVA中的多线程,Java中的线程详解:http://www.cnblogs.com/riskyer/p/3263032.html
为了方便,Android封装了一些类,如:AsyncTask,HandlerThread等。

Runnable和Thread有什么区别?

看Thread的源码,Thread实现了Runnable接口,Thread里最终被线程执行的任务是Runnable,而非Thread。
Thread只是对Runnable的包装,并且通过一些状态对Thread进行管理与调度。
Runnalbe接口定义了可执行的任务,它只有一个无返回值的run()函数。

线程的状态

线程有四种状态,任何一个线程肯定处于这四种状态中的一种:

  1. 产生(New):线程对象已经产生,但尚未被启动,所以无法执行。
    如通过new产生了一个线程对象后没对它调用start()函数之前。
  2. 可执行(Runnable):每个支持多线程的系统都有一个排程器,排程器会从线程池中选择一个线程
    并启动它。当一个线程处于可执行状态时,表示它可能正处于线程池中等待排排程器启动它;
    也可能它已正在执行。如执行了一个线程对象的start()方法后,线程就处于可执行状态,
    但显而易见的是此时线程不一定正在执行中。
  3. 死亡(Dead):当一个线程正常结束,它便处于死亡状态。
    如一个线程的run()函数执行完毕后线程就进入死亡状态。
  4. 停滞(Blocked):当一个线程处于停滞状态时,系统排程器就会忽略它,不对它进行排程。
    当处于停滞状态的线程重新回到可执行状态时,它有可能重新执行。如通过对一个线程调用wait()函数后,
    线程就进入停滞状态,只有当两次对该线程调用notify或notifyAll后它才能两次回到可执行状态。

线程的wait、sleep、join和yield(面试必备)

  1. wait()
    让当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法。
    使当前线程暂停执行并释放对象锁标示,让其他线程可以进入synchronized数据块,
    当前线程被放入对象等待池中。当调用notify()方法后,
    将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,
    只有锁标志等待池中线程能够获取锁标志;如果锁标志等待池中没有线程,则notify()不起作用。
    notifyAll()则从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中。
  2. sleep()
    使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁。也就是说如果有synchronized同步快,其他线程仍然不能访问共享数据。注意该方法要捕捉异常。
  3. join()
    join()方法使当前线程停下来等待,直至另一个调用join方法的线程终止。线程在被激活后不一定马上就运行,而是进入到可运行线程的队列中。但是join()可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException。
  4. yield()
    Yield()方法是停止当前线程,让同等优先权的线程运行。如果没有同等优先权的线程,那么Yield()方法将不会起作用。
线程方法 是否释放同步锁 是否需要在同步的代码块中调用 方法是否已废弃 是否可以被中断
sleep()
wait()
suspend 1.6废弃 是
resume() 1.6废弃 是
join()

扩展:http://zheng12tian.iteye.com/blog/1233638
http://dylanxu.iteye.com/blog/1322066

关于wait和notify、notifyAll的运用例子:

    public static void main(String[] args) {
        waitAndNotifyAll();
    }

    private static Object object = new Object();
    private static void waitAndNotifyAll() {
        System.out.println("主线程启动");
        Thread thread = new WaitThread();
        thread.start();
        long startTime = System.currentTimeMillis();
        try {
            synchronized (object) {
                System.out.println("主线程等待");
                object.wait();
            }
        } catch (Exception e) {
            // TODO: handle exception
        }
        long time = System.currentTimeMillis() - startTime;
        System.out.println("主线程继续->等待耗时:" + time + " ms");
    }

    static class WaitThread extends Thread {
        @Override
        public void run() {
            // TODO Auto-generated method stub
            try {
                synchronized (object) {
                    Thread.sleep(3000);
                    object.notifyAll();
                }
            } catch (Exception e) {
                // TODO: handle exception
            }
        }
    }

运行后的结果

主线程启动
主线程等待
主线程继续->等待耗时:3000 ms

与多线程相关的方法--Callable、Future和FutureTask

Runnable和Callable功能大致类似,不同的是Callable是一个泛型接口,切有一个返回值为Call()函数,而Runnable的run()函数不能讲结果返回给客户程序。Callable的声明:

    public interface Callable<V>{
        V call() throws Exception;
    }

==可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口==

Future提供了对Runnable或Callable任务的执行结果进行取消,查询是否完成,获取结果,设置结果操作,分别对应cancel,isDone,get,set函数。get方法会阻塞,直到任务返回结果;
说到底,Future只是定义了一些规范的接口,而FutureTask才是具体实现类,FutureTask实现了RunnableFuture<V>,而RunnableFuture实现了Runnable有实现了Future<V>这2个接口,所以FutureTask具备了他们的能力

关于Runnable、Callable、FutureTask的运用例子:

public static void main(String[] args) {
        try {
            futureWithRunnable();
            futureWithCallable();
            futureTask();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    static ExecutorService mExecutor = Executors.newSingleThreadExecutor();
    
    private static void futureTask() throws InterruptedException,ExecutionException {
        // TODO Auto-generated method stub
        FutureTask<Integer> result = new FutureTask<Integer>(new Callable<Integer>() {

            @Override
            public Integer call() throws Exception {
                // TODO Auto-generated method stub
                return fibc(20);
            }
        });
        mExecutor.submit(result);
        System.out.println("FutureTask:"+result.get());
    }

    private static void futureWithCallable() throws InterruptedException,ExecutionException  {
        // TODO Auto-generated method stub
        Future<Integer> result = mExecutor.submit(new Callable<Integer>() {

            @Override
            public Integer call() throws Exception {
                // TODO Auto-generated method stub
                return fibc(20);
            }
            
        });
        System.out.println("Callable:"+result.get());
    }
    /**
     * runnable无返回值,所以get()的值为null
     */
    private static void futureWithRunnable() throws InterruptedException,ExecutionException {
        //提交runnable,
        Future<?> result = mExecutor.submit(new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                fibc(20);
            }
        });
        System.out.println("runnable:"+result.get());
    }
    //效率低夏的 斐波那契数列
    private static int fibc(int i) {
        // TODO Auto-generated method stub
        if (i == 0) {
            return 0;
        }
        if (i == 1) {
            return 1;
        }
        return fibc(i-1) + fibc(i-2);
    }

结果:

runnable:null
Callable:6765
FutureTask:6765

在上面的例子中,第一个结果为null是因为Runnable没有回调结果,所以get的值为null
第二个Callable那个是通过Future的get函数得到结果,
FutureTask则是一个RunnableFuture<V>,既实现了Runnable又实现了Future<V>,另外
还可以包装Runnable(实际转成Callable)和Callable<V>,提交给ExecuteService来执行后也可以通过
返回的Future对象的get函数得到执行结果,在线程体没有执行完成时,主线程一直阻塞等待,执行完则直接返回结果。

线程池

引子:当需要频繁创建多个线程进行耗时操作时,每次通过new Thread实现性能很差,缺乏统一管理,可能无限制新建线程导致线程之间的竞争,可能占用多系统资源导致死锁,缺乏定时执行,定期执行,线程中断等功能;

这时线程池就派上用场了,Java提供了4种线程池,它能有效管理调度线程,避免资源消耗,优点:

1,重用存在的线程,减少对象创建、销毁的开销;
2,可有效控制最大并发线程数、提高系统资源的使用率,同事避免过多资源竞争,避免堵塞;
3,提供定时执行、定期执行、单线程、并发数控制等功能。

java中的线程池扩展:http://cuisuqiang.iteye.com/blog/2019372

==线程池都实现了ExecutorService接口,该接口定义了线程池需要实现的接口,如submit、execute、shutdown等==

启动指定数量的线程(ThreadPoolExecutor)与定时执行任务(ScheduledThreadPoolExecutor)

1,ThreadPoolExecutor 是线程池的实现之一,功能是启动指定数量的线程以及将任务添加到一个队列中,并且将任务分发给空闲的线程。
2,ScheduledThreadPoolExecutor在我们需要定时执行一些任务的场景使用,通过Executors和newScheduledThreadPool函数就可方便地创建定时执行任务的线程池。

扩展:
ThreadPoolExecutor详解 http://blog.chinaunix.net/uid-20577907-id-3519578.html
Java线程池使用说明 http://www.oschina.net/question/565065_86540
JAVA线程池的分析和使用 http://www.infoq.com/cn/articles/java-threadPool/
Java 理论与实践: 线程池与工作队列 http://www.ibm.com/developerworks/cn/java/j-jtp0730/index.html

AysncTask的原理

AysncTask是解决Thread和Handler更新UI时的代码臃肿,多任务无法精确控制等缺点而产生的,它的诞生使得创建异步任务变得更加简单,不在需要编写任务线程和Handler实例,相对Handler和Thread来说易于使用
==使用注意:==

  • 异步任务的实例必须在UI线程中创建
  • execute(Params... params)方法必须在UI线程中调用
  • 不能在doInBackground(Params... params)中更改UI组件的信息
  • 一个任务实例只能执行一次,如果执行第二次将会抛出异常
  • 不要在程序中直接调用 onPreExecute()、onPostExecute()、doInBackgroud 和 onProgressUpdate 方法;

AysncTask的执行原理

  1. doInBackground(Params... params) 是一个抽象方法,继承AsyncTask必须覆写此方法。
  2. onPreExecute()、onProgressUpdate(Progress... values)、onPostExecute(Result result)、onCancelled()这几个方法体都是空的,需要的时候可以选择性地覆写它们。
  3. publishProgress(Progress... values) 是final修饰的,不能覆写,只能调用,一般都会在doInBackground(Params... params)中调用此方法来更新进度条;

实现一个简单的AsyncTask

HandlerThread是自带消息队列的Thread类型,当线程

public abstract class SimpleAsyncTask<Result> {
    private static final HandlerThread ht = new HandlerThread("SimpleAsyncTask", Process.THREAD_PRIORITY_BACKGROUND);
    static{
        ht.start();
    }

    final Handler mUIHandler = new Handler(Looper.getMainLooper());
    final Handler mAsyncHandler = new Handler(ht.getLooper());

    protected void onPreExecute(){}
    protected void onPostExecute(Result result){}
    protected abstract Result doInBackground();
    public final SimpleAsyncTask<Result> execute(){
        onPreExecute();
        mAsyncHandler.post(new Runnable() {
            @Override
            public void run() {
                postResult(doInBackground());
            }
        });
        return this;
    }

    private void postResult(final Result result){
        mUIHandler.post(new Runnable() {
            @Override
            public void run() {
                onPostExecute(result);
            }
        });
    }
}

总结:
多线程编程在应用开发中随处可见,网络请求、IO操作等耗时操作都需要异步执行,线程池是进行异步操作的重要方式,在概念上十分简单,并且封装良好,基本满足需求。自行实现一个行为正确的线程池并不是那么容易,需要解决死锁、资源不足、和wait()以及notify()等复杂问题。
因此建议在Executor类族的基础上正确的运用而不建议自定义线程池。

参考书目:《Android从小工到专家》,《Android开发艺术探索》

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

推荐阅读更多精彩内容