这一章本来是讲安卓跨进程通信的,也就是IPC。但由于实在是纠结于进程和线程在同步和通信上的异同,就索性查了一大波资料,结合自己的认识,把这一篇扩充成了进程的通信和同步,以及线程的通信和同步。希望以此告别纠结之旅。
线程同步方式
我查阅了很多网上的资料,但其实发觉很多的博文上可能局限于当时的版本原因,还有一些同步的方式并没能列举,下面我将列举所有的线程同步方式,如有不足请指出。
1、synchronized
同步锁,可以用来修饰方法,修饰代码块,这个网上例子很多,也比较容易使用,但是有一些地方可能容易混淆。
- 修饰成员方法时,当有多个线程同时访问某一个对象的多个同步方法时,只有得到同步锁的可以执行,其他的都会被阻塞,而此对象的非同步方法不会阻塞,非相同对象的所有方法不会阻塞。
- 修饰类方法时,当有线程进入方法时,其他访问类的其他同步方法的线程将被阻塞。对象的同步方法访问不受影响。
- 修饰代码块的时候,当多个线程运行到synchronized的代码块时,如果上锁的对象已经有线程取得了同步锁,则其他线程被阻塞。这里比较的是其引用的对象是否已经有锁,假如
String a="sss",b="sss";
由于a==b是成立的,所以当有线程获得了a的锁时,其他尝试获取b的锁的线程也会被阻塞,因为a和b所指向的对象时相同的。这个和a.equal(b)无关。
即有线程进入了synchronized(a){...}
代码块,则尝试进入synchronized(b){...}
的也会被阻塞,因为a==b为真。
2、wait() notify()方法
这个不多讲。Object自带的方法,虽然不觉得好用。
3、Lock、ReadWriteLock接口
实现这两个接口的分别是ReentrantLock
和ReentrantReadWriteLock
,通过在需要同步的代码位置写上类似
myLock.lock();
myLock.unlock();
myLock.readLock().lock();
myLock.readLock().unlock();
myLock.writeLock().lock();
myLock.writeLock().unlock();
即可申请锁和解锁。构造函数里可设置是否为公平锁,公平锁是先来先得的形式,不公平锁是随机得的形式,不分先后,默认不公平。建议在try中申请,finally中释放。
4、Semaphore信号量
可以参考这个链接了解:http://www.cnblogs.com/whgw/archive/2011/09/29/2195555.html
通过Semaphore semp = new Semaphore(5);
来构造一个初始的信号量,再在需要申请信号量的代码中写下semp.acquire(2);
申请不定数量的资源,默认为1,在完成后释放即可,释放时也可释放自定数量的资源semp.release(2);
默认释放1。
5、CountDownLatch和CyclicBarrier
这两个有些相似,相同点是都有点类似于等待一组线程完成任务后再继续进行。
构造CountDownLatch:CountDownLatch latch = new CountDownLatch(3);
,再在子线程中完成部分任务后调用latch.countDown();
即可让计数-1,这里不会阻塞代码运行。而真正阻塞执行的地方是调用了latch.await();
的地方。只有等计数器为0是才会继续执行。
构造CyclicBarrier:CyclicBarrier barrier=new CyclicBarrier(3);
构造方法还支持添加一个Runnable作为结束时执行的方法。在需要等待其他线程的地方调用barrier.await();
即可,直到计数器为0时恢复执行。
附上一个可以参考的链接:http://blog.csdn.net/shihuacai/article/details/8856407
6、volatile
记清楚,volatile只是保证两点:
- 在访问volatile被修饰的数据时,一定会从堆中读取。或者说一定保证读到最新的数据。
- 禁止指令重排volatile修饰的数据。
以上意味着如果在多线程中修改volatile修饰的数据依然会不安全,但如果只有一个线程修改,其他线程只是访问,则可以满足需要,而且这比其他同步方式更高效。另外,不要滥用volatile,因为它会阻止相关数据的指令重排,也就意味着降低一定的效率。
有一些博客上讲了ThreadLocal
,但个人认为不太实用,甚至觉得没法真正满足线程同步的需求,就没列举。
线程通信方式
1、Handler和基于Handler的方式
比如Activity的runOnUiThread,View的post系列方法都是基于Handler机制实现的。关于Handler机制,我的另一篇博文有所介绍:http://blog.csdn.net/qq_22123283/article/details/70195780
2、PipedInputStream、PipedOutputStream、PipedReader、PipedWritter
关于InputStream和Reader以及OutputStream和Writter的区别,根据此链接:http://blog.sina.com.cn/s/blog_6d3183b50101cri5.html ,可以认为区别就是Reader是按字符读取,读取的是字符流,而InputStream是按字节读取,读取的是字节流。
这个方式用起来比较容易,但是每个管道都是单向的。
3、同步方式(如果不进行区分)
同步也可以认为是一种线程的通信,如果不特意区分的话,上述的线程同步方式均可作为通信方式,只不过其通信的意义不太一样。
进程通信方式(针对安卓环境)
先列一下,之后补充细节。
1、Intent+Bundle
2、文件共享
SharedPreferences除外,因为其有缓冲机制。
3、Messenger
使用Messenger
时注意,它是串行处理消息的。一个典型的客户端和服务端双向通过Messenger通信的流程是:
- 创建客户端
Handler
,创建客户端Messenger clientMessenger=new Messenger(clientHandler);
。 - 服务端创建
Handler
,创建Messenger serviceMessenger=new Messenger(serviceHandler);
在onBind
方法中返回serviceMessenger.getBinder()
。 - 客户端在
ServiceConnection
的onServiceConnected
方法中获取服务器的Messenger serviceMessenger=new Messenger(binder);
,通过这个Messenger
就可以向服务端发送Message
了。如果需要服务端回应,需要写message.replyTo=clientMessenger;
即可。
和所有的AIDL一样,如果是主线程进行这样的通信,服务端的代码一定不能耗时过久,因为客户端访问的线程是被阻塞/挂起的。
4、AIDL
此方式建议多上手练习,比较复杂一些。流程主要如下:
- 如果需要用到自己实现
Parcelable
接口的对象,则需要新建一个对应的AIDL文件,在里面声明parcelable 'Class'
即可。比如parcelable Book;
- 创建服务端要实现的接口AIDL文件。接口方法的参数如果要用到上述的
Parcelable
对象,则需要用import
导入,同时需要在参数类型前面加上in
、out
或inout
,因为底层实现是有不同的开销的。 - build项目,自动生成对应的Java代码。然后在服务端实现接口,方法类似
Binder mBinder = new IBookManager.Stub() { //implement methods... }
。并在服务端的onBind
方法中返回此binder
。 - 客户端在
onServiceConnected
方法中用类似IBookManager bookManager = IBookManager.Stub.asInterface(service);
的代码将binder
转换成接口,并正常调用接口方法即可。同理此客户端线程会被挂起/阻塞。
进阶问题:
- 使用
RemoteCallbackList<T>
来注册回调接口,其他方式实现的方法将不能取消注册,因为binder最后返回的完全不是所传递的对象,所以更不可能在服务端找到对应客户端的对象。而使用这个可以解决问题。 - 当
AIDL
过多使用时,考虑使用BinderPool
方案,这其实是在服务端完成多个AIDL接口的实现,而在onBind
方法中只返回binderPoolImpl
的实现,BinderPool
也在服务端实现,主要就是在一个IBinder queryBinder(int binderCode)
方法中,根据bidnerCode
去选择返回要返回的IBinder
即可。于是就可以在一个Service
的前提下实现多个AIDL通信方式。 - 请注意Binder的安全性,在
onBind
环节检查Permission
或在接口方法中检查权限,防止任何应用都能访问。
5、ContentProvider
需要重写6个方法,值得注意的是onCreate方法是运行在请求此provider的线程中的,其他方法则是运行在BinderPool
中。添加Permission
或readPermission, writePermission
增强安全性。UriMatcher的使用可以帮助分析要选择的table
。
6、Socket
对localhost
的自定端口发起通信连接即可。
进程同步方式(操作系统知识)
这方面更多是指操作系统上的知识,可能在不同的具体系统上,方式不一定相同,可能更多可能更少。其中前2个方式是进程同步,后面4个是进程通信,因为有的情况下可以认为通信和同步是一个意思,所以都列在这里了。