使用原子变量

Java 9并发编程指南 目录

原子变量在Java版本5中引入,用于对单个变量进行原子操作。当使用普通变量时,Java实现的每个操作均被转换为Java字节代码指令,当编译程序时JVM能够理解这些指令。例如,当给变量分配值时,Java中只使用一个指令,但是当编译程序时,JVM语言汇总会转换成不同的指令。当使用共享变量的多个线程时,会导致数据不一致错误。

为避免这些问题,Java引入原子变量。当线程正在对原子变量执行操作时,如果其他线程希望对同一个变量执行操作,则类实现包含检查操作是否以原子方式完成的机制。基本上,此操作获取变量值,将值更改为局部变量,然后尝试用新值更改旧值。如果旧值不变,则被替代,如果没有,此方法将重新开始操作。这个操作称之为比较和设置,实现以下三个步骤修改变量的值:

  1. 得到变量的值,即变量的旧值。
  2. 在临时变量中更改变量值,即变量的新值。
  3. 如果旧值等于变量的实际值,则用新值替换旧值。如果另一个线程更改变量的值,则旧值可能与实际值不同。

一些变量,例如LongAccumulator类,将能够在此类的一些方法中执行的操作作为参数接收。这些操作必须没有任何副作用,因为它们可能在每次值更新中被多次执行。

原子变量不使用锁或其它同步机制来保护对其值的访问。它们的所有操作都基于比较和设置,保证多个线程能够同时处理一个原子变量,而不会产生数据不一致的错误,此外也简化了实现 。

Java 8中新增了四个新的原子类,首先是LongAdder和DoubleAdder类,存储由不同线程频繁更新的长整型和双精度值。使用AtomicLong类能够得到与LongAdder类相同的功能,但是前者提供了更好的性能。另外两个类是LongAccumulator和DoubleAccumulator。这些类与前面的类相似,但这里需要在构造函数中指定两个参数:

  • 计数器的初始值。
  • 能够表示为lambda表达式的LongBinaryOperator或DoubleBinaryOperator。此表达式接收变量的旧值和要应用的增量,并返回变量的新值。

本节将学习如何使用原子变量实现银行账户和两个不同的任务:一个任务把钱存入账户,另一个是从账户中取出钱。本范例实现将使用AtomicLong类。

准备工作

本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。

实现过程

通过如下步骤实现范例:

  1. 创建名为Account的类模拟银行账户:

    public class Account {
    
  2. 声明名为balance的私有AtomicLong属性,存储账户余额。此外声明名为operation的私有LongAdder属性和名为commission的私有DoubleAccumulator属性:

    	private final AtomicLong balance;
    	private final LongAdder operations;
    	private final DoubleAccumulator commission;
    
  3. 实现类构造函数初始化这些属性。对于DoubleAccumulator类,其标识值为0,我们将用0.2为参数的增量结果来更新实际值:

    	public Account() {
    		balance = new AtomicLong();
    		operations = new LongAdder();
    		commission = new DoubleAccumulator((x,y)-> x+y*0.2, 0);
    	}
    
  4. 实现获得这三个属性值的方法:

    	public long getBalance() {
    		return balance.get();
    	}
    	
    	public long getOperations() {
    		return operations.longValue();
    	}
    	
    	public double getCommission() {
    		return commission.get();
    	}
    
  5. 实现名为setBalance()的方法来设置balance属性值。还要用reset()方法初始化operation和commission属性:

    	public void setBalance(long balance) {
    		this.balance.set(balance);
    		operations.reset();
    		commission.reset();
    	}
    
  6. 实现名为addAmount()的方法来增加balance属性值,使用LongAdder类的increment()方法增加operations属性值,以及accumulate()方法按20%的增量金额值作用在commision对象上:

    	public void addAmount(long amount) {
    		this.balance.getAndAdd(amount);
    		this.operations.increment();
    		this.commission.accumulate(amount);
    	}
    
  7. 实现名为substractAmount()的方法减少balance属性值。由于伴随着addAmount()方法,我们修改operations和commission值:

    	public void subtractAmount(long amount) {
    		this.balance.getAndAdd(-amount);
    		this.operations.increment();
    		this.commission.accumulate(amount);
    	}
    }
    
  8. 创建名为Company的类,指定其实现Runnable接口。此类模拟公司支付的款项:

    public class Company implements Runnable{
    
  9. 声明名为account的私有Account属性:

    	private final Account account;
    
  10. 实现类构造函数,初始化属性:

	public Company(Account account) {
		this.account=account;
	}
  1. 实现任务的run()方法,使用账户的addAmount()方法将数量为1000的钱分10次添加到余额里:

    	@Override
    	public void run() {
    		for (int i=0; i<10; i++){
    			account.addAmount(1000);
    		}
    	}
    }
    
  2. 创建名为Bank的类,指定其实现Runnable接口。此类模拟从账户中提取钱:

    public class Bank implements Runnable{
    
  3. 声明名为account的私有Account属性:

    	private final Account account;
    
  4. 实现类构造函数,初始化属性:

    	public Bank(Account account) {
    		this.account=account;
    	}
    
  5. 实现任务的run()方法,使用账户的subtractAmount()方法将数量为1000的钱分10次从余额里取出:

    	@Override
    	public void run() {
    		for (int i=0; i<10; i++){
    			account.subtractAmount(1000);
    		}
    	}
    }
    
  6. 创建名为Main的类实现本范例的主类,添加main()方法:

    public class Main {
    	public static void main(String[] args) {
    
  7. 创建Account对象,设置余额为1000:

    		Account account=new Account();
    		account.setBalance(1000);
    
  8. 创建新的Company任务和执行此任务的线程:

    		Company company=new Company(account);
    		Thread companyThread=new Thread(company);
    
  9. 创建新的Bank任务和执行此任务的线程:

    		Bank bank=new Bank(account);
    		Thread bankThread=new Thread(bank);
    
  10. 输出账户的初始余额到控制台:

    		System.out.printf("Account : Initial Balance: %d\n", account.getBalance());
    
  11. 启动线程:

    		companyThread.start();
    		bankThread.start();
    
  12. 使用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对象。然后,执行一次银行操作和一次公司操作,这样账户最终余额与初余额相同。

当执行范例时,将会看到最终余额是如何与初始余额相同的。下图显示本范例在控制台输出的执行信息:

pics/07_04.jpg

扩展学习

如引言所述,Java中还有其它的原子类。例如AtomicBoolean、AtomicInteger和AtomicReference。

LongAdder还提供如下一些方法:

  • add():通过指定为参数的值来增加内部计数器的值
  • decrement():内部计数器减1
  • reset():返回内部值为0

也可以使用与LongAdder相似的DoubleAdder类,但是此类没有increment()和decrement()方法,并且内部计数器是double值。

还可以使用与DoubleAccumulator相似的LongAccumulator类,但是内部计数器是long型。

更多关注

第二章“基础线程同步”中的“同步方法”小节

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值