文章目录
一.原子问题
在并发编程Bug源头中介绍过,原子问题的源头是线程切换,解决方案禁用线程切换。
CPU控制线程切换,无论单核CPU还是多核CPU,保证同一时刻只有一个线程执行,称为互斥,就能够保证对共享变量的修改时互斥,就能保证原子性。
二.锁模型
互斥的解决方案是锁,把一段需要互斥执行的代码称为临界区。
这个锁模型展示的是锁和锁要保护的资源是有关系,很多并发Bug是因为关联关系,产生奇怪的问题。
三.解决方案
2.1 synchronized
锁是一种通用的技术方案,Java语言提供的synchronized 关键字,就是锁的一种实现。
//Java 编译器会在 synchronized 修饰的方法或代码块前后自动加上加锁 //lock() 和解锁 unlock(),这样做的好处就是加锁 lock() 和解锁 //unlock() 一定是成对出现的。
class X {
// 修饰非静态方法
synchronized void foo() {
// 临界区
}
// 修饰静态方法
synchronized static void bar() {
// 临界区
}
// 修饰代码块
Object obj = new Object();
void baz() {
synchronized(obj) {
// 临界区
}
}
}
Java隐式规则
- 当synchronized 修饰静态方法时候,锁定的是当前类Class对象,在上面的例子中就是Class X;
- 当synchronized 修饰非静态方法时候,锁定的是当前实例对象this。
class SafeCalc {
long value = 0L;
synchronized long get() {
return value;
}
synchronized void addOne() {
value += 1;
}
}
get() 方法和 addOne() 方法都需要访问 value 这个受保护的资源,这个资源用 this 这把锁来保护。线程要进入临界区 get() 和 addOne(),必须先获得 this 这把锁,这样 get() 和 addOne() 也是互斥的。
四.保护资源
4.1 关系简介
受保护资源和锁之间关系是N:1的关系,多把锁来保护同一个资源,在并发领域不行,同一把锁来保护多个资源是可以的。
4.2 保护没有关联关系的多个资源
账户类 Account 有两个成员变量,分别是账户余额 balance 和账户密码 password。取款 withdraw() 和查看余额 getBalance() 操作会访问账户余额 balance,我们创建一个 final 对象 balLock 作为锁(类比球赛门票);而更改密码 updatePassword() 和查看密码 getPassword() 操作会修改账户密码 password,我们创建一个 final 对象 pwLock 作为锁(类比电影票)。不同的资源用不同的锁保护,各自管各自的,很简单。
class Account {
// 锁:保护账户余额
private final Object balLock
= new Object();
// 账户余额
private Integer balance;
// 锁:保护账户密码
private final Object pwLock
= new Object();
// 账户密码
private String password;
// 取款
void withdraw(Integer amt) {
synchronized(balLock) {
if (this.balance > amt){
this.balance -= amt;
}
}
}
// 查看余额
Integer getBalance() {
synchronized(balLock) {
return balance;
}
}
// 更改密码
void updatePassword(String pw){
synchronized(pwLock) {
this.password = pw;
}
}
// 查看密码
String getPassword() {
synchronized(pwLock) {
return password;
}
}
}
4.3 保护有关联关系的多个资源
例如银行业务里面的转账操作,账户 A 减少 100 元,账户 B 增加 100 元。这两个账户就是有关联关系的。
用 Account.class 作为共享的锁(有问题,后续解释)
class Account {
private int balance;
// 转账
void transfer(Account target, int amt){
synchronized(Account.class) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
该解决方案,存在性能问题,因为把所有银行业务转账行为,都变成串行了。
现实世界里,账户转账操作是支持并发的,而且绝对是真正的并行,银行所有的窗口都可以做转账操作。只要我们能仿照现实世界做转账操作,串行的问题就解决了。
并行解决银行转账问题
把问题具象化,假设柜员在拿账本的时候,可能遇到以下三种情况:
- 文件架上恰好有转出账本和转入账本,那就同时拿走;
- 如果文件架上只有转出账本和转入账本之一,那这个柜员就先把文件架上有的账本拿到手,同时等着其他柜员把另外一个账本送回来;
- 转出账本和转入账本都没有,那这个柜员就等着两个账本都被送回来。
@Data
public class AccountClassLock3 {
private int balance;
public AccountClassLock3(int balance) {
this.balance = balance;
}
public void transfer(AccountClassLock3 target,int amt){
//锁定转出账户
synchronized (this){
//锁定转入账户
synchronized (target){
if(this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000; i++) {
AccountClassLock3 a = new AccountClassLock3(200);
AccountClassLock3 b = new AccountClassLock3(200);
AccountClassLock3 c = new AccountClassLock3(200);
Thread t1 = new Thread(()->{
a.transfer(b,100);
});
Thread t2 = new Thread(()->{
b.transfer(c,100);
});
t1.start();
t2.start();
t2.join();
System.out.println(a.getBalance());
System.out.println(b.getBalance());
System.out.println(c.getBalance());
System.out.println("---------------");
}
}
}
这样的锁时细粒度锁,使用细粒度锁可以提高并行度,是优化性能的一个重要手段,但是使用细粒度锁是有代价的,这个代价就是可能会导致死锁。
死锁实验
现实世界里的一种特殊场景。如果有客户找柜员张三做个转账业务:账户 A 转账户 B 100 元,此时另一个客户找柜员李四也做个转账业务:账户 B 转账户 A 100 元,于是张三和李四同时都去文件架上拿账本,这时候有可能凑巧张三拿到了账本 A,李四拿到了账本 B。张三拿到账本 A 后就等着账本 B(账本 B 已经被李四拿走),而李四拿到账本 B 后就等着账本 A(账本 A 已经被张三拿走),他们要等多久呢?他们会永远等待下去…因为张三不会把账本 A 送回去,李四也不会把账本 B 送回去。我们姑且称为死等吧。
@Data
public class AccountClassLock3 {
private int balance;
public AccountClassLock3(int balance) {
this.balance = balance;
}
public void transfer(AccountClassLock3 target,int amt){
//锁定转出账户
synchronized (this){
//锁定转入账户
synchronized (target){
if(this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100000; i++) {
AccountClassLock3 a = new AccountClassLock3(200);
AccountClassLock3 b = new AccountClassLock3(200);
AccountClassLock3 c = new AccountClassLock3(200);
Thread t1 = new Thread(()->{
a.transfer(b,100);
});
Thread t2 = new Thread(()->{
b.transfer(a,100);
});
t1.start();
t2.start();
t2.join();
System.out.println(a.getBalance());
System.out.println(b.getBalance());
//System.out.println(c.getBalance());
System.out.println("---------------");
}
}
}
这是示例运行情况,dump记录,线程正在WAITING,解决方案后续文章介绍。
4.4 小结
原子性本质,不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性要求,操作的中间状态对外不可见。
参考
《Java并发编程实战》
公众号
微信公众号(bigdata_limeng)