synchronized 이해하기

이수찬·2023년 9월 18일

java의 synchronized 키워드는 동시성 이슈를 막기 위해 사용한다.
동시성 이슈에 대해 먼저 알아보자.

동시성 이슈

동시성 이슈란 공유자원에 여러 스레드가 접근하여, race condition과 같은 문제가 발생하는 것을 말한다.

  • race condition : 두 개 이상의 프로세스가 공통 자원을 번갈아가며 읽거나 쓰는 동작을 할 때, 공용 데이터에 대한 접근이 어떤 순서에 따라 이루어졌는지에 따라 그 실행 결과가 같지 않고 달라지는 상황을 말한다. 즉 두 개의 스레드가 하나의 자원을 놓고 서로 사용하려고 경쟁하는 상황을 말한다.

결국 공유 자원에 여러 프로세스가 동시에 접근하여 자료의 일관성을 해치는 결과가 나타난다.

synchronized는 여러 해결책중 상호 배제 원칙을 통해 이 문제를 해결한다.

java에서 synchronized는 2가지 방식으로 사용할 수 있다.

  • 메소드에 선언
  • 필요한 코드 레벨에 선언

우선 메소드에 선언한 synchronized 에 대해 먼저 알아보자.


1. 메소드에 선언한 synchronized

public synchronized void setSharedResource(String resource) {
        sharedResource = resource;

        try {
            long sleep = (long) (Math.random() * 100);
            Thread.sleep(sleep);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        if (!Objects.equals(sharedResource, resource)) {
            System.out.println(sharedResource + " is not same as " + resource);
        }
    }
  • 위 코드와 같이 메소드 선언부에 synchronized를 사용할 수 있다.
  • 메소드에 선언한 synchronized는 해당 메소드에 스레드가 접근하면 메소드 전체를 락으로 잠근다.
  • 그로 인해 다른 스레드는 접근하지 못하며, 락이 풀린 이후에 접근하여 동시성 이슈를 막는다.

 

전체 코드

public class SyncTest {

    private String sharedResource;

    public static void main(String[] args) {

        SyncTest syncTest = new SyncTest();

        System.out.println("SyncTest start");

        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                syncTest.setSharedResource("resource1");
            }
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                syncTest.setSharedResource("resource2");
            }
        }).start();

        System.out.println("SyncTest end");
    }

    public synchronized void setSharedResource(String resource) {
        sharedResource = resource;

        try {
            long sleep = (long) (Math.random() * 100);
            Thread.sleep(sleep);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        if (!Objects.equals(sharedResource, resource)) {
            System.out.println(sharedResource + " is not same as " + resource);
        }
    }
}
  • 위 코드를 보면 2개의 스레드에서 공유 자원에 접근하는 것을 볼 수 있다.
  • 출력 결과를 확인하면 아무것도 출력되지 않는 것을 알 수 있다.

 

메소드에 synchronized를 선언하는 방식은 문제가 있는데 상호 배제 원리를 사용하여, 스레드가 메소드에 접근해 메소드가 종료되기 전까지 락이 걸려 다른 스레드가 접근하지 못한다.

  • 이는 성능 문제로 이어진다.
  • 이런 문제를 완화시키기 위해서 메소드 전체가 아닌, 락을 필요한 부분에만 걸면 된다.
  • 결국 락이 필요한 코드에만 synchronized를 사용하면 된다.

2. 필요한 코드에만 synchronzied 선언

만약 list1를 사용하는 코드에는 lock이 필요하지만, list2를 사용하는 코드에는 lock이 필요없다고 가정해보자.

public void add(int value) {

        /*
          code that lock is not needed
         */
        if (!list2.contains(value)) {
            list2.add(value);
        }

        synchronized (this) {
            if (!list1.contains(value)) {
                list1.add(value);
            }
        }
    }
  • 위 코드에서는 synchronized(this) 를 통해 필요한 코드 부분에만 lock을 걸었다.

  • 그런데 왜 this를 사용하는 걸까?

    • synchronized block에 인자값은 lock을 걸 대상이다.
    • synchronized(this)로 표기하면 해당 객체 안에 있는 모든 synchronized block에 lock이 걸린다.

     

전체 코드

public class MethodSyncTest {

    private List<Integer> list1 = new ArrayList<>();
    private List<Integer> list2 = new ArrayList<>();

    public static void main(String[] args) {

        MethodSyncTest mSyncTest = new MethodSyncTest();

        System.out.println("mSyncTest start");

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                mSyncTest.add(i);
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                mSyncTest.add(i);
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join(3000);
            thread2.join(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("mSyncTest end");

        System.out.println("list1 size : " + mSyncTest.list1.size()); // 항상 100
        System.out.println("list2 size : " + mSyncTest.list2.size()); // 종종 100 이 넘는다.
    }

    public void add(int value) {

        /*
          code that Sync is not needed
         */
        if (!list2.contains(value)) {
            list2.add(value);
        }

        synchronized (this) {
            if (!list1.contains(value)) {
                list1.add(value);
            }
        }
    }
}
  • 코드를 실행해보면, list1의 size는 항상 100이 나오는 반면, list2의 size는 종종 100이 넘는다.

     

2-1. synchronzied(this) 와 synchronized(.class)의 차이점

public class SyncCompareTest {

    private static List<Integer> list = new ArrayList<>();

    public static void main(String[] args) {

        SyncCompareTest t1 = new SyncCompareTest();
        SyncCompareTest t2 = new SyncCompareTest();

        System.out.println("syncCompareTest start");

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                t1.add(i);
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                t2.add(i);
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join(3000);
            thread2.join(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("syncCompareTest end");

        System.out.println("list1 size : " + list.size());
    }

}
  • 위 코드에서는 add() 메소드를 어떻게 만들어야 list.size()의 값이 100이 나오도록 보장할 수 있을까?

 

(this) 방식을 먼저 살펴보자.

public void add(int value) {

        synchronized (this) {
            if (!list.contains(value)) {
                list.add(value);
            }
        }
    }
  • 위 코드는 size의 값이 100임을 보장하지 못한다.
  • 여러개의 스레드객체를 생성하는 경우에는 t1, t2가 서로 다른 this를 가지기 때문에 t1, t2의 동기화되지 못한다.

 

그럼 (.class) 방식은 어떨까?

 public void add(int value) {

        synchronized (SyncCompareTest.class) {
            if (!list.contains(value)) {
                list.add(value);
            }
        }
    }
  • .class에 대한 동기화는 class 자체에 대한 동기화이므로 .class를 통해서 생성하는 모든 스레드에 대해 동기화가 이루어진다.

0개의 댓글