Looper.loop()是否导致主线程卡死问题探讨

知乎一个问题很好: 在执行ActivityThread的main函数体内,执行了Looper.loop()函数,这是个for(; ;)循环为何不会导致主线程进入死循环体从而被卡死呢?

首先理解什么叫做“线程进入死循环”, 在循环体内就是一段可执行的子程序,由于for(; ;)的调度导致这段子程序持续不断的在执行, 也就是持续的占用CPU资源, 从而导致当前线程的循环体外的子程序无法执行, 导致线程卡死的状态。

而实际上, 在Looper的loop循环体内是不会导致当前线程被卡死的,原因如下:
由于MessageQueue的阻塞机制, 主线程会处于阻塞状态, 保证了程序的不退出, 所以可以看作是一种“假卡死”状态, 倘若这个循环体结束了意味着主线程该停止运行了, 那么app可以退出了。

  • MessageQueue的阻塞机制: 在循环体内第一件事儿就是从mq里面取出一个msg操作, 即queue.next(); 此操作为阻塞操作, 如果当前线程阻塞的话就会释放掉CPU的占用, 进入阻塞状态.

而在这个循环体内是如何调用到循环体外部的子程序呢?

  • 通过从messageQueue取出来的msg的targetHandler机制, 把取出来的消息dispatch到对应的handler那里, 在handler的handleMessage函数里面去执行一些逻辑, 诸如onCreate,onResume之类的函数. 这样就可以在循环体内执行循环体外的子程序了。

ActivityThread内的所有message就是来自其他线程send过来的,全部都放到了这个MessageQueue对象中; 例如binder线程的消息也是通过handler方式传递给ActivityThread的queuede。
而activity的生命周期调度也是靠ActivityThread的H(extends Handler)类, 任何一个与生命周期相关的消息, 都会把target设置为H, 这样才能够确保在for(; ;)中取出的每一个message都能够通过message.target.sendMessage(msg)的方式通知H类的handleMessage()方法,进而调用不同的生命周期处理函数。

让ActivityThread进入”假卡死”状态, 一来不需要持续的占用CPU资源, 二来可以保证应用的不退出, 随时可以相应消息avaliable, 而我们的手机大多情况下都是处于等待用户输入的状态, ActivityThread的假卡死状态是最适合这种模式的交互方式的设备。

ServiceManager

  • ServiceManager作为一个Binder(Service)的管理中心,是理解android中Binder通信原理的重要环节之一:

    • 为了管理Binder(Service),需要如下几个函数:

      • getService(String serviceName);
      • checkService(String serviceName);
      • addService(String serviceName);
      • listService();
    • 所以把如上的功能抽象为一个接口也就是IServiceManager,而此接口需要继承IInterface接口 //TODO 为何要继承它呢 ?

    • ServiceManager需要实现的这些功能,然而它没有继承这个IServiceManager而是持有着IServiceManager对象,任何的上述操作都会转交给IServiceManager对象去执行,也就是一个单纯的代理关系。

    • IServiceManager的初始化是调用了ServiceManagerNative.asInterface(BinderInternal.getContextObject());在这个asInterface()函数中需要注意了:

      • 首先,getContextObject()这个函数获取的是应用的全局Context对象,可以把他看做是一个应用层跟底层Binder沟通的对象即可。—–> obj = context(Ibinder对象);
        l
      • 其次在判断这个context对象不为空后去实例化IServiceManager对象,IServiceManager in = (IServiceManager)obj.queryLocalInterface(“android.os.IServiceManager”);那么在queryLocal(Binder?)过程发生了什么?
        • 遍历所有实现了Binder对象(也就是查询Service)的接口的本地对象,如果本地没有一个serviceManager对象的话,选择下一步
        • 如果本地没有任何ServiceManager对象,那么就执行new ServiceManagerProxy(obj). 那么这个代理是干嘛的呢?
      • 当本地并没有一个IServiceManager的存在的话,就需要去创建一个ServiceManagerProxy对象,它实现了IServiceManager接口,作为一个内部类存在于ServiceManagerNative中。内部结构如下:
        • 持有一个用户内存空间的binder对象,也就是记录下这个用户内存空间里的Binder对象(Service)。
        • 初始化过程也是建立用户内存空间的IBinder对象 == mRemote 的联系
        • getService(String serviceName):获取Service的流程:
          • 初始化两个Parcel对象一个用来装想要获取的service的信息,一个作为存储来自查询结果(也就是一个IBinder对象)的。
          • 通过用户内存空间上的IBinder对象的transact(GET_SERVICE_TRANSACTION, data, reply, 0)函数实现向真正的ServiceManager查询serviceName的过程。
          • transact()函数的实现过程中让clientBinder进入到内核态挂起,binder驱动最终会唤醒server端的onTransact(int code, Parcel data, Parcel reply, int flags)函数。在server端执行的操作就是读取client端传入的参数,调用getService(string name)函数从而得到IBinder对象,装入到reply流中去。 (由此可见,ServiceManager本身也是一个特殊的Binder对象,查询serviceName都经过了client发送binder数据到binder驱动,驱动再次发送binder数据到managerService这整个流程.可以查看ServiceManagerNative这个类,本身是继承了Binder对象的。) //疑问 这个transact函数是如何确定一个serverBinder对象呢?也就是说binder驱动如何判定调用哪个binder的onTransact函数呢?
            • 而实际上这个查询的过程可以通过一个缓存来解决HashMap sCache。
      • 最终的结果就是实现了ServiceManager中的IServiceManager对象 == new ServiceManagerProxy()对象的结果。完成client端的ServiceManager(也就是一个ServiceManagerProxy对象)的初始化过程。
    • 而最重要的ServiceManagerNative的初始化工作只做了一件事儿:

      • 调用Binder的attachInterface(this,descriptor)的工作,正式的把serviceManager对象跟Binder类中的IInterface mOwner绑定在一起。
      • 而Binder类中的queryLocalInterface()函数返回的就是这个mOwner对象。
      • 可见,此时的ServiceManagerNative就是一个server端的binder对象。

