Skip to content

✨ Feat : Counter #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/gradle-test-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: spring-templates/spring-concurrency-thread
fail_ci_if_error: true
flags: integration
fail_ci_if_error: true
verbose: true
11 changes: 6 additions & 5 deletions .github/workflows/gradle-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ name: Run gradlew test

on:
pull_request:
branches-ignore:
- main
branches:
- develop
- feature/**

jobs:
build:
Expand Down Expand Up @@ -36,6 +37,6 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: spring-templates/spring-concurrency-thread
fail_ci_if_error: true
verbose: true
flags: unittests
fail_ci_if_error: true
verbose: true
flags: ${{ github.ref == 'refs/pull/develop' && 'integration' || 'unittests' }}
10 changes: 10 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
coverage:
status:
project:
default:
target: 40%
threshold: 10%
patch:
default:
target: 30%
threshold: 10%
38 changes: 38 additions & 0 deletions src/main/java/com/thread/concurrency/counter/BatchingCounter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.thread.concurrency.counter;

import org.springframework.stereotype.Component;

import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Component
public class BatchingCounter implements Counter {
private static final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
private static final ConcurrentLinkedQueue<Integer> jobQueue = new ConcurrentLinkedQueue<>();
private static volatile int count = 100;

public BatchingCounter() {
Runnable runnableTask = () -> {
while (!jobQueue.isEmpty()) {
synchronized (this) {
var value = jobQueue.poll();
count += value == null ? 0 : value;
}
}
};
// context switching을 최소화하는 최소한의 시간마다 실행하여 성능 향상
scheduledExecutorService.scheduleAtFixedRate(runnableTask, 4, 5, TimeUnit.MILLISECONDS);
}

@Override
public void add(int value) {
jobQueue.add(value);
}

@Override
public int show() {
return count;
}
}
26 changes: 26 additions & 0 deletions src/main/java/com/thread/concurrency/counter/LockCounter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.thread.concurrency.counter;

import org.springframework.stereotype.Component;

import java.util.concurrent.locks.ReentrantLock;

@Component
public class LockCounter implements Counter {
private static final ReentrantLock lock = new ReentrantLock();
private static int count = 100;

@Override
public void add(int value) {
lock.lock();
try {
count += value;
} finally {
lock.unlock();
}
}

@Override
public int show() {
return count;
}
}
34 changes: 34 additions & 0 deletions src/main/java/com/thread/concurrency/counter/PollingCounter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.thread.concurrency.counter;

import org.springframework.stereotype.Component;

@Component
// use technique spin-lock(busy waiting)
// this approach can lead to high CPU usage if the lock is heavily contended
public class PollingCounter implements Counter {
private static int count = 100;
private static volatile boolean lock = false;

private static void doAdd(int value) {
count += value;
}

@Override
public void add(int value) {
while (true) {
if (!lock) {
synchronized (PollingCounter.class) {
lock = true;
doAdd(value);
lock = false;
break;
}
}
}
}

@Override
public int show() {
return count;
}
}
64 changes: 64 additions & 0 deletions src/test/java/com/thread/concurrency/counter/CounterTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.thread.concurrency.counter;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CountDownLatch;
import java.util.stream.Stream;

import static java.lang.Thread.sleep;

@SpringBootTest
public class CounterTest {

public static Stream<Counter> counterProvider() {
return Stream.of(new BatchingCounter(), new LockCounter(), new PollingCounter(), new BasicCounter());
}

private static void assertThen(Counter counter, int expectedValue, int actualValue) {
System.out.println("Expected value: " + expectedValue);
System.out.println("Actual value: " + actualValue);
if (counter instanceof BasicCounter) {
System.out.println("BasicCounter is not thread-safe");
Assertions.assertNotEquals(expectedValue, actualValue);
} else {
System.out.println("Counter is thread-safe");
Assertions.assertEquals(expectedValue, actualValue);
}
}

@ParameterizedTest
@MethodSource("counterProvider")
public void stressTest(Counter counter) throws InterruptedException {
int initialValue = counter.show();
int nThreads = 100;
int nAddsPerThread = 1000;
int valueToAdd = 1;
int expectedValue = initialValue + nThreads * nAddsPerThread * valueToAdd;


// define runnable job
CountDownLatch latch = new CountDownLatch(nThreads);
Runnable job = () -> {
try {
latch.countDown(); // decrease the count
latch.await(); // wait until the count reaches 0
for (int i = 0; i < nAddsPerThread; i++) {
counter.add(valueToAdd);
}
} catch (InterruptedException ignored) {
}
};

// start nThreads threads
for (int i = 0; i < nThreads; i++) {
Thread.ofVirtual().start(job);
}

sleep(300); // wait for all threads to finish

assertThen(counter, expectedValue, counter.show());
}
}