原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性
线程切换是原子性问题的源头
在单核机器上,可以通过禁止线程切换来保证一个线程一直持有 CPU 使用权,多次操作具有原子性
在多核机器上,多线程可以同时执行,禁止线程切换并不能保证原子性
互斥锁模型
同一时刻只有一个线程执行,即互斥
需要注意的是,锁和受保护资源是相关联的
原子性的本质是加锁和解锁操作的中间状态对外不可见
锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字是锁的一种实现
synchronized 修饰方法和代码块
class X {
// 修饰非静态方法
public synchronized void a() {
// 临界区
}
// 修饰静态方法
public synchronized static void b() {
// 临界区
}
// 修饰代码块
Object o = new Object();
public void c() {
synchronized(o) {
// 临界区
}
}
}
synchronized 是 JVM 层面的锁,编译器会在 synchronized 修饰的方法或代码块前后自动加上加锁和解锁操作
synchronized 锁的粒度
修饰静态方法:对类对象(class)加锁
修饰普通方法:对当前对象(this)加锁
synchronized 保证可见性
Happens-Before 规则中有一条是一个线程的解锁对另一个线程的加锁是可见的
配合传递性规则可得,一个线程对加锁资源的修改对另一个线程是可见的
必须用同一把锁保护一个资源,否则不能实现操作资源的互斥性
class Calc {
static long value = 0L;
synchronized long get() {
return value;
}
synchronized static void addOne() {
value += 1;
}
}
addOne 方法是类对象的锁, get 方法是当前对象的锁,两个线程执行 addOne 和 get 是不互斥的
一把锁保护多个资源
class Account {
private int balance;
// 转账
synchronized void transfer(Account target, int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
transfer 方法加了当前对象的锁,所以不同 Account 对象加的锁是不一样的,无法保证互斥访问
解决的方法是提供一个 Account 类各个对象共有的锁,如构造传入相同的对象用于加锁、对静态变量加锁或者对 Account.class 加锁
死锁产生的背景
保护多个资源的互斥访问,可以选择一个公共的对象进行加锁,但是这样锁的粒度太大,往往会降低并发度
优化的方案是选择若干个范围更小的锁,一个线程获取所有锁之后才能对资源进行访问
如果线程 A 获取了锁 1,急需获取锁 2,但线程 B 已经持有锁 2,也在等待锁 1,这样就一直陷入等待
死锁的示例代码
class Account {
private int balance;
// 转账
void transfer(Account target, int amt){
// 锁定转出账户
synchronized(this) {
// 锁定转入账户
synchronized(target) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
当两个线程操作两个 Account 对象互相转账的时候,可能出现各自锁住了自己的账户对象,一直等待另一个,产生死锁
产生死锁的条件
避免死锁的方式
破坏请求与保持条件
避免请求一个锁后等待另一个锁,可以将多个锁的获取过程优化为一次性获取
class Allocator {
private Set<Object> locks = new HashSet<>();
// 一次性申请所有锁
synchronized boolean lock(Object from, Object to) {
if (locks.contains(from) || locks.contains(to)) {
return false;
} else {
locks.add(from);
locks.add(to);
}
return true;
}
// 释放锁
synchronized void release(Object from, Object to) {
locks.remove(from);
locks.remove(to);
}
}
使用一个单例的 Allocator 对象,在获取锁前循环等待
while (!allocator.lock(this, target)) {}
破坏不可剥夺条件
JDK 并发包内提供了高级的锁 API,可以设置等待锁的时间,时间到了自动放弃,避免一直等待
破坏环路条件
可以对资源进行排序,例如转账示例代码中,对两个 Account 对象可以先排序再加锁,这样多个对象执行转账方法时,按顺序获取锁,不存在循环等待