synchronized에 대해

최창효·2025년 6월 27일
post-thumbnail

Thread Interference

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}
    @Test
    void counterTest() throws InterruptedException {
        Counter counter = new Counter();
        int threadCount = 1000;

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            executorService.submit(() -> {
                counter.increment();
                counter.decrement();
                latch.countDown();
            });
        }

        latch.await();
        executorService.shutdown();

        int result = counter.value();
        System.out.println("Final counter value: " + result);
    }

  • 모든 스레드가 동일하게 1을 더하고 이후 1을 뺍니다. 아무런 값 변화가 없이 Counter의 c값이 0일거 같지만 실제로는 0이 되지 않을 수 있습니다

이처럼 여러 스레드가 동일한 자원에 동시에 접근해 사용했을 때 발생할 수 있는 동시성 문제를 막는 방법 중 하나로 synchronized키워드가 있습니다.

synchronized

public class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }
}
    @Test
    void counterTest2() throws InterruptedException {
    	// Counter -> SynchronizedCounter로 변경
        SynchronizedCounter counter = new SynchronizedCounter();
        int threadCount = 1000;

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            executorService.submit(() -> {
                counter.increment();
                counter.decrement();
                latch.countDown();
            });
        }

        latch.await();
        executorService.shutdown();

        int result = counter.value();
        System.out.println("Final counter value: " + result);
    }

  • synchronized메서드를 가진 SynchronizedCounter를 사용하자 0이라는 값을 잘 반환했습니다.

synchronized키워드

  • synchronized키워드는 메서드 앞에 선언하여 사용하거나(synchronized method), 또는 synchronized 구문을 만들어 사용할 수 있습니다(synchronized statement).
    • SynchronizedCounter는 메서드 앞에 synchronized 키워드를 선언해 사용했습니다.
    • 아래와 같이 synchronized 구문을 만들어 사용할 수도 있습니다. 아래 코드는 SynchronizedCounter의 public synchronized void increment() {}메서드와 동일합니다.
      	public void increment() {
          	synchronized(this) {
              	c++;
              }
          }
  • 생성자에는 synchronized키워드를 사용할 수 없습니다. final 필드는 객체가 생성된 이후에는 수정이 불가능하기 때문에 객체 생성이 완료된 이후라면 굳이 synchronized를 쓰지 않아도 안전합니다.

synchronized method

  • 하나의 객체에 있는 synchronized메서드는 동시에 둘 이상 실행될 수 없습니다. 위 예제에서 A스레드가 public synchronized void increment()를 실행하고 있다면, 다른 스레드는 public synchronized void increment()는 물론이고 public synchronized void decrement()메서드도 실행할 수 없습니다. 이는 같은 객체 안에 있는 synchronized메서드는 모두 동일한 락(this)을 사용한다는 의미입니다.
  • synchronized메서드가 종료되면 해당 객체의 synchronized메서드를 호출하는 다른 스레드들에 대해 happens-before 관계가 자동으로 성립됩니다. happens-before는 A작업이 B작업보다 먼저 일어났음을 보장한다는 의미로, A가 한 메모리 변경 내용이 B에게 보인다는 의미입니다.

synchronized statement

  • synchronized statement는 공유락(Intrinsic Lock)을 제공할 객체를 반드시 선언해야 합니다.

  • 사용 형태(예시)

    public void method() {
    	// ...
        synchronized(공유락을 제공할 객체) {        
        	// do something
        }        
        // ...
    }
  • 두 메서드가 사용하는 자원이 다르다면 다음과 같이 synchronized statement를 활용할 수 있습니다

    public class MsLunch {
        private long c1 = 0;
        private long c2 = 0;
        private Object lock1 = new Object();
        private Object lock2 = new Object();
    
        public void inc1() {
            synchronized(lock1) {
                c1++;
            }
        }
    
        public void inc2() {
            synchronized(lock2) {
                c2++;
            }
        }
    }
    • A스레드가 inc1을 실행하고 있을때 다른 스레드가 inc2를 실행하게 가능합니다

Intrinsic Lock

  • 모든 객체는 고유한 Intrinsic Lock(Monitor Lock)을 가지고 있습니다.
  • synchronized 메서드를 호출하면 메서드가 속한 객체의 Intrinsic Lock을 자동으로 획득하고 메서드가 끝날 때 이 Lock을 해제합니다. 예외로 인해 메서드가 비정상적으로 종료되더라도 Lock을 반드시 해제합니다.
  • synchronized키워드가 사용하는 Lock이 바로 이 Intrinsic Lock입니다.

static + synchronized

  • static synchronized는 인스턴스가 아닌 클래스 단위로 Lock이 발생합니다.

    public class StaticSynchronizedCounter {
        private static int c = 0;
    
        public static synchronized void increment() {
            c++;
        }
    
        public static synchronized void decrement() {
            c--;
        }
    
        public static synchronized int value() {
            return c;
        }
    }    
       @Test
        void counterTest3() throws InterruptedException {
            int threadCount = 1000;
    
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            CountDownLatch latch = new CountDownLatch(threadCount);
    
            for (int i = 0; i < threadCount; i++) {
                executorService.submit(() -> {
                    StaticSynchronizedCounter.increment();
                    StaticSynchronizedCounter.decrement();
                    latch.countDown();
                });
            }
    
            latch.await();
            executorService.shutdown();
    
            int result = StaticSynchronizedCounter.value();
            System.out.println("Final counter value: " + result);
        }
  • static synchronized는 일반적인 synchronized와 Lock을 공유하지 않습니다.

Reentrant Synchronization

  • 스레드는 다른 스레드가 이미 소유한 Lock을 획득할 수 없지만, 자신이 이미 소유하고 있는 Lock은 다시 획득할 수 있습니다. 이는 synchronized블록이 중첩됐을 때 자신이 획득한 Lock때문에 자기 자신이 다음으로 진입하지 못하는 경우를 방지합니다.
    public void inc1() {
        synchronized(this) {
        	// do Something
    		synchornized(this) {
            	// do Something2
            }
        }
    }    	

Atomic Access

  • atomic하다는 건 동작이 중단 없이 완전히 실행되거나, 아예 실행되지 않는 것을 의미합니다. 중간에 멈출 수 없으며 all or nothing의 성격을 지닙니다. 또한 해당 동작이 완료되기 전까지는 side-effects가 외부에 보이지 않습니다.
  • volatile변수에 쓰기가 일어나면 그 쓰기는 이후 해당 변수를 읽는 모든 작업에 대해 happens-before관계를 형성합니다. 즉, 해당 변수의 변경 내용은 항상 다른 스레드에서 보입니다. 단순히 결과값만 보이는게 아니라 변경이 진행되기까지의 side-effects도 모두 외부에 보이게 됩니다.
  • volatile은 단순히 가시성만을 보장하며, synchronized와 달리 복합 연산에 대한 원자성은 보장하지 않습니다. 따라서 volatile로 선언한 변수를 단순 flag로 사용하는 건 좋지만, 이 값을 count++처럼 연산하는 건 좋지 않습니다.

References

profile
기록하고 정리하는 걸 좋아하는 백엔드 개발자입니다.

0개의 댓글