3互斥锁

原子性

原子性问题的源头是线程切换
原子性外在表现为不可分割,本质是多个资源间有一致性的要求,操作的中间状态对外不可见。

如何解决原子性问题?
禁用线程切换不就解决了吗?操作系统线程切换依赖CPU中断,所以禁用CPU发生中断就能禁用线程切换。

禁用中断不能保证多线程原子性
单CPU禁止中断禁止切换线程单线程连续执行,可保证原子性。
多CPU只能保证CPU上线程连续执行,不能保证同一时刻只有一个线程执行,如果两个线程同时写long型变量高32位,就可能出现诡异BUG。

互斥可保证共享变量修改的原子性
同一时刻只有一个线程执行成为互斥。

锁模型

  1. 创建保护资源R的锁:LR
  2. 加锁操作:lock(LR)
  3. 临界区:一段代码
    • 受保护资源:R
  4. 解锁操作 :unlock(LR)

sychronized
修饰静态方法锁住this.class
修饰实例方法锁住当前对象this

Happens-before规则:对一个锁的解锁Happens-Before于后续对此锁的加锁,指的是前一个线程的解锁操作对后一个线程的加锁操作可见,传递性得出前一个线程对共享变量的修改对后一个线程可见。所以可以解决并发Count++问题。

细粒度锁
细粒度的锁可提高并行度,是重要优化手段
细粒度锁导致死锁(竞争资源的线程因互相等待,导致永久阻塞的现象)

死锁

发生死锁的四个条件

  1. 互斥,共享资源X和Y只能被一个线程占用;
  2. 占有且等待,线程T1已经取得共享资源 X,在等待共享资源Y的时候,不释放共享资源 X;
  3. 不可抢占,其他线程不能强行抢占线程T1占有的资源;
  4. 循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待。

避免死锁

破坏其中一个即可避免死锁
锁本身互斥无法破坏,其他三个条件如何做呢?

  1. 占用切等待,一次性申请所有资源,则不再存在等待
  2. 不可抢占,申请其他资源时失败可主动释放
  3. 循环等待,按序申请资源

破坏占用切等待

一次性申请资源,需声明一个角色管理自愿申请(单例),如下转账操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//转账前申请到两个账户的锁
class Allocator {
private List<Object> als =
new ArrayList<>();
// 一次性申请所有资源
synchronized boolean apply(Object from, Object to){
if(als.contains(from)||als.contains(to)){
return false;
} else {
als.add(from);
als.add(to);
}
return true;
}
// 归还资源
synchronized void free(
Object from, Object to){
als.remove(from);
als.remove(to);
}
}


class Account {
// actr 应该为单例
private Allocator actr;
private int balance;
// 转账
void transfer(Account target, int amt){
// 一次性申请转出账户和转入账户,直到成功
while(!actr.apply(this, target))

try{
// 锁定转出账户
synchronized(this){
// 锁定转入账户
synchronized(target){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
} finally {
actr.free(this, target)
}
}
}

存在问题
那在高并发下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
2
3
while(条件不满足) {
wait();
}

改进后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Allocator {
private List<Object> als;
// 一次性申请所有资源
synchronized void apply(Object from, Object to){
// 经典写法
while(als.contains(from)||als.contains(to)){
try{
wait();
}catch(Exception e){}
}

als.add(from);
als.add(to);
}
// 归还资源
synchronized void free(
Object from, Object to){
als.remove(from);
als.remove(to);
notifyAll();
}
}

sleep,wait区别?
不同点

  • wait 释放锁,sleep不释放
  • wait只能在持有锁时可用
  • wait无参调用进入waiting状态,需唤醒
  • sleep(1000L),wait(1000L)进入time_waiting状态,不需唤醒

相同点:都会让出CPU

破坏不可抢占

破坏循环等待

需要对资源进行排序,然后按序申请资源
如id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请。
①~⑥处的代码对转出账户(this)和转入账户(target)排序,然后按照序号从小到大的顺序锁定账户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Account {
private int id;
private int balance;
// 转账
void transfer(Account target, int amt){
Account left = this ①
Account right = target; ②
if (this.id > target.id) { ③
left = target; ④
right = this; ⑤
} ⑥
// 锁定序号小的账户
synchronized(left){
// 锁定序号大的账户
synchronized(right){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
}
}

预防死锁成本需要评估,选择成本最低的方案。
破坏占用且等待条件,锁了所有的账户,且用了死循环,不过好在 apply() 这个方法基本不耗时。

问题

如何判断多线程的阻塞导致的问题呢?有什么工具吗?
可以用top命令查看Java线程的cpu利用率,用jstack来dump线程。
开发环境可以用java visualvm查看线程执行情况