개발 공부/Java

이펙티브 자바 아이템 32 - 제네릭과 가변인수를 함께 쓸 때는 신중하라 - 완벽 공략

개발인생 2022. 12. 20. 10:16
반응형

아이템 32 - 제네릭과 가변인수를 함께 쓸 때는 신중하라 - 완벽 공략

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

ThreadLocal

ThreadLocal 은 쓰레드 지역, 쓰레드 범위의 변수이다.

ThreadLocal 의 개념없이 여러 쓰레드에서 어떤 객체가 가지고 있는 멤버 변수를 사용한다면

쓰레드 안전성을 신경써서 코딩을 해야한다.

그렇지 않으면 경합 또는 경쟁조건 (Race-Condition) , 교착상태 (deadlock) , Livelock 등이 발생할 수 있다.

ThreadLocal 을 사용하면 쓰레드 전용 지역 변수 를 만들어 문제를 해결할 수 있다.

import java.text.SimpleDateFormat;

public class ThreadLocalExample implements Runnable {

   private SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd HHmm");

   public static void main(String[] args) throws InterruptedException {
      ThreadLocalExample obj = new ThreadLocalExample();
      for (int i = 0; i < 10; i++) {
         Thread t = new Thread(obj, "" + i);
         Thread.sleep(new Random().nextInt(1000));
         t.start();
      }
   }

   @Override
   public void run() {
      System.out.println("Thread Name= " + Thread.currentThread().getName() + " default Formatter = " + formatter.get().toPattern());
      try {
         Thread.sleep(new Random().nextInt(1000));
      } catch (InterruptedException e) {
         e.printStackTrace();
      }

      formatter = new SimpleDateFormat();

      System.out.println("Thread Name= " + Thread.currentThread().getName() + " formatter = " + formatter.get().toPattern());
   }
}

위의 코드에서 formatter 객체 자체는 쓰레드 안전하지 않다.

즉, 여러 쓰레드에서 동시 다발적으로 접근해 변경했을 떄 다른 쓰레드에 전파될 수 있다는 뜻이다.

위의 코드에서는 쓰레드에서 formatter = new SimpleDateFormat(); 코드를 실행해 formatter 필드의 값을 변경한다.

이때 변경된 formatter 필드가 다른 쓰레드에도 영향을 주게된다.

public class ThreadLocalExample implements Runnable {

    // SimpleDateFormat is not thread-safe, so give one to each thread
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(obj, "" + i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name= " + Thread.currentThread().getName() + " default Formatter = " + formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        formatter.set(new SimpleDateFormat());

        System.out.println("Thread Name= " + Thread.currentThread().getName() + " formatter = " + formatter.get().toPattern());
    }
}

ThreadLocal 을 사용해 문제를 해결할 수 있다.

private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

ThreadLocal<SimpleDateFormat> 처럼 ThreadLocal제네릭 타입 을 사용해 어떤 타입을 담을 것인지 선언한다.

사용할 때는 formatter.get() 처럼 get() 을 사용해 꺼낸다.

값을 넣어줄 때는 formatter.set() 처럼 set() 을 사용한다.

위의 코드를 실행해보면 다른 쓰레드에 영향을 받지않고 formatter 의 값을 변경할 수 있다.

ThreadLocal 을 사용하면 synchronized 없이 멀티 쓰레드 환경에서 안전하게 쓰레드 범위 에 해당하는 변수를 만들어 사용할 수 있다.

ThreadLocal 은 어플리케이션이 실행되는 그 어느 순간에도 해당 쓰레드 내에서는 공용된 저장소 이다.

즉, ThreadLocal 변수를 파라미터로 전달하지 않아도 된다는 뜻이다.

대표적인 예로 스프링의 트랜젝션 관리ThreadLocal 을 활용해 하고있다.


ThreadLocalRandom

ThreadLocalRandom 에 대해서 이해하려면 Random 에 대해서 알아야한다.

