原子变量在Java版本5中引入,用于对单个变量进行原子操作。当使用普通变量时,Java实现的每个操作均被转换为Java字节代码指令,当编译程序时JVM能够理解这些指令。例如,当给变量分配值时,Java中只使用一个指令,但是当编译程序时,JVM语言汇总会转换成不同的指令。当使用共享变量的多个线程时,会导致数据不一致错误。
为避免这些问题,Java引入原子变量。当线程正在对原子变量执行操作时,如果其他线程希望对同一个变量执行操作,则类实现包含检查操作是否以原子方式完成的机制。基本上,此操作获取变量值,将值更改为局部变量,然后尝试用新值更改旧值。如果旧值不变,则被替代,如果没有,此方法将重新开始操作。这个操作称之为比较和设置,实现以下三个步骤修改变量的值:
- 得到变量的值,即变量的旧值。
- 在临时变量中更改变量值,即变量的新值。
- 如果旧值等于变量的实际值,则用新值替换旧值。如果另一个线程更改变量的值,则旧值可能与实际值不同。
一些变量,例如LongAccumulator类,将能够在此类的一些方法中执行的操作作为参数接收。这些操作必须没有任何副作用,因为它们可能在每次值更新中被多次执行。
原子变量不使用锁或其它同步机制来保护对其值的访问。它们的所有操作都基于比较和设置,保证多个线程能够同时处理一个原子变量,而不会产生数据不一致的错误,此外也简化了实现 。
Java 8中新增了四个新的原子类,首先是LongAdder和DoubleAdder类,存储由不同线程频繁更新的长整型和双精度值。使用AtomicLong类能够得到与LongAdder类相同的功能,但是前者提供了更好的性能。另外两个类是LongAccumulator和DoubleAccumulator。这些类与前面的类相似,但这里需要在构造函数中指定两个参数:
- 计数器的初始值。
- 能够表示为lambda表达式的LongBinaryOperator或DoubleBinaryOperator。此表达式接收变量的旧值和要应用的增量,并返回变量的新值。
本节将学习如何使用原子变量实现银行账户和两个不同的任务:一个任务把钱存入账户,另一个是从账户中取出钱。本范例实现将使用AtomicLong类。
准备工作
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
通过如下步骤实现范例:
-
创建名为Account的类模拟银行账户:
public class Account {
-
声明名为balance的私有AtomicLong属性,存储账户余额。此外声明名为operation的私有LongAdder属性和名为commission的私有DoubleAccumulator属性:
private final AtomicLong balance; private final LongAdder operations; private final DoubleAccumulator commission;
-
实现类构造函数初始化这些属性。对于DoubleAccumulator类,其标识值为0,我们将用0.2为参数的增量结果来更新实际值:
public Account() { balance = new AtomicLong(); operations = new LongAdder(); commission = new DoubleAccumulator((x,y)-> x+y*0.2, 0); }
-
实现获得这三个属性值的方法:
public long getBalance() { return balance.get(); } public long getOperations() { return operations.longValue(); } public double getCommission() { return commission.get(); }
-
实现名为setBalance()的方法来设置balance属性值。还要用reset()方法初始化operation和commission属性:
public void setBalance(long balance) { this.balance.set(balance); operations.reset(); commission.reset(); }
-
实现名为addAmount()的方法来增加balance属性值,使用LongAdder类的increment()方法增加operations属性值,以及accumulate()方法按20%的增量金额值作用在commision对象上:
public void addAmount(long amount) { this.balance.getAndAdd(amount); this.operations.increment(); this.commission.accumulate(amount); }
-
实现名为substractAmount()的方法减少balance属性值。由于伴随着addAmount()方法,我们修改operations和commission值:
public void subtractAmount(long amount) { this.balance.getAndAdd(-amount); this.operations.increment(); this.commission.accumulate(amount); } }
-
创建名为Company的类,指定其实现Runnable接口。此类模拟公司支付的款项:
public class Company implements Runnable{
-
声明名为account的私有Account属性:
private final Account account;
-
实现类构造函数,初始化属性:
public Company(Account account) {
this.account=account;
}
-
实现任务的run()方法,使用账户的addAmount()方法将数量为1000的钱分10次添加到余额里:
@Override public void run() { for (int i=0; i<10; i++){ account.addAmount(1000); } } }
-
创建名为Bank的类,指定其实现Runnable接口。此类模拟从账户中提取钱:
public class Bank implements Runnable{
-
声明名为account的私有Account属性:
private final Account account;
-
实现类构造函数,初始化属性:
public Bank(Account account) { this.account=account; }
-
实现任务的run()方法,使用账户的subtractAmount()方法将数量为1000的钱分10次从余额里取出:
@Override public void run() { for (int i=0; i<10; i++){ account.subtractAmount(1000); } } }
-
创建名为Main的类实现本范例的主类,添加main()方法:
public class Main { public static void main(String[] args) {
-
创建Account对象,设置余额为1000:
Account account=new Account(); account.setBalance(1000);
-
创建新的Company任务和执行此任务的线程:
Company company=new Company(account); Thread companyThread=new Thread(company);
-
创建新的Bank任务和执行此任务的线程:
Bank bank=new Bank(account); Thread bankThread=new Thread(bank);
-
输出账户的初始余额到控制台:
System.out.printf("Account : Initial Balance: %d\n", account.getBalance());
-
启动线程:
companyThread.start(); bankThread.start();
-
使用join()方法等待线程执行结束,输出最终余额、操作次数和累计的账户佣金到控制台:
try { companyThread.join(); bankThread.join(); System.out.printf("Account : Final Balance: %d\n", account.getBalance()); System.out.printf("Account : Number of Operations: %d\n", account.getOperations()); System.out.printf("Account : Accumulated commisions: %f\n", account.getCommission()); } catch (InterruptedException e) { e.printStackTrace(); } } }
工作原理
本范例的关键在Account类中,我们声明了名为balance的AtomicLong变量,用来存储账户余额;名为operations的LongAdder变量来存储在账户上进行的操作次数;以及名为commission的DoubleAccumulator变量来存储交易佣金值。在commission对象构造函数中,指定佣金值增量为0.2*y。因此,我们指定变量的实际值是传递到accumulate()方法中的参数值乘以0.2。
为了实现返回balance属性值的getBalance()方法,用到了AtomicLong类的get()方法。为了实现返回操作次数的long值得getOperations()方法,用到了longValue()方法。为了实现getCommission()方法,用到了DoubleAccumulator类的setBalance()方法。为了实现设置balance属性值的setBalance()方法,用到了AtomicLong类的set()方法。
为了实现addAmount()方法用来增加账户余额的输入,用到了AtomicLong类的getAndAdd()方法,此方法返回值并按指定为参数的值递增。我们还使用LongAdder类的increment()方法将变量的值加1,以及DoubleAccumulator类的collect()方法将commission属性的值按照指定的表达式递增。需要注意的是addAmount()方法总的来说不是原子的,尽管它调用了三个原子操作。
最后,为了实现subtractAmount()方法用来递减balance属性值,用到了getAndAdd()方法,还包括调用的LongAdder和DoubleAccumulator类的increment()和accumulate()对象。
然后,实现两个不同的任务:
- Company类模拟公司增加账户余额。此类的每次任务执行存入1000的金额10次。
- Bank类模拟银行从账户中取出钱。此类的每次任务执行取出1000的金额10次。
在Main类中,创建初始余额为1000的Account对象。然后,执行一次银行操作和一次公司操作,这样账户最终余额与初余额相同。
当执行范例时,将会看到最终余额是如何与初始余额相同的。下图显示本范例在控制台输出的执行信息:
扩展学习
如引言所述,Java中还有其它的原子类。例如AtomicBoolean、AtomicInteger和AtomicReference。
LongAdder还提供如下一些方法:
- add():通过指定为参数的值来增加内部计数器的值
- decrement():内部计数器减1
- reset():返回内部值为0
也可以使用与LongAdder相似的DoubleAdder类,但是此类没有increment()和decrement()方法,并且内部计数器是double值。
还可以使用与DoubleAccumulator相似的LongAccumulator类,但是内部计数器是long型。
更多关注
第二章“基础线程同步”中的“同步方法”小节