0%

16.5.3 同步方法

16.5.3 同步方法

与同步代码块对应,Java的多线程安全支持还提供了同步方法,

什么是同步方法

同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。

同步方法的同步资源监视器是调用该同步方法的对象

对于synchronized修饰的实例方法(非statIc方法)而言,无须显式指定同步监视器,同步方法的同步监视器是this,也就是调用该同步方法的对象.

什么样的类是线程安全的类

通过使用同步方法可以非常方便地实现线程安全的类,线程安全的类具有如下特征:

  1. 该类的对象可以被多个线程安全地访问
  2. 每个线程调用该对象的任意方法之后都将得到正确结果。
  3. 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。

前面介绍了可变类和不可变类,其中不可变类总是线程安全的,因为它的对象状态不可改变;
但可变对象需要额外的方法来保证其线程安全。例如上面的Account就是一个可变类,它的accountNobalance两个成员变量都可以被改变,当两个线程同时修改Account对象的balance成员变量的值时,程序就出现了异常。
下面Account类对balance的访问设置成线程安全的,那么只要把修改balance的方法变成同步方法即可

程序示例

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class Account
{
// 封装账户编号、账户余额两个成员变量
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}

// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}

// 提供一个线程安全draw()方法来完成取钱操作
public synchronized void draw(double drawAmount)
{
// 账户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

上面程序中增加了一个代表取钱的draw()方法,并使用了synchronized关键字修饰该方法,把该方法变成同步方法,该同步方法的同步监视器是this,因此对于同一个Account账户而言,任意时刻只能有一个线程获得对Account对象的锁定,然后进入draw()方法执行取钱操作—这样也可以保证多个线程并发取钱的线程安全

因为Account类中已经提供了draw()方法,而且取消了setBalance()方法, DrawThread线程类需要改写,该线程类的run()方法只要调用Account对象的draw()方法即可执行取钱操作。run()方法代码片段如下。

上面的DrawThread类无须自己实现取钱操作,而是直接调用accountdraw()方法来执行取钱操作。由于已经使用synchronized关键字修饰了draw()方法,同步方法的同步监视器是this,而this总代表调用该方法的对象—在上面示例中,调用draw()方法的对象是account,因此多个线程并发修改同一份account之前,必须先对account对象加锁。这也符合了”加锁→修改→释放锁”的逻辑。

Account里定义draw()方法,而不是直接在run()方法中实现取钱逻辑,这种做法更符合面向对象规则。
在面向对象里有一种流行的设计方式: Domain Driven Design(领域驱动设计,DDD),这种方式认为每个类都应该是完备的领域对象,例如Account代表用户账户,应该提供用户账户的相关方法;通过draw()方法来执行取钱操作(实际上还应该提供transfer等方法来完成转账等操作),而不是直接将setBalance()方法暴露出来任人操作,这样才可以更好地保证Account对象的完整性和一致性.

如何减少线程安全的负面影响

可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略。

  • 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步(竞争资源也就是共享资源)。例如上面Account类中的accountNo实例变量就无须同步,所以程序只对draw()方法进行了同步控制。
  • 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。

JDK所提供的StringBuilderStringBuffer就是为了照顾单线程环境和多线程环境所提供的类,

  • 在单线程环境下应该使用StringBuilder来保证较好的性能;
  • 当需要保证多线程安全时,就应该使用String Buffer.

原文链接: 16.5.3 同步方法