第三章 多线程

第三章 多线程

在Android3.0后禁止在主线程中进行网络访问请求,否则会抛出异常。其目的是为了保证UI的流畅性,通常都会将耗时(I/O操作,网络请求)操作放到子线程中进行。
在Android应用启动时会默认有一个主线程(UI线程),在这个线程中会关联一个消息队列,所有的操作都会被封装成消息然后交给主线程来处理。
子线程需要操控UI时,最常用的手段就是利用Handler来更新UI,Handler将一个消息Post到UI线程中,然后在Handler的handlerMessage方法中进行处理。Handler就是一个消息处理器,将消息传递给消息队列,然后再由相应的线程从消息队列中逐个取出消息,并且执行。

主线程的消息队列是在Activity.main方法中创建的,通过Looper.prepareMainLooper()创建,最后执行Looper.loop()来启动消息循环。
需要更新UI的Handler一定要在主线程中进行创建
默认情况下,消息队列只有一个,主线程的消息队列,这个消息队列是在ActivityThread.main方法中创建的,通过Looper.prepareMainLooper()来创建,最后执行Looper.loop()来启动消息循环的。
一个线程只有一个消息队列,一个Looper,可以有多个Handler。
在子线程中创建Handler时需要注意,需要先调用Looper.prepare()方法,该方法会为当前的线程创建并绑定一个(只能绑定一个)Looper对象。而主线程中为什么不用手动调用Looper.prepare()方法,因为在主线程main方法中当自动会调用Looper.prepareMainLooper()方法,在该方法中调用了prepare()方法。同时需要启动消息循环:调用Looper.loop()方法。如果忘记调用该方法,即使发送了msg,也得不到处理。

子线程handlerMessage 得不到消息的原因

在prepare方法中new looper(), 并且通过set方式绑定到当前的线程上。

Looper.loop方法的实质就是:

先通过 myLooper()方法获取到当前的Looper对象,然后再通过Looper对象的mQuque方法获取到消息队列。之后进入一个死循环while(true),Message msg = queue.next();来不断取下一个消息,并通过msg.target.dispatchMessage(msg),来将消息进一步分发,进行处理。(其中msg.target是一个Handler类型)


Hanler的dispatchMessage方法详解:
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
        //此处mCallback是new Handler时可以作为参数传入的CallBack接口类型(其中主要又一个方法HandlerMessage()方法,用以回调时处理消息) 
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

    //补充handleCallback方法,即执行callback(Runnable类型)的run方法
    private static void handleCallback(Message message) {
        message.callback.run();
    }

简单描述就是:先查看msg.callbak(Runnabkle类型),如果有的话执行handleCallback方法,否则查看当前Handler是否在new的时候有传入回调对象callback,如果有的话,那么将当前的msg交付给callback去处理(调用handlerMessage方法)。如果上述条件都不满足,那么将消息交付给自己的handlerMessage方法去处理。

Handler 消息的发送:
大致这7种方式:
1) post(Runnable),
2) postAtTime(Runnable, long), 
3) postDelayed(Runnable, long), 
4) sendEmptyMessage(int), 
5) sendMessage(Message), 
6) sendMessageAtTime(Message, long), 
7) sendMessageDelayed(Message, long)  

其中Delayed方法实质都时在其内部调用了AtTime的方法,只不过传入delay的时间加上当前的时间得到了AtTime参数而已。而post和sendMessage方法同样也是调用了postAtTime方法或sendMessage方法,只不过时间参数传入0而已。而post的方式会通过内部调用getMessage方法将Runnable封装成一个Message对象,然后通过调用sendMessageDelay方法来发送消息。 综上所述:发送消息最终都将调用handlerMessageAtTime方法,在该方法中获取到当前的消息队列(mQueue),通过queue对象的enqueueMessage(msg,uptimeMillis)将消息插入消息队列中。而Looper会通过mqueue.next()不断的从消息队列中取出消息并通过Handler的dispatchMessage()将消息进行分发处理。

Talk is Cheap, Show me the code
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

