16.5.3 同步方法
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()
方法代码片段如下。
1 | public class DrawThread extends Thread { |
上面的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
来保证较好的性能; - 当需要保证多线程安全时,就应该使用
StringBuffer
.
- 在单线程环境下应该使用