스레드 안전성에 대해서는 문서에 synchronized 키워드가 있는지 보면 알 수 있다고 하는데, 보통 Javadoc이 만드는 문서에는 synchronized 키드워가 들어가지 않는데, 거기에는 이유가 있다. synchronized 키워드는 메서드의 구현 상세에 해당하는 정보이며, 공개 API의 일부가 아니기 때문이다. 병렬적으로 사용해도 안전한 클래스가 되려면, 어떤 수준의 스레드 안전성을 제공하는 클래스 인지 문서에 명확하게 남겨야 한다.


  • 변경 불가능 - 이 클래스로 만든 객체들은 상수다. 따라서 외부적인 동기화 메커니즘 없이도 병렬적 이용이 가능하다. String, Long, BigInteger 등이 그 예다.
  • 무조건적 스레드 안전성 - 변경이 가능하지만 적절한 내부 동기화 메커니즘을 갖추고 있어서 외부적으로 동기화 메커니즘을 적용하지 않아도 병렬적으로 사용할 수 있다. Random, ConcurrentHashMap이 그 예다.
  • 조건부 스레드 안전성 - 무조건전 스레드 안전성과 거의 같은 수준이나, 몇몇 스레드는 외부적 동기화가 없이는 병렬적으로 사용할 수 없다. 
  • 스레드 안전성 없음 - 해당 객체들은 변경이 가능하다. 이러한 객체들을 병렬적으로 이용하려면 메서드를 호출하는 부분을 클라이언트가 선택한 외부적 동기화 수단으로 감싸야 한다. synchronized를 써야 한다는 뜻인가..?

요약

모든 클래스는 자신의 스레드 안전성 수준을 문서로 분명히 남겨야 한다. 


watit와 notify를 정확하게 사용하는 것이 어렵기 때문에 자바 1.5 플랫폼에서 지원하는 고수준 병행성 유틸리티를 반드시 이용해야 한다. java.util.concurrent에 포함된 이 유틸리티들은 실행자 프레임워크, 병행 컬렉션, 그리고 동기자의 세 가지 범주로 나눌 수 있다.

  • 병행 컬렉션은 List, Queue, Map 등의 표준 컬렉션 인터페이스에 대한 고성능 병행 컬렉션 구현을 제공한다. 이 컬렉션들은 병행성을 높이기 위해 동기화를 내부적으로 처리한다. 따라서 컬렉션 외부에서 병행성을 처리하는 것은 불가능하다. 락을 걸어봐야 효과가 없을뿐만 아니라 프로그램만 느려진다. 병행컬렉션에는 예를들어, ConcurrentMap이란 것이 있는데 Map을 확장해서 몇 가지 메서드를 추가하였다. 그 가운데 putIfAnsent(key,value) 메소드는 는 키에 해당하는 값이 없을떄만 주어진 값을 넣고, 이미 있는 경우는 기존 값을 반환한다. 없는 경우에는 null을 반환하기 때문에 반환되는 값을 체크하여 사용하면 효율적이다.
  • 동기자는 스레드들이 서로를 기다릴 수 있도록 하여, 상호 협력이 가능하게 한다. 

카운트라운 래치는 일회성 배리어로서 하나 이상의 스레드가 작업을 마칠 때까지 다른 여러 스레드가 대기할 수 있도록 한다. 또한 특정구간의 실행시간을 잴 때는 System.currentTimeMillis 대신 System.nanoTime을 사용해야 한다. 그래야 시스템의 실시간 클락 변동에도 영향을 받지 않으며, 더 정밀하다

요약

새로 만드는 프로그램에 wait나 notify를 사용할 이유는 거의없다. wait나 notify를 이용하는 기존 코드를 유지보수해야 한다면, while문 안에서 wait를 호출하는 표준 숙어를 따라야 한다.

그리고 실무에서는 일반적으로 notify 대신 notifyAll을 이용하도록 해야 한다.



자바 1.5부터 자바 플랫폼에는 java.util.concurrent가 추가되었다. 이 패키지에는 실행자 프레임워크 라는 것이 들어 있는데, 요연성이 높은 인터페이스 기반 태스크 실행 프레임워크다. 이 프렘워크를 사용하면 스레드를 직접 관리하는 작업 큐를 한 줄의 코드로 생성할 수 있다.


 실행자가 스스로 자연스럽게 종료되도록 하려면 다음과 같이 하면 된다. 