public final boolean post(Runnable r)
    {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }

    //getPostMessage方法将runnable封装到一个Message对象中 

     private static Message getPostMessage(Runnable r) {
     //此处使用Message.obtion()方法而不是使用new Message方法的原因时,节省资源,new的方式一定会创建一个新的Message对象,而obtion方法会查看当前是否有未回收的msg对象,有的话就不重新创建了。
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

子线程创建Handler的3步:
1.Looper.prepare()为当前线程创建Looper
2.new Handler()创建Handler
3.Looper.loop()启动循环

Android 中的多线程 

 Thread 仅仅是一个Runnable的包装类。Runnable接口定义了可执行的任务,它只有一个run()方法。

面试常客 wait,sleep,join,yield
  1. wait( )方法 当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁,使得其它线程可以访问。
    唤醒当前等待池中的线程的方法:notify(),notifyAll(),指定睡眠时间三种方式。
    注意 :wait(),notify(),notifyAll()必须放在synchronized block中,否则会抛出异常.

  2. sleep()方法:该方法是Thread的静态方法,使调用线程进入睡眠状态。不释放资源的原因是:sleep是静态方法,静态方法无法修改对象的机锁

  3. join()方法:Thread对象.join()方法,(插队效果),等待目标线程执行之后再继续执行,等待线程进入阻塞状态。

  4. yield()方法:线程礼让,目标线程由执行状态转化为就绪状态,也就是让出执行权限,让其它线程得以有限执行,当其它线程能否优先执行是未知的。让出时间给就绪状态的线程。

wait与notify常用于等待机制的实现,当条件未满足时调用wait进入等待状态,一旦条件满足,调用notify或notifyAll唤醒等待的线程继续执行.

与线程池相关的Callable,Future,FutureTask

以上三个与Runnable的区别就是,只能运行在线程中,而Runnable既能运行在Thread中也能运行在线程池中。Runnable执行完毕后不能将结果返回,而Callable可以,定义一个泛型,用于指定返回结果的类型。 Runnable与Callable都好像脱缰的野马,一旦运行起来就无法终止,因此应用开发更需要可管理的“战马”Future。他引进了任务执行结果的进行取消,查询是否完成,获取结果,设置操作结果的方法,分别对应于: cancel,isDone,get,set方法。其中get方法会阻塞,直到任务获取返回结果。
Future仅仅是定义了一些规范的借口,而FutureTask才是它的实现类,FutueTask实现了RunnableFuture接口,它是实现了Runnable又实现了Future两个接口,因此FutureTask具备它们两个的能力。FutureTask具有两个构造方法,传入FatureTask(Callable callable)和传入FutureTask(Runnable r,V result)。实际中传入Runnable的构造方法也会通过RunnableAdapter转化为Callable的形式,该类实现了Callable接口,在其抽象方法call()中调用Runnable 的run(),return result;即可。

线程池

线程池的优点:
  1. 重用存在的线程,减少对象创建,销毁的开销
  2. 可有效的控制最大病发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞
  3. 提供定时执行,定期执行,单线程,并发数控制等功能。
线程池简单的原理介绍就是会常见多个线程并且进行管理,提交给线程池的任务会被线程池指派给其中的线程进行执行,通过线程池的统一调度使得多线程的使用更简单,高效。
线程池都实现了ExecutorService接口,该接口定义了线程池中需要实现的接口。如: submit,execute,shutdown等。它的实现有 ThreadPoolExecutor(常用) 和 ScheduledThreadPoolExecutor.
通常我们不会使用new 方式来创建线程池,因为参数相对复杂一些,因此JDK 提供了一个Executors工厂类来简化这个过程.
ThreadPoolExecutor 功能是创建指定数目的线程以及将任务添加到一个队列中,并且将任务分发给空闲的线程。
ExecutorService的生命周期:
  1. 运行:创建后就进入到了运行状态
  2. 关闭:当调用shutdown() 方法后,便进入了关闭状态,此时ExecutorSevice不再接受新任务,但他还在继续执行已经提交了的任务。
  3. 终止:在关闭状态下,所有已经提交的任务都完成即进入了终止状态。
构造方法即参数解释

ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler)

  1. corePoolSize:核心线程数,线程池默认启动后是空的,只有任务来临时才回创建线程以处理任务。prestartAllCoreThreads方法可以在启动线程池的时候启动所有核心线程等待任务。
  2. maximumPoolSize: 允许创建的最大线程数。当workQueue设定为无界队列时该参数无效。作用是调整实际运行时的线程数量。具体解释:当一个任务来临时,如果当前线程数小于corePoolSize,则创建一个新线程去处理任务。如果当前线程数数量大于corePoolSize时且小于maximumPoolSize,那么加入workQueue,只有在任务队列workQueue满时才创建新的线程。如果corePoolSize和maximumPoolSize设置相同的话,那么表示固定数量的线程池。如果将maximumPoolSize设定为Integer.MAX_VALUE时,则允许线程池适应任意数量的并发任务。
  3. keepAliveTime: 当前线程数大于corePoolSize时,空闲线程存活时间(超过时间被杀死)
  4. unit:存活时间的单位
  5. workQueue:任务队列
  6. threadFactory: 线程工厂,定制线程的创建过程,一般不设定
  7. handler: 拒绝策略,当线程池和任务队列都满的时候所要采取的处理策略。
