16.5.2 同步代码块

16.5.2 同步代码块

同步代码块

Java的多线程支持引入了同步监视器来解决同步问题,**使用同步监视器的通用方法就是同步代码块**。

同步代码块的语法格式

1
2
3
4
synchronized(object)
{
// 同步代码块的内容...
}

什么是同步监视器

synchronized关键字后面的括号里的参数object就是同步监视器

同步代码块的含义

同步代码块的含义是:
线程开始执行同步代码块之前,必须先获得对同步监视器的锁定

任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。

同步监视器的选择标准

虽然Java程序允许使用任何对象作为同步监视器,但同步监视器的目的是阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器

程序 使用同步代码块 同步取钱

对于上面的取钱模拟程序,应该考虑使用账户(account)作为同步监视器,把程序修改成如下形式。

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
public class DrawThread extends Thread {
// 模拟用户账户
private Account account;
// 当前取钱线程所希望取的钱数
private double drawAmount;

public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}

// 当多条线程修改同一个共享数据时,将涉及数据安全问题。
public void run() {
// 使用account作为同步监视器,任何线程进入下面同步代码块之前,
// 必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
// 这种做法符合:“加锁 → 修改 → 释放锁”的逻辑
synchronized (account) {
// 账户余额大于取钱数目
if (account.getBalance() >= drawAmount) {
// 吐出钞票
System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为: " + account.getBalance());
} else {
System.out.println(getName() + "取钱失败!余额不足!");
}
}
// 同步代码块结束,该线程释放同步锁
}
}

上面程序使用synchronizedrun()方法里的方法体修改成同步代码块,该同步代码块的同步监视器是account对象,这样的做法符合”加锁→修改→释放锁”的逻辑。

加锁 修改 释放锁

任何线程在修改指定资源之前,

  • 首先对该资源加锁,在加锁期间其他线程无法修改该资源,
  • 当该线程修改完成后,
  • 该线程释放对该资源的锁定。

通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全性。

DrawThread修改为上面所示的情形之后,多次运行该程序,总可以看到如下的运行结果:

1
2
3
甲取钱成功!吐出钞票:800.0
余额为: 200.0
乙取钱失败!余额不足!