본문 바로가기
스터디/이펙티브 자바

[이펙티브 자바] 10. 동시성

by 디토20 2022. 10. 12.
반응형

 

 

[이펙티브 자바] 10. 동시성

 

 

아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라

  • synchronized 키워드는 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장한다.
  • 즉, 객체를 하나의 일관된 상태에서 다른 일관된 상태로 변화시킨다.
  • long double 외의 변수를 읽고 쓰는 동작은 원자적(atomic)이다. 이 말을 듣고 "성능을 높이려면 원자적 데이터를 읽고 쓸 때는 동기화하지 말아야겠다"고 생각하기 쉬운데, 아주 위험한 발상이다.
  • 동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다.
  • 공유 중인 가변 데이터를 비록 원자적으로 읽고 쓸 수 있을지라도 동기화에 실패하면 처참한 결과로 이어질 수 있다. 다음 스레드를 중지하는 코드를 살펴보자

 

잘못된 코드 - 이 프로그램은 얼마나 오래 살까?

public class StopThread {
	private static boolean stopRequested;

	public static void main(String[] args) throws InterruptedException {
		Thread backgroundThread = new Thread(() -> {
			int i = 0;
			while (!stopRequested) 
				i ++;
		});
		backgroundThread.start();

		TimeUnit.SECONDS.sleep(1);
		stopRequested = true;
	}
}

 

  • 이 스레드는 1초후에 중지될 것처럼 보이지만 동기화 하지 않으면 메인 스레드가 수정한 값을 백그라운드 스레드가 언제 보게 될지 보장할 수 없다.
  • 동기화가 빠지면 가상 머신이 아래와 같이 최적화를 수행할 수도 있는 것이다.
// 원래 코드
while (!stopRequested)
   i++;

// 최적화 된 코드
if (!stopRequested)
    while (true)
        i++;

 

 

 

적절히 동기화한 코드

public class StopThread {
	private static boolean stopRequested;

	private static synchronized void requestStop() {
		stopRequested = true;
	}
	
	private static synchronized boolean stopRequested() {
		return stopRequested;
	}

	public static void main(String[] args) throws InterruptedException {
		Thread backgroundThread = new Thread(() -> {
			int i = 0;
			while (!stopRequested()) 
				i ++;
		});
		backgroundThread.start();

		TimeUnit.SECONDS.sleep(1);
		requestStop();
	}
}

 

  • stopRequested 필드를 동기화해 접근하면 기대한 1초 후에 종료된다.
  • 쓰기 메서드와 읽기 메서드 모두를 동기화 했음에 주목하자. 쓰기와 읽기 모두가 동기화되지 않으면 동작을 보장하지 않는다.

 

 

더 속도가 빠른 대안

public class StopThread {
	private static volatile boolean stopRequested;

	public static void main(String[] args) throws InterruptedException {
		Thread backgroudThread = new Tread(() -> {
			int i = 0;
			while (!stopRequested) 
				i ++;
		});
		backgroundThread.start();

		TimeUnit.SECONDS.sleep(1);
		stopRequested = true;
	}
}

 

  • stopRequested 필드를 volatile으로 선언하면 동기화를 생략해도 된다.
  • volatile 한정자는 배타적 수행과는 상관 없지만 항상 가장 최근에 기록된 값을 읽게 됨을 보장한다.
  • volatile은 주의해서 사용해야 한다. 예를 들어 다음은 일련번호를 생성할 의도로 작성한 메서드이다.

 

잘못된 코드 - 동기화가 필요하다!

private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
	return nextSerialNumber++;
}

 

  • 위 코드는 문제가 없어 보이지만, 이 역시 동기화가 없이는 올바로 동작하지 않는다.
  • 증가 연산자(++)는 코드상으로는 하나지만 실제로는 nextSerialNumber 필드에 두번 접근한다. 먼저 값을 읽고, 그 다음 새로운 값을 저장하는 것이다.
  • 만약 두번째 스레드가 이 두 접근 사이를 비집고 들어와 값을 읽어가면 첫 번째 스레드와 똑같은 값을 돌려받게 되는데, 프로그램이 잘못된 결과를 계산해내는 이런 오류를 안전 실패라고 한다.
  • generatedSerialNumber 메서드에 synchronized 한정자를 붙이고 nextSerialNumber에 volatile을 제거하면 문제가 해결 된다.
  • 이 과정을 java.util.concurrent.atomic 패키지의 AtomicLong으로 간단하게 구현할 수 있다.
  • 이 패키지에는 락 없이도 스레드 안전한 프로그래밍을 지원하는 클래스들이 담겨있다.

 