workQueue常用实现
  1. ArrayBlockingQueue:数组实现,先进先出,队列满则调用拒绝策略。
  2. LinkedBlockingQueue:链表实现,先进先出,基本不会满,所以可以忽略maximumPoolSize及拒绝策略handler的设定。
  3. SynchronousQueue: 将任务交给线程而不是将它加入队列,实际上这个队列是空的,每一个出入操作都需要另一个调用移除操作,如果新任务到来,没有可用线程,则嗲调用拒绝策略。将maximumPoolSize设定为Integer.MAX_VALUE和SynchronousQueue时,就相当于Executors.newCacheThreadPool(),一个无界有缓存的线程池。
  4. PriorityBlockingQueue: 具有优先级的任务队列,可自定义优先级顺序,默认时按自然排序。
拒绝策略
  1. AbortPolicy: 拒绝任务,抛出异常,默认的策略。
  2. CallerRunsPolicy: 拒绝任务,如果该线程池还没有关闭,那么将这个新任务执行在调用线程中。
  3. DiscardOldestPolicy:移除工作队列头部的任务,然后重试执行程序,这样处理结果是最后添加的任务反而有可能被执行,先前被加入的都被抛弃了。
  4. DiscardPolicy:拒绝任务,被抛弃,不做任何事情。
Android中最常使用的就是Executors.newFixedThreadPool(int size)方法来启动固定数量的线程池

原因:资源有限。corePoolSize与maximumPoolSize都设定为size,使用LinkedBlockingQueue,存活时间默认为0.

有时可能需要任务尽快地被执行,这就需要线程池中线程足够多也就是说此时需要空间换取时间,线程越多资源消耗越大,但是,在正常情况下并发量就越大,执行速度也越快。
ScheduledThreadPoolExecutor--定时执行任务的线程池
创建方式:ScheduledThreadPoolExecutor executor = Executors.newScheduledThreadPool(3);
//initialDelay 第一次执行延迟,period:执行周期
executor.scheduleAtFixedRateRunnable (Runnable command,long initialDelay,long period,TimeUnit unit)
线程池的使用准则:
  1. 不要对那些同步等待其它任务执行结果的任务排队,这可能导致死锁。
  2. 理解任务:要有效的调整线程池的大小,可以从了解正在排队的任务,则正在执行的任务正在做什么入手。
  3. 调整线程池的大小基本就是避免两类问题:线程太少或线程太多。
HashTable与HashMAp
HashTable是HashMap线程安全的实现,不过HashTable效率低下,在线程1使用put添加元素时,线程2不能使用put添加元素,同时也不能通过get获取到元素。HashTable效率低下的根本原因是,所有的线程必须竞争同一把锁。类比交通堵塞:你抢一步,我抢一步,最后谁都走不了。将数据分成段,分段锁表,那么不同段之间就可以并发执行,这就是ConcurrentHashMap所使用的锁分段技术.
阻塞队列 BlockingQueue

当队列满了时候,再次调用put方法添加元素时,调用的线程将会进入到阻塞状态,直到队列不在处于填满状态。

