Theory
JAVA/Kotlin
Synchronized

@Async

  • Spring에서 비동기 처리를 위한 어노테이션
  • Public Method에만 이용이 가능하며 해당 메서드는 반드시 비동기 실행이 되도록 보장한다.
  • @Async 동작을 별도로 설정하지 않으면 Proxy 모드로 동작하게 된다.
  • Proxy 모드로 동작하게 되면 동일한 클래스 내의 메서드 호출동기적으로 동작하게 된다.
    • Proxy 모드 :
    • 프록시 객체를 생성하여 실제 객체를 대신 호출한다.
    • AOP Proxy을 통해 비동기 실행을 구현한다.
예시코드
@Configuration
@EnableAsync
class AsyncConfig {
    @Bean(name = "threadPoolTaskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(50); //
        return executor;
    }
}
 
class AsyncTmp {
    @Async
    public void asyncMethod() {
        System.out.println("비동기 메서드 시작");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("비동기 메서드 종료");
    }
 
    public void main() {
        System.out.println("메인 메서드 시작");
        asyncMethod();
        System.out.println("메인 메서드 종료");
    }
}

Synchronized

synchronized 키워드를 메서드에 쓰면 해당 메서드는 임계영역이 되는거다.

⚠️

Synchronized의 문제점
blocking을 사용하여 공유 객체를 동기화하기 때문에 특정 스레드에 락을 걸었을 경우 이 락으로 인해서 무한히 대기하는 스레드가 생길 수 있다. 이러한 문제로 인해서 성능저하가 일어날 수 있다.

class SharedObject {
    private int count = 0;
 
    public void plus() {
      synchronized (this) {
        count++;
      }
    }
 
    public void minus() {
      synchronized (this) {
        count--;
      }
    }
    public int getCount() {
        return count;
    }
}

synchronized 블럭과 synchronized 키워드의 차이점은 synchronized 블럭해당 블럭에 대해서만 임계영역을 설정할 수 있지만 synchronized 키워드메서드 전체에 대해서 임계영역을 설정한다.
예를 들어서 아래 코드는 race condition이 발생할 수 있다.

class SharedObject {
    private int count = 0;
 
    public void plus() {
      synchronized (this) {
        count++;
      }
      count++;
    }
 
    public void minus() {
      synchronized (this) {
        count--;
      }
      count--;
    }
    public int getCount() {
        return count;
    }
}

static synchronized 블럭

static method 안에 synchronized block을 지정하면 클래스 단위로 Lock이 발생한다. block의 인자로 정적 인스턴스나 클래스를 사용한다.

public class MyClass {
 
    private static int count = 0;
 
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                synchronized (MyClass.class) {
                    count++;
                }
            }
        });
 
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                synchronized (MyClass.class) {
                    count--;
                }
            }
        });
 
        thread1.start();
        thread2.start();
 
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        System.out.println(count);
    }
}

Volatile

가시성 문제를 해결하기 위한 키워드이다.
가시성 문제란 멀티 스레드 환경에서 각 CPU는 메인 메모리 변수 값을 참조하지 않고, 각 CPU의 캐시 영역에서 메모리를 참고한다. 멀티 스레드 환경에서 메인 메모리와 CPU의 캐시값이 다른 경우 발생할 수 있는 문제를 가시성 문제라고 한다.

non-volatile 변수메인 메모리로부터 CPU 캐시에 값을 복사하여 작업을 수행한다. 여러 스레드가 각각 non-volatile 값을 읽을 때 CPU 캐시에 저장된 값이 다를 경우, 값의 불일치 발생(가시성 문제)한다.
volatile 키워드가 붙은 자원은 read, write 작업이 CPU Cache Memory가 아닌 Main Memory에서 이루어진다. 메인 메모리에 저장하고 읽어오기 때문에 값의 불일치(가시성 문제)를 해결할 수 있다.

아래의 코드를 간단하게 살펴보면 다음 코드는 stopRequested 변수가 1초 뒤에 true로 변경되고 금방 종료될 것처럼 보이지만 실제로는 종료되지 않거나 아주 오래 걸린다. 이유를 간단하게 설명하자면 main Thread는 1번 CPU에서 실행되고 backgroundThread는 2번 CPU에서 실행되는데 1번 CPU의 캐시 공간에는 stopRequested 변수가 1초뒤 true로 변경되지만 2번 CPU의 캐시 공간에는 stopRequested 변수가 변경되지 않은 상태로 남아있기 때문에 backgroundThread는 계속해서 while문을 돌게된다.

public class Main {
 
    private static boolean stopRequested = false;
 
    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested) {
                i++;
            }
            System.out.println("i: " + i);
        });
        backgroundThread.start();
 
        Thread.sleep(1000);
        stopRequested = true;
    }
}

이러한 문제를 해결하기 위해서 stopRequested 변수에 volatile 키워드를 붙여주면 된다.

public class Main {
 
    private static volatile boolean stopRequested  = false;
 
    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested) {
                i++;
            }
            System.out.println("i: " + i);
        });
        backgroundThread.start();
 
        Thread.sleep(1000);
        stopRequested = true;
    }
}

Atomic type

자바의 concurrency API에서 제공되는 타입이다.
해당 타입은 blocking 방식이 아닌 non-blocking 방식으로 원자성을 보장하며 사용시 내부적으로 CAS(Compare-And-Swap) 알고리즘을 사용해서 Lock없이 동기화로 처리할 수 있다.

ℹ️

원자성
어떠한 작업이 실행될 때, 언제나 완전하게 진행되어 종료되거나 그럴 수 없는 경우 실행을 하지 않는 경우를 말한다.

public class AtomicBooleanExample {
 
    private static AtomicBoolean flag = new AtomicBoolean(false);
 
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            flag.set(true);
        });
 
        Thread thread2 = new Thread(() -> {
            while (!flag.get()) {
                // do something
            }
            System.out.println("flag is set to true");
        });
 
        thread1.start();
        thread2.start();
 
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class AtomicReferenceExample {
 
    private static AtomicReference<String> name = new AtomicReference<>("John");
 
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            name.set("Alice");
        });
 
        Thread thread2 = new Thread(() -> {
            String currentName = name.get();
            System.out.println("current name: " + currentName);
        });
 
        thread1.start();
        thread2.start();
 
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
 

CAS Algorithm

  • 인자로 기존값(Compared Value)과 변경할 값(Exchnaged Value)을 전달한다.
  • 기존값이 현재 메모리가 가지고 있는 값과 같다면 변경할 값을 반영하여 true를 반환한다.
  • 반대로 기존값이 현재 메모리가 가지고 있는 값과 다르다면 값을 반영하지 않고 false를 반환한다.
public class AtomicInteger extends Number implements java.io.Serializable { 
    private volatile int value; 
 
    public final int incrementAndGet() { 
        int current; 
        int next; 
        do { 
            current = get(); next = current + 1; 
        } 
        while (!compareAndSet(current, next)); 
        
        return next; 
    } 
        
    public final boolean compareAndSet(int expect, int update) { 
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 
    } 
}
 

Reference