线程与线程之间的通信
一,为什么要线程通信?
1>多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务, 并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
2>当然如果我们没有使用线程通信来进行多线程共同操作同一份数据的话,虽然可以实现,但是在很大程度会造成多线程之间对同一共享变量的争夺,那样的话势必为造成很多错误和损失!
3>所以,我们才引出了线程之间的通信,多线程之间的通信能够避免对同一共享变量的争夺。
二,什么是线程通信?
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。
三,线程之间通信方式:
1.是通过共享变量,线程之间通过该变量进行协作通信;
2.通过队列(本质上也是线程间共享同一块内存)来实现消费者和生产者的模式来进行通信;
1.于是我们引出了等待唤醒机制:(wait()、notify())就是在一个线程进行了规定操作后,就进入等待状态(wait), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify);
线程等待与唤醒的概述
线程的等待与唤醒又称为线程之间的通信,等待与唤醒机制是实现两个或多个线程在执行任务过程相互配合相互协作的一种技术。
void wait():等待,让出cpu进入等待状态(如果一个线程内调用了该方法,那么该线程就停止运行,等待其他线程唤醒,或者其他线程调用notifAll方法)
线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法),这样它才能重新获得锁的拥有权和恢复执行。
要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或synchronized块中。
void notify():唤醒,随机唤醒一个正在等待的线程,让其进入可运行状态(解除了调用wait方法线程的等待状态,让其变成可运行状态)
void notifyAll():唤醒所以进入等待状态的线程,让其都进入可运行状态
以上方法都定义在类:Object中
因为,这些方法在操作同步中的线程的时候,都必须标示其所操作线程所持有的锁(被该锁的对象调用),而只有同一个对象监视器下(同一个锁上)的被等待线程,可以被持有该锁的线程唤醒,(无法唤醒不同锁上的线程)所以,等待和唤醒的必须是同一个对象的监视器①下(同一个锁上)的线程。
而锁可以是任意已近确定的对象, 能 被任意对象调用的方法应当定义在 Object类中。
监视器(锁):同一个对象的监视器下(同一个锁上)的线程,一次只能执行一个:就是拥有监视器所有权(持有锁)的那一个线程。
注意事项: 使用wait,notify,notifyAll方法必须是锁对象调用,必须在同步代码块,或者是同步方法和cock和unlcok方法中间调用。
package com.zeng.awaitNotify;
import java. util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 通过共享一个变量,wait()+notify() 来践行通信
* wait()和notify()方法要求在调用时线程已经获得了对象的锁,因此对这两个方法的调用需要放在synchronized方法或synchronized块中。
*
* 针对两个线程的时候 没有问题
* 针对线程一多的时候, 就必须要用notifyAll()
* @author leo-zeng
* */
public class NumberHolder {
private int number;
public synchronized void increase(){
while(number !=0){
try {
//若是nuber 不为0 时 等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//能执行到这里,说明 已经被唤醒了,并且为0
number ++;
System.out.println("我要递增:"+number);
//通知在等待的线程
notifyAll();
}
public synchronized void decrease(){
//这里用while 不用 if 是因为保证可能多线程中,杜绝可能累加/递减会进行多次的可能。
while(number ==0){
try {
//若是等于零的时候 等待唤醒
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//能执行到这里,说明 已经被唤醒了,并且不为0
number --;
System.out.println("我要递减:"+number);
notifyAll();
}
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
NumberHolder holder =new NumberHolder();
//执行任务
pool.execute(new IncreaseThread(holder));
pool.execute(new DecreaseThread(holder));
pool.execute(new IncreaseThread(holder));
pool.execute(new DecreaseThread(holder));
pool.shutdown();
try {
pool.awaitTermination(300,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 累加的类
* @author leo
* */
class IncreaseThread extends Thread{
private NumberHolder numberHolder;
public IncreaseThread(NumberHolder numberHolder) {
this.numberHolder =numberHolder;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
//每次都有不多的延迟
try {
Thread.sleep((long)Math.random()*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//执行新增操作
numberHolder.increase();
}
}
}
class DecreaseThread extends Thread{
private NumberHolder holder;
public DecreaseThread(NumberHolder holder){
this.holder =holder;
}
@Override
public void run() {
for (int i = 0; i <20; i++) {
//每次都有不多的延迟
try {
Thread.sleep((long)Math.random()*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//执行递减函数
holder.decrease();
}
}
}
2.通过队列来实现线程的通信
这里用的是java.util.concurrent包中linkedBlockingQueue 来进行线程间交互;
java.util.concurrent.LinkedBlockingQueue 是一个基于单向链表的、范围任意的(其实是有界的)、FIFO 阻塞队列。访问与移除操作是在队头进行,添加操作是在队尾进行,并分别使用不同的锁进行保护,只有在可能涉及多个节点的操作才同时对两个锁进行加锁。
这里通过共享一个队列的信息,实现生产者和消费者
package com.zeng.awaitNotify;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 通过linkedblockingQueue 构建线程间通信
* @author leo-zeng
*
*/
public class LinkedBlockingQueueTest {
public static void main(String[] args) {
LinkedBlockingQueue
ExecutorService threadPool = Executors.newFixedThreadPool(10);
threadPool.execute(new Producer(queue));
threadPool.execute(new Consumer(queue));
if(!threadPool.isShutdown()){
threadPool.shutdown();
try {
threadPool.awaitTermination(300, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{
private LinkedBlockingQueue
public Producer(LinkedBlockingQueue
this.queue =queue;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("生产出:"+i);
try {
Thread.sleep(100);
queue.put(new String("producer:"+i));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer extends Thread{
private LinkedBlockingQueue> queue;
public Consumer(LinkedBlockingQueue
this.queue =q;
}
@Override
public void run() {
while(true){
try {
System.out.println("consumer 消费了:"+queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程的状态(线程的生命周期)
NEW 新建状态,刚刚创建完成还没开启的状态
RUNNABLE 可运行状态,有资格执行,可能在执行中,有可能不是在执行中
BLOCKED 锁阻塞状态,要等待其他线程释放锁对象
WAITING 无限等待,一个线程等待另一个线程执行一个(唤醒)动作
TIMED_WAITING 计时等待,这一状态一直保持到超过规定的时间,或者收到唤醒动作
TERMINATED 死亡状态,任务执行完毕的状态
进程与进程之间的通信
进程间通信(IPC,InterProcess Communication)是指进程间数据交互的过程。
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
Android底层是基于Linux,而Linux基于安全考虑,是不允许两个进程间直接操作对方的数据,这就是进程隔离
在Linux系统中,虚拟内存机制为每个进程分配了线性连续的内存空间,操作系统将这种虚拟内存空间映射到物理内存空间,每个进程有自己的虚拟内存空间,进而不能操作其他进程的内存空间,每个进程只能操作自己的虚拟内存空间,只有操作系统才有权限操作物理内存空间.进程隔离保证了每个进程的内存安全,但是在大多数情形下,不同进程间的数据通讯是不可避免的,因此操作系统必须提供跨进程通信机制。
虽然Android是基于Linux,但并不能继承Linux中的进程通信的方式,Android有着自己进程间通信方式。
线程与进程的关系
形象理解:如果说把安卓系统比喻成一片土壤,可以把App看做扎根在这片土壤上的工厂,每个APP一边对应一个进程,那么线程就像是工厂的生产线,其中,主线程好比是主生产线,只有一条,子线程就像是副生产线,可以有很多条。
关系:一个APP一般对应一个进程和有限个线程。
对应一个进程:但可以在AndroidManifest中给四大组件指定属性:android:process开启多线程模式
有限个线程:线程是一种受限的系统资源,不可无限制的产生且线程的创建和销毁都有一定的开销。
为何要进行IPC
所有运行在不同进程的四大组件,只要他们之间需要通过内存在共享数据,都会共享失败。这是由于Android为每个应用分配了独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这会导致在不同的虚拟机中访问同一个类的对象会产生多份副本。
定义多进程
Android应用中使用多进程只有一个办法(用NDK的fork来做除外),就是在AndroidManifest.xml中声明组件时,用android:process属性来指定。
不知定process属性,则默认运行在主进程中,主进程名字为包名。
android:process = package:remote,将运行在package:remote进程中,属于全局进程,其他具有相同shareUID与签名的APP可以跑在这个进程中。
android:process = :remote ,将运行在默认包名:remote进程中,而且是APP的私有进程,不允许其他APP的组件来访问。
多进程引发的问题
静态成员和单例失效:每个进程保持各自的静态成员和单例,相互独立。
线程同步机制失效:每个进程有自己的线程锁。
SharedPreferences可靠性下降:不支持并发写,会出现脏数据。
Application多次创建:不同进程跑在不同虚拟机,每个虚拟机启动会创建自己的Application,自定义Application时生命周期会混乱。
综上,不同进程拥有各自独立的虚拟机,Application,内存空间,由此引发一系列问题。
基于Binder的IPC方式:(优点:传输效率高,可操作性强:传输效率主要影响因素是内存拷贝的次数,拷贝次数越少,传输速率越高。)
进程间通信
Bundle/Intent传递数据:
可传递基本类型,String,实现了Serializable或Parcellable接口的数据结构。Serializable是Java的序列化方法,Parcellable是Android的序列化方法,前者代码量少(仅一句),但I/O开销较大,一般用于输出到磁盘或网卡;后者实现代码多,效率高,一般用户内存间序列化和反序列化传输。
https://www.jb51.net/article/101781.htm
文件共享:
对同一个文件先后写读,从而实现传输,Linux机制下,可以对文件并发写,所以要注意同步。顺便一提,Windows下不支持并发读或写。
https://blog.csdn.net/hzw2017/article/details/80978663
Messenger:
Messenger是基于AIDL实现的,服务端(被动方)提供一个Service来处理客户端(主动方)连接,维护一个Handler来创建Messenger,在onBind时返回Messenger的binder。
双方用Messenger来发送数据,用Handler来处理数据。Messenger处理数据依靠Handler,所以是串行的,也就是说,Handler接到多个message时,就要排队依次处理。
AIDL:
AIDL通过定义服务端暴露的接口,以提供给客户端来调用,AIDL使服务器可以并行处理,而Messenger封装了AIDL之后只能串行运行,所以Messenger一般用作消息传递。
通过编写aidl文件来设计想要暴露的接口,编译后会自动生成响应的java文件,服务器将接口的具体实现写在Stub中,用iBinder对象传递给客户端,客户端bindService的时候,用asInterface的形式将iBinder还原成接口,再调用其中的方法。
https://blog.csdn.net/hzw2017/article/details/81048650
ContentProvider:
系统四大组件之一,底层也是Binder实现,主要用来为其他APP提供数据,可以说天生就是为进程通信而生的。自己实现一个ContentProvider需要实现6个方法,其中onCreate是主线程中回调的,其他方法是运行在Binder之中的。自定义的ContentProvider注册时要提供authorities属性,应用需要访问的时候将属性包装成Uri.parse("content://authorities")。还可以设置permission,readPermission,writePermission来设置权限。 ContentProvider有query,delete,insert等方法,看起来貌似是一个数据库管理类,但其实可以用文件,内存数据等等一切来充当数据源,query返回的是一个Cursor,可以自定义继承AbstractCursor的类来实现。
https://blog.csdn.net/hzw2017/article/details/81123791
Socket:
Android不允许在主线程中请求网络,而且请求网络必须要注意声明相应的permission。然后,在服务器中定义ServerSocket来监听端口,客户端使用Socket来请求端口,连通后就可以进行通信。
https://blog.csdn.net/hzw2017/article/details/81210979