BlockingQueue主要的方法介绍:

  1. add(e)将元素e添加到队列中,添加成功返回true,否则抛出异常
  2. offer(e)将元素添加到队列中,如果添加成功返回true,否则返回false
  3. offer(e,time,unit) 将元素e加入到队列中,如果等待指定时间仍未添加成功,那么返回false,否则返回true
  4. put(e)将元素加入到队列,如果队列满了,那么进入阻塞状态,直到队列有空间可以添加元素位置
  5. take()取走队首对象,如果未空则进入阻塞状态,直到有新元素添加到队列中
  6. poll(time,unit) 取出并移除队首队列,如果等待指定时候后扔未取到元素则返回null
  7. element() 取队首元素,如果队列为空,则抛出异常
  8. peek() 取队首,如果队列为空,返回null
  9. remove() 取并移除队首元素,如果队列为空,则抛出异常

BlockQueue在Android JDK中有多个实现:
1. ArrayBlockingQueue时数组实现的,线程安全的,有边界的阻塞队列。FIFO,尾进头出.
2. LinkedBlockingQeque单项链表的实现,FIFO,吞吐亮一半高于基于数组实现的BlockingQueue.
3. LinkedBlockingDeque双向列表的实现,同时支持FIFO和FILO,可以从头尾同时操作(插入/删除),并且该阻塞队列时支持线程安全。还可以选择容量,防止过度膨胀,如果不指定默认大小为Integer.MAX_VALUE.

同步关键字:synchronized

synchronized 是一种基于语言的粗略锁,能够作用与对象,函数,class。每个对象都有一个锁,谁能拿到这个锁谁就得到了访问权限。当synchronized作用于函数时,实际上锁的是对象,同步代码块也是锁定对象。当synchronized作用于class时则是锁定这个Class类,并非某个具体对象。锁Class情况:synchronized(A.class)或public synchronized class A。

锁引用对象与锁class对象的区别

对于锁class对象来说,它的作用是防止多个线程同时访问添加了synchronized锁的代码块;而synchronizd作用于引用对象是防止其它线程访问同一对象中的synchronized代码块或方法。

显示锁ReentrantLock 与 Condition

显示锁ReentrantLock与内置锁synchronized相比实现了相同的语义,但是具有更高的灵活性。

  1. 获取释放锁的灵活性
  2. 轮训锁和定时锁
  3. 公平性

二者显著区别是:显示锁可以将锁的获取与释放分开,而内置锁则不能。显示锁还提供了轮巡锁和定时锁。

ReentracntLock的基本操作
  1. lock()获取锁
  2. trylock()尝试获取锁
  3. trylock(long timeout,TimeUnit unit)尝试获取锁,如果在指定的时间还获取不到锁,那么超时
  4. unlock()释放锁
  5. newCondition()获取锁的Condition
ReentrantLock常用的形式如下:

        Lock lock = new ReentrantLock();
        public void doSth(){
        lock.lock();
            try{
                //do something
            }catch(Exception e){

            }finlly{
            lock.unlock();
            }
        }

注意lock一定要在finally中进行释放同步,而使用同步,JVM将确保锁会自动释放,这也就是Lock没有完全替代掉synchronized的原因

Condition用于实现线程间的通讯,它是为了解决Object.wait(),notify(),notifyAll()难以使用的问题。

它的方法如下:

  1. await()线程等待
  2. await(int time,TimeUnit unit)线程等待特定的时间,超过时间则为超时
  3. signal()随机唤醒某个等待线程
  4. signAll() 唤醒所有等待中的线程

示例代码

信号量semaphore

semaphore是一个计数信号量,它的本质是一个“共享锁”。信号量维护了一个信号量的许可集,线程可以通过调用acquire()来获取信号量的许可。当信号量中有可用许可时,线程能获取该许可,否则线程必须等待,直到有可用的许可为止。线程可以通过release()来释放它锁持有的信号量的许可。
常用方法:acquire(),release(),semaphore.availablePermits()剩余许可

示例代码

循环栏栅CyClicBarrier 是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点。因为该barrier在释放等待线程后可以重用,所以称他为循环的barrier。只有当指定个数的线程都调用了CyclicBarrier.await()方法之后,后续的代码才会执行。

示例代码

闭锁CountDownLatch

CountDownLatch同步辅助类,在完成一组正在其它线执行的操作之前,它允许一个或多个线程一直等待,直到条件被满足。

示例代码
CountDownLatch和CyclicBarrier类似,不过有一些不同。

  1. CountDownLatch的作用是允许一个或多个线程等待其它线程执行完成执行,而CyclicBarrier则是允许N个线程互相等待。
  2. CountDownLatch的计数器无法重置,而CyclicBarrier可以。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值