자바

Java 동시성 -1- with synchronized

정한_s 2023. 6. 16. 15:56

Java는 처음 부터 다중 스레드 환경을 지원하도록 설계되었습니다. 자바에서 가장 처음 동시성을 제어하기 위해 사용된 구조는 가본적인 ‘synchronized’ 키워드와 wait-notify 메커니즘입니다. 추후에 복잡한 동시성을 제어하기 위해 더 많은 기능들이 생겨났습니다.
주요한 동시성 요소를 가볍게 살펴보고, synchronized의 lock 범위를 중점적으로 설명하겠습니다.  
(이후의 내용은 추후에 다루겠습니다)

java 주요 동시성 요소

  • 스레드 등장 : java 1.0 java의 기본적은 동시성 단위입니다. 쓰레드는 프로세스 내에서 실행되는 독립적인 실행 경로입니다. 각 스레드는 자신만의 스택을 가지지만, 힙 메모리는 공유됩니다. 스레드는 java.lang.Thread 클래스나 java.lang.Runnable 인터페이스를 사용하여 생성할 수 있습니다.
  • 동기화(synchronization) 등장 : java 1.0 synchronized 키워드는 한 번에 하나의 스레드만 공유 자원에 액세스할 수 있게 하여, 데이터 일관성을 보장합니다
  • wait() & notify() 등장 : java1.0 함수가 호출 될때, 스레드가 대상 객체의 고유락을 가지고 있어야 한다. 즉 synchronized 블록 내에서 실행되어야 한다 wait() : Object 클래스에서 제공하는 메소드로, 현재 스레드를 일시적으로 중지하고, 해당 객체의 lock을 해제합니다. 다른 스레드들이 동기화 블록 또는 메소드에 접근할 수 있게 됩니다 notify() : Object 클래스에서 제공하는 메소드로, wait() 상태에 있는 스레드 중 하나를 임의로 선택하여 실행 상태로 변경합니다.
  • Volatile 등장 : java 1.0 → java 5 개선 변수를 스레드에 안전하게 공유하도록 보장합니다. volatile 변수의 모든 읽기 및 쓰기 작업은 메인 메모리에서 직접 수행되며, 따라서 한 스레드에서의 변경 사항이 다른 스레드에 즉시 보여집니다.
  • java.util.concurrent 패키지 등장: java5 이후로 점차 기능 확장 고급 동시성 패턴을 지원하기 위한 클래스와 인터페이스를 제공합니다. 예를 들어, ExecutorService, Future, CountDownLatch, CyclicBarrier, Semaphore, ConcurrentHashMap 등의 클래스와 인터페이스가 있습니다.
  • automic 클래스 등장: java5 원자적 연산을 지원하는 클래스를 제공합니다. 이 클래스들을 사용하면, 복잡한 동기화를 피하고 멀티스레드 환경에서 단일 변수에 대해 원자적 연산을 수행할 수 있습니다.
  • completablefuture 등장 : java 8 Future 인터페이스를 확장하여, 비동기 계산의 완료를 나타내는 클래스입니다. CompletableFuture는 연산이 완료되면 결과를 제공하며, 계산이 아직 완료되지 않았다면 완료될 때까지 기다릴 수 있습니다.
  • reactive streams 등장 : java9 ava 9에서 도입된 Reactive Streams API는 데이터 흐름을 비동기적으로 처리하고, 백 프레셔(Back Pressure)를 지원하는 프로그래밍 패러다임을 제공합니다.

synchronized 정리

synchronized는 4가지 사용방법이 있습니다.

synchronized method, synchronized block, static synchronized method, static synchronized block 총 4가지로 입니다.

static이 붙지 않은 것은 instance lock 으로 static이 붙은 것은 class lock 으로 불리기도 합니다.

각각 사용법과 lock 적용 범위를 설명하겠습니다.

1. synchronized method

synchronized method는 인스턴스에 대하여 lock을 건다. 다음과 같은 상황을 확인한다

class TestSync {

    public static synchronized void classLock(String s){
        System.out.println("class lock "+s);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("class unLock "+s);

    }
    public synchronized void instanceLock(String s){
        System.out.println("instance lock "+s);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("instance unLock "+s);
    }

    public void nonLock(String s){
        System.out.println("no lock"+s);
    }

}

public class Main {
    public static void main(String[] args) throws Exception {
        TestSync a = new TestSync();
        TestSync b = new TestSync();
        Thread thread1 = new Thread(()->{
            a.instanceLock("1");
        });
        Thread thread2 = new Thread(()->{
            b.instanceLock("2");
        });
        Thread thread3 = new Thread(()->{
            b.nonLock("3");
        });
        thread1.start();
        Thread.sleep(500);
        thread2.start();
        Thread.sleep(500);
        thread3.start();

    }
}

----
결과 

