文章目录
- 前言
- 一、Java 语言提供的锁技术:synchronized
- 一把锁可以保护多个资源,但是一个资源只能有一把锁来保护
- 二、保护多个资源
- 1.保护多个没有关联关系的资源
- 2.保护有关联关系的多个资源
- 三、死锁
- 1.优化上例的串行执行
- 2.死锁
- 1. 破坏占用且等待条件
- 2.破坏不可抢占条件
- 3.破坏循环等待条件
- 总结
前言
“同一时刻只有一个线程执行”这个条件非常重要,我们称之为互斥。通过互斥来保证原子性。
需要互斥执行的代码称为临界区
加锁本质就是在锁对象的对象头中写入当前线程id,但是new object每次在内存中都是新对象,所以加锁无效。
一、Java 语言提供的锁技术:synchronized
class X {
// 修饰非静态方法
synchronized void foo() {
// 临界区
}
// 修饰静态方法
synchronized static void bar() {
// 临界区
}
// 修饰代码块
Object obj = new Object();
void baz() {
synchronized(obj) {
// 临界区
}
}
}
Java 的一条隐式规则:
当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X;
当修饰非静态方法的时候,锁定的是当前实例对象 this。
一把锁可以保护多个资源,但是一个资源只能有一把锁来保护
class SafeCalc {
static long value = 0L;
synchronized long get() {
return value;
}//方法加锁,锁的是this
synchronized static void addOne() {
value += 1;
}//静态方法加锁,锁类(SafeCalc)
}
锁的是变量value,但是两把锁,所以错误
二、保护多个资源
1.保护多个没有关联关系的资源
用不同的锁对受保护资源进行精细化管理,能够提升性能。这种锁还有个名字,叫细粒度锁。
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;
}
}
}
账户余额和修改密码由两个锁来实现
2.保护有关联关系的多个资源
class Account {
private Object lock;
private int balance;
private Account();
// 创建 Account 时传入同一个 lock 对象
public Account(Object lock) {
this.lock = lock;
}
// 转账
void transfer(Account target, int amt){
// 此处检查所有对象共享的锁
synchronized(lock) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
案例实际不可行,所有转账都是串行化执行
三、死锁
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;
}
}
}
}
}
在 transfer() 方法内部,我们首先尝试锁定转出账户 this(先把转出账本拿到手),然后尝试锁定转入账户 target(再把转入账本拿到手),只有当两者都成功时,才执行转账操作。这个逻辑可以图形化为下图这个样子。
使用细粒度锁是有代价的,这个代价就是可能会导致死锁。
2.死锁
一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。
死锁的发生条件:
1.互斥,共享资源 X 和 Y 只能被一个线程占用;
2.占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
3.不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
4.循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。
只要破坏一个就能避免死锁
1. 破坏占用且等待条件
同时申请转出账户和转入账户这两个资源
class Allocator {
private List<Object> als =
new ArrayList<>();
// 一次性申请所有资源
synchronized boolean apply(
Object from, Object to){
if(als.contains(from) ||
als.contains(to)){
return false;
} else {
als.add(from);
als.add(to);
}
return true;
}
// 归还资源
synchronized void free(
Object from, Object to){
als.remove(from);
als.remove(to);
}
}
class Account {
// actr 应该为单例
private Allocator actr;
private int balance;
// 转账
void transfer(Account target, int amt){
// 一次性申请转出账户和转入账户,直到成功
while(!actr.apply(this, target));
try{
// 锁定转出账户
synchronized(this){
// 锁定转入账户
synchronized(target){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
} finally {
actr.free(this, target)
}
}
}
2.破坏不可抢占条件
synchronized 是做不到的
3.破坏循环等待条件
破坏这个条件,需要对资源进行排序,然后按序申请资源。
class Account {
private int id;
private int balance;
// 转账
void transfer(Account target, int amt){
Account left = this ①
Account right = target; ②
if (this.id > target.id) { ③
left = target; ④
right = this; ⑤
} ⑥
// 锁定序号小的账户
synchronized(left){
// 锁定序号大的账户
synchronized(right){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
总结
“原子性”的本质是什么?其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。例如,在 32 位的机器上写 long 型变量有中间状态(只写了 64 位中的 32 位),在银行转账的操作中也有中间状态(账户 A 减少了 100,账户 B 还没来得及发生变化)。所以解决原子性问题,是要保证中间状态对外不可见。