public class RandomExample {

    public static void main(String[] args) {
        Random random = new Random();
        System.out.println(random.nextInt(10));
    }

    private int value;

    public synchronized int compareAndSwap(int expectedValue, int newValue)
    {
        int readValue = value;
        if (readValue == expectedValue)
            value = newValue;
        return readValue;
    }
}

Random 클래스는 내부적으로 nextInt() 등과 같이 next 가 들어간 메서드 호출마다

// Random 클래스 내부
    protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            oldseed = seed.get();
            nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
    }

내부적으로 next 메서드를 호출하게 된다.

코드를 보면 AtomicLong 이 있는데

AtomicLongLong 을 표현하는 레퍼런스이다.

Atomic 이 붙은 클래스들은 java.util.concurrent 패키지에 들어있는 멀티 쓰레드 환경에 필요한 유틸리티 중 하나이다.

lock 을 사용하지 않고 멀티 쓰레드 환경 에서 안전하게 사용할 수 있는 클래스이다.

lock 을 사용하는 메커니즘은 해당 메서드에 들어가기 위해서는 무조건 열쇠를 가지고 들어가야 하는 매커니즘이다.

열쇠가 없으면 기다려야한다.

lock 을 기다리고, 주고받는 과정에서 성능에 많은 영향을 끼친다는 단점이 있다.

lock 없이 멀티 쓰레드에서 안전하게 쓸 수 있는 방법은 열쇠를 확인하지 않고 무조건 문을 열고 들어가는 것 이다.

문을 열고 들어갔을 때 누군가가 있으면 다시 문을 닫고 나가고 잠시 후 다시 문을 열고 들어가고 하는 식이다.

lock 을 사용하는 방법을 연쇄적은 락킹 이라고 하고, lock 없이 사용하는 방법을 낙관적인 락킹 이라고 한다.

Atomic 클래스들은 낙관적인 락킹 방법을 사용한다.

Atomic 클래스들이 사용하고 있는 로직에 걸맞는 이름은 compareAndSwap 이다.

    public synchronized int compareAndSwap(int expectedValue, int newValue)
    {
        int readValue = value;
        if (readValue == expectedValue)
            value = newValue;
        return readValue;
    }

단순하게 보면 내가 원래 가지고 있었어야 했던 값을 가지고 있다면 내가 원하는 값으로 수정하는 것이다.

내가 기대했던 상태와 일치하면 내가 작업을 실행해도 된다 판단하는 것이고,

기대했던 상태와 일치하지 않는다면 다른 쓰레드가 값을 바꿧다는 것이므로 작업에 실패하게 된다.

작업에 실패했을 때 할 수 있는 행동에는 예외를 던지거나 다시 시도해보는 등 여러가지 방법이 있다.

이렇게 동작하기 때문에 멀티 쓰레드 환경에서 Random 의 인스턴스가 공유되서 사용이되고 next() 가 많이 호출되면

next() 내의 compareAndSet 메서드가 실패하는 경우가 생긴다.

즉, 둘 중 어느 한 쓰레드는 반드시 실패하다 재시도를 하는 경우가 발생해 성능에 조금이라도 문제가 발생할 수 있다.

public class RandomExample {

    public static void main(String[] args) {
        ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
        System.out.println(threadLocalRandom.nextInt(10));
    }

    private int value;

    public synchronized int compareAndSwap(int expectedValue, int newValue)
    {
        int readValue = value;
        if (readValue == expectedValue)
            value = newValue;
        return readValue;
    }
}

만약 짧은 시간내에 여러 쓰레드에서 Random 클래스의 next() 가 많이 호출되는 상황이라면

Random 클래스 대신 한 쓰레드 내에서만 사용되는 ThreadLocalRandom 을 사용하자.

ThreadLocalRandom.current() 을 호출하면 현재 쓰레드에 할당되어있는 ThreadLocalRandom 을 가져오게 된다.

반응형