1. 학습 목표
- synchronized, reentrantlock 개념 숙지
- 동시성 문제를 해결하기 위한 예시코드 작성
2. 학습 내용
synchronized
멀티 스레드의 큰 장점으로는 자원의 공유가 있다. 하지만 해당 자원을 여러게의 쓰레드에서 접근하게 되면 반드시 안정성과 신뢰성에 문제가 발생할것.
안좋은 예시 코드는 다음과 같다.
private int counter = 0; // 공유자원
@Test
public void testThreadSafety() throws InterruptedException {
int numberOfThreads = 100;
CountDownLatch countDownLatch = new CountDownLatch(numberOfThreads); // Thread 선언
IntStream.range(0, numberOfThreads).forEach(i -> {
new Thread(() -> {
IntStream.range(0, 1000).forEach(j -> incrementCounter());
countDownLatch.countDown();
}).start();
});
countDownLatch.await();
System.out.println("Final counter value : " + counter); // 값이 100000보다 작은값이 나옴
}
private void incrementCounter() {
counter++; // 이 부분이 동시성 문제를 일으킬 수 있음
}
보면 정확히 100개의 thread에서 1000씩 counter를 증가시키는 코드임을 확인할 수 있다.
하지만 여기서 incrementCounter에서 공유 자원인 counter에 동시에 접근하기 때문에 counter의 값이 들쑥날쑥 함을 확인할 수 있었다.
이를 해결하기 위해 필요한것이 synchronized.
private int counter = 0; // 공유자원
@Test
public void testThreadSafety() throws InterruptedException {
int numberOfThreads = 100;
CountDownLatch countDownLatch = new CountDownLatch(numberOfThreads); // Thread 선언
IntStream.range(0, numberOfThreads).forEach(i -> {
new Thread(() -> {
IntStream.range(0, 1000).forEach(j -> incrementCounter());
countDownLatch.countDown();
}).start();
});
countDownLatch.await();
System.out.println("Final counter value : " + counter); // 값이 100000이 나옴
}
// synchronized를 사용함으로
// 현재 사용중인 Thread를 제외한 다른 Thread에서 데이터에 접근할 수 없음
private synchronized void incrementCounter() {
counter++;
}
}
자바에서 synchronized는 lock의 사용을 좀 더 간편하게 제공하기 위한 내장 키워드로 lock 객체를 생성하거나 관리할 필요 없이 동기화를 구현할 수 있게 해주는 기능을 제공한다.
C는 Mutex부터 죄다 구현해야했는데 이래서 고오급 언어가 좋다
synchronized 키워드는 다음 네 가지 유형의 블록에 쓰인다.
인스턴스 메소드
스태틱 메소드
인스턴스 메소드 코드블록
스태틱 메소드 코드블록
인스턴스 메서드 동기화
private synchronized void incrementCounter() {
counter++;
}
아까 사용했던 이 코드가 인스턴스 레벨에서 동기화를 시도한 코드이다.
한 인스턴스를 기준으로 동기화가 이뤄지기 때문에 한 시점에 오직 하나의 쓰레드만이 동기화된 인스턴스 메소드를 실행할 수 있다.
인스턴스 당 한 쓰레드
static 메서드 동기화
private static synchronized void incrementCounter() {
counter++;
}
얘는 하나의 클래스 객체를 기준으로 동기화가 이뤄진다.
JVM 안에 클래스 객체는 클래스 당 하나만 존재할 수 있어 오직 한 쓰레드만 동기화된 스테틱 메서드를 실행할 수 있다.
잘 이해가 안되서 Claude한테 코드로 설명해달라고 했음
public class SynchronizedExample {
private int instanceCounter = 0;
private static int staticCounter = 0;
public synchronized void incrementInstanceCounter() {
instanceCounter++;
System.out.println(Thread.currentThread().getName() + " - Instance counter: " + instanceCounter);
}
public static synchronized void incrementStaticCounter() {
staticCounter++;
System.out.println(Thread.currentThread().getName() + " - Static counter: " + staticCounter);
}
public static void main(String[] args) throws InterruptedException {
SynchronizedExample obj1 = new SynchronizedExample();
SynchronizedExample obj2 = new SynchronizedExample();
// 두 개의 스레드가 같은 객체(obj1)의 인스턴스 메서드를 호출
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) obj1.incrementInstanceCounter();
}, "Thread-1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) obj1.incrementInstanceCounter();
}, "Thread-2");
// 두 개의 스레드가 정적 메서드를 호출
Thread t3 = new Thread(() -> {
for (int i = 0; i < 5; i++) SynchronizedExample.incrementStaticCounter();
}, "Thread-3");
Thread t4 = new Thread(() -> {
for (int i = 0; i < 5; i++) SynchronizedExample.incrementStaticCounter();
}, "Thread-4");
t1.start(); t2.start(); t3.start(); t4.start();
t1.join(); t2.join(); t3.join(); t4.join();
System.out.println("Final obj1 instance counter: " + obj1.instanceCounter);
System.out.println("Final obj2 instance counter: " + obj2.instanceCounter);
System.out.println("Final static counter: " + staticCounter);
}
}
incrementInstanceCounter같은 경우 각각의 new로 생성된 각각의 인스턴스에서 메서드가 실행되기 때문에 서로 독립적으로 counter가 증가하지만 static은 정적 변수임으로 최종적으로 값이 10이 된다.
인스턴스 레벨과 class 레벨의 스코프 차이가 있는거였음
이해 완
인스턴스 메소드 안의 동기화 블록
private void incrementCounter() {
System.out.println("응애에용");
synchronized (this) {
counter++;
}
}
이건 안봐도 뭐를 이점으로 가져갈 수 있는지 알 수 있음
보다 세밀하게 동기화가 가능하고 효율적인 멀티스레딩 코드를 작성할 수 있음
필요한 부분만 동기화.
그 외에는 인스턴스 레벨의 동기화와 동일하다.
static 메서드 안의 동기화 블록
private static void incrementCounter() {
System.out.println("응애에용");
synchronized (this) {
counter++;
}
}
이것도 마찬가지
Reentrantlock
얘는 자바에서 사용하는 가장 일반적인 lock이란다.
synchronized와는 달리 좀 더 세밀한 lock의 제어가 가능하며 오래 기다린 스레드에 우선권을 줄 수 있단다.
다양한 추가 기능이 있다는데 우선 위 예시코드를 Reentrantlock을 사용해서 변경해보자
private final ReentrantLock lock = new ReentrantLock(); // ReentrantLock 객체 선언
@Test
public void testThreadSafetyReentrantLock() throws InterruptedException {
int numberOfThreads = 100;
CountDownLatch countDownLatch = new CountDownLatch(numberOfThreads); // Thread 선언
IntStream.range(0, numberOfThreads).forEach(i -> {
new Thread(() -> {
IntStream.range(0, 1000).forEach(j -> incrementCounter());
countDownLatch.countDown();
}).start();
});
countDownLatch.await();
System.out.println("Final counter value : " + counter); // 값이 100000이 나옴
}
private void ReentrantLockincrementCounter() {
lock.lock(); // Lock을 획득
try {
counter++;
} finally {
lock.unlock(); // Lock을 해제
}
}
어떤 느낌인지 감이 온다.
바로 실습으로 수강신청 API를 만들어봐야겠다.
'코딩딩 > Java' 카테고리의 다른 글
ReentrantLock (0) | 2024.09.04 |
---|---|
Synchronized를 이용한 동시성 실습 (0) | 2024.09.01 |
모던 자바 인 액션 Chapter 3 (0) | 2024.08.26 |
모던 자바 인 액션 Chapter 2 (1) | 2024.08.16 |
모던 자바 인 액션 Chapter 1 (0) | 2024.08.15 |