考虑如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SyncTest implements Runnable {

int count = 0;

@Override
public synchronized void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
}
}

public static void main(String[] args) {
SyncTest syncTest = new SyncTest();
SyncTest syncTest1 = new SyncTest();

Thread thread = new Thread(syncTest, "thread1");
Thread thread2 = new Thread(syncTest1, "thread2");

thread.start();
thread2.start();
}
}

我们在run方法上加了synchronized,也就意味着锁住的是该对象,也就是synchronized(this),当我们实例化两个对象时,他们是互相不影响的,所以打印结果如下:

image-20230420161141896

另一段代码:

1
2
3
4
5
6
7
8
9
10
class Account {
private int balance;
// 转账
synchronized void transfer(Account target, int amt) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}

这样写,在转账的场景中并不能解决问题。

因为synchronized锁的是当前对象,它并不能锁住传进来的target,也就是说,如果此时实例化的对象是A,然后调用A.transfer(B, 100),它只能锁住A的balance,并不能锁住B的balance还会存在另一个线程修改B的balance的问题。

一开始看到这里有一个疑问,为什么不直接在接口中锁主呢,像如下写法:

1
2
3
4
5
6
@RequestMapping("/test")
public JSONObject testInterface(@RequestBody Map<String, Object> map) throws IOException {
synchronized {
return crudServiceImpl.getCrudValue(map);
}
}

这种写法会导致另外一个问题,也就是说我在A给B转账时,会进行加锁,导致C给D转账的时候,也无法进行。也就是说这种写法会导致锁的粒度太大,在实际场景中是无法使用的。

第一个优化版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}
}
}
}

这样写可以保证this 和target都被锁住,达到我们预期地效果。但是也存在问题,如果A给B转账地同时,B也要给A转账,那么就会出现死锁。两者同时锁this,也就是对象A和对象B都被锁住,然后往下执行时,发现请求资源已经被锁,导致死锁。

第二个优化版本,防止死锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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);
}
}
}

这个版本中,Allocator是一个单例,也就意味着只有一个,然后在加锁之前,先判断是否可以同时获取两个对象的锁,如果可以,再进行加锁。

注意

上述代码都忽略了在两个线程中new 出来的对象不是同一个的问题。也就是说,transfer里传入的对象,在另一个线程中也是这两个。仅作为演示破会死锁。

而事实情况是,如果这段代码放在两个不同的线程中运行,线程A实例化account C和D,线程B实例化account C和D,在线程A执行transfer(C,D),在线程B执行transfer(D,C)还是有可能会发生死锁。

而上述代码的逻辑是建立在线程A 实例化的C和D 与线程B实例化的C和D是同一个对象。