Language/Java

[Java] Synchronized 키워드를 알아보겠습니다.

강서월 2025. 1. 3. 17:43

Synchronized 키워드

Java 는 synchronized 키워드를 사용하여 멀티 스레드 환경에서 스레드 간 동기화와 상호 배제를 보장하기 위해 모니터 락(Monitor Lock)을 내부적으로 사용합니다. 이 락은 하나의 스레드만 임계 구역에 접근하도록 하여 데이터의 무결성을 보장합니다.

 

이 과정에서 JVM 은 다음 작업을 수행합니다.

  1. 락을 얻기 위한 경쟁 처리
  2. 락 상태 업데이트
  3. 락 소유자가 해제될 때 다른 대기 중인 스레드에게 락을 전달

synchronized 키워드는 다음과 같은 경우에 사용할 수 있습니다.

  1. synchronized method
  2. static synchronized method
  3. synchronized block

1. Synchronized method

Synchronized Method인스턴스 단위로 락을 걸어 synchronized가 선언된 메서드 실행 중 다른 스레드가 해당 인스턴스의 synchronized 메서드에 접근하지 못하도록 제한합니다.

예제 1 : 하나의 인스턴스를 공유하는 경우
public class Method {
    public static void main(String[] args) {
        Method method1 = new Method(); //인스턴스 생성

        Thread thread1 = new Thread(() -> {
            System.out.println("스레드1 시작 " + LocalDateTime.now());
            method1.syncMethod1("스레드1");
            System.out.println("스레드1 종료 " + LocalDateTime.now());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("스레드2 시작 : " + LocalDateTime.now());
            method1.syncMethod2("스레드2");
            System.out.println("스레드2 종료 " + LocalDateTime.now());
        });

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


    private synchronized void syncMethod1(String msg) {
        System.out.println(msg + "의 syncMethod1 실행중 " + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private synchronized void syncMethod2(String msg) {
        System.out.println(msg + "의 syncMethod2 실행중 " + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
실행 결과 :
스레드1 시작 2025-01-03T16:02:25.561005
스레드2 시작 : 2025-01-03T16:02:25.560922
스레드1의 syncMethod1 실행중 2025-01-03T16:02:25.563279
스레드1 종료 2025-01-03T16:02:28.574908
스레드2의 syncMethod2 실행중 2025-01-03T16:02:28.574908
스레드2 종료 2025-01-03T16:02:31.577862

syncMethod1과 syncMethod2는 같은 객체(method1)에 대해 락을 공유하므로, thread1이 메서드를 종료하기 전까지 thread2는 대기합니다.

 

예제 2 : 서로 다른 객체를 참조하는 경우
public class Method {
    public static void main(String[] args) {
        Method method1 = new Method();
        Method method2 = new Method(); // 별도 인스턴스 생성

        Thread thread1 = new Thread(() -> {
            System.out.println("스레드1 시작 " + LocalDateTime.now());
            method1.syncMethod1("스레드1");
            System.out.println("스레드1 종료 " + LocalDateTime.now());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("스레드2 시작 " + LocalDateTime.now());
            method2.syncMethod2("스레드2");
            System.out.println("스레드2 종료 " + LocalDateTime.now());
        });

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

 

실행 결과 :
스레드1 시작 2025-01-03T16:03:45.752625
스레드2 시작 : 2025-01-03T16:03:45.752969
스레드2의 syncMethod2 실행중 2025-01-03T16:03:45.755568
스레드1의 syncMethod1 실행중 2025-01-03T16:03:45.755413
스레드1 종료 2025-01-03T16:03:48.774111
스레드2 종료 2025-01-03T16:03:48.773879

method1과 method2는 서로 다른 객체이므로 락이 공유되지 않습니다. 따라서 두 스레드가 동시에 실행됩니다.

 

 

인스턴스 단위로 락을 공유하지만, synchronized 키워드가 붙은 메소드에 대해서만 lock 을 공유합니다.

예제3 : 하나의 인스턴스가 synchronized 가 붙은 메소드를 호출하는 스레드들과 일반 메소드를 호출하는 경우
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

public class Method {

    public static void main(String[] args) {

        Method method = new Method();
        Thread thread1 = new Thread(() -> {
            System.out.println("스레드1 시작 " + LocalDateTime.now());
            method.syncMethod1("스레드1");
            System.out.println("스레드1 종료 " + LocalDateTime.now());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("스레드2 시작 " + LocalDateTime.now());
            method.syncMethod2("스레드2");
            System.out.println("스레드2 종료 " + LocalDateTime.now());
        });

        Thread thread3 = new Thread(() -> {
            System.out.println("스레드3 시작 " + LocalDateTime.now());
            method.method3("스레드3");
            System.out.println("스레드3 종료 " + LocalDateTime.now());
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }

    private synchronized void syncMethod1(String msg) {
        System.out.println(msg + "의 syncMethod1 실행중" + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private synchronized void syncMethod2(String msg) {
        System.out.println(msg + "의 syncMethod2 실행중" + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void method3(String msg) {
        System.out.println(msg + "의 method3 실행중" + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
결과 :
스레드2 시작 2025-01-03T16:07:21.200069
스레드3 시작 2025-01-03T16:07:21.200079
스레드1 시작 2025-01-03T16:07:21.200314
스레드2의 syncMethod2 실행중2025-01-03T16:07:21.203064
스레드3의 method3 실행중2025-01-03T16:07:21.203124
스레드3 종료 2025-01-03T16:07:26.222513
스레드2 종료 2025-01-03T16:07:26.222513
스레드1의 syncMethod1 실행중2025-01-03T16:07:26.222513
스레드1 종료 2025-01-03T16:07:31.228128

하나의 객체에서 여러 스레드가 synchronized 메서드를 호출하면 락이 걸려 대기합니다. 하지만, 일반 메서드는 락이 걸리지 않아 동시에 실행되는 것을 알 수 있습니다.

 

2. static synchronized method

static 키워드가 포함된 synchronized 메소드는 인스턴스가 아닌 클래스 단위로 lock 을 공유합니다.

예제 1 : static synchronized method 가 붙은 메소드를 여러 스레드가 실행하는 경우
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

public class StaticMethod {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("스레드1 시작 " + LocalDateTime.now());
            syncStaticMethod1("스레드1");
            System.out.println("스레드1 종료 " + LocalDateTime.now());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("스레드2 시작 " + LocalDateTime.now());
            syncStaticMethod2("스레드2");
            System.out.println("스레드2 종료 " + LocalDateTime.now());
        });

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

    public static synchronized void syncStaticMethod1(String msg) {
        System.out.println(msg + "의 syncStaticMethod1 실행중" + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static synchronized void syncStaticMethod2(String msg) {
        System.out.println(msg + "의 syncStaticMethod2 실행중" + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
실행 결과 :
스레드1 시작 2025-01-03T16:16:06.664  
스레드2 시작 2025-01-03T16:16:06.664269  
스레드1의 syncStaticMethod1 실행중 2025-01-03T16:16:06.665145  
스레드1 종료 2025-01-03T16:16:11.679765  
스레드2의 syncStaticMethod2 실행중 2025-01-03T16:16:11.679779  
스레드2 종료 2025-01-03T16:16:16.683827

syncStaticMethod1과 syncStaticMethod2는 클래스 단위로 락을 공유하므로, 한 메서드가 실행 중이면 다른 메서드는 대기합니다.

 

클래스 단위에 거는 lock 과 인스턴스 단위에 거는 lock 은 공유가 되지 않기 때문에 혼용해서 사용하면 동기화 이슈가 발생하게 됩니다.

예제 2 : static synchronized method 와 synchronized method 를 혼용하는 경우
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

public class StaticMethod {
    public static void main(String[] args) {

        StaticMethod method = new StaticMethod();

        Thread thread1 = new Thread(() -> {
            System.out.println("스레드1 시작 " + LocalDateTime.now());
            syncStaticMethod1("스레드1");
            System.out.println("스레드1 종료 " + LocalDateTime.now());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("스레드2 시작 " + LocalDateTime.now());
            syncStaticMethod2("스레드2");
            System.out.println("스레드2 종료 " + LocalDateTime.now());
        });

        Thread thread3 = new Thread(() -> {
            System.out.println("스레드3 시작 " + LocalDateTime.now());
            method.syncMethod3("스레드3");
            System.out.println("스레드3 종료 " + LocalDateTime.now());
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }

    public static synchronized void syncStaticMethod1(String msg) {
        System.out.println(msg + "의 syncStaticMethod1 실행중" + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static synchronized void syncStaticMethod2(String msg) {
        System.out.println(msg + "의 syncStaticMethod2 실행중" + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void syncMethod3(String msg) {
        System.out.println(msg + "의 StaticMethod3 실행중" + LocalDateTime.now());
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
결과:
스레드3 시작 2025-01-03T16:16:06.664
스레드2 시작 2025-01-03T16:16:06.664269
스레드1 시작 2025-01-03T16:16:06.663121
스레드2의 syncStaticMethod2 실행중2025-01-03T16:16:06.665209
스레드3의 StaticMethod3 실행중2025-01-03T16:16:06.665145
스레드2 종료 2025-01-03T16:16:11.679765
스레드1의 syncStaticMethod1 실행중2025-01-03T16:16:11.679779
스레드3 종료 2025-01-03T16:16:11.682843
스레드1 종료 2025-01-03T16:16:16.683827

일반 synchronized 메소드를 추가하면, static synchronized 메소드를 사용하는 스레드1 과 스레드2 간에는 동기화가 잘 지켜지는 것을 알 수 있습니다. 그러나 synchronzied 메소드를 사용한 스레드3은 개발자가 의도한 동기화가 잘 지켜지지 않았습니다.

 

3. synchronized block

인스턴스의 block 단위로 lock 을 걸며, 2가지의 사용방법이 있습니다.

  • synchronized(this)
  • synchronized(Object)

Synchronized(this) 를 사용하면, 모든 synchronized block 에 lock 이 걸립니다.

예제 1: Synchronized(this) 를 사용하는 경우
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

public class Block1 {
    public static void main(String[] args) {
        Block1  block = new Block1();

        Thread thread1 = new Thread(() -> {
            System.out.println("스레드1 시작 " + LocalDateTime.now());
            block.syncBlockMethod1("스레드1");
            System.out.println("스레드1 종료 " + LocalDateTime.now());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("스레드2 시작 " + LocalDateTime.now());
            block.syncBlockMethod2("스레드2");
            System.out.println("스레드2 종료 " + LocalDateTime.now());
        });

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

    private void syncBlockMethod1(String msg) {
        synchronized (this) {
            System.out.println(msg + "의 syncBlockMethod1 실행중" + LocalDateTime.now());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void syncBlockMethod2(String msg) {
        synchronized (this) {
            System.out.println(msg + "의 syncBlockMethod2 실행중" + LocalDateTime.now());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

결과 :
스레드1 시작 2025-01-03T16:26:08.656798  
스레드2 시작 2025-01-03T16:26:08.656862  
스레드1의 method1 실행중 2025-01-03T16:26:08.659802  
스레드1 종료 2025-01-03T16:26:11.678866  
스레드2의 method2 실행중 2025-01-03T16:26:11.678865  
스레드2 종료 2025-01-03T16:26:14.680457

여러 스레드가 들어와서 서로 다른 synchronized block 을 호출해도 this 를 사용해 자기 자신에 lock 을 걸었기 때문에 인스턴스 자체에 락을 걸어 모든 synchronized 블록을 동기화합니다. 그러나 위 방식은 모든 블럭에 lock 이 걸리기 때문에 상황에 따라 비효율적일 수 있습니다.

 

synchronized(Object) 를 사용하여 블록마다 다른 lock 이 걸리게 할 수 있습니다.

예제 2: synchronized(Object)
public class Block2 {

    private final Object o1 = new Object();
    private final Object o2 = new Object();

    public static void main(String[] args) {

        Block2 block = new Block2();

        Thread thread1 = new Thread(() -> {
            System.out.println("스레드1 시작 " + LocalDateTime.now());
            block.syncBlockMethod1("스레드1");
            System.out.println("스레드1 종료 " + LocalDateTime.now());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("스레드2 시작 " + LocalDateTime.now());
            block.syncBlockMethod2("스레드2");
            System.out.println("스레드2 종료 " + LocalDateTime.now());
        });
        thread1.start();
        thread2.start();
    }

    private void syncBlockMethod1(String msg) {
        synchronized (o1) {
            System.out.println(msg + "의 syncBlockMethod1 실행중" + LocalDateTime.now());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void syncBlockMethod2(String msg) {
        synchronized (o2) {
            System.out.println(msg + "의 syncBlockMethod2 실행중" + LocalDateTime.now());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
결과 :
스레드1 시작 2025-01-03T16:29:24.697035
스레드2 시작 2025-01-03T16:29:24.696902
스레드2의 syncBlockMethod2 실행중2025-01-03T16:29:24.699335
스레드1의 syncBlockMethod1 실행중2025-01-03T16:29:24.699075
스레드1 종료 2025-01-03T16:29:25.719566
스레드2 종료 2025-01-03T16:29:25.718522

스레드1과 스레드2간의 동기화가 지켜지지 않은 것을 확인할 수 있습니다. 따라서 this 가 아닌 o1 과 o2 객체를 만들어 인자로 넘겨주면 동시에 lock 이 걸려야 하는 부분을 따로 지정해줄 수 있습니다.

 

 

위의 내용을 정리하자면 다음과 같습니다.

  • synchronized method 는 인스턴스 단위로 락을 공유합니다.
    • 이 때, synchronized 키워드가 붙은 메소드에 대해서만 락이 걸립니다. (일반 메서드는 동기화가 되지 않습니다.)
  • static synchronized method 는 클래스 단위로 락을 공유합니다.
  • static synchronized method 와 synchronized method 를 혼용하면, 동시성 문제가 야기할 수 있습니다.
  • synchronized block 은 두 가지 방법으로 사용할 수 있습니다.
    • synchronized(this) 를 사용하는 경우 모든 synchronized block 에 락이 걸립니다.
    • synchronized(Object) 를 사용하는 경우 동시에 lock 이 걸려야 하는 부분을 따로 지정할 수 있습니다.

 

 

참조
https://github.com/jvm-hater/java-study/blob/main/2%EC%A3%BC%EC%B0%A8/synchronized.md