private static final AtomicLong nextSerialNumber = 0;

public static int generateSerialNumber() {
	return nextSerialNumber++;
}

 

 

동기화 문제를 피하는 가장 좋은 방법

  • 동기화 문제를 피하는 가장 좋은 방법은 가변 데이터를 공유하지 않는 것이다.
  • 불변 데이터만 공유하거나 아무것도 공유하지 말자.
  • 가변 데이터는 단일 스레드에서만 사용하자

 

 

 

아이템 79. 과도한 동기화는 피하라

  • 과도한 동기화는 성능을 떨어뜨리고, 교착상태에 빠뜨리고, 심지어 예측할 수 없는 동작을 낳기도 한다.
  • 응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안 된다.
  • 예를 들어 동기화된 영역 안에서는 재정의할 수 있는 메서드를 호출하면 안되며, 클라이언트가 넘겨준 함수 객체를 호출해서도 안된다.
  • 이는 교착상태에 빠뜨리거나, 데이터를 훼손할 수도 있다.

 

잘못된 코드들

동기화 블록 안에서 외계인 메서드를 호출한다.

public class ObservableSet<E> extends ForwardingSet<E> {

    public ObservableSet(Set<E> set) {
        super(set);
    }

    private final List<SetObserver<E>> observers = new ArrayList<>();

    public void addObserver(SetObserver<E> observer) {
        synchronized (observers) {
            observers.add(observer);
        }
    }
    
    public boolean removeObserver(SetObserver<E> observer) {
        synchronized (observers) {
            return observers.remove(observer);
        }
    }
    
    private void notifyElementAdded(E element) {
        synchronized (observers) {
            for(SetObserver<E> observer : observers) {
                observer.added(this, element);
            }
        }
    }
    
    @Override
    public boolean add(E element) {
        boolean added = super.add(element);
        if(added) {
            notifyElementAdded(element);
        }
        return added;
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        boolean result = false;
        for (E element : c) {
            result |= add(element); //notifyElementAdded를 호출
        }
        return result;
    }
}


// 다음 프로그램은 0부터 99까지를 출력한다. 
// 평상시에는 앞서와 같이 집합에 추가된 정숫값을 출력하다가, 그 값이 23이면 자기 자신을 제거하는 관찰자를 추가해보자.
public static void main(String[] args) {
	ObservableSet<Integer> set = new ObservableSet<>(New HashSet<>());
	
	set.addObserver(new SetObserver<Integer>() {
		public void added(ObservableSet<Integer> s, Integer e) {
			System.out.println(e);
			if (e == 23) s.removeObserver(this);
		}
	});

	for (int i = 0; i < 100; i++) 
		set.add(i);
}

 

  • 관찰자들은 addObserver와 removeObserver 메서드를 호출해 구독을 신청하거나 해지한다. 눈으로 보기에 ObservableSet은 잘 동작할 것 같다.
  • 이 프로그램은 23까지 출력한 다음 ConcurrentModificationException을 던진다. 관찰자의 added 메서드 호출이 일어난 시점이 notifyElementAdded가 관찰자들의 리스트를 순회하는 도중이기 때문이다.
  • added 메서드는 ObservableSet removeObserver 메서드를 호출하고, 이 메서드는 다시 observers.remove 메서드를 호출한다. 여기서 문제가 발생한다.
  • 리스트에서 원소를 제거하려 하는데, 마침 지금은 이 리스트를 순회하는 도중이다. 즉 허용되지 않은 동작이다.

 

쓸데없이 백그라운드 스레드를 사용하는 관찰자

set.addObserver(new SetObserver<Integer>() {
    public void added(ObservableSet<Integer> s, Integer e) {
        System.out.println(e);
        if (e == 23) {
            ExecutorService exec = Executors.newSingleThreadExecutor();
            try {
                exec.submit(() -> s.removeObserver(this)).get();
            } catch (ExecutionException | InterruptedException ex) {
                throw new AssertionError(ex);
            } finally {
                exec.shutdown();
            }
        }
    }
});

 

  • 이 프로그램을 실행하면 예외는 나지 않지만 교착상태에 빠진다.
  • 백그라운드 스레드가 s.removeObserver를 호출하면 관찰자를 잠그려 시도하지만 메인 스레드가 이미 락을 쥐고 있기 때문에 락을 얻을 수 없다.
  • 그와 동시에 메인 스레드는 백그라운드 스레드가 관찰자를 제거하기만을 기다리는 중이다. 바로 교착상태다!
  • 실제 시스템에서도 동기화된 영역 안에서 외계인 메서드를 호출하여 교착상태에 빠지는 사례는 자주 있다.

 

