「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。
原子性与互斥
-
原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性
-
线程切换是原子性问题的源头
在单核机器上,可以通过禁止线程切换来保证一个线程一直持有 CPU 使用权,多次操作具有原子性
在多核机器上,多线程可以同时执行,禁止线程切换并不能保证原子性
-
互斥锁模型
同一时刻只有一个线程执行,即互斥
需要注意的是,锁和受保护资源是相关联的
原子性的本质是加锁和解锁操作的中间状态对外不可见
synchronized
-
锁是一种通用的技术方案,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 对象可以先排序再加锁,这样多个对象执行转账方法时,按顺序获取锁,不存在循环等待
-