공유 객체
여러 개의 스레드가 동일한 객체를 참조 및 공유하고 있을때, 해당 객체를 공유객체
공유 변수 사용하기
💡
ReentrantLock
이나Synchronized
키워드 없이 번갈아가려고 하면 ”공유변수” 를 사용하면 된다.
Busy Waiting
이나Spinlock
과 같은 형태를 포함할 수 있기 때문에 CPU 자원을 비효율적으로 사용할 수도 있다.- 또한 코드의 복잡성을 증대시키고, 잘못할 경우 데드락(Deadlock)이나 라이브락(Livelock)과 같은 문제를 일으킬 수 있다.
public class WorkObject {
private static int counter = 0;
private volatile int turn = 1;
public void methodA() {
for (int i = 0; i < 10; i++) {
while (turn != 1) {
// Busy waiting
}
counter++;
System.out.println("methodA");
turn = 2;
}
}
public void methodB() {
for (int i = 0; i < 10; i++) {
while (turn != 2) {
// Busy waiting
}
counter++;
System.out.println("methodB");
turn = 1;
}
}
}
여기서 volatile
키워드는 다양한 스레드에 의해 액세스될 때 메모리 일관성 오류를 방지하기 우해서 사용된다. (실제 프로덕션 환경에서는 권장하지 않는다.)
volatile
변수를 사용할 경우에 변수를 메인 메모리에 저장하게 된다. 따라서 매번 변수의 값을 CPU cache
가 아니라 Main Memory
에서만 읽게 된다. 또한 매번 write
도 메인 메모리에 하게 된다.
이 키워드를 사용하지 않는 MultiThread
어플리케이션은 Task를 수행하는 동안 성능 향상을 위해서 메인메모리에 읽은 변수의 값을 CPU Cache
에 저장한다. 그럿다보니, 멀티 스레드 환경에서는 변수 값 불일치 문제가 발생할 수 있다.
ReentrantLock
💡
ava.util.concurrent.locks
패키지에 속하는Lcok
인터페이스 구현체를 이용해서 동기화시킨다.
synchronized
키워드에 비해서 더 세밀한 제어를 가능하게 한다. 또한, 재진입이 가능하므로 동일한 스레드가 이미 획득한 락을 다시 획득할 수 있게 해준다.
특정 조건 하에서 스레드가 블록되지 않고 다른 스레드에 의해 실행 순서가 변경될 수 있도록 하는 등 유연한 동기화가 가능하다.
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class WorkObject {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean turnA = true; // 처음에는 A의 차례
public void methodA() {
lock.lock(); // 락 획득
try {
for (int i = 0; i < 10; i++) {
while (!turnA) { // A의 차례가 아니면 대기
condition.await();
}
System.out.println("methodA");
turnA = false; // B의 차례로 변경
condition.signal(); // 대기 중인 스레드 중 하나를 깨움
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 락 해제
}
}
public void methodB() {
lock.lock(); // 락 획득
try {
for (int i = 0; i < 10; i++) {
while (turnA) { // B의 차례가 아니면 대기
condition.await();
}
System.out.println("methodB");
turnA = true; // A의 차례로 변경
condition.signal(); // 대기 중인 스레드 중 하나를 깨움
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 락 해제
}
}
}
주의할 점은, RettrantLock
을 사용할 때는 항상 finally
블록에서 락을 해제해야한다. 왜냐하면 예외를 발생하더라도 락이 정상적으로 해제되도록 보장하기 위함이다.
Semaphore
💡
java.util.concurrent
패키지에 있는 동시성 유틸리티로, 한 번에 한개 이상의 스레드가 자원에 접근할 수 있도록 제어하는 메커티즘을 제공한다.
리소스의 제한된 수의 허가를 관리하는데 사용되는데, 리소스에 접근하기 전에 허가를 획득해야하며, 작업을 마친 후에는 허가를 반납하도록 한다.
- acquire(): 허가를 획득한다. 만약 사용 가능한 허가가 없다면, 현재 스레드는 허가가 사용 가능해질 때까지 대기한다.
- release(): 허가를 반납한다. 이 연산은 다른 스레드가 허가를 획득할 수 있도록 만든다.
import java.util.concurrent.Semaphore;
public class WorkObject {
private final Semaphore semA = new Semaphore(1); // methodA 시작 허가
private final Semaphore semB = new Semaphore(0); // methodB 시작 대기
public void methodA() {
try {
for (int i = 0; i < 10; i++) {
semA.acquire(); // methodA 실행 허가를 얻음
System.out.println("methodA");
semB.release(); // methodB 실행을 허가함
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public void methodB() {
try {
for (int i = 0; i < 10; i++) {
semB.acquire(); // methodB 실행 허가를 얻음
System.out.println("methodB");
semA.release(); // methodA 실행을 다시 허가함
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Semaphore
인스턴스를 사용한다.
하나는 methodA
의 실행을 제어하고, 다른 하나는 methodB
의 실행을 제어한다. methodA
는 시작할 수 있지만, methodB
는 methodA
가 먼저 실행되어 semB
에 허가를 제공하기 전까지 실행될 수 없다. 이러한 방식으로, 두 메소드가 번갈아 가며 실행되도록 할 수 있다.
Synchronized
💡
sysnchronized
키워드를 사용해서 할 수 있다.
특정 객체에 대한 동기화된 블록 또는 메소드 내에서 작업을 수행하며, 해당 블록이나 메소드는 한 번에 하나의 스레드만 실행할 수 있다.
public class WorkObject {
private static int counter = 0;
public synchronized void methodA() {
for (int i = 0; i < 10; i++) {
counter++;
System.out.println("methodA");
notify();
try {
wait();
} catch (InterruptedException e) {
}
}
notifyAll(); // 모든 대기 중인 스레드 깨우기
}
public synchronized void methodB() {
for (int i = 0; i < 10; i++) {
counter++;
System.out.println("methodB");
notify();
try {
wait();
} catch (InterruptedException e) {
}
}
notifyAll(); // 모든 대기 중인 스레드 깨우기
}
}
methodA
와 methodB
에서 사용된 synchronized
키워드는 메소드 전체를 동기화 블록으로 만들어, 해당 메소드가 속한 객체의 모니터 락을 획득합니다. 메소드가 실행될 때, 스레드는 이 락을 획득하고, 메소드 실행이 완료되면 락을 해제한다. 만약 다른 스레드가 동시에 접근하려고 시도하면, 락이 해제될 때까지 대기해야 한다.
여기서 notify()
메소드 호출은 해당 객체의 대기 중인 다른 스레드 중 하나를 깨워 실행을 계속하도록 한다. 그리고 wait()
메소드 호출은 현재 스레드를 대기 상태로 만들어, 다른 스레드가 실행을 계속할 수 있도록 한다. methodA
와 methodB
에서 이러한 메커니즘을 사용하여, 두 메소드가 번갈아 가며 실행될 수 있도록 구현되어 있다.
'Language Study > Java' 카테고리의 다른 글
Java 17: LTS와장기적 사용의 이유 (3) | 2024.10.12 |
---|---|
[모던 자바 인 액션] 스트림(Stream) (1) | 2024.07.25 |
[모던 자바 인 액션] 람다 표현식 (2) | 2024.07.23 |
[모던 자바 인 액션] 동작 파라미터화 코드 전달하기 (1) | 2024.07.23 |
[모던 자바 인 액션] 자바 8,9,10,11: 무슨 일이 일어나고 있는가? (1) | 2024.07.23 |