동기화 영역에서는 가능한 한 일을 적게 하자

  • 락을 얻고, 공유 데이터를 검사하고, 필요하면 수정하고, 락을 놓는다.
  • 오래 걸리는 작업이라면 동기화 영역 바깥으로 옮기는 방법을 찾아보자.

 

동기화의 단점

  • 동기화가 초래하는 진짜 비용은 락을 얻는데 드는 CPU 시간이 아니다.
  • 바로 경쟁하느라 낭비하는 시간, 즉 병렬로 실행할 기회를 잃고, 모든 코어가 메모리를 일관되게 보기 위한 지연시간이 진짜 비용이다.
  • 가상머신의 코드 최적화를 제한한다는 점도 과도한 동기화의 또다른 숨은 비용이다.

 

 

가변 클래스를 사용할 경우

  • 첫 번째, 동기화를 전혀 하지 말고, 그 클래스를 동시에 사용해야 하는 클래스가 외부에서 알아서 동기화하게 하자.
  • 두 번째, 동기화를 내부에서 수행해 스레드 안전한 클래스로 만들자. 단, 클라이언트가 외부에서 객체 전체에 락을 거는것보다 동시성을 월등하게 개선할 수 있을때만 두번째 방법을 선택한다.

 

 

 

아이템 80. 스레드보다는 실행자, 태스크, 스트림을 애용해라

  • java.util.concurrent 패키지는 실행자 프레임워크라고 하는 인터페이스 기반의 유연한 태스크 실행 기능을 담고 있다.
// 작업 큐를 생성한다. 
ExcutorService exec  = Executors.newSingleThreadExcutor();

// 이 실행자에 실행할 태스크(task; 작업)를 넘기는 방법이다.
exec.execute(runnable);

// 실행자를 우아하게 종료시키는 방법이다(이 작업이 실패하면 VM 자체가 종료되지 않을 것이다)
exec.shutdown();

 

실행자 서비스의 주요 기능들

  • 특정 태스크가 완료되기를 기다린다.
  • 태스크 모음 중 아무것 하나(invokeAny()) 혹은 모든 태스크(invokeAll())가 완료되기를 기다린다.
  • 실행자 서비스가 종료하기를 기다린다.(awaitTermination())
  • 완료된 태스크들의 결과를 차례로 받는다.(ExecutorCompletionService())
  • 태스크를 특정 시간에 혹은 주기적으로 실행하게 한다.(ScheduledThreadPoolExecutor 이용)

 

 

그 외 기능

  • 큐를 둘 이상의 스레드가 처리하게 하고 싶다면 간단히 다른 정적 팩터리를 이용하여 다른 종류의 실행자 서비스(스레드 풀)를 생성하면 된다.
  • 평범하지 않은 실행자를 원한다면 ThreadPoolExecutor 클래스를 직접 사용해도 된다. 이 클래스로는 스레드 풀 동작을 결정하는 거의 모든 속성을 설정할 수 있다.
  • 작은 프로그램이나 가벼운 서버라면 Executors.newCachedThreadPool을 사용하라. 특별히 설정할 게 없고 일반적인 용도에 적합하게 동작한다.
  • 무거운 프로덕션 서버에서는 스레드 개수를 고정한 Executors.newFixedThreadPool을 선택하거나 완전히 통제할 수 있는 ThreadPoolExecutor를 직접 사용하는 편이 훨씬 낫다.

 

주의할 점

  • 작업 큐를 손수 만드는 일을 삼가야 하고, 스레드를 직접 다루는것도 삼가야 한다.
  • 스레드를 직접 다루면 Thread가 작업 단위와 수행 메커니즘 역할을 모두 수행하게 된다.
  • 반면 실행자 프레임워크에서는 작업 단위와 실행 메커니즘이 분리된다.
  • 작업 단위는 태스크로 나타내고, 태스크를 수행하는 일반적인 메커니즘이 실행자 서비스다.

 

ForkJoinTask

  • 자바 7이 되면서 실행자 프레임워크는 포크-조인 태스크를 지원하도록 확장되었다. 포크-조인 태스크, 즉 ForkJoinTask의 인스턴스는 작은 하위 태스크로 나뉠 수 있고, ForkJoinPool을 구성하는 스레드들이 이 태스크들을 처리하며, 일을 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와 대신 처리할 수도 있다.
  • 이러한 포크-조인 태스크를 직접 작성하고 튜닝하기란 어려운 일이지만, 포크-조인 풀을 이용해 만든 병렬 스트림을 이용하면 적은 노력으로 그 이점을 얻을 수 있다. 물론 포크-조인에 적합한 형태의 작업이어야 한다.

 

 

 

아이템 81. wait와 notify보다는 동시성 유틸리티를 애용하라

  • wait과 notifiy는 올바르게 사용하기가 아주 까다로우니 고수준 동시성 유틸리티를 사용하자.
  • java.util.concurrent의 고수준 유틸리티는 실행자 프레임워크, 동시성 컬렉션, 동기화 장치로 나뉘어진다.

 

동시성 컬렉션

  • List, Queue, Map 같은 표준 컬렉션 인터페이스에 동시성을 가미해 구현한 고성능 컬렉션이다.
  • 높은 동시성에 도달하기 위해 동기화를 각자의 내부에서 수행한다.
  • 동시성 컬렉션에서 동시성을 무력화하는 건 불가능하며, 외부에서 락을 추가로 사용하면 오히려 속도가 느려진다.

 

상태 의존적 메서드

  • 동시성 컬렉션의 동시성을 무력화하지 못하기 때문에 여러 메서드를 원자적으로 묶어 호출하는 것도 불가능해, 여러 동작을 하나의 원자적 동작으로 묶는 상태 의존적 메서드가 추가되었다.
  • 예를 들면 putIfAbsent는 Map의 디폴트 메서드인데 인자로 넘겨진 key가 없을 때 value를 추가한다. 기존 값이 있으면 그 값을 반환하고 없는 경우에는 null을 반환한다. String의 intern 메서드를 아래와 같이 흉내를 낼 수 있다.
private static final ConcurrentMap<String, String> map =
        new ConcurrentHashMap<>();

public static String intern(String s) {
    String result = map.get(s);
    if (result == null) {
        result = map.putIfAbsent(s, s);
        if (result == null) {
            result = s;
        }
    }
    return result;
}

 

  • 동기화한 컬렉션보다 동시성 컬렉션을 사용해야 한다.
  • 예를 들어 Collections의 synchronizedMap보다는 ConcurrentHashMap을 사용하는 것이 훨씬 좋다.
  • 동기화된 맵을 동시성 맵으로 교체하는 것 하나만으로 성능이 개선될 수 있다.

 

 

동기화 장치

  • 스레드가 다른 스레드를 기다릴 수 있게 하여 서로의 작업을 조율할 수 있도록 해준다.
  • 대표적인 동기화 장치로는 CountDownLatch와 Semaphore가 있으며 CyclicBarrier와 Exchanger도 있다.
  • 가장 강력한 동기화 장치로는 Phaser가 있다.
  • CountDownLatch는 하나 이상의 스레드가 또 다른 하나 이상의 스레드 작업이 끝날 때까지 기다린다. 생성자 인자로 받는 정수값은 래치의 countdown 메서드를 몇 번 호출해야 대기하고 있는 스레드들을 깨우는지 결정한다.
  • 예를 들어 어떤 동작들을 동시에 시작해 모두 완료하기까지의 시간을 재는 코드를 아래와 같이 작성할 수 있다.

 

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

        ExecutorService executorService = Executors.newFixedThreadPool(5);
        try {
            long result = time(executorService, 3,
                    () -> System.out.println("hello"));
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }

    public static long time(Executor executor, int concurrency,
                            Runnable action) throws InterruptedException {
        CountDownLatch ready = new CountDownLatch(concurrency);
        CountDownLatch start = new CountDownLatch(1);
        CountDownLatch done = new CountDownLatch(concurrency);

        for (int i = 0; i < concurrency; i++) {
            executor.execute(() -> {
                // 타이머에게 준비가 됐음을 알린다.
                ready.countDown();
                try {
                    // 모든 작업자 스레드가 준비될 때까지 기다린다.
                    start.await();
                    action.run();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    // 타이머에게 작업을 마쳤음을 알린다.
                    done.countDown();
                }
            });
        }

        ready.await(); // 모든 작업자가 준비될 때까지 기다린다.
        long startNanos = System.nanoTime();
        start.countDown(); // 작업자들을 깨운다.
        done.await(); // 모든 작업자가 일을 끝마치기를 기다린다.
        return System.nanoTime() - startNanos;
    }
}

 

  • 위 코드에서 executor는 concurrency 매개변수로 지정한 값만큼의 스레드를 생성할 수 있어야 한다.
  • 그렇지 않으면 메서드 수행이 끝나지 않는데 이를 스레드 기아 교착 상태라고 한다.
  • 또, 시간을 잴 때는 시스템 시간과 무관한 System.nanoTime을 사용하는 것이 더 정확하다.

 

wait와 notify 메서드

  • 새로운 코드라면 wait, notify가 아닌 동시성 유틸리티를 사용해야 한다. 하지만 사용할 수밖에 없는 상황이라면 반드시 그 객체를 잠근 동기화 영역 안에서 사용해야 하며, 항상 대기 반복문(wait loop) 안에서 사용해야 한다.
synchronized (obj) {
    while (조건이 충족되지 않았다) {
        obj.wait(); // 락을 놓고, 깨어나면 다시 잡는다.
    }

    ... // 조건이 충족됐을 때의 동작을 수행한다.
}

 

 

 

아이템 82. 스레드 안전성 수준을 문서화하라

  • 멀티스레드 환경에서도 API를 안전하게 사용하려면 클래스가 지원하는 스레드 안정성 수준을 정확히 명시해야 한다.
  • 다음 목록은 스레드 안전성이 높은 순으로 나열한 것으로 일반적인 경우를 나타낸다

 

1. 불변(immutable)

  • 해당 클래스의 인스턴스는 마치 상수와도 같아서 외부 동기화도 필요 없다.
  • 예를 들면 String, Long, BigInteger

2. 무조건적인 스레드 안전(unconditionally thread-safe)

  • 해당 클래스의 인스턴스는 수정될 수 있지만 내부에서도 충실히 동기화하여 별도의 외부 동기화없이 동시에 사용해도 안전하다.
  • 예를 들면 AtomicLong, ConcurrentHashMap

3. 조건부 스레드 안전(conditionally thread-safe)

  • 무조건적인 스레드 안전성과 같지만 일부 메서드는 동시에 사용하려면 외부 동기화가 필요하다.
  • Collections.synchronized 래퍼 메서드가 반환한 컬렉션

4. 스레드 안전하지 않음(not thread-safe)

  • 클래스의 인스턴스는 수정될 수 있으며 동시에 사용하려면 각각의 메서드 호출을 클라이언트가 외부 동기화 로직으로 감싸야 한다.
  • 예를 들면 ArrayList, HashMap

5. 스레드 적대적(thread-hostile)

  • 외부 동기화로 감싸더라도 멀티스레드 환경에서 안전하지 않다.
  • 이러한 클래스는 동시성을 고려하지 않고 만들다보면 우연히 만들어진다.

 

 

동기화에 대한 문서화

  • 조건부 스레드 안전한 클래스는 주의하여 문서화해야 한다. 어떠한 순서로 호출할 때 외부 동기화 로직이 필요한지 그리고 그 순서대로 호출하려면 어떤 락 혹은 락을 얻어야만 하는지 알려주어야 한다.
  • 반환 타입만으로 명확히 알 수 없는 정적 팩토리 메서드라면 자신이 반환하는 객체에 대한 스레드 안전성을 문서화해야 한다.
  • 예를 들면 Collections.synchronizedMap의 API의 문서에는 아래와 같이 명시되어 있다.
/**
 * synchronizedMap이 반환한 맵의 컬렉션 뷰를 순회하려면 반드시 그 맵을 락으로 사용해 수동으로 동기화하라.
 
 *  Map<K, V> = Collections.synchronizedMap(new HashMap());
 *  Set<K> s = m.keySet();  // 동기화 블록 밖에 있어도 된다
 *      ...
 *  synchronized (m) {  // s가 아닌 m을 사용해 동기화해야 한다!
 *     for(K key : s)
 *.       key.f();
 *  }
 * 이대로 따르지 않으면 동작을 예측할 수 없다.
 */

 

외부에 공개된 Lock

  • 외부에 공개된 락(Lock)을 사용하면 유연한 코드를 만들 수 있지만 그만한 대가가 따른다.
  • 클라이언트가 공개된 락을 가지고 놓지 않는 서비스 거부 공격(denial-of-service attack)을 수행할 수 있다. (참고로 synchronized 메서드도 공개된 락에 속함)
  • 그렇기 때문에 아래와 같은 비공개 락 객체를 사용해야 한다.
// 비공개 락 객체, final 선언!
private final Object lock = new Object();

public void someMethod() {
    synchronized(lock) {
        // do something
    }
}
 

 

  • 비공개 락 객체는 클래스 바깥에서는 볼 수 없으니 클라이언트가 그 객체의 동기화에 관여할 수 없다.
  • 여기서 lock 멤버를 final로 선언한 이유는 우연히라도 락 객체가 교체되는 상황을 방지하기 위함이다.

 

 

 

아이템 83. 지연 초기화는 신중히 사용하라

지연 초기화

  • 필드의 초기화 시점을 그 값이 처음 필요할때까지 늦추는 기법
  • 주로 최적화에 사용되나, 클래스와 인스턴스 초기화 때 발생하는 위험한 순환 문제를 해결하는 효과도 있다.
  • 다른 모든 최적화와 마찬가지로 "하지마라"
  • 클래스 혹은 인스턴스 생성시의 초기화 비용은 줄지만, 필드 접근 비용은 커진다.
  • 다른 최적화와 마찬가지로 상황에 따라 성능을 오히려 느려지게 할 수도 있다.
  • 멀티스레드의 경우 지여 초기화가 까다롭다.
  • 대부분의 상황에서 일반적인 초기화가 지연 초기화보다 낫다.

 

 

지연 초기화가 필요한 경우

  • 해당 클래스의 인스턴스 중 그 필드를 사용하는 인스턴스의 비율이 낮은 반면, 그 필드를 초기화 하는 비용이 크다면 사용하자.
  • 지연 초기화 사용시 성능 측정은 필수!

 

지연 초기화가 초기화 순환성을 깨뜨릴 것 같으면  synchronized 접근자를 사용하자

private volatile FieldType field;

private synchronized FieldType getField() {
    if (field == null) { 
        field = computeFieldValue();
    }
    return field;
}

 

성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스 관용구를 사용하자.

private static class FieldHolder {
    static final FieldType field = computerFieldValue();
}

private static FieldType getField() { return FieldHolder.field; }

 

  • getField가 처음 호출되는 순간 FieldHolder.field가 처음 읽히면서, 비로소 FieldHolder 클래스 초기화를 촉발한다.

 

 

성능 때문에 인스턴스 필드를 지연초기화해야 한다면 이중검사 관용구를 사용하자.

private volatile FieldType field;

private FieldType getField() {
    FieldType result = field;
    if (result != null) {  // 첫 번째 검사(락 사용 안 함)
        return result;
    }
    
    synchronized(this) {
        if (field == null) {  // 두 번째 검사(락 사용)
            field = computeFieldValue();
        }
        return field;
    }
}

 

 

초기화가 중복해서 일어날 수 있다면 단일검사 관용구를 사용하자.

private volatile FieldType field;

private FieldType getField() {
    FieldType result = field;
    if (result == null)
        field = result = computeFieldValue();
    return result;

}

 

 

 

 

아이템 84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라

  • 운영체제마다 스케줄링 정책은 다를수 있기 때문에 프로그램은 이 정책에 좌지우지돼서는 안된다.
  • 정확성이나 성능이 스레드 스케쥴러에 따라 달라지는 프로그램은 다른 플랫폼에 이식하기 어렵다.

 

견고하고 빠릿하고 이식성 좋은 프로그램을 작성하는 가장 좋은 방법

  • 실행 가능한 스레드의 평균적인 수를 프로세서 수보다 지나치게 많아지지 않게 하는것이다.
  • 실행 준비가 된 스레드들은 맡은 작업을 완료할 때 까지 계속 실행되도록 만들면 스케줄링 정책에 따라 시스템 동작이 크게 달라지지 않는다.
  • 스레드는 당장 처리해야 할 작업이 없다면 실행돼서는 안 된다.
  • 스레드 풀 크기를 적절히 설정하고 작업은 짧게 유지하되, 너무 짧으면 작업을 분배하는 부담이 오히려 성능을 떨어뜨릴 수 있다.
  • 스레드는 절대 바쁜 대기 상태가 되면 안된다.

 

 

 

 

 

728x90
반응형

댓글