실행자 태스크를 이용하면 할 수 있는 것이 많다.

  • 특정 태스크가 종료되기를 대기
  • 임의의 태스크들이 종료되기를 대기
  • 실행자 서비스가 자연스럽게 종료되기를 기다릴 수 도 있음.
  • 태스크가 끝날때 마다 결과값을 차례로 가져옴.
 작은 프로그램이거나 부하가 크지 않은 서버를 만들 때는 보통 Executors.newCashedThreadPool을 사용한다. 설정이 필요 없고, 보통 많은 일을 잘 처리하기 때문이다. 하지만 부하가 심한 환경에 들어갈 서버를 만들 때는 적절하지 않는데, 캐시 기반 스레드 풀의 경우, 작업은 큐에 들어가는 것이 아니라 실행을 담당하는 스레드에 바로 넘겨진다. 서버 부하가 심해서 모든 CPU가 100%에 가깝게 이용되고 있는 상황에서 새 태스크가 들어오면 더 많은 스레드가 만들어 질 것이고 상황이 더 나빠질 것이다. 따라서 부하가 심한 곳에서는 Executors.newFixedThreadPool을 이용해서 스레드 개수가 고정된 풀을 만들어 사용하는 것이 좋다.

 작업 큐를 손수 구현하는 것은 삼가야 할 뿐 아니라, 일반적으로는 스레드를 직접 이용하는 것도 피하는 것이 좋다. Thread는 작업의 단위였을 뿐 아니라 작업을 실행하는 메커니즘 이였다. 하지만 작업과 실행 메커니즘이 분리되므로써 더 이상 중요한 역할을 하지 못한다. 중요한 것은 작업의 단위이며, 태스크라 부른다. 태스크에는 두 가지 종류가 있는데, Runnable과 Callable이다. 태스크를 실행하는 일반 메커니즘은 실행자 서비스다. 



아직은 이해하기가 너무 어렵다.. 추후에 다시 정리해보록 하자..

synchronized 키워드는 특정 메서드나 코드 블록을 한번에 한 스레드만 사용하도록 보장한다. 동기화는 스레드가 일관성이 깨진 객체를 관측할 수 없도록 할 뿐 아니라(하나의 스레드만 이용), 동기화 메서드나 동기화 블록에 진입한 스레드가 동일한 락의 보호 아래 이루어진 모든 변경의 영향을 관측할 수 있도록 보장한다(기존의 스레드가 변경한 내용을 새로운 스레드가 관측 가능). 


 성능을 높이기 위해서는 원자적 데이터를 읽거나 쓸 때 동기화를 피해야 한다는 이야기를 자주 들어봤을 것이다. 상호 배제성뿐 아니라 스레드 간의 안정적 통신을 위해서도 동기화는 반드시 필요하다. 즉 하나의 스레드만 이용할 수 있도록 하는 것의 용도 뿐만아니라 스레드 간의 안정적 통신을 위해서는 동기화를 사용해야 한다는 뜻이다. 자바의 메모리 모델 명세 때문이다. 메모리 모델은 한 스레드가 만든 변화를 다른 스레드가 볼 수 있게 되는 시점과, 그 절차를 규정하기 때문이다. 즉 기존의 스레드가 변경한 내용을 새로운 스레드가 알도록 하는 것을 규정한다는 것.


위의 클래스는 Thread.stop 대신 boolean 필드를 이용하여 스레드를 제어 했다. stop메서드는 안전성이 결여되어있으므로 사용하면 안된다. 위의 프로그램은 1초가 지나면 main 스레드가 stopRequested의 값을 true로 바꾸므로, 후면 스레드가 실행하는 순환문도 그 때 종료될 것 같다. 하지만 그렇지 않다.

이 프로그램의 문제는 동기화 메커니즘을 적용하지 않은 탓에 main스레드가 변경한 stopRequest의 새로운 값을 후면 스레드가 언제쯤 보게 될지 알 수가 없다는 것이다. 위의 프로그램은 아래와 같이 변경되어야 한다.


 하지만 StopThread의 동기화 메서드가 하는일은 동기화 없이도 원자적이다. 다시 말해서, 이들 메서드에 동기화를 적용한 것은 상호배제성을 달성하기 위해서가 아니라, 순전히 스레드 간 통신 문제를 해결하기 위해서 였다는 것이다. 동기화를 실행하는 비용이 크기 때문에 좀더 개선하기 위해서는 stopRequested를 volatile로 선언하면 최적화에서 제외되므로 좀 더 나은 성능을 얻을 수 있다. 

요약

변경 가능한 데이터를 공유하지 않는 것이 가장 좋다. 그러나 공유해야만 하는 경우에는 해당 데이터를 읽거나 쓰는 모든 스레드는 동기화를 수행해야 한다는 것이다. 동기화를 하지 않으면 다른 스레드가 만든 변경사항을 관측할 수 있으리라는 보장을 할 수 가 없다.  변경 가능 데이터를 적절히 동기화하지 않으면 생존 오류나 안전 오류(2개 이상의 스레드에 안전하지 않음. 변경도중 다른스레드가 접근)가 생긴다. 



