3互斥锁
原子性
原子性问题的源头是线程切换
原子性外在表现为不可分割,本质是多个资源间有一致性的要求,操作的中间状态对外不可见。
如何解决原子性问题?
禁用线程切换不就解决了吗?操作系统线程切换依赖CPU中断,所以禁用CPU发生中断就能禁用线程切换。
禁用中断不能保证多线程原子性
单CPU禁止中断禁止切换线程单线程连续执行,可保证原子性。
多CPU只能保证CPU上线程连续执行,不能保证同一时刻只有一个线程执行,如果两个线程同时写long型变量高32位,就可能出现诡异BUG。
互斥可保证共享变量修改的原子性
同一时刻只有一个线程执行成为互斥。
锁
锁模型
- 创建保护资源R的锁:LR
- 加锁操作:lock(LR)
- 临界区:一段代码
- 受保护资源:R
- 解锁操作 :unlock(LR)
sychronized
修饰静态方法锁住this.class
修饰实例方法锁住当前对象this
Happens-before规则:对一个锁的解锁Happens-Before于后续对此锁的加锁,指的是前一个线程的解锁操作对后一个线程的加锁操作可见,传递性得出前一个线程对共享变量的修改对后一个线程可见。所以可以解决并发Count++问题。
细粒度锁
细粒度的锁可提高并行度,是重要优化手段
细粒度锁导致死锁(竞争资源的线程因互相等待,导致永久阻塞的现象)
死锁
发生死锁的四个条件
- 互斥,共享资源X和Y只能被一个线程占用;
- 占有且等待,线程T1已经取得共享资源 X,在等待共享资源Y的时候,不释放共享资源 X;
- 不可抢占,其他线程不能强行抢占线程T1占有的资源;
- 循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待。
避免死锁
破坏其中一个即可避免死锁
锁本身互斥无法破坏,其他三个条件如何做呢?
- 占用切等待,一次性申请所有资源,则不再存在等待
- 不可抢占,申请其他资源时失败可主动释放
- 循环等待,按序申请资源
破坏占用切等待
一次性申请资源,需声明一个角色管理自愿申请(单例),如下转账操作
1 | //转账前申请到两个账户的锁 |
存在问题
那在高并发下synchronized(Account.class)会使得所有转账串行化,使用apply方法能提高转账的吞吐量。
但apply方法也有问题,在同一个账户转账操作并发量高的场景下,apply方法频繁失败,转账的线程会不断的阻塞唤醒阻塞唤醒,开销大。
也许应该改进一下由Allocator负责在有资源的情况下唤醒调用apply的线程?
等待通知机制
wait/notify/notifyAll,持有锁才可使用,且是同一把锁。
notify代表条件曾经满足,只能保证在通知时间点条件满足。而被通知线程的执行时间点和通知的时间点基本上不会重合,所以当线程执行的时候,很可能条件已经不满足了(保不齐有其他线程插队)。
notify有风险,可能导致某些线程永远不会被通知,尽量使用 notifyAll
如:
资源 A、B、C、D,线程1申请到AB,线程2申请到CD,此时线程3申请AB,线程4申请CD,线程3,4进入等待队列。
之后线程1归还AB,使用notify通知线程等待队列的线程,可能唤醒线程4,此时4继续等待,而3再也没有机会被唤醒了
1 | while(条件不满足) { |
改进后
1 | class Allocator { |
sleep,wait区别?
不同点
- wait 释放锁,sleep不释放
- wait只能在持有锁时可用
- wait无参调用进入waiting状态,需唤醒
- sleep(1000L),wait(1000L)进入time_waiting状态,不需唤醒
相同点:都会让出CPU
破坏不可抢占
破坏循环等待
需要对资源进行排序,然后按序申请资源
如id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请。
①~⑥处的代码对转出账户(this)和转入账户(target)排序,然后按照序号从小到大的顺序锁定账户
1 | class Account { |
预防死锁成本需要评估,选择成本最低的方案。
破坏占用且等待条件,锁了所有的账户,且用了死循环,不过好在 apply() 这个方法基本不耗时。
问题
如何判断多线程的阻塞导致的问题呢?有什么工具吗?
可以用top命令查看Java线程的cpu利用率,用jstack来dump线程。
开发环境可以用java visualvm查看线程执行情况