synchronized vs volatile.md

java的线程内存模型分为了主内存和工作线程, 由于线程运行于不同的CPU中, 所以说不同的线程是没办法读取到彼此之间的内存数据的, 而多个线程之所以看起来能够操作主内存的某一个共享变量, 实际的过程是各个线程把此变量copy到自己的工作内存中,实际是对copy的操作,最后刷入到主内存中。而此过程中各个线程无法确保自己copy到的变量的值就是最新的值, 比方说十个线程对同一个变量执行+1操作, 可能第十个线程去copy这个变量的时候其他九个线程还没把最新的执行结果刷入到主内存中,导致第十个线程+1得到的结果不等于原来的变量+10的期望值。所以需要来保证线程的安全性

synchronized:

synchronized修饰的是某一个变量或者方法, 保证它所在的内存块加了一个同步锁, 每一个线程对synchronized修饰过了的内存区域操作的时候都会获取同步锁, 只有获取了同步锁的线程才可以操作住内存, 在释放同步锁的时候把最新值刷入到住内存中。依靠锁的唯一可获取性保证了共享内存的正确性.

volatile:

保证了多个线程之间共享变量的可见性, (默认的情况下操作一个共享变量是先把这个变量copy到自己的工作内存执行操作的, 导致其他线程对共享内存的最新值的不可见性)
既然synchronized的原理是给内存块添加同步锁,那么volatile的实现原理是什么呢?
通过编译后对比没有添加volatile的变量, 发现有volatile变量编译后多了一条指令:
lock addl $0x0,(%esp);
这条指令保证了两件事:

  • 当前线程的工作区域中被volatile修饰的内存块的数据刷新到主内存中;(保证主内存中是最新值)
  • 刷新当前线程的工作内存中变量值到主内存的同时还会导致其他线程中的工作内存中缓存的该共享变量地址指向的数据值无效.(剔除了非最新值对住内存中共享变量值污染的可能性)。

虽然volatile的消耗低,但是volatile只是提供了变量的可见性(具体方式参考上面说的),却没有提供原子性,(synchronized的原子性由锁的特征提供).这会导致一些问题,诸如多线程计数不准确问题.

原子操作指的是一组不可被中断的操作, 以i++为例子, 它实际上包含了load(from main memory) , add(in work memory) ,save(to main memory) 三个操作, 在多处理器环境中同时执行这个操作就可能导致这组指令被打断, 可能在cpu1在执行add指令还没执行到save指令的时候, cpu2就从main memory中load i的值此时的i并非是最新的value. 因此说i++操作不是一个原子性的操作. 需要借助concurrent包里面的atomaticInteger来实现此目的, 它确保了一个操作的原子性, 那么是如何实现的呢?

volatile实现了一个目标: 确保了其他线程读取到的i变量是最新的value,然而离计算出正确的i的值还缺少一个步骤: 如何保证多个线程在load到最新的值之后依次的把各自内存中修改过后的变量i save到主内存中? 然而volatile限制了这个功能: 被volatile修饰过的变量的值一旦被改动,将会导致拷贝了此变量到工作内存中的其他线程无法再次操作这个变量的副本, 也无法save回主内存. 所以用volatile的话还是得同时配合synchronized来实现线程安全的目标啊.例如DCL

那么原子操作的实现原理是什么呢?
锁就不说了,说说compare and swap算法来实现自旋CAS —> 自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止
那么单次的CAS是怎样的呢?

boolean compareAndSwap(int mainmemoryValue,int workmemoryOldValue,int workMemoryNewValue){
    if(mainMemoryValue != workMemoryOldValue){
        return false;
    }
    mainmemoryValue = workMemoryNewValue;
    return true;
}

