■ 并发原理
单核系统:线程交替执行,由于交替又快又多,给人一种同时执行的感觉
多核系统:不仅可以交替执行线程,而且可以重叠执行线程
补充: 本章指的并发主要指的是线程间的并发
■ 常见的并发机制
■ 不同系统的并发机制
■ 互斥的需求
■ 互斥的方案
* dekker算法
/** * dekker算法基本约束: * 某一时刻对某一内存地只能进行一次访问 * 1.设置flag做为两个线程进入临界区的密钥,当一个线程失败,其他仍可访问 * - 每个线程只能改变自身的flag,只能检查其他线程的flag而不能改变 * - 当一个线程要进入临界区,需周期性检查另一个线程flag,直到另一线程不在临界区 * - 当线程进入临界区时,应立即设置自身flag为true,表明占领临界区 * - 当线程离开临界区时,应立即设置自身flag为false,表明释放临界区 * 2.设置turn用于安排临界区的访问顺序,访问线程必须重复读取turn值直到被允许进入临界区 * - 当turn值等于线程号,该线程可以进入其临界区 * - 否则,该线程必须被强制等待(忙等待或自旋等待) */ public class dekker { //观察两个线程的状态 boolean[] flag = {false,false}; //表示临界区访问权限的轮转,初始权利给p1 -- 安排执行顺序避免谦让造成的活锁问题 int turn = 1; public void p0(){ while (true){ //设置p0的flag为true,同时检查p1的flag flag[0] = true; while (flag[1]){ //当临界区不可用时,判断当前临界区权限是否是p1 if (turn == 1){ // 用于处理活锁问题 //当临界区权限是p1时,需要将p0设置为false,使得p1能进入临界区,用于处理死锁问题 flag[0] = false; //循环校验turn的权限(空自旋),直到p1执行完毕将权限交给p0 while (turn == 1){ /** do nothing 空自旋 **/ } flag[0] = true;//此时p1应已执行完毕,应当禁止p1进入临界区 } } //当p1的flag为false时,p0可以立即进入临界区 /** critical section 临界区 **/ //当临界区执行完之后,turn设置为1,将临界区访问权限交换给p1 //并将p0的flag设置为false,释放临界区,使得p1可进入临界区 turn = 1; flag[0] = false; /** do otherthings **/ } } public void p1(){ while (true){ //设置p1的flag为true,同时检查p0的flag flag[1] = true; while (flag[0]){ //当临界区不可用时,判断当前临界区权限是否是p0 if (turn == 0){ //用于处理活锁问题 //当临界区权限是p0时,需要将p1设置为false,使得p0能进入临界区,用于处理死锁问题 flag[1] = false; //循环校验turn的权限(空自旋),直到p0执行完毕将权限交给p1 while (turn == 0){ /** do nothing 空自旋 **/ } flag[1] = true;//此时p0应已执行完毕,应当禁止p0进入临界区 } } //当p0的flag为false时,p1可以立即进入临界区 /** critical section 临界区 **/ //当临界区执行完之后,turn设置为0,将临界区访问权限交换给p0 //同时将p1的flag设置为false,释放临界区,使得p0可进入临界区 turn = 0; flag[1] = false; /** do otherthings **/ } } public static void main(){ /** 并发执行p0 p1 读者有兴趣可自己验证一下**/ } }
* peterson算法
/** * peterson算法比dekker算法更加简单出色而且很容易推广到多个线程 * 1.互斥保护验证:p0角度 * - 当po设置flag[0]=true,则p1不能进入临界区 * - 当p1已进入临界区,而flag[1]=true,p0不能进入临界区 * 2.避免相互阻塞验证:p0角度 * - 当p0在while循环中被阻塞,此时flag[1]=true且turn=1 * - 当flag[1]=false或turn=0,此时p0可以进入临界区 * 3.复杂度:该算法用简单的交替进入临界区的方式降低了并发互斥的复杂度 */ public class peterson { boolean[] flag = {false,false};//表明每个互斥线程的位置 int turn = 0;//解决同时发生的冲突 public void p0(){ while (true){ flag[0] = true; //每次都要显式设置turn=1并作为while空自旋条件,迫使其他线程也有进入临界区的机会 //这也是解决互斥的一个简洁方案,大家依次来,不能重复独占 turn = 1; while (flag[1] && turn == 1){ /** do nothing 空自旋**/ } /** critical section 临界区 **/ flag [0] = false; /** do otherthings **/ } } public void p1(){ while (true){ flag[1] = true; turn = 0; while (flag[0] && turn == 0){ /** do nothing 空自旋**/ } /** critical section 临界区 **/ flag [1] = false; /** do otherthings **/ } } public static void main(){ /** 并发执行p0 p1 读者有兴趣可自己验证一下**/ } }
■ 信号量
* 信号量的实现 (cas版)
/** * 设计原则:任何时候只能一个线程可以用wait和signal操作控制一个信号量 * 要求:semwait和semsingal操作必须作为原子原语实现 * semaphore信号量(以下都简称sem)的属性 * flag : 表示信号量是否可用,默认是0 * count: * 当>=0时,表示可执行semwait而不被挂起的线程数 * 当<0时,表示挂起在信号量的等待队列的线程数 * queue: 表示信号量相关联的等待队列,被阻塞的线程需放入该队列中 * ps:这里我们选用boolean版本的cas */ semwait(sem){ //当发现sem.flag不为0时,就自旋等待直到为0 //补充一点:忙等待可以保证队列操作的同步, //但由于wait和signal执行时间短,其开销还是很小的 while(!compare_and_swap(sem.flag,0,1)); sem.count--; if(sem.count < 0){ /** 该线程进入sem.queue等待队列中并被阻塞 **/ } sem.flag = 0; } semsignal(sem){ //当发现sem.flag不为0时,就自旋等待直到为0 while(!compare_and_swap(sem.flag,0,1)); sem.count++; if(sem.count <= 0){ /** 从sem.queue等待队列中移出,被移出的线程进入就绪队列**/ } sem.flag = 0; }
* 信号量实现互斥
final int n = /** 线程数 **/ int s = 1;//semaphore public void p(int i){ while(true){ semwait(s); /** critical zone 临界区 **/ semsignal(s); /** do other 其他部分 **/ } }
■ 消息传递 (大家比较熟悉的并发机制,当然有时间的话还会专门介绍一个mq,比如 rabbit mq )
1. 消息传递的概述
2. 消息结构
3. 消息通信情况
■ concurrent 包结构
■ concurrent 包整体类图
■ concurrent包实现机制
1. 底层-硬件指令支持
2. 中间层-基础数据结构+算法支持
3. 高层-并发类库支持
ps: 感谢 黄志鹏kira 的友情赞助,继续加油写出更棒的文章,后续文章会不断改进迭代~