아래 코드는 멀티스레드 환경에서 심각한 데이터 불일치 문제를 일으킬 수 있습니다. 본 포스트에서는 Java의 다양한 동기화 메커니즘을 통해 이러한 문제를 해결하는 방법을 알아보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
|
public class ThreadUnsafeCounter {
private int count = 0;
public void increment() {
count++; // 이 연산은 원자적이지 않습니다!
}
public int getCount() {
return count;
}
}
|
cs |
1. synchronized 키워드
Java에서 가장 기본적인 동기화 메커니즘은 synchronized
키워드입니다.
메소드 동기화
1
2
3
4
5
6
7
8
9
10
11
|
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
|
cs |
블록 동기화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class BlockSynchronizedCounter {
private int count = 0;
private final Object lock = new Object(); // 명시적 락 객체
public void increment() {
synchronized(lock) {
count++;
}
}
public int getCount() {
synchronized(lock) {
return count;
}
}
}
|
cs |
성능 비교 및 분석
synchronized
의 주요 특징:
- 내부적으로 모니터(monitor)를 사용하여 상호 배제를 구현
- Java 6 이후 적응형 락(adaptive lock) 메커니즘 도입으로 성능 개선
- 단순하지만 경쟁 상황에서 성능 저하 가능성 존재
2. volatile 키워드
volatile
은 가시성(visibility) 문제를 해결하지만 원자성(atomicity)은 보장하지 않습니다.
1
2
3
4
5
6
7
8
9
10
11
|
public class VolatileExample {
private volatile boolean flag = false;
public void setFlag() {
flag = true; // 다른 스레드에 즉시 가시적
}
public boolean isFlag() {
return flag;
}
}
|
cs |
3. java.util.concurrent.atomic 패키지
원자적 연산을 위한 특수 클래스들을 제공합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 원자적 연산
}
public int getCount() {
return count.get();
}
}
|
cs |
성능 측정 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
public class SynchronizationBenchmark {
public static void main(String[] args) throws InterruptedException {
final int THREADS = 10;
final int ITERATIONS = 1_000_000;
// 다양한 카운터 유형 테스트
ThreadUnsafeCounter unsafeCounter = new ThreadUnsafeCounter();
SynchronizedCounter syncCounter = new SynchronizedCounter();
AtomicCounter atomicCounter = new AtomicCounter();
// Atomic 카운터 벤치마크
long start = System.nanoTime();
testCounter(atomicCounter, THREADS, ITERATIONS);
long end = System.nanoTime();
System.out.println(“Atomic time: “ + (end – start) / 1_000_000 + “ms”);
System.out.println(“Atomic final count: “ + atomicCounter.getCount());
// 더 많은 벤치마크 코드…
}
private static void testCounter(Counter counter, int threads, int iterations)
throws InterruptedException {
Thread[] threadArray = new Thread[threads];
for (int i = 0; i < threads; i++) {
threadArray[i] = new Thread(() –> {
for (int j = 0; j < iterations; j++) {
counter.increment();
}
});
}
for (Thread t : threadArray) t.start();
for (Thread t : threadArray) t.join();
}
interface Counter {
void increment();
int getCount();
}
}
|
cs |
4. Lock 인터페이스
java.util.concurrent.locks
패키지의 Lock 인터페이스는 synchronized
보다 유연한 락 메커니즘을 제공합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockCounter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 반드시 unlock 호출
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
|
cs |
ReentrantLock vs synchronized
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
public class LockPerformanceTest {
private static final int ITERATIONS = 1_000_000;
public static void main(String[] args) {
// ReentrantLock 테스트
ReentrantLock lock = new ReentrantLock();
long startLock = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
lock.lock();
try {
// 간단한 연산
} finally {
lock.unlock();
}
}
long endLock = System.nanoTime();
// synchronized 테스트
Object syncObj = new Object();
long startSync = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
synchronized(syncObj) {
// 동일한 간단한 연산
}
}
long endSync = System.nanoTime();
System.out.println(“ReentrantLock time: “ + (endLock – startLock) / 1_000_000 + “ms”);
System.out.println(“Synchronized time: “ + (endSync – startSync) / 1_000_000 + “ms”);
}
}
|
cs |
5. ReadWriteLock
읽기 작업이 많고 쓰기 작업이 적은 경우에 유용한 동기화 메커니즘입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteCounter {
private int count = 0;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public void increment() {
lock.writeLock().lock(); // 쓰기 락
try {
count++;
} finally {
lock.writeLock().unlock();
}
}
public int getCount() {
lock.readLock().lock(); // 읽기 락 (여러 스레드가 동시에 읽기 가능)
try {
return count;
} finally {
lock.readLock().unlock();
}
}
}
|
cs |