instance lock 1
instance lock 2
instance unLock 1
instance unLock 2

결과를 보았을 때, 고유한 객체에 락이 존재하고, 해당 락은 고유한 객체의 synchronized 메소드에만 적용된다는 것을 알 수 있다.

2. static synchronized method

static synchronized method는 클래스에 대하여 lock을 건다. 다음과 같은 상황을 보자

class TestSync {

    public static synchronized void classLock(String s){
        System.out.println("class lock "+s);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("class unLock "+s);

    }
    public synchronized void instanceLock(String s){
        System.out.println("instance lock "+s);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("instance unLock "+s);
    }

    public void nonLock(String s){
        System.out.println("no lock"+s);
    }

}

public class Main {
    public static void main(String[] args) throws Exception {
        TestSync a = new TestSync();
        TestSync b = new TestSync();
        Thread thread1 = new Thread(()->{
            a.classLock("1");
        });
        Thread thread2 = new Thread(()->{
            b.classLock("2");
        });
        Thread thread3 = new Thread(()->{
            a.nonLock("4");
            a.instanceLock("3");
        });
        thread1.start();
        Thread.sleep(500);
        thread2.start();
        Thread.sleep(500);
        thread3.start();

    }
}
-----

class lock 1
no lock4
instance lock 3 
class unLock 1
class lock 2
instance unLock 3
class unLock 2

클래스 락은 모든 객체에서 적용되기 때문에 thread1이 끝난 이후에 thread2가 시작되었다. 또한 인스턴스의 lock과 클래스 단위의 lock은 공유하지 않는다. 위의 결고를 보았을 때 thread1이 끝나기를 thread2가 기다리고 있지만 thread3은 다른 쓰레드와 상관없이 시행되었다.

3. synchronized block

synchronized block은 인스턴스의 block단위로 lock을 건다. 이때, lock객체를 지정해줘야한다.

class TestSync {

    public static void classLock(String s) {
        System.out.println("class function start "+s);
        synchronized (TestSync.class){
            System.out.println("class lock "+s);
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("class unLock "+s);
        }
        System.out.println("class function start "+s);

    }
    public void instanceLock(String s){
        System.out.println("instance function start "+s);
        synchronized (this){
            System.out.println("instance lock "+s);
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("instance unLock "+s);
        }
        System.out.println("instance function end "+s);

    }

    public void nonLock(String s){
        System.out.println("no lock"+s);
    }

}

public class Main {
    public static void main(String[] args) throws Exception {
        TestSync a = new TestSync();
        TestSync b = new TestSync();
        Thread thread1 = new Thread(()->{
            a.instanceLock("1");
        });
        Thread thread2 = new Thread(()->{
            b.instanceLock("2");
        });
        Thread thread3 = new Thread(()->{
            a.nonLock("3");
        });
        thread1.start();
        Thread.sleep(500);
        thread2.start();
        Thread.sleep(500);
        thread3.start();

    }
}
----
결과 
instance function start 1
instance lock 1
instance function start 2
instance lock 2
no lock3
instance unLock 1
instance function end 1
instance unLock 2
instance function end 2

결과 처럼 function이 수행되고 인스턴스 synchronized block을 만났을 때 동기화가 되는 것을 알 수 있다. instance lock은 객체마다 가지고 있으므로 대기하지 않는다

4. static synchronized block

static synchronized block은 클래스 block단위로 lock을 건다. 이때, lock .class를 지정해줘야한다.

class TestSync {

    public static void classLock(String s) {
        System.out.println("class function start "+s);
        synchronized (TestSync.class){
            System.out.println("class lock "+s);
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("class unLock "+s);
        }
        System.out.println("class function end "+s);

    }
    public void instanceLock(String s){
        System.out.println("instance function start "+s);
        synchronized (this){
            System.out.println("instance lock "+s);
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("instance unLock "+s);
        }
        System.out.println("instance function end "+s);

    }

    public void nonLock(String s){
        System.out.println("no lock"+s);
    }

}

public class Main {
    public static void main(String[] args) throws Exception {
        TestSync a = new TestSync();
        TestSync b = new TestSync();
        Thread thread1 = new Thread(()->{
            a.classLock("1");
        });
        Thread thread2 = new Thread(()->{
            b.classLock("2");
        });
        Thread thread3 = new Thread(()->{
            a.nonLock("3");
        });
        thread1.start();
        Thread.sleep(500);
        thread2.start();
        Thread.sleep(500);
        thread3.start();

    }
}

------
결과
class function start 1
class lock 1
class function start 2
no lock3
class unLock 1
class function end 1
class lock 2
class unLock 2
class function end 2

결과 처럼 function이 수행되고 클래스(.class) synchronized block을 만났을 때 동기화가 되는 것을 알 수 있다. class lock은 모든 객체가 공유하므로 threa1을 thread2가 대기한다.