API 설계자가 예외를 발생할 수 있다고 선언했다면, 그것은 API를 이용할 프로그래머에게 무언가를 알리려는 것이다. 무시하지마라! . 호출 대상 메서드를 빈 CATCH 블록이 붙은 TRY 문으로 감싸면, 예외를 쉽게 무시할 수 있다.



 빈 catch 블록은 예외를 선언학 목적,즉 예외적 상황을 반드시 처리하도록 강제한다는 목적에 배치된다. 예외를 무시하는 것은 화재 경보를 무시하는 것이나 같다. 정말로 불이 났는지 아무도 확인할 수 없도록 만드는 것이다. 적어도 catch 블록 안에는 예외를 무시해도 괜찮은 이유라도 주석을 남겨놔야 한다. 또 정말로 무시해도 되는 상황이라면 로그를 남겨서, 이러한 경우가 많이 발생할 경우 수정할 수 있도록이라도 해야한다. 

 빈 catch 블ㄹ록으로 예외를 무시하는 프로그램은 오류가 생겨도 조용히 실행을 계속한다. 그러다 어느 순간, 오류와는 아무 상관없도 없는 지점에서 불시에 죽어버린다. 예외를 적절히 처리하면 오류로 프로그램이 죽는 것을 피할 수 있다. 



일반적으로 이야기해서, 메서드 호출이 정상적으로 처리되지 못한 객체의 상태는, 메서드 호출전 상태와 동일해야 한다. 이 속성을 만족하는 메서드는 실패원자성을 갖추었다고 한다.


 실패 원자성을 달성하는 방법은 여러가지다. 

  • 변경 불가능 객체로 설계하는 방법. 변경 불가능한 객체의 경우, 실패원자성은 덤이다. 메서드 호출이 실패하면 새로운 객체가 만들어지지 못할 수는 있게지만 기존 객체의 일관성이 깨지진 않는다. 변경 가능한 객체의 경우에는 실제 연산을 수행하기 전에 인자 유효성을 검사하는 것이 가장 보편적인 방법이다. 

물론 빈 스택에서 뭔가를 뽑아내려 하면, 굳이 첫 두줄이 없어도 예외가 나긴 하겠지만, 첫 두줄이 없으면 size필드의 일관성이 깨져서 음수로 바뀌게 된다. 그러니 이 메서드를 다시 호출하면 계속 문제가 생길 것이다.

  • 실패할 가능성이 있는 코드는 전부 객체 상태를 바꾸는 코드 앞에 배치하는 것.  계산을 실제로 수행해 보기 전에는 인자를 검사할 수 없을 때 이용 가능한 방법
  • 연산 수행 도중에 발생하는 오류를 가로채는 복구 코드를 작성하는 것. 이 복구 코드는 연산이 시작되기 이전 상태로 객체를 되돌린다. 디스크 기반의 지속성 자료 구조에 주로 사용되는 기법이다.
  • 객체의 임시 복사본상에서 필요한 연산을 수행하고, 연산이 끝난 다음에 임시 복사본의 내용으로 객체 상태를 바꾸는 것. 데이터를 임시 자료구조에 복사한 다음에 훨씬 신속하게 실행될 수 있는 연산이라면 이 접근법이 자연스럽다. 

 실패 원자성은 일반적으로 권장되는 덕목이지만 언제나 달성할 수 있는 것은 아니다. 예를 들어, 같은 객체를 여러 스레드가 적절한 동기화 없이 동시에 변경할 경우, 객체 상태의 일관성을 깨질 수 있다.

요약

메서드 명세에 포함된 예외가 발생하더라도 객체 상태는 메서드 호출 이전과 동일하게 유지되어야 한다는 것이다. 이 규칙을 지키지 못할 시에는 객체 상태가 어떻게 변하는지 API 문서에 명확하게 서술해야 한다.


무점검 예외 때문에 프로그램이 죽으면, 시스템은 자동적으로 해당 예외의 스택 추적 정보를 출력한다. 이 정보는 해당 예외 객체의 toString 메서드가 예외 정보를 문자열로 변환한 결과다. 따라서 toString 메서드가 반환하는 문자열에 오류 원인에 관계된 정보를 최대한 많이 담아두는 것이 아주 유용하다.  다시 말해서, 예외의 상세 메세지에는 원인 분석에 이용될 오류 정보가 포착되어 있어야 한다는 것이다.


 오류 정보를 포착해 내기 위해서는, 오류의 상세 메세지에 "예외에 관계된" 모든 인자와 필드의 값을 포함 시켜야 한다. 예를 들어 IndexOutOfBounds Exception의 예외 메세지에는 첨자의 하한과 상한, 그리고 그 범위를 벗어난 첨자값이 포함되어 있어야 한다. 그 정보를 보면 많은 정보를 얻을 수 있기 때문이다. 


 예외의 상세 메세지를 사용자 레벨 오류 메시지와 혼동해서는 안 된다. 사용자 레벨 오류 메시지는 최종 사용자가 이해할 수 있어야 한다. 하지만 예외에  대한 상세 메시지는 프로그래머나 서비스 담당자가 오류 원인을 분석하기 위한 것이다. 따라서 가독성보다는 내용이 훨씬 중요하다.  이런 상세 메시지에 담는 한가지 방법은, 상세한 정보를 요구하는 생성자를 만드는 것이다.



+ Recent posts