자바는 멀티 스레드를 지원하는 언어이기 때문에 여러 스레드가 자원을 공유하는 경우 문제점이 생길 수 있다.
여러 스레드에서 동시에 자원에 접근하는 경우 Race Condition이 발생하고 서로 자원을 사용하려는 경쟁 상태로 인해 자원을 일관성을 지킬 수 없게 된다.
public class Board {
private int view = 0;
public void view() {
view++;
}
public int getView() {
return view;
}
}
public class MyThread implements Runnable{
Board board;
public MyThread(Board board) {
this.board = board;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
board.view();
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Board board = new Board();
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(new MyThread(board));
thread.start();
}
Thread.sleep(25000);
System.out.println(board.getView());
}
}
예를 들어, 100명의 사람이 한 게시물을 50번 조회할 때 개발자의 생각으로는 조회수가 5000이 나와야 하지만 동시성 이슈로 인해 결과가 5000보다 작은 값이 도출되는 것을 볼 수 있다.
이러한 동시성 문제를 위해 자바는 여러 해결책을 가지고 있다.
synchronized 키워드는 임계영역(자원에 대한 동시 접근을 막고 독점을 보장해주는 영역) 을 만들어 공유 자원에 대한 동시 접근을 막아준다.
synchronized 키워드를 사용하는 방식은 2가지로 특정 영역을 임계영역으로 만들거나 메서드 전체를 임계영역으로 설정할 수 있다.
public class Board {
private Integer view = 0;
// 특정 영역에 synchronized
public void view() {
synchronized (view) {
view++;
}
}
public int getView() {
return view;
}
}
public class Board {
private int view = 0;
// 메서드에 synchronized
public synchronized void view() {
view++;
}
public int getView() {
return view;
}
}
Lock 인터페이스를 이용해 Lock 객체의 lock() 메서드를 통해 잠그고, unlock()메서드를 사용해 잠금 해제를 할 수 있다.
public class Board {
private int view = 0;
private Lock lock = new ReentrantLock();
// 명시적 Lock
public void view() {
lock.lock();
try {
// 임계 영역
view++;
} finally {
lock.unlock();
}
}
public int getView() {
return view;
}
}
자바의 Concurrent 패키지는 동시성 이슈를 해결하기 위한 다양한 클래스와 인터페이스를 제공하기 때문에 이를 사용하면 내부적으로 스레드 간 안전한 데이터 공유를 보장하므로 동시성 이슈를 예방할 수 있다.
public class Board {
private AtomicInteger view = new AtomicInteger(0);
// Concurrent패키지 Atomic 사용
public void view() {
view.incrementAndGet();
}
public int getView() {
return view.get();
}
}
이와 같은 방법들을 사용한다면 처음 개발자의 예상과 같이 5000의 결과가 도출되는 것을 볼 수 있다.