一、并发设计原理
1. 基本概念
1.1 并发和并行
1.2. 同步
同步方式有两种:
- 控制同步:当一个任务的开始依赖于另一个任务的结束时,第二个任务不能在第一个任务完成之前开始。
- 数据访问同步:当两个或更多任务访问共享变量时,任意时间里,只有一个任务可以访问该变量。
与同步密切相关的一个概念是临界段。临界段是一段代码,由于它可以访问共享资源、因此在任何给定的时间内,只能够被一个任务执行。
互斥是用来保证这一要求的机制,而且可以采用不同的方式来实现。
并发系统中有不同的同步机制。
- 信号量
- 监视器
线程安全:如果共享数据的所有用户都受到同步机制的保护,那么代码就是线程安全的。
1.3 不可变对象
不可变对象初始化后,不能修改其可视状态(其属性值)。如果想修改一个不可变对象,就必须创建一个新的对象。
不可变对象的主要优点在于它是线程安全的。
1.4 原子操作和原子变量
与应用程序的其他任务相比,原子操作是一种发生在瞬间的操作。在并发应用程序中,可以通过一个临界段来实现原子操作,以便对整个操作采用同步机制。
原子变量是一种通过原子操作来设置和获取其值的变量。
1.5 共享内存与消息传递
任务可以通过两种不同的方法来互相通信:
- 共享内存: 任务在读取和写入值的时候使用相同的内存区域。为了避免出现问题,对该共享内存的访问必须在一个由同步机制保护的临界段内完成。
- 信息传递: 当一个任务需要与另一个任务通信时,它会发送一个遵循预定义协议的消息。如果发送方保持阻塞并等待响应,那么该通信就是同步的;
如果发送方在发送消息后继续执行自己的流程,那么该通信就是异步的。
2. 并发应用程序中可能出现的问题
2.1 数据竞争
如果有两个或者多个任务在临界段之外对一个共享变量进行写入操作,没有使用任何同步机制,那么应用程序可能存在数据竞争。
2.2 死锁
当两个或多个任务在等待必须由另一线程释放的某个共享资源,而该线程又正在等待必须由前述任务之一释放的另一共享资源时,并发应用程序就出现了死锁。
Coffman条件:
- 互斥
- 占有并等待条件
- 不可剥夺
- 循环等待
避免死锁的机制: - 忽略它们
- 检测
- 预防
- 规避
2.3 活锁
如果系统中有两个任务,它们总是因对方的行为而改变自己的状态,那么就出现了活锁。最终结果是它们陷入了状态变更的循环而无法继续向下执行。
2.4 资源不足
当某个任务在系统中无法获取维持其继续执行所需的资源时,就会出现资源不足。解决资源不足需要确保公平原则。
2.5 优先权反转
当一个低优先权的任务持有了一个高优先级任务所需的资源时,就会发生优先权反转。这样的话,低优先权的任务就会在高优先权的任务之前执行。
3. 并发设计模式
3.1 信号模式
public void task1(){
section1();
commonObject.notify();
}
public void task2(){
commonObject.wait();
section2();
}
3.2 会合模式
public void task1(){
section1_1();
commonObject1.notify();
commonObject2.wait();
section1_2();
}
public void task2(){
section2_1();
commonObject2.notify();
commonObject1.wait();
section2_2();
}
3.3互斥模式
public void task(){
preCriticalSection();
try{
lockObject().lock();//临界段开始
criticalSection();
}catch(Exception e){}
finally{
lockObject().unlock();//临界段结束
postCriticalSecion();
}
}
3.4多元复合模式
public void task(){
preCriticalSection();
semaphoreObject.acquire();
criticalSection();
semaphoreObject.release();
postCriticalSecion();
}
3.5 栅栏模式
public void task(){
preSyncPoint();
barrierObject();
postSyncPoint();
}