以上是java伪代码, 实际上的实现是c++完成的, 唯一的不同就是此算法的第一个参数是一个指向mainMemory的变量的一个指针, 只有当来自workMemory的变量值等于主内存中该变量的值的时候才能认为是衔接上一条指令的操作,将workMemory中最新的值刷入到主内存指针指向的value.从而保证了一组操作的原子性。

CAS依旧存在的限制:

  • ABA问题:
    两个进程, 一个进程在load到主线程变量A的值后因某种原因挂掉了, 后来的进程2再次load这个变量A把他的值从1改到了10再从10改回了1, 此时的进程1重启了发现变量A的值没有改变, 那么进程1是无法感知到进程2对变量a的1->10,10->1的这个过程。可能在java中只是值的变动感知不到这个影响,但是如果发生在c++中的指针比较交换的话,就容易发生问题: 具体的例子参考wiki 比较并交换

  • 由于自旋的过程就是不断的执行cas,倘若cas长时间不成功, 就导致cpu长时间的空转

I/O

java中的file对象实际上只是一个抽象的概念,FileDescriptor才是真正意义上指代file的对象。

输出流负责写入数据,输入流负责负责读取数据。
相比于输入输出流,缓冲流有一块区域叫做缓冲区,缓冲区用于数据流的缓存,如果向缓冲流中读取数据,首先是从缓冲区中读取数据,如果此时的缓冲区为空,系统会自动的从磁盘中读取数据,尽可
能的填充满缓冲区,此时从设备中读取的数据并不会直接发送给外部设备而是暂存于缓冲区中。
在处理输入流的时候一般都是一个一个字节的读取输入流内的数据,不过也可以通过一段缓冲区来指定读取的字节数:

byte[] buffer = new buffer[1024];
while(inputStream.read(buffer) != -1){
    outputStream.write(buffer);
}

可以把文件想象出一个用于装字节的容器,FileInputStream 想像成一个水管用于抽取文件中的字节s,对应的FileOutputStream就是一个专门用于向容器中注入字节s
哎,其实主要是根据命名去理解stream太蛋疼了,文件输出流不应该是用于抽取文件字节 文件输入流不应该是用于注入字节的吗?咋刚好相反呢 ?
或者把输入输出流以buffer作为基准,输入流需要把文件的内容输出到这一块buffer来,所以对应的是 read 方法,对应的输出流则是把buffer的内容输入到文件内所以对应的方法是write。
数据流有一块区域称之为缓冲区,上面说了缓冲区是干嘛的,每次输出数据的时候数据最先到达的是缓冲区而不是直接传到磁盘中去,只有当缓冲区满了或者调用了关闭流的函数才能把数据传输到
磁盘存储中,所以说调用close一方面是为了释放资源另一方面也是为了把最后一次输入到缓冲区的数据强制刷到磁盘中,否则的话无法保证数据的完整性。

利用缓冲机制来提升读取数据流的效率:
实际上每次调用read都会从OS请求分发一个字节,一般的做法就是一个while循环取出所有数据这样未免效率略低,所以可以采用缓冲机制,一次性从os请求一个数据块放在数据流的缓冲区,
每次读取字节跳过os直接从缓冲区读取字节这样减少与os的交互从而提升效率。而自带缓冲机制的则是BufferedInputStream, 所以需要把数据源包装一下从而获得可用缓冲区。

nio是基于数据块的普通io是基于数据流的,前者把最耗时的io操作(也就是把数据流填充到缓冲区或者将缓冲区的数据流提取出来)都转移给了操作系统去完成,以前都是基于一个一个字节来传数据
此时可以以一个缓冲区的容积(也就是所谓的数据块)作为基本单位来传输数据。
普通io操作数据流有个api是 read(byte[])/write(byte[]) 看起来像是一次消费了byte数组长的数据(可以看作数据块),然而实际上实现的方式是遍历这个字节数组,真正去消费数据流还是一个
write(int oneByte)/read(int oneByte),所以说面向流的io是很慢的。不过一个字节一个字节的传输有一个好处就是可以精准的过滤每一个字节,可以为一个数据流创建多个过滤器,例如如下代码:
new BufferedInputStream(new FileInputStream(“textByte.txt”))

序列化的问题
如果一个对象a持有了另外一个对象b的引用,那么此时如果想要序列化掉a,需要把对象b的引用地址序列化到对象a中去吗?
当然是不行的,即使把对象b的引用放入了对象a中那么再次加载对象a无法保证引用指向的地址依旧对应的是对象b,它的对应的引用地址可能已经被回收而新生成的对象b地址可能在其他的地方。
具体的做法就是为对象引用建立一个model并且赋予它一个序列号,比如说对象a饮用到了对象b,首先要做的就是把对象b的值存储起来,然后跟对象a建立起关系(也就是说对象a引用到了对象b,
也就是objB->modelBNum)这样的话在反序列化的时候就可以把对象a完整的取出来而对象b也能够重新实例化出来并且恢复序列化前跟对象a之间的联系。