16.5.3 同步方法
与同步代码块对应,Java
的多线程安全支持还提供了同步方法,
什么是同步方法
同步方法就是使用synchronized
关键字来修饰某个方法,则该方法称为同步方法。
同步方法的同步资源监视器是调用该同步方法的对象
对于synchronized
修饰的实例方法(非statIc
方法)而言,无须显式指定同步监视器,同步方法的同步监视器是this
,也就是调用该同步方法的对象.
什么样的类是线程安全的类
通过使用同步方法可以非常方便地实现线程安全的类,线程安全的类具有如下特征:
- 该类的对象可以被多个线程安全地访问
- 每个线程调用该对象的任意方法之后都将得到正确结果。
- 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。
前面介绍了可变类和不可变类,其中不可变类总是线程安全的,因为它的对象状态不可改变;
但可变对象需要额外的方法来保证其线程安全。例如上面的Account
就是一个可变类,它的accountNo
和balance
两个成员变量都可以被改变,当两个线程同时修改Account
对象的balance
成员变量的值时,程序就出现了异常。
下面将Account
类对balance
的访问设置成线程安全的,那么只要把修改balance
的方法变成同步方法即可。
程序示例
1 | public class Account |
上面程序中增加了一个代表取钱的draw()
方法,并使用了synchronized
关键字修饰该方法,把该方法变成同步方法,该同步方法的同步监视器是this
,因此对于同一个Account
账户而言,任意时刻只能有一个线程获得对Account
对象的锁定,然后进入draw()
方法执行取钱操作—这样也可以保证多个线程并发取钱的线程安全
因为Account
类中已经提供了draw()
方法,而且取消了setBalance()
方法, DrawThread
线程类需要改写,该线程类的run()
方法只要调用Account
对象的draw()
方法即可执行取钱操作。run()
方法代码片段如下。
上面的DrawThread
类无须自己实现取钱操作,而是直接调用account
的draw()
方法来执行取钱操作。由于已经使用synchronized
关键字修饰了draw()
方法,同步方法的同步监视器是this
,而this
总代表调用该方法的对象—在上面示例中,调用draw()
方法的对象是account
,因此多个线程并发修改同一份account
之前,必须先对account
对象加锁。这也符合了”加锁→修改→释放锁”的逻辑。
在Account
里定义draw()
方法,而不是直接在run()
方法中实现取钱逻辑,这种做法更符合面向对象规则。
在面向对象里有一种流行的设计方式: Domain Driven Design
(领域驱动设计,DDD)
,这种方式认为每个类都应该是完备的领域对象,例如Account
代表用户账户,应该提供用户账户的相关方法;通过draw()
方法来执行取钱操作(实际上还应该提供transfer
等方法来完成转账等操作),而不是直接将setBalance()
方法暴露出来任人操作,这样才可以更好地保证Account
对象的完整性和一致性.
如何减少线程安全的负面影响
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略。
- 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步(竞争资源也就是共享资源)。例如上面
Account
类中的accountNo
实例变量就无须同步,所以程序只对draw()
方法进行了同步控制。 - 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。
JDK
所提供的StringBuilder
、 StringBuffer
就是为了照顾单线程环境和多线程环境所提供的类,
- 在单线程环境下应该使用
StringBuilder
来保证较好的性能; - 当需要保证多线程安全时,就应该使用
String Buffer
.
原文链接: 16.5.3 同步方法