1초 후 메인 스레드에서 stopRequested 필드의 값을 true로 변경하고 있지만, 실행해보면 변경된 값을 읽어오지 못하고 무한 루프를 돌고 있는 것을 확인할 수 있다. 원인은 메인 스레드와 백그라운드 스레드가 stopRequested 필드를 동기화 없이 공유하고 있기 때문이다.
synchronized 메서드를 이용한 동기화
해당 문제를 해결하기 위해선 stopRequested 필드에 접근하는 메서드를 동기화 하는 방법이 있다.
쓰기(requestStop)와 읽기(stopRequested) 메서드 모두 synchronized 키워드를 사용하고 있는데,
읽기와 쓰기 전부 synchronized 키워드를 사용하지 않으면 동기화가 제대로 이루어지지 않는다.(간혹 잘 동작하는 것 처럼 보일 수 있지만 실제론 그렇지 않다.)
volatile 필드를 이용한 동기화
volatile 키워드를 사용하여 해당 필드를 읽고 쓰는 동작이 항상 메인 메모리에 반영되도록 보장하는 방법이 있다.
위의 문제는 volatile 키워드를 사용하여 문제를 해결했지만, 해당 키워드는 필드를 읽고 쓰는 통신만 보장하며, 동시성을 보장하지는 않는다.
classAddTest {privatestaticvolatileint count =0;publicstaticvoidmain(String[] args) throwsInterruptedException {Thread thread1 =newThread(() -> {for (int i =0; i <1000000; i++) { count++; } });Thread thread2 =newThread(() -> {for (int i =0; i <1000000; i++) { count++; } });thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(count); // 1592872 }}
위 코드를 실행해보면 2000000이 나와야 할 것 같지만, 실제로는 1592872(2000000이 아닌 값)가 나오는 것을 확인할 수 있다.
이는 volatile 키워드는 해당 필드를 읽고 쓰는 통신만 보장하며, 동시성을 보장하지는 않기 때문이다.
synchronized 블록을 이용한 동기화
처음 나왔던 synchronized 키워드를 사용하여 해당 필드를 읽고 쓰는 통신과 동시성을 보장하는 방법이 있다.
classAddTest {privatestaticint count =0;privatestaticsynchronizedvoidadd() { count++; }publicstaticvoidmain(String[] args) throwsInterruptedException {Thread thread1 =newThread(() -> {for (int i =0; i <1000000; i++) { add(); } });Thread thread2 =newThread(() -> {for (int i =0; i <1000000; i++) { add(); } });thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(count); }}
Atomic 클래스를 이용한 동기화
멀티 쓰레드 환경에서 동기화 문제를 별도의 synchronized 키워드 없이 해결할 수 있는 방법으로 java.util.concurrent.atomic 패키지에 있는 Atomic 클래스를 사용하는 방법이 있다.
(내부적으로 volatile 키워드와 CAS 알고리즘을 사용하여 동시성 문제를 해결하고 있다.)
importjava.util.concurrent.atomic.AtomicInteger;classAddTest {privatestaticfinalAtomicInteger count =newAtomicInteger(0);publicstaticvoidmain(String[] args) throwsInterruptedException {Thread thread1 =newThread(() -> {for (int i =0; i <1000000; i++) {count.incrementAndGet(); } });Thread thread2 =newThread(() -> {for (int i =0; i <1000000; i++) {count.incrementAndGet(); } });thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(count.get()); }}
결론
가변 데이터를 공유하는 방법을 다루었지만, 더 복잡한 로직에서는 문제가 어디서 발생할지 예측할 수 없으므로 가변 데이터를 공유하지 않는 방법을 사용하는 것이 가장 좋다.
만약 멀티 스레드 환경에서 가변 데이터를 공유해야 한다면 아래 사항을 주의하자.
해당 데이터를 읽고 쓰는 메서드 전부 synchronized 키워드를 사용하여 동기화
volatile 키워드를 사용하면 통신은 보장되지만, 동시성은 보장되지 않는 것을 주의
가변 데이터가 java.util.concurrent.atomic 패키지에서 제공한다면 해당 클래스를 고려해도 좋음