上一节讲了引起并发问题原因中的可见性和有序性。 通过利用Java内存模型开发者可以很好的避开上述问题。 本节我们来探索剩下的一个引起并发问题的原因:原子性。
什么是原子性?
即一个或多个操作在CPU执行的过程中不被中断的特性。
互斥
一段代码同一时刻只有一个线程运行,称为互斥。 当我们能保证对共享变量的写是互斥的,就能保证原子性 了。
临界区
将一段需要互斥执行的代码称为临界区。
Java提供的锁技术:synchronized
1)当修饰静态方法时,锁定的是当前类Class对象
2)当修饰非静态方法时,锁定的是当前对象
死锁
一组互相竞争资源的线程因相互等待,导致永久阻塞的现象。
如何解决死锁问题
一旦发生死锁通常没有好的方法,只能重启应用。 重点是在预防死锁。
死锁发生的条件:
1)互斥,共享资源A和B只能被一个线程占用
2)占有且等待,线程一已经取得了资源A,在等待B资源的时候不会释放资源A
3)不可抢占,其他线程不能强行抢占线程A占有的资源
4)循环等待。 如A线程等待B占有的资源,线程B又等待A线程占有的资源
以上4个条件缺一不可,反过来意味着我们只要破坏其中一条即可解决死锁问题。
这其中第一条无法破除,因为用锁就是为了互斥性。
第二条占用且等待,我们可以一次性申请所有的资源
以转账为示例,将同时申请两个账户的锁操作放到临界区中,保证只能一个线程同时申请到两个锁资源,示例代码如下:
class Account {
private static final Allocate allocate = new Allocate();
private int balance;
public Account(int balance) {
this.balance = balance;
}
void transfer(Account target, int amount) {
while (allocate.apply(this, target))
;
try {
synchronized (this) {
synchronized (target) {
if (this.balance >= amount) {
this.balance -= amount;
target.balance += amount;
}
}
}
} finally {
allocate.free(this, target);
}
}
}
class Allocate {
private List<Object> locks = new ArrayList<>();
synchronized boolean apply(Object lock1, Object lock2) {
if (locks.contains(lock1) || locks.contains(lock2)) {
return false;
}
locks.add(lock1);
locks.add(lock2);
return true;
}
synchronized void free(Object lock1, Object lock2) {
locks.remove(lock1);
locks.remove(lock2);
}
}
第三条不可抢占,当占用部分资源的线程在进一步申请其他资源时,如果申请不到,可以主动放弃它已占有的资源
这一点默认synchronized锁是做不到的。即语言层面无法做到,但SDK可以,java.util.concurrent并发包下提供的Lock类可以解决此类问题。
第四条循环等待,可以靠按序申请资源来预防。
这一点比较好理解,即保持不同线程加锁的顺序一致,能避免互相持有对方锁的情况。
小结
当使用互斥锁时,要分析多个资源之间的关系,如果没关系,每个资源一把锁即可,如果资源之间有关系,就要选择一个粒度更大的锁,能覆盖所有相关的资源。
当使用细粒度锁在锁定多个资源时,要注意死锁的问题,能及时识别风险很重要。破除死锁即可用上面列出的三个方法,但使用不同方法时页需要比较优劣,上面转账的示例采用了破坏占用且等待的方法,实际上就不如破除不可抢占和破除循环等待简单高效。