解决什么问题
互斥锁解决了并发程序中的原子性问题
禁止CPU中断
- 原子性:一个或多个操作在CPU执行的过程中不被中断的特性
- 原子性问题点源头是线程切换,而操作系统依赖CPU中断来实现线程切换的
- 单核时代,禁止CPU中断就能禁止线程切换
- 同一时刻,只有一个线程执行,禁止CPU中断,意味着操作系统不会重新调度线程,也就禁止了线程切换
- 获得CPU使用权的线程可以不间断地执行
- 多核时代
- 同一时刻,有可能有两个线程同时在执行,一个线程执行在CPU1上,一个线程执行在CPU2上
- 此时禁止CPU中断,只能保证CPU上的线程不间断执行,但并不能保证同一时刻只有一个线程执行
- 互斥:同一时刻只有一个线程执行
- 如果能保证对共享变量的修改是互斥的,无论是单核CPU还是多核CPU,都能保证原子性
简易锁模型
- 临界区:一段需要互斥执行的代码
- 线程在进入临界区之前,首先尝试加锁lock()
- 如果成功,则进入临界区,此时该线程只有锁
- 如果不成功就等待,直到持有锁的线程解锁
- 持有锁的线程执行完临界区的代码后,执行解锁unlock()
锁和资源
synchronized
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class X { synchronized void foo() { }
synchronized static void bar() { }
Object obj = new Object();
void baz() { synchronized (obj) { } } }
|
- 锁是一种通用的技术方案,Java语言提供的锁实现:
synchronized
- Java编译器会在synchronized修饰的方法或代码块前后自动加上lock()和unlock()
- 当synchronized修饰静态方法时,锁定的是_当前类的Class对象_
- 当synchronized修饰实例方法时,锁定的是_当前实例对象this_
count += 1
1 2 3 4 5 6 7 8 9 10 11
| public class SafeCalc { private long value = 0L;
public long get() { return value; }
public synchronized void addOne() { value += 1; } }
|
- 原子性
- synchronized修饰的临界区是互斥的
- 因此无论是单核CPU还是多核CPU,只有一个线程能够执行addOne,能保证原子性
- 可见性
- 管程中锁的规则:对一个锁的解锁Happens-Before于后续对这个锁的加锁
- 结合Happens-Before的传递性原则,易得下面的结论
- 前一线程在临界区修改的共享变量(该操作在解锁之前),对后续进入临界区(该操作在加锁之后)的线程是可见的
- 因此,多个线程同时执行addOne,可以保证可见性,即假如有N个线程并发调用addOne,最终结果一定是N
- get
- 执行addOne方法后,value的值对get方法的可见性是无法保证的
- 解决方案:get方法也用synchronized修饰
锁模型
- get()和addOne()都需要访问资源value,而资源value是用this这把锁来保护的
- 线程要进入临界区get()和addOne(),必须先获得this这把锁,因此get()和addOne()也是互斥的
锁与受保护资源
受保护资源和锁之间合理的关联关系应该是N:1
的关系
不同的锁
1 2 3 4 5 6 7 8 9 10 11
| public class SafeCalc { private static long value = 0L;
public long get() { return value; }
public synchronized static void addOne() { value += 1; } }
|
- 用两个锁(this和SafeCalc.class)保护同一个资源value(静态变量)
- 临界区get()和addOne()是用两个锁来保护的,因此两个临界区没有互斥关系
- 临界区addOne()对value的修改对临界区get()也没有可见性保证,因此会导致并发问题
多个资源
可以用同一把锁保护多个资源,但不能用多把锁保护一个资源
无关联资源
- 无关联资源
- 针对账户余额(余额是一种资源)的取款操作
- 针对账户密码(密码是一种资源)的更改操作
- 可以为账户余额和账户密码分配不同的锁来解决并发问题,不同的资源用不同的锁来保护
- 也可以用同一把锁保护多个资源,例如可以用this这把锁保护账户余额和账户密码,但这样性能太差
- 用不同的锁对受保护资源进行精细化管理,能够提升性能,这种锁称为细粒度锁
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
| public class Account { private final Object balLock = new Object(); private final Object pwLock = new Object(); private Integer balance; private String password;
public void withdraw(Integer amt) { synchronized (balLock) { if (balance > amt) { balance -= amt; } } }
public Integer getBalance() { synchronized (balLock) { return balance; } }
public void updatePassword(String pw) { synchronized (pwLock) { password = pw; } }
public String getPassword() { synchronized (pwLock) { return password; } } }
|
有关联资源
账户A的余额和账户B的余额是有关联关系的,需要保证转账操作没有并发问题
1 2 3 4 5 6 7 8 9 10 11 12
| public class Account { private int balance;
public void transfer(Account target, int amt) { if (balance > amt) { balance -= amt; target.balance += amt; } } }
|
synchronized this
1 2 3 4 5 6 7 8 9 10 11 12
| public class Account { private int balance;
public synchronized void transfer(Account target, int amt) { if (balance > amt) { balance -= amt; target.balance += amt; } } }
|
- 上述代码中,临界区内有两个资源,分别是
this.balance
和target.balance
- this这把锁可以保护自己的余额
this.balance
,但无法保护他人的余额target.balance
- 场景
- 假设有A、B、C三个账户,余额都是200
- 用两个线程分别执行两个转账操作:线程1执行账户A给账户B转账100,线程2执行账户B给账户C转账100
- 预期结果:账户A余额为100,账户B余额为200,账户C余额为300
- 假设线程1和线程2分别在两颗CPU上同时执行,实际上两个线程并不互斥
- 线程1锁定的是账户A的实例A.this,线程2锁定的是账户B的实例B.this,因此,两个线程可以同时进入临界区transfer
- 线程1和线程2刚开始执行时都有可能读到账户B的余额是200,导致账户B最终的余额是300或100,但绝不会是200
- 300:线程2写B.balance -> 线程1写B.balance
- 100:线程1写B.balance -> 线程2写B.balance
同一把锁
- 条件:_锁能覆盖所有受保护的资源(粒度更大)_
- 方案1:让所有对象都持有一个唯一性的对象,该对象在创建Account时传入
- 方案2:Class对象作为共享的锁
- Account.class是所有Account实例共享的,并且Class对象时JVM在加载类时创建的,能保证唯一性,代码也更简单
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
| public class Account { private Object lock; private int balance;
private Account() { }
public Account(Object lock) { this.lock = lock; }
public void transfer(Account target, int amt) { synchronized (lock) { if (balance > amt) { balance -= amt; target.balance += amt; } } } }
public class Account { private int balance;
public void transfer(Account target, int amt) { synchronized (Account.class) { if (balance > amt) { balance -= amt; target.balance += amt; } } } }
|
原子性的本质
- 原子性的外在表现:不可分割
- 原子性的本质:_多个资源之间有一致性的要求,操作的中间状态对外不可见_
- 中间状态:例如在32位机器上写long型变量,转账操作(账户A减少100,但账户B还未增加100)
参考资料
Java并发编程实战