java aba问题,

  java aba问题,

  00-1010 1.导言2。比较和交换3。ABA问题3.1 ABA问题的实际场景:账户余额修改3.2账户余额修改产生的问题4。银行取款问题代码5演示。值类型和引用类型的场景6。解决方案7。Java 8中的解决方案。摘要

  00-1010我们将学习并发编程中的ABA。同时,了解问题的根源和解决方法。

  00-1010要了解根本原因,首先回顾一下比较和交换的概念。比较和交换(CAS)是无锁算法中的一种常用技术。当共享数据可以并发修改时,一个线程将修改共享内存。修改之后,另一个线程修改共享内存的尝试将会失败。

  我们每次更新都是通过两种信息来实现的:要更新的值和原始值。首先,Compare and swap将原始值与当前获得的值进行比较。如果是,将该值更新为要设置的值。

  00-1010执行campare和swap时,会出现故障。比如一个线程先读取共享内存数据值A,然后因为某种原因,线程暂时挂起,而另一个线程暂时先将共享内存数据值改为B,再改回A,然后挂起线程恢复,通过CAS比较,最终比较结果不变。这样就能通过检验,这就是ABA的问题。CAS比较之前将读取原始数据,随后是原子CAS操作。由于并发操作,这种差距最终可能会导致问题。

  00-1010以便通过实例论证ABA问题。我们创建一个银行帐户类,它维护一个整数变量来记录帐户余额。这个类有两个功能:一个存钱,一个取钱。这些操作使用CAS来修改帐户余额。

  00-1010让我们考虑两个线程操作同一个帐户的情况。线程1取钱的时候先读取余额,然后通过CAS操作进行比较。然后,线程1可能由于某种原因被阻塞。同时,线程2在线程1挂起时,也通过CAS机制对同一个账号进行两次操作。首先改变原来的值,这个值刚才已经被线程1读取了。然后线程2将该值更改为原始值。

  一旦线程1被恢复,在线程1看来,什么都没有改变。Cas将成功执行。

  00-1010创建账户类,余额记录账户余额。事务记录成功执行的事务的数量。CurrentThreadCASFailureCount记录CAS操作失败的次数。

  然后我们实现一个存款方法,存款,和一个取款方法,取款。为了演示ABA问题,实现了一个maybeWait方法来延迟等待。

  最终代码如下:

  public class Account { private atomic integer balance;私有AtomicInteger transactionCountprivate thread local integer currentThreadCASFailureCount;public Account(){ this . balance=new atomic integer(0);this . transaction count=new atomic integer(0);this . currentthreadcasfailurecount=new thread local();this . currentthreadcasfailurecount . set(0);} public int get balance(){ return balance . get();} public int getTransactionCount(){ return transaction count . get();} public int getCurrentThreadCASFailureCount(){ return optional . of nullable(currentthreadcasfailurecount . get())。or else(0);} public boolean withdraw(int amount){ int current=get balance();m

  aybeWait(); boolean result = balance.compareAndSet(current, current - amount); if (result) { transactionCount.incrementAndGet(); } else { int currentCASFailureCount = currentThreadCASFailureCount.get(); currentThreadCASFailureCount.set(currentCASFailureCount + 1); } return result; } private void maybeWait() { if ("thread1".equals(Thread.currentThread().getName())) { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public boolean deposit(int amount) { int current = balance.get(); boolean result = balance.compareAndSet(current, current + amount); if (result) { transactionCount.incrementAndGet(); } else { int currentCASFailureCount = currentThreadCASFailureCount.get(); currentThreadCASFailureCount.set(currentCASFailureCount + 1); } return result; } }接着我们对上述代码进行测试。通过maybeWait方法,模拟出现ABA问题。

  

 @Test public void abaProblemTest() throws InterruptedException { final int defaultBalance = 50; final int amountToWithdrawByThread1 = 20; final int amountToWithdrawByThread2 = 10; final int amountToDepositByThread2 = 10; Assert.assertEquals(0, account.getTransactionCount()); Assert.assertEquals(0, account.getCurrentThreadCASFailureCount()); account.deposit(defaultBalance); Assert.assertEquals(1, account.getTransactionCount()); Thread thread1 = new Thread(() -> { // this will take longer due to the name of the thread Assert.assertTrue(account.withdraw(amountToWithdrawByThread1)); // thread 1 fails to capture ABA problem Assert.assertNotEquals(1, account.getCurrentThreadCASFailureCount()); }, "thread1"); Thread thread2 = new Thread(() -> { Assert.assertTrue(account.deposit(amountToDepositByThread2)); Assert.assertEquals(defaultBalance + amountToDepositByThread2, account.getBalance()); // this will be fast due to the name of the thread Assert.assertTrue(account.withdraw(amountToWithdrawByThread2)); // thread 1 didnt finish yet, so the original value will be in place for it Assert.assertEquals(defaultBalance, account.getBalance()); Assert.assertEquals(0, account.getCurrentThreadCASFailureCount()); }, "thread2"); thread1.start(); thread2.start(); thread1.join(); thread2.join(); // compareAndSet operation succeeds for thread 1 Assert.assertEquals(defaultBalance - amountToWithdrawByThread1, account.getBalance()); //but there are other transactions Assert.assertNotEquals(2, account.getTransactionCount()); // thread 2 did two modifications as well Assert.assertEquals(4, account.getTransactionCount()); }

 

  

5.值类型与引用类型的场景

上面的例子中使用了getBalance()方法获取了一个值类型数据。由于使用的是值类型,虽然出现ABA问题,但未对结果造成影响。如果我们操作的是引用类型,那么最终会保存不同的引用对象,会带来意外的结果。

 

  对于引用类型,下面以链栈为例说明。

  线程A希望将A结点出栈,此时读取栈顶元素A,准备执行CAS操作,此时由于某种原因阻塞。线程B开始执行,执行出栈A、B。随后将D、C、A结点压入栈中。线程A恢复执行。接着执行CAS,比较发现栈顶结点A没有被修改。随后将栈顶结点改为B。由于B线程在第二步时,已经将B结点移除,A线程修改后发生错误。栈的结构发生破坏。

 

  接着我们通过下面的代码进行演示:

  

 static class Stack { private AtomicReference<Node> top = new AtomicReference<>(); static class Node { String value; Node next; public Node (String value) { this.value = value; } } //出栈 public Node pop(int time) { Node newTop; Node oldTop; do { oldTop = top.get(); if (oldTop == null) { return null; } newTop = oldTop.next; try { //休眠一段时间,模拟ABA问题 TimeUnit.SECONDS.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } while (!top.compareAndSet(oldTop, newTop)); return oldTop; } public void push (Node node) { Node oldTop; do { oldTop = top.get(); node.next = oldTop; } while (!top.compareAndSet(oldTop, node)); } public AtomicReference<Node> getTop() { return top; } } @Test public void testStack() throws Exception{ Stack stack = new Stack(); Stack.Node a = new Stack.Node("A"); Stack.Node b = new Stack.Node("B"); // 初始化栈结构 stack.push(b); stack.push(a); // ABA 测试 Thread t1 = new Thread(() -> { stack.pop(2); }); Stack.Node c = new Stack.Node("C"); Stack.Node d = new Stack.Node("D"); Thread t2 = new Thread(() -> { stack.pop(0); stack.pop(0); stack.push(d); stack.push(c); stack.push(a); }); // t1.start(); t2.start(); TimeUnit.SECONDS.sleep(5); Stack.Node top = stack.getTop().get(); do { System.out.println(top.value); top = top.next; } while (top != null); }

 

  

6. 解决方法

hazard pointer:首先出现问题是因为,多个线程操作共享数据,并未感知到别的线程正在对共享数据进行操作。通过hazard pointer介绍[1],其基本思想就是每个线程维护一个操作列表,在操作一个结点时将其记录。如果一个线程要做结点变更,先搜索线程操作列表,看是否有其它线程操作。如果有则此次操作执行失败。不变性:从上述栈的例子中可以看到,在对结点A进行比较时,由于A依然是多个线程共享并复用,因此CAS会成功。如果每次操作时,新创建对象而不是复用。这样CAS就会正常提示失败。但这样可能会创建大量对象。

 

  

7. Java中的解决方法

Java中提供了两个类来解决这个问题。

 

  AtomicStampedReferenceAtomicMarkableReference在原有类的基础上,除了比较与修改期待的值外,增加了一个时间戳。对时间戳也进行CAS操作。这也称为双重CAS。从上例中看到。每次修改一个结点,其时间戳都发生变化。这样即使共享一个复用结点,最终CAS也能返回正常的结果。

  

 

  

8. 总结

本文介绍了CAS产生ABA问题的背景,通用解决办法及Java中的解决办法。对于值类型有时发生ABA问题可能并不会造成问题。但对于引用类型,就可能造成歧义,同时破坏数据结构。通过链栈的演示,我们可以有所了解ABA产生的问题。

 

  以上为个人经验,希望能给大家一个参考,也希望大家多多支持盛行IT。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: