Map<K,V>m=Collections.synchronizedMap(newHashMap<K,V>());...Set<K>s=m.ketSet();// 동기화 블록 안에 있을 필요가 없음...synchronized(m){// s가 아니라 m에 동기화for(Kkey:s)key.f();}
example lock 객체 숙어
// DoS 공격을 피하기 위한 private lock 객체 숙어privatefinalObjectlock=newObject();publicvoidfoo(){synchronized(lock){...}}
결론
어떤 방식(말로 풀건, 스레드 안정성 어노테이션등)을 이용해서 모든 클래스는 자신의 스레드 안정성을 분명히
밝혀야한다. - synchronized 키워드는 문서적으로 전혀 의미없다.
무조건적 스레드 안정성을 가진다면 private lock 객체 숙어 를 사용을 고려하라.
Rule 71 - 초기화 지연은 신중하게 하라
초기화 지연(lazy initialization) : 필드 초기호를 실제로 값이 쓰일 때까지 미루는 것
대부분의 경우 초기화 지연을 하는 것 보단 일반 초기화를 하는 것이 낫다.
초기화 숙어들
일반적인 초기화
privatefinalFieldTypefield=computeFieldValue();
일반적인 방법
지연 초기화 - synchronized
// 동기화된 접근자를 사용한 객체 필드 초기화 지연 방법privateFieldTypefield;synchronizedFieldTypegetField(){if(feild==null)filed=computeFieldValue();returnfield;}
안전하긴 하지만 항상 동기화가 걸리기 때문에 성능이 나쁨
지연초기화 - 초기화 지연 담당클래스 숙어
// 정적 필드에 대한 초기화 지연 담당 클래스 숙어privatestaticclassFieldHolder{staticfinalFieldTypefield=computeFieldValue();}staticFieldTypegetField(){returnFieldHolder.field;}
안전하면서 성능도 챙기는 좋은 숙어
단점으로는 한번 재초기화를 하기 힘들다.
지연초기화 - Double check
// 이중 검사 패턴을 통해 객체 필드 초기화를 지연시키는 숙어privatevolatileFieldTypefield;FieldTypegetField(){FieldTyperesult=field;if(result==null){// 첫 번째 검사 (락 없음)synchronized(this){result=field;if(result==null)// 두 번째 검사 (락 있음)field=result=computeFieldValue();}}returnresult;}
부득이하게 담당클래스를 이용할 수 없거나 재초기화가 필요한 경우 사용
한 번은 동기화 전, 한 번은 동기화 후에 검사해(그래서 double)서 단 한 번만 락이 걸리게 한다.
결론
대부분의 초기화 지연을 시키지 말아야한다.
객체 필드는 Double check 숙어
정적 필드는 초기화 지연 담당클래스 숙어
Rule 72 - 스레드 스케줄러에 의존하지 마라
정확성을 보장하거나 성능을 높이기 위해 스레드 스케줄러 에 의존하는 프로그램은 이식성이 떨어진다.
즉 플랫폼(OS) 환경에 따라서 스레드 우선순위가 달라질 수 있기 때문에 실행 결과가 달라질 수 있다.
멀티 스레드 관리 방안
스레드 풀을 적절하게 : 실행 가능 스레드의 평균적인 수가 프로세서 수보다 너무 많아지지 않도록 하는 것
태스크를 적당히 작게 : 너무 작으면 context switching 이슈가 생김
서로 독립적으로
바쁘게 대기하지 않는다 : 공유객체에 너무 자주 접근하지 않는다.
스레드 우선순위에 대한 이슈를 해결하기 위해서 Thread.yield()나 스레드 우선순위를 바꾸는 기법 을
사용하지 말자. 별 효능이 없다.
프로그램의 구조를 바꿔서 실행 가능한 스레드의 수를 줄이는 것이 좋은 해결책이다.
만약 병행성 테스트가 목적이라면 차라리 Thread.sleep(1)를 사용하라. - Thread.sleep(0)은 바로
return 하므로 효과가 없다
결론
플랫폼이 제공하는 스레드 스케줄러에 의존하지 말고 적절한 멀티스레드 관리 방안을 마련하라.
마찬가지로 우선순위를 조작하는 메서드에도 의존하지 마라 - 그저 힌트 수준이지 강제성이 없다.
Rule 73 - 스레드 그룹은 피하라
스레드 그룹은 거의 폐기수순이다. - 스레드 안정성에도 취약하다
쓸모 있는 기능이 무점검 예외를 던졌을 때 가져오는 유일한 수단인 ThreadGroup.uncaughtException
이 있었는데 이 마저도 Thread.setUncaughtExceptionHandler가 제공되었으니 이것을 사용 하라.
// 적절히 동기화한 스레드 종료 예제publicclassStopThread{privatestaticbooleanstopRequested;privatestaticsynchronizedvoidrequestStop(){stopRequested=true;}privatestaticsynchronizedbooleanstopRequested(){returnstopRequested;}publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadbackgroundThread=newThread(newRunnable(){@Overridepublicvoidrun(){inti=0;while(!stopRequested())i++;}});backgroundThread.start();TimeUnit.SECONDS.sleep(1);requestStop();}}
2. volatile
// 적절히 동기화한 스레드 종료 예제publicclassStopThread{privatestaticvolatilebooleanstopRequested;publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadbackgroundThread=newThread(newRunnable(){@Overridepublicvoidrun(){inti=0;while(!stopRequested)i++;}});backgroundThread.start();TimeUnit.SECONDS.sleep(1);stopRequested=true;}}
volatile은 상호배제를 구현하지 않지만 가장 최근에 기록된 값을 조회하도록 보장한다.
example 잘못된 volatile
// 잘못된 예제 - 동기화가 필요하다!privatestaticvolatileintnextSerialNumber=0;publicstaticintgenerateSerialNumber(){returnnextSerialNumber++;}
문제는 ++연산자가 원자적이지 않다는데 있다. 이 연산자는 두가지 연산을 순서대로 시행한다.
특정 스레드만이 데이터 객체를 변경할 수 있도록 하고, 변경이 끝난 뒤에야 다른 스레드와 공유하도록 할 때는
객체 참조를 공유하는 부분에만 동기화를 적용하는 객체
결론
변경 가능한 데이터를 공유할 때는 해당 데이터를 읽거나 쓰는 모든 스레드는 동기화를 수행해야 한다
Rule 67 - 과도한 동기화는 피하라
동기화 시 발생할 수 있는 문제
교착상태(deadlock)
비결정적 동작(nondeterministic behavior)
동기화 메서드나 블록 안에서 클라이언트에게 프로그램 제어 흐름을 넘기지 마라
동기화가 적용된 영역(synchronized) 안에서는 재정의 가능 메서드나 클라이언트가 제공한 함수 객체 메서드를
호출하지 말라는 것 - 불가해(alien) 메서드이기 때문 -
제어권이 다른 객체로 위임되는데 위임되는 메서드 안에서는 동기화 제어를 할 수 없기 때문이다.
// 다중 스레드에 안전한 구독자 집합 : CopyOnWriteArrayList 이용privatefinalList<SetObserver<E>>observers=newCopyOnWriteArrayList<SetObserver<E>>();publicvoidaddObserver(SetObserver<E>observer){observers.add(observer);}publicbooleanremoveObserver(SetObserver<E>observer){returnobservers.remove(observer);}privatevoidnotifyElementAdded(Eelement){for(SetObserver<E>observer:observers){observer.added(this,element);}}
핵심
동기화 영역 안에서 수행되는 작업의 양을 가능한 줄여야 한다.
동기화의 진짜 비용은 병렬성을 활용할 기회를 잃는다는 것 이다.
static 필드(또는 단일객체 내 필드)를 변경하는 메서드가 있을 때는 해당 필드에 대한 접근을 반드시 동기화
해야 한다.
// ConcurrentMap으로 구현한 병행 정규화 맵 - 더 빠르다!publicstaticStringintern(Strings){Stringresult=map.get(s);if(result==null){result=map.putInAbsent(s,s);if(result==null)result=s;}returnresult;}
추천 클래스
ConcurrentHashMap
CopyOnWriteArrayList
BlockingQueue : 봉쇄 연상(blocking operaton : task) 가능
task는 큐의 맨 앞(head) 원소를 제거한 다음 반환하는데, 큐가 비어있는 경우에는 대기(wait)
생산자-소비자큐 라고 불리기도 함
생산자 스레드가 큐에 작업들일 집어 넣고, 소비자 스레드가 꺼내어 처리
ThreadPoolExecutor을 비롯한 대부분의 ExecutorService 구현은 BlockingQueue 를 사용
동기자
쓰레드들의 병행성을 제어해서 서로 상호협력이 가능하게 함
CountDownLatch
일회성 배리어(barrier)로서 하나 이상의 스레드가 작업을 마칠 때까지 다른 여러 스레드가 대기하게 한다.
대기 중인 스레드가 진행할 수 있으려면 그 횟수만큼 countdown 메서드가 호출되어야 한다.
example
// 작업의 병령 수행 시간을 재는 간단한 프레임워크publicstaticlongtime(Executorexecutor,intconcurrency,finalRunnableaction)throwsInterruptedException{// 작업 준비finalCountDownLatchready=newCountDownLatch(concurrency);// 작업 시작(타이머)finalCountDownLatchstart=newCountDownLatch(1);// 작업 완료finalCountDownLatchdone=newCountDownLatch(concurrency);for(inti=0;i<concurrency;i++){executor.execute(newRunnable(){publicvoidrun(){ready.countDown();// 타이머에게 준비됨을 알림try{start.await();// 다른 작업스레드가 준비될 때까지 대기}catch(InterruptedExceptione){Thread.currentThread().interrupt();}finally{done.countDown();// 타이머에게 끝났음을 알림}}})}ready.await();// 모든 작업 스레드가 준비될 때까지 대기longstartNanos=System.nanoTime();start.countDown();// 출발done.await();// 모든 작업 스레드가 끝날 때까지 대기returnSystem.nanoTime()-startNanos;}
작업(Runnable 인자)을 병렬로 실행할 횟수(concurrency) 만큼 돌려서 실행되는 시간을 구하는 메서드
작업을 쓰레드에 등록을 해서
전체 작업 쓰레드가 준비를 대기한 다음
시작시간을 재고
전체 작업이 시작되고
전체 작업이 마감이 된 후
진행된 시간을 계산해서 반환
주의사항
실행자(Executor인자)가 주어진 병행수(concurrency) 보다 같거나 많은 수의 스레드를 동시에 생성할
수 있어야함
시간을 잴 때는 System.currentTimeMillis 대신 System.nanoTime를 사용한다.
더 정밀하고, 시스템의 실시간 클락의 변동에도 영향을 받지 않는다.
Samaphore
기타
CycleBarrier, Exchanger
레거시코드 (wait, notify)
example 표준숙어
// wait 메서드를 사용하는 표준숙어synchronized(obj){while(실패조건)obj.wait();// 락을 해제하고 대기풀(wating pool) 에서 락 획득 대기// 락을 획득(스레드를 깨움) 후 해야할 실제작업}
while 문으로 실패조건을 확인하는 이유?
if로 체크할 경우 wait 이후에 다시 락을 획득 했을 시 실패 조건 재검증이 안되서 일관성이 깨질 수 있음
락을 다시 획득하는(스레드를 다시 깨울) 경우 가능하면 notify 대신 notifyAll을 사용하라
JVM이 알아서 락을 획득할 스레드의 우선순위를 적절하게 정해줘서 영원히 잠드는(대기상태인) 스레드가 없을 것이다.
악의적으로 또는 객체의 API 노출 등의 실수로 notify가 호출되는 경우 무한 대기가 걸릴 수 있음.
결론
자바API가 신규로 제공하는 고수준의 병행성 유틸리티를 사용하라. notify, wait를 사용할 이유가 없다.
만약 기존 코드를 유지보수 해야하는 상황이라면 위의 표준숙어를 이용해서 안전하게 처리하고, 일반적으로
notify 대신 notifyAll을 사용하도록 하라