PART1:异步模式之生产者与消费者
- 生产者与消费者问题,到底是个啥问题嘛?其实就是:
- 生产者在生成数据后,放在一个缓冲区中,消费者从缓冲区取出数据处理,
任何时刻,只能有一个生产者或消费者可以访问缓冲区
,由此产生的问题就是:- 任何时刻只能有一个线程操作缓冲区,说明操作缓冲区是临界代码,
需要互斥
,你不保护人家生产者还没有生产出来消费者拿啥消费。 缓冲区空时,消费者必须等待生产者生成数据;缓冲区满时,生产者必须等待消费者取出数据。说明生产者和消费者需要同步
。
- 任何时刻只能有一个线程操作缓冲区,说明操作缓冲区是临界代码,
- 其实就是打手去买包子吃,要是打手们和包子铺不协商好,包子做多了或者做少了不够吃就相当于坏事了:
- 打手就是消费者,用钱去包子铺买包子吃
- 包子铺就是生产者
- 知道了咱们需要通过互斥保护一个人操作的原子性,需要通过同步来保护多个人操作之间的顺序性:那咱们怎样实现这俩条件呢?我们需要三个信号量:
- 互斥信号量 mutex:用于互斥访问缓冲区,初始化值为 1
- 资源信号量 fullBuffers:用于消费者询问缓冲区是否有数据,有数据则读取数据,初始化值为 0(表明缓冲区一开始为空)【消费者问缓冲区大管家,你那里有包子了没,有包子我就拿包子吃呀】
- 资源信号量 emptyBuffers:用于生产者询问缓冲区是否有空位,有空位则生成数据,初始化值为 n (缓冲区大小)【生产者问大管家缓冲区,你那里还有空的放包子的笼屉没,有我就不歇了,要起来干活了】
- 与下面的保护性暂停中的GuardObject不同,不需要产生结果和消费结果的线程一一对应
消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是FIFO,并且消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
JDK中各种阻塞队列,采用的就是这种模式
- 线程之间的通信问题约等于生产者消费者问题,面试这样问。相当于A线程和B线程同时访问变量nums,然后A实现nums+1,B实现nums-1,你如果A线程和B线程不相互通信的话是不可能规规矩矩实现nums+1然后nums-1的,指不定哪一步就出错了
- 或者说的具体一点:怎样解决生产者消费者中容易出现的问题呢
- 解决方法一:找个中介,替包子铺和打手双方跑腿
- 解决方法二:信号灯法,设置一个标志位解决
- 解决方法三:用synchronized实现多线程同步,加个锁呗(咱们可以用(wait()方法+notify()方法),输出一些打印结果做个小实验)
会有个问题,
- 解决方法四:“用Lock模板圈实现多线程同步(await()方法+signal()方法),输出正常打印结果,不管两个线程,四个线程,八个线程…这里依旧正确,并且可以控制线程有序执行”
- 啥叫啥叫Lock模板圈呢,下面就是:先回顾一下Lock:
然后下面是举的例子,节选,了解一下Lock模板圈的用法
用condition的目的是:
- 啥叫啥叫Lock模板圈呢,下面就是:先回顾一下Lock:
- 解决方法一:找个中介,替包子铺和打手双方跑腿
- 生产者在生成数据后,放在一个缓冲区中,消费者从缓冲区取出数据处理,
- 生产者与消费者问题延伸之哲学家就餐问题
- 这个问题又到底是个啥问题呢?我觉得就是因为服务员没礼貌不懂规矩,你要是放五双筷子而不是五只筷子不就没这事了嘛
- 5 个老大哥哲学家,闲着没事做,围绕着一张圆桌吃面;
巧就巧在,这个桌子只有 5 支筷子,每两个哲学家之间放一支筷子
;哲学家围在一起先思考,思考中途饿了就会想进餐;这些哲学家需要拿到左右两边的筷子才能进餐;吃完后,会把两支筷子分别放回左右的原处,继续思考;所以要是不控制就会出现有人永远拿不到筷子也就吃不到饭
。
- 5 个老大哥哲学家,闲着没事做,围绕着一张圆桌吃面;
- 那问题出现了如何解决问题呢?
- 方法一:
用信号量的方式,也就是 PV 操作来尝试解决它,拿起筷子就相当于用 P 操作
,代表有筷子那就直接用,没有筷子时就等待其他哲学家放回筷子。方案一的问题在于,会出现所有哲学家同时拿左边筷子的可能性这种解法存在一个极端的问题:假设五位哲学家同时拿起左边的筷子,桌面上就没有筷子了, 这样就没有人能够拿到他们右边的筷子,也就说每一位哲学家都会阻塞了,很明显这发生了死锁的现象。
- 方法二:
方案一会发生同时竞争左边筷子导致死锁的现象,那么我们就在拿筷子前,加个互斥信号量
。互斥信号量的作用就在于,只要有一个哲学家进入了临界区,也就是准备要拿筷子时,其他哲学家都不能动,只有这位哲学家用完筷子了,才能轮到下一个哲学家进餐。但是这种方法二只能有一位哲学家,按道理是能可以有两个哲学家同时进餐的,所以从效率角度上,这不是最好的解决方案。 - 方法三:我们可以避免哲学家可以同时拿左边的筷子,采用分支结构,根据哲学家的编号的不同,而采取不同的动作。
即让偶数编号的哲学家先拿左边的筷子后拿右边的筷子,奇数编号的哲学家先拿右边的筷子后拿左边的筷子
。方案三即不会出现死锁,但是也可以两人同时进餐。 - 方案四:
我们用一个数组 state 来记录每一位哲学家的三个状态,分别是在进餐状态、思考状态、饥饿状态(正在试图拿筷子)。那么,一个哲学家只有在两个邻居都没有进餐时,才可以进入进餐状态
。方案四同样不会出现死锁,也可以两人同时进餐
- 方法一:
- 那哲学家进餐问题对我们有啥用呢,知道这个东西能干啥:
哲学家进餐问题对于互斥访问有限的竞争问题(如 I/O 设备)一类的建模过程十分有用
。
- 这个问题又到底是个啥问题呢?我觉得就是因为服务员没礼貌不懂规矩,你要是放五双筷子而不是五只筷子不就没这事了嘛
- 生产者与消费者问题延伸之读者写者问题:
- 首先说明,学了这货有啥用:
著名的问题是读者-写者【读者只会读取数据,不会修改数据,而写者即可以读也可以修改数据】,它为数据库访问建立了一个模型
- 读者写者,问题是啥呀:数据库哪里,肯定就是脏读、幻读、不可重复读呗
- 读-读允许:同一时刻,允许多个读者同时读
- 读-写互斥:没有写者时读者才能读,没有读者时写者才能写
- 写-写互斥:没有其他写者时,写者才能写
- 咋解决这个问题呢:
- 方法一:使用信号量的方法解决:读者优先策略【读者优先中只要后续有读者到达,读者就可以进入读者队列, 而写者必须等待,直到没有读者到达。没有读者到达会导致读者队列为空,即 rCount==0,此时写者才可以进入临界区执行写操作。】
- 信号量 wMutex:控制写操作的互斥信号量,初始值为 1
- 读者计数 rCount:正在进行读操作的读者个数,初始化为 0
- 信号量 rCountMutex:控制对 rCount 读者计数器的互斥修改,初始值为 1
- 方法二:只要有写者准备要写入,写者应尽快执行写操作,后来的读者就必须阻塞。如果有写者持续不断写入,则读者就处于饥饿:写者优先策略
- 信号量 rMutex:控制读者进入的互斥信号量,初始值为 1
- 这里 rMutex 的作用,开始有多个读者读数据,它们全部进入读者队列,此时来了一个写者,执行了 P(rMutex) 之后,后续的读者由于阻塞在 rMutex 上,都不能再进入读者队列,而写者到来,则可以全部进入写者队列,因此保证了写者优先。同时,第一个写者执行了 P(rMutex) 之后,也不能马上开始写,必须等到所有进入读者队列的读者都执行完读操作,通过 V(wDataMutex) 唤醒写者的写操作。
- 信号量 wDataMutex:控制写者写操作的互斥信号量,初始值为 1
- 写者计数 wCount:记录写者数量,初始值为 0
- 信号量 wCountMutex:控制 wCount 互斥修改,初始值为 1
- 信号量 rMutex:控制读者进入的互斥信号量,初始值为 1
- 方法三:公平策略。【开始来了一些读者读数据,它们全部进入读者队列,此时来了一个写者,执行 P(falg) 操作,使得后续到来的读者都阻塞在 flag 上,不能进入读者队列,这会使得读者队列逐渐为空,即 rCount 减为 0。这个写者也不能立马开始写(因为此时读者队列不为空),会阻塞在信号量 wDataMutex 上,读者队列中的读者全部读取结束后,最后一个读者进程执行 V(wDataMutex),唤醒刚才的写者,写者则继续开始进行写操作】
- 优先级相同
- 写者、读者互斥访问
- 只能一个写者访问临界区
- 可以有多个读者同时访问临界资源
- 方法一:使用信号量的方法解决:读者优先策略【读者优先中只要后续有读者到达,读者就可以进入读者队列, 而写者必须等待,直到没有读者到达。没有读者到达会导致读者队列为空,即 rCount==0,此时写者才可以进入临界区执行写操作。】
- 首先说明,学了这货有啥用:
PART2-1:同步模式之保护性暂停
- 同步模式之保护性暂停:Guarded Suspension,
用在一个线程等待另一个线程的执行结果的情境下
- 有一个结果需要从一个线程传递到另一个线程, 让他们关联同一个GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见上面生产者消费者)
JDK中,join 的实现、Future 的实现,采用的就是此同步模式
- 同步模式之保护性暂停的实现
- 增加延时:get方法加一个timeout参数表示要等待多久,public object get(long timeout)
- 同步模式之保护性暂停的实现
- 因为要等待另一方的结果,因此归类到同步模式
- 有一个结果需要从一个线程传递到另一个线程, 让他们关联同一个GuardedObject
PART2-2:同步模式之Balking
- Balking (犹豫) 模式用在一个线程发现另一个线程或本线程已经做了某-件相同的事,那么本线程就无需再做了,直接结束返回
PART3-1:学了半天多线程,如果我要实现先打印2再打印1
这种多线程运行顺序,有几种方法:
- 方法一:
- 方法二:ReentrantLock中的await()和signal()
- 方法三:LockSupport的park()和unpark()方法
PART3-2:实现多线程交替输出:
- 方法一:用整数标记,帮我们一把
- 方法二:用Reentrant
- park和unpark
巨人的肩膀:
B战各位老师
java并发编程实战
java并发线程之美