所谓的双线程锁指的是这种锁适用于俩个线程运行的前提下. 下面我们依次给出了三种双线程锁解决方案:
双线程算法遵循以下俩点约定:
- 线程标志为
0
或者1
. 若当前线程调用者的标志为i,则另一方的调用者为1 - i - 通过ThreadId.get()获取自己的标志
- 互斥: 俩个线程的临界区是没有重叠的,那么我们撑这俩个临界区是互斥的.
- 无死锁: 如果一个线程尝试获得一个锁,那么总会成功获得这个锁.
- 无饥饿:每一个尝试获得锁的线程都能成功. 当线程调用一个锁方法的时候,这个方法立即返回,我们便称这个锁是无饥饿的.
LockOne
1 | class LockOne { |
假设线程A对应flag[0]标志,线程B对应flag[1]标志,那么我们得出下面这一个流程:
write_A(flag[0] = true) -> read_A(flag[1] == false) -> CS_A
这段话的意思是线程A将flag[0]
的值置为true然后读取flag[1]
的值,这个过程称为CS_A
事件write_B(flag[1] = true) -> read_B(flag[0] == false) -> CS_B
这段话的意思是线程B将flag[1]
的值置为true然后读取flag[0]
的值,这个过程称为CS_B
事件
我们验证一下LockOne算法是否满足互斥
1 | 假设这个算法不是互斥的,也就是无法得到`CS_A -> CS_B`且`CS_B -> CS_A`. |
LockOne算法满足了互斥,但是如果俩个线程并发执行的话,就会进入死锁,同样我们来证明一下
1 | 假设 |
至于说为什么要使用volatile
关键字,这是为了保证flags
变量的内存可见性,因为Java会将这段代码
1 | while(flag[j]) {} |
编译后的代码进行了提升优化,加上volatile
关键字,就是告诉编译器,不要提升优化我的代码.
LockTwo
1 | class LockTwo { |
同样我们假设有俩个事件发生
write_A(lock = 1) -> read_A(lock == 1) -> CS_A
write_B(lock = 2) -> read_B(lock == 2) -> CS_B
很明显任何线程调用加锁操作都会造成死循环. 但是,如果锁调用交叉调用的话直到A线程释放锁,B线程就一直在阻塞着. 因此只要这俩个事件并发执行就能完成互斥要求.1
write_A(lock = 1) -> write_B(lock = 2) -> read_A(lock == 2) -> read_B(lock == 2)
Peterson
算法实现
1 | class Peterson { |
同样的我们看看俩个线程依次调用锁过程(假设线程A对应flag[0]标志,线程B对应flag[1]标志):
1 | write_A(flag[1] = true) -> write_A(lock = 0) -> read_A(flag[0] == false) -> read_A(lock == 0) -> CS_A |
好,首先我们看一下
CS_A
先于CS_B
事件执行的话,那么B线程会进入锁等待.
CS_A
和CS_B
事件并发执行我们分俩种情况分析:
1 | write_A(flag[1] = true) -> write_A(lock = 0) -> write_B(flag[0] = true) -> write_B(lock = 1) -> read_A(flag[1] == true) -> read_A(lock == 0) -> read_B(flag[1] == true) -> read_B(lock == 1) |
同样的A线程事件先于B线程事件,我们看到A线程并没有进入锁等待,而是B线程进入了锁等待
1 | write_A(flag[1] = true) -> write_A(lock = 0) -> write_B(flag[0] = true) -> write_B(lock = 1) -> read_B(flag[1] == true) -> read_B(lock == 1) -> read_A(flag[1] == true) -> read_A(lock == 0) |
我们发现这个锁算法仍然是有问题的.