From a6fc46e3f6ed643215ae21ca28d22b095c4a839c Mon Sep 17 00:00:00 2001 From: Gergo Petrik Date: Mon, 4 May 2020 16:40:20 +0200 Subject: [PATCH 1/4] ABA problem in concurrency --- .../java/com/baeldung/abaproblem/Account.java | 34 +++++++ .../baeldung/abaproblem/AccountUnitTest.java | 93 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 core-java-modules/core-java-concurrency-advanced-3/src/main/java/com/baeldung/abaproblem/Account.java create mode 100644 core-java-modules/core-java-concurrency-advanced-3/src/test/java/com/baeldung/abaproblem/AccountUnitTest.java diff --git a/core-java-modules/core-java-concurrency-advanced-3/src/main/java/com/baeldung/abaproblem/Account.java b/core-java-modules/core-java-concurrency-advanced-3/src/main/java/com/baeldung/abaproblem/Account.java new file mode 100644 index 0000000000..0204c31fea --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-3/src/main/java/com/baeldung/abaproblem/Account.java @@ -0,0 +1,34 @@ +package com.baeldung.abaproblem; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class Account { + + private AtomicInteger balance = new AtomicInteger(0); + + public int getBalance() { + return balance.get(); + } + + public boolean withdraw(int amount) throws InterruptedException { + int current = getBalance(); + if (current < amount) { + throw new RuntimeException("Not sufficient balance"); + } + precessBalance(); + return balance.compareAndSet(current, current - amount); + } + + private void precessBalance() throws InterruptedException { + if ("thread 1".equals(Thread.currentThread().getName())) { + TimeUnit.SECONDS.sleep(2); + } + } + + public boolean deposit(int amount) { + int current = balance.get(); + return balance.compareAndSet(current, current + amount); + } + +} diff --git a/core-java-modules/core-java-concurrency-advanced-3/src/test/java/com/baeldung/abaproblem/AccountUnitTest.java b/core-java-modules/core-java-concurrency-advanced-3/src/test/java/com/baeldung/abaproblem/AccountUnitTest.java new file mode 100644 index 0000000000..ab88a0f447 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-3/src/test/java/com/baeldung/abaproblem/AccountUnitTest.java @@ -0,0 +1,93 @@ +package com.baeldung.abaproblem; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AccountUnitTest { + + private Account account; + + @BeforeEach + public void setUp() { + account = new Account(); + } + + @Test + public void zeroBalanceInitializationTest() { + assertEquals(0, account.getBalance()); + } + + @Test + public void depositTest() { + final int moneyToDeposit = 50; + + assertTrue(account.deposit(moneyToDeposit)); + + assertEquals(moneyToDeposit, account.getBalance()); + } + + @Test + public void withdrawTest() throws InterruptedException { + final int defaultBalance = 50; + final int moneyToWithdraw = 20; + + account.deposit(defaultBalance); + + assertTrue(account.withdraw(moneyToWithdraw)); + + assertEquals(defaultBalance - moneyToWithdraw, account.getBalance()); + } + + @Test + public void withdrawWithoutSufficientBalanceTest() { + assertThrows(RuntimeException.class, () -> account.withdraw(10)); + } + + @Test + public void abaProblemTest() throws InterruptedException { + final int defaultBalance = 50; + + final int amountToWithdrawByThreadA = 20; + final int amountToWithdrawByThreadB = 10; + final int amountToDepositByThreadB = 10; + + account.deposit(defaultBalance); + + Thread threadA = new Thread(() -> { + try { + // this will take longer due to the name of the thread + assertTrue(account.withdraw(amountToWithdrawByThreadA)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }, "tread 1"); + + Thread threadB = new Thread(() -> { + + assertTrue(account.deposit(amountToDepositByThreadB)); + assertEquals(defaultBalance + amountToDepositByThreadB, account.getBalance()); + try { + // this will be fast due to the name of the thread + assertTrue(account.withdraw(amountToWithdrawByThreadB)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + // thread 1 didn't finish yet, so the original value will be in place for it + assertEquals(defaultBalance, account.getBalance()); + + }, "thread 2"); + + threadA.start(); + threadB.start(); + threadA.join(); + threadB.join(); + + // compareAndSet operation succeeds for thread 1 + assertEquals(defaultBalance - amountToWithdrawByThreadA, account.getBalance()); + } +} From 68dc88528e488db5aef951d8578bcfb33c46cd81 Mon Sep 17 00:00:00 2001 From: Gergo Petrik Date: Thu, 7 May 2020 11:45:40 +0200 Subject: [PATCH 2/4] added transaction recording --- .../java/com/baeldung/abaproblem/Account.java | 21 ++++++++++++++++--- .../baeldung/abaproblem/AccountUnitTest.java | 14 +++++++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/core-java-modules/core-java-concurrency-advanced-3/src/main/java/com/baeldung/abaproblem/Account.java b/core-java-modules/core-java-concurrency-advanced-3/src/main/java/com/baeldung/abaproblem/Account.java index 0204c31fea..558245283a 100644 --- a/core-java-modules/core-java-concurrency-advanced-3/src/main/java/com/baeldung/abaproblem/Account.java +++ b/core-java-modules/core-java-concurrency-advanced-3/src/main/java/com/baeldung/abaproblem/Account.java @@ -1,34 +1,49 @@ package com.baeldung.abaproblem; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class Account { private AtomicInteger balance = new AtomicInteger(0); + private List transactionDates = new ArrayList<>(); public int getBalance() { return balance.get(); } + public List getTransactionDates() { + return transactionDates; + } + public boolean withdraw(int amount) throws InterruptedException { int current = getBalance(); if (current < amount) { throw new RuntimeException("Not sufficient balance"); } precessBalance(); - return balance.compareAndSet(current, current - amount); + boolean result = balance.compareAndSet(current, current - amount); + if (result) { + transactionDates.add(System.currentTimeMillis()); + } + return result; } private void precessBalance() throws InterruptedException { - if ("thread 1".equals(Thread.currentThread().getName())) { + if ("thread1".equals(Thread.currentThread().getName())) { TimeUnit.SECONDS.sleep(2); } } public boolean deposit(int amount) { int current = balance.get(); - return balance.compareAndSet(current, current + amount); + boolean result = balance.compareAndSet(current, current + amount); + if (result) { + transactionDates.add(System.currentTimeMillis()); + } + return result; } } diff --git a/core-java-modules/core-java-concurrency-advanced-3/src/test/java/com/baeldung/abaproblem/AccountUnitTest.java b/core-java-modules/core-java-concurrency-advanced-3/src/test/java/com/baeldung/abaproblem/AccountUnitTest.java index ab88a0f447..457580b96c 100644 --- a/core-java-modules/core-java-concurrency-advanced-3/src/test/java/com/baeldung/abaproblem/AccountUnitTest.java +++ b/core-java-modules/core-java-concurrency-advanced-3/src/test/java/com/baeldung/abaproblem/AccountUnitTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -19,6 +20,7 @@ public class AccountUnitTest { @Test public void zeroBalanceInitializationTest() { assertEquals(0, account.getBalance()); + assertTrue(account.getTransactionDates().isEmpty()); } @Test @@ -55,7 +57,9 @@ public class AccountUnitTest { final int amountToWithdrawByThreadB = 10; final int amountToDepositByThreadB = 10; + assertTrue(account.getTransactionDates().isEmpty()); account.deposit(defaultBalance); + assertEquals(1, account.getTransactionDates().size()); Thread threadA = new Thread(() -> { try { @@ -64,7 +68,7 @@ public class AccountUnitTest { } catch (InterruptedException e) { throw new RuntimeException(e); } - }, "tread 1"); + }, "thread1"); Thread threadB = new Thread(() -> { @@ -80,7 +84,7 @@ public class AccountUnitTest { // thread 1 didn't finish yet, so the original value will be in place for it assertEquals(defaultBalance, account.getBalance()); - }, "thread 2"); + }, "thread2"); threadA.start(); threadB.start(); @@ -89,5 +93,11 @@ public class AccountUnitTest { // compareAndSet operation succeeds for thread 1 assertEquals(defaultBalance - amountToWithdrawByThreadA, account.getBalance()); + + //but there are other transactions + assertNotEquals(2, account.getTransactionDates().size()); + + // thread 2 did two modifications as well + assertEquals(4, account.getTransactionDates().size()); } } From 12ef94be3959112f2b177c570b33c77cc09264b2 Mon Sep 17 00:00:00 2001 From: Gergo Petrik Date: Sun, 17 May 2020 10:07:47 +0200 Subject: [PATCH 3/4] added revision number and cas failure count --- .../java/com/baeldung/abaproblem/Account.java | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/core-java-modules/core-java-concurrency-advanced-3/src/main/java/com/baeldung/abaproblem/Account.java b/core-java-modules/core-java-concurrency-advanced-3/src/main/java/com/baeldung/abaproblem/Account.java index 558245283a..ee1bdcd55b 100644 --- a/core-java-modules/core-java-concurrency-advanced-3/src/main/java/com/baeldung/abaproblem/Account.java +++ b/core-java-modules/core-java-concurrency-advanced-3/src/main/java/com/baeldung/abaproblem/Account.java @@ -1,39 +1,53 @@ package com.baeldung.abaproblem; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class Account { - private AtomicInteger balance = new AtomicInteger(0); - private List transactionDates = new ArrayList<>(); + private AtomicInteger balance; + private AtomicInteger transactionCount; + private ThreadLocal currentThreadCASFailureCount; + + public Account() { + this.balance = new AtomicInteger(0); + this.transactionCount = new AtomicInteger(0); + this.currentThreadCASFailureCount = new ThreadLocal<>(); + this.currentThreadCASFailureCount.set(0); + } public int getBalance() { return balance.get(); } - public List getTransactionDates() { - return transactionDates; + public int getTransactionCount() { + return transactionCount.get(); } - public boolean withdraw(int amount) throws InterruptedException { + public int getCurrentThreadCASFailureCount() { + return currentThreadCASFailureCount.get(); + } + + public boolean withdraw(int amount) { int current = getBalance(); - if (current < amount) { - throw new RuntimeException("Not sufficient balance"); - } - precessBalance(); + maybeWait(); boolean result = balance.compareAndSet(current, current - amount); if (result) { - transactionDates.add(System.currentTimeMillis()); + transactionCount.incrementAndGet(); + } else { + int currentCASFailureCount = currentThreadCASFailureCount.get(); + currentThreadCASFailureCount.set(currentCASFailureCount + 1); } return result; } - private void precessBalance() throws InterruptedException { + private void maybeWait() { if ("thread1".equals(Thread.currentThread().getName())) { - TimeUnit.SECONDS.sleep(2); + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } } } @@ -41,7 +55,10 @@ public class Account { int current = balance.get(); boolean result = balance.compareAndSet(current, current + amount); if (result) { - transactionDates.add(System.currentTimeMillis()); + transactionCount.incrementAndGet(); + } else { + int currentCASFailureCount = currentThreadCASFailureCount.get(); + currentThreadCASFailureCount.set(currentCASFailureCount + 1); } return result; } From c9453fe33a0a296e6d41161b9fb4b6e27ecd3bef Mon Sep 17 00:00:00 2001 From: Gergo Petrik Date: Mon, 18 May 2020 16:33:03 +0200 Subject: [PATCH 4/4] adding account unit test --- .../baeldung/abaproblem/AccountUnitTest.java | 65 +++++++++---------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/core-java-modules/core-java-concurrency-advanced-3/src/test/java/com/baeldung/abaproblem/AccountUnitTest.java b/core-java-modules/core-java-concurrency-advanced-3/src/test/java/com/baeldung/abaproblem/AccountUnitTest.java index 457580b96c..aa5f0f7997 100644 --- a/core-java-modules/core-java-concurrency-advanced-3/src/test/java/com/baeldung/abaproblem/AccountUnitTest.java +++ b/core-java-modules/core-java-concurrency-advanced-3/src/test/java/com/baeldung/abaproblem/AccountUnitTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; public class AccountUnitTest { @@ -20,7 +19,8 @@ public class AccountUnitTest { @Test public void zeroBalanceInitializationTest() { assertEquals(0, account.getBalance()); - assertTrue(account.getTransactionDates().isEmpty()); + assertEquals(0, account.getTransactionCount()); + assertEquals(0, account.getCurrentThreadCASFailureCount()); } @Test @@ -44,60 +44,55 @@ public class AccountUnitTest { assertEquals(defaultBalance - moneyToWithdraw, account.getBalance()); } - @Test - public void withdrawWithoutSufficientBalanceTest() { - assertThrows(RuntimeException.class, () -> account.withdraw(10)); - } - @Test public void abaProblemTest() throws InterruptedException { final int defaultBalance = 50; - final int amountToWithdrawByThreadA = 20; - final int amountToWithdrawByThreadB = 10; - final int amountToDepositByThreadB = 10; + final int amountToWithdrawByThread1 = 20; + final int amountToWithdrawByThread2 = 10; + final int amountToDepositByThread2 = 10; - assertTrue(account.getTransactionDates().isEmpty()); + assertEquals(0, account.getTransactionCount()); + assertEquals(0, account.getCurrentThreadCASFailureCount()); account.deposit(defaultBalance); - assertEquals(1, account.getTransactionDates().size()); + assertEquals(1, account.getTransactionCount()); + + Thread thread1 = new Thread(() -> { + + // this will take longer due to the name of the thread + assertTrue(account.withdraw(amountToWithdrawByThread1)); + + // thread 1 fails to capture ABA problem + assertNotEquals(1, account.getCurrentThreadCASFailureCount()); - Thread threadA = new Thread(() -> { - try { - // this will take longer due to the name of the thread - assertTrue(account.withdraw(amountToWithdrawByThreadA)); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } }, "thread1"); - Thread threadB = new Thread(() -> { + Thread thread2 = new Thread(() -> { - assertTrue(account.deposit(amountToDepositByThreadB)); - assertEquals(defaultBalance + amountToDepositByThreadB, account.getBalance()); - try { - // this will be fast due to the name of the thread - assertTrue(account.withdraw(amountToWithdrawByThreadB)); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } + assertTrue(account.deposit(amountToDepositByThread2)); + assertEquals(defaultBalance + amountToDepositByThread2, account.getBalance()); + + // this will be fast due to the name of the thread + assertTrue(account.withdraw(amountToWithdrawByThread2)); // thread 1 didn't finish yet, so the original value will be in place for it assertEquals(defaultBalance, account.getBalance()); + assertEquals(0, account.getCurrentThreadCASFailureCount()); }, "thread2"); - threadA.start(); - threadB.start(); - threadA.join(); - threadB.join(); + thread1.start(); + thread2.start(); + thread1.join(); + thread2.join(); // compareAndSet operation succeeds for thread 1 - assertEquals(defaultBalance - amountToWithdrawByThreadA, account.getBalance()); + assertEquals(defaultBalance - amountToWithdrawByThread1, account.getBalance()); //but there are other transactions - assertNotEquals(2, account.getTransactionDates().size()); + assertNotEquals(2, account.getTransactionCount()); // thread 2 did two modifications as well - assertEquals(4, account.getTransactionDates().size()); + assertEquals(4, account.getTransactionCount()); } }