3. Thread
스레드
Last updated
스레드
Last updated
스레드의 탄생
1990년대 초 웹브라우저는 물론 Web 등이 나오던 시기. 사람들은 유즈넷 뉴스와 HTTP, CLI(Command line interface)등을 주로 사용하였음. 2,400 bps 모뎀을 통해 유명한 FTP 사이트로부터 몇 킬로바이트짜리 공개 소프트웨어를 다운받으려고하면 아래와 같은 에러를 볼 수 있었음.
요즘처럼 수십억 사용자가 아닌 수백만 사용자가 인터넷을 하던 시절이었지만 과부화, 병목현상에 빠진 사이트들이 많았음. 당시에는 FTP와 같은 서버들이 새로운 연결을 위해 새로운 프로세스를 만드는 방법을 사용했기 때문.
HTTP 방식을 사용했을 땐 이러한 문제가 보다 덜 했지만(이미지 등은 대부분 FTP보다 파일 크기가 작기도 했고) 파일 전송 후에 연결을 바로 종료하기 때문에, 웹 사용자는 FTP 사용자만큼 서버에 부담이 되지 않았음. 하지만 사용량이 증가함에 따라 같은 문제가 발생
웹 서버 성능 향상 방법들
(1) 프로세스 재사용
먼저 일정 크기의 프로세스를 미리 생성함.
서버에 요청이 들어오면 먼저 queue에 저장하고 각 프로세스는 queue로부터 요청을 제거함.
프로세스를 생성 및 종료하는 오버헤드가 없어졌기 때문에 모든 요청에 있어서 새로운 프로세스를 생성하는 방법보다 훨씬 좋은 방법.
(2) 상대적으로 가벼운 스레드를 사용.
각각의 독립적인 프로세스는 자신의 메모리 영역을 할당받지만, 스레드는 프로세스의 메모리를 공유하기 때문에 리소스에 대한 부담이 없다. 프로세스 대신 스레드를 사용함으로써 서버의 성능을 높일 수 있는 세 가지 요소가 있다. 이 요소들을 재사용 가능한 스레드 풀(thread pool)과 결합함으로써 동일한 하드웨어와 네트워크 환경에서 더 빠르게 실행시킬 수 있다. 대부분의 자바 가상머신은 4000-20000개의 스레드가 동시에 실행되면 메모리가 고갈되어 뻗어 버림. 하지만 thread pool을 사용함으로써 수백개 이하의 스레드로 분당 수천개의 연결을 처리할 수 있다.
스레드간 메모리 공유를 하기 때문에 변수나 자료구조에 예상치 못한 이슈가 생길 수 있음. 그렇다고 배타적 접근만을 허용하면 데드락(dead lock)이 발생할 수도 있음.
스레드 대체 방법 → 11장
수천 개의 연결을 동시에 지속적으로 유지해야 할 필요가 있다면 스레드 대신 비동기 입출력(I/O)에 대해서 고민해야 함. selector(셀렉터)를 사용하면 단일 스레드 내에서 소켓 그룹에 쿼리를 하고, 읽거나 쓸 준비가 된 소켓을 찾아서 순차적으로 처리할 수 있음. I/O를 스레드가 아닌 채널과 버퍼로 설계해야 함 → 11장
샤딩(sharding)을 함으로써 다수의 서버로 애플리케이션을 관리. consistency(일관성)에 관련된 설계 문제가 발생할 수 있지만 단일 시스템보다 더 많은 확장성과 안정성을 제공함.
스레드 실행
소문자 t로 시작하는 thread는 가상 머신에서 독립적인 실행 단위를 말함.
대문자 T로 시작하는 Thread는 java.lang.Thread의 인스턴스를 말함.
thread와 Thread는 1:1로 연결되어 있음.
스레드에게 일을 주려면
(1) Thread 클래스를 서브클래싱(subclassing)하여 run()메소드를 오버라이드하거나,
(2) Runnable 인터페이스를 구현하고 Runnable 객체를 Thread 생성자로 전달하는 방법이 있음. 이 방법이 스레드 자체로부터 깔끔하게 분리가 된다는 장점. 하지만 두 방법 모두 run() 메소드가 핵심.
스레드 서브클래스 만들기
네트워크 프로그램의 경우 프로그램의 실행 속도보다 네트워크를 통해 데이터를 전달되는 과정이 있다보니 속도가 느리다. 결국 대기하느라 많은 시간을 쓰게 된다. 이 대기 시간은 다른 스레드가 다른 입력 소스로부터 데이터를 받아서 처리하거나 입력에 관련없는 일들을 처리하는데 사용할 수 있는 시간임.! 귀중한 시간!
Runnable 인터페이스 구현하기
Java는 단일 상속의 특성을 갖는 언어이다. 그렇기에 Thread를 직접 상속받기 보다는 Runnable 인터페이스 받아 상속받는 형태로 구현하는 방식으로 Thread를 사용할 수 있다.
그리고 객체지향 설계에서는 상속보다는 구성(Composition)을 선호하는 원칙이 있습니다. 클래스간 강력한 결합은 코드를 유연하게 만들지 못하고 변경이 어렵게 만들 수 있습니다. Runnable 인터페이스를 구현함으로써 클래스는 더 많은 기능을 가질 수가 있습니다.
Runnable 인터페이스 구현은 스레드 풀을 활용하는게 더 쉽고, 단일 작업 단위로 간주되어 스레드 풀에서 더 효과적으로 관리가 가능함.
private 필드의 digest의 값이 초기화(설정) 되기 전에 메인 프로그램에서 getDigest() 메서드가 사용되어 값을 얻으려 하면 null을 반환하여 NullPointException
이 발생할 수 있는 코드임.
경쟁 조건(race condition)
위의 문제를 해결하기 위해 dr.getDigest() 호출을 main메서드 뒤로 이동해 볼 수 있음.
폴링(polling)
동기화를 목적으로 상태를 주기적ㅇ으로 검사, 조건을 만족할 때 작업을 처리하는 방식
코드에서는 while과 if null 체크포인트로 polling 을 수행.
메인 스레드가 작업의 종료 상태를 확인하느라 너무 바쁜 나머지 작업 스레드가 일할 시간이 없을 수 있다. 좋은 해결책까진 아님.
콜백 //TODO
스레드가 작업이 끝났을 때 자신을 생성한 클래스를 다시 호출하는 방식을 콜백(callback)이라고 함. 이 방법을 사용하면 메인 프로그램은 스레드가 종료되길 기다리는 동안 유휴상태(sleep)로 머물 수 있으며 실행 중인 스레드의 시간을 뺏지 않아도 된다.
폴링방식에 비해 CPU 시간을 낭비하지 않고 좀 더 유연하기에 많은 스레드와 갹체, 클래스가 엮인 복잡한 상황에 대처가능하다.
Future, Callable 그리고 Executor
자바5에서 멀티스레드의 복잡한 내용을 감추고 콜백을 좀 더 쉽게 사용할 수 있도로ㅑㄱ 새로운 접근 방법을 제공한다. 스레드를 직접 생성하는게 아니라, 필요할 때 스레드를 생성하여 제공하는 ExcecutorService를 사용한다. Callable객체를 만들고 ExecutorService에 등록한 다음 Future 객체를 반환받는다. 그리고 나중에 작업에 대한 결과를 Future를 통해 얻는다. Future사용 시 이미 결과가 준비되어 있는 경우 즉시 결과 값을 구할 수 있지만, 그렇지 않은 경우 폴링 스레드는 준비가 될 때까지 블록된다. 이 방식의 장점은 스레드를 생성하고 여러 스레드로부터 원하는 순서대로 값을 얻어 올 수 있다.
공공 도서관은 책을 구입하고 보관하는데에 있어서 개인보다 돈을 많이 쓴다. 하지만 개인의 입장에서 책을 읽기 위한 비용은 개인 책장보다는 공공도서관의 훨씬 저렴하다. 이것이 리소스 공유의 장점.
스레드는 도서관에서 책을 빌리는 사람과 비슷. 중아으이 리소스 풀(resource pool)로 부터 스레드를 빌려 씀. 두개 이상의 스레드가 같은 리소스를 사용하지만 않는다면 멀티스레드 프로그램이 각 프로세스가 별도의 리소스를 유지하는 멀티프로세스보다 훨씬 효율적임.
하지만 동시에 같은 리소스에 접근하게 된다면 앞선 스레드가 리소스 사용을 끝낼 때까지 기다려야 한다. 만약 기다리지 않고 접근한다면 리소스가 손상될 수가 있다.
아래와 같은 경우에는 배타적인 접근을 고려할 수 있다.
동기화 블록
synchronized 블록은 같은 객체에 대해 동기화하는 모든 코드를 병렬이 아닌 순서대로 실행되도록 한다. 즉, 다른 클래스와 스레드에 있는 코드가 System.out 객체에 대해 (접근)동기화를 하고 있다면 그 코드 또한 병렬로 실행될 수 없으며 순서대로 실행된다.
하지만 다른 객체에 대한 동기화나 다른 객체라도 동기화를 하지 않은 코드는 이 코드와 병렬로 실행될 수 있다.
동기화가 필요한 리소스에 대하여 동기화 없이 접근할 경우 데이터가 손상될 수 있으나, 자바는 다른 스레드에서 동기화 없이 접근할 경우 이를 차단할 방법은 제공하지 않는다. 자바는 같은객체에 대하여 동기화한 스레드에 대해서만 보호할 수 있다.
동기화는 두개 이상의 스레드가 같은 객체에 대한 참조가 있을 때만 이슈가 됨.
동기화 메소드
일반적으로 동기화를 할 때 메서드 자체에 대한 동기화를 많이 사용한다. 메서드 선언에 synchronized
지정자(modifier)를 추가함으로써 this가 참조하는 현재 객체의 메서드 전체를 동기화할 수 있다.
하지만 (1)성능저하, (2)데드락 발생 가능성, (3) 실제 보호해야 할 객체를 보호하지 못할 수 있음. 위의 예제에서는 두 스레드가 동시에 out에 접근하는 것을 막아야 한다. 하지만 다른 클래스에서 out을 참조하게 되면 동기화를 깨트리게 된다. 하지만 위의 코드에서는 out은 private인스턴스 변수이기에 LogFile 객체를 동기화 하는 것은 좋은 방법.! out객체에 대한 참조가 외부로 노출된 것이 없기에 LogFile을 통하지 않고서는 호출할 방법이 없다. LogFile 객체에 대한 동기화는 out객체에 대한 동기화와 같은 효과.!
동기화를 피할 수 있는 방법
로컬 변수 사용
: 각 스레드에서 사용 할 로컬변수를 아예 새로 만든다.
자바 기본 타입
: 자바는 인자를 참조(by reference)가 아닌 값(by value)로 전달하기 때문에 다른 스레드로부터 변경 될 위험이 없음. ex)Math.sqrt() : 값. 결과를 반환함
메서드 인자로 String(불변타임. immutable type)을 사용하는 경우에도 안전. 하지만 StringBuilder는 안전하지 않음
private과 final을 사용하면 스레드로부터 가장 안전하게 클래스를 만들 수 있음.(java.lang.String, Integer,Double…등등)
thread-unsafe 클래스를 thread-safe클래스의 private 필드로만 사용하는 방법.
safe클래스를 safe한 방법으로만 사용하면서, unsafe한 객체를 외부에서 접근만 할 수 없게한다면 unsafe는 스레드로부터 안전해짐.
두 스레드가 같은 리소스 집합에 대해서 배타적인 접근이 필요한데, 해당 리소스에 대해 이미 배타적인 접근 권한을 가지고 있을 때 발생함. 멀티스레드의 서버의 경우에, 수백만 개의 요청 중에 단 하나의 요청에서 발생한 문제로 서버가 중지된다.
데드락을 피하기 위한 가장 중요한 기법은 불필요한 동기화를 피하는 것이다. 객체를 불변(immutable)로 구현을 하거나 객체의 로컬 복사본을 만들어서 return하는 식으로 스레드의 안정성을 보장해 줄 방법이 있으면 구현하는게 좋다. 또한 자바 객체 내부적으로 동기화를 사용하기 때문에 개발자가 알지 못하는 부분에서 동기화가 발생할 수 있고 생각하는 것보다 많은 객체에서 동시에 동기화가 될 수 있다.
트래픽이 쉽지 않기에 데드락이 발생할 수 있는지 유심히 살펴보고 코드를 설계하는게 중요하다. 예를 들면 다수의 객체가 하나 이상의 리소스 집합을 공유하는 상황일 때 그 리소스를 사용하는 순서가 같도록 설계를 해야한다.
모든 가상 머신은 특정 시점에 실행될 스레드를 결정하는 스레드 스케줄러를 제공한다. 크게 선점형(preemptive)과 협력형(cooperative)두 가지가 있다.
선점형 스레드 스케줄러는 스레드에게 공평하게 분배된 CPU시간(time slice)이 소진되었을 때, 스레드를 중지시키고 다른 스레드에게 CPU제어권을 넘겨주는 일을 결정한다. 협력형 스레드 스케줄러는 CPU제어권을 다른 스레드에게 넘겨주기 전에 실행 중인 스레드가 스스로 실행을 중단할 때까지 기다린다.
협력형 스레드 스케줄링을 사용하는 가상머신은 CPU 기아 상태를 일으키기 더 쉽다. 높은 우선순위의 배타적인 스레드 하나가 전체 CPU시간을 독점할 수 있기 때문에 선점형보다 CPU기아 상태에 더 쉽게 노출된다.
모든 자바 가상 머신은 우선순위에 따른 선점형 스케줄링 방식의 사용이 보장된다. 즉 높은 우선순위의 스레드를 실행하기 위해 실행중인 낮은 우선순위의 스레드를 중지시키게 된다. (높은 우선순위의 스레드가 낮은 우선순위 스레드를 선점한 것)
우선순위가 같은 다수의 스레드가 실행을 준비하고 있다면 선점형일 경우에는 대기 중인 다음 스레드를 위해 종종 스레드를 중지시킴. 하지만 협력형인 경우에는 정지점에 도달하지 않거나 CPU제어를 포기하지 않으면 기아 상태에 빠지게 됨. 때문에 주기적으로 실행을 중지하여 다른 스레드가 실행할 기회를 가질 수 있도록 하는것이 중요.(하지만 대부분 선점형임)
스레드가 다른 스레드를 위해 실행을 중지 혹은 중지할 준비가 되었음을 알려주는 10가지 방법이 있다.
I/O 블록
동기화된 객체에 의한 블록
명시적인 양보
sleep 상태
다른 스레드 종료
객체 대기(wait)
스레드 종료(finish)
높은 우선순위 스레드에 의한 선점
스레드 일시 정지(suspend)
스레드 멈춤(stop)
블로킹(blocking)
스레드가 리소스를 얻기 위해 멈추고 기다려야 하는 상황에 발생.
네트워크 프로그램의 스레드가 I/O 블로킹에 의해 자발적으로 CPU에 대한 제어권을 포기하는 경우가 있다. 종종 네트워크를 통해 받거나 보낼 데이터를 기다리기 위해 블록됨.
동기화된 메소드나 영역에 진입할 때 블록될 수 있음. lock이 해제될 때까지 중지됨
I/O에 의한 블로킹이든 Lock에 의한 블로킹이든 블로킹 된 스레드는 어떤 락도 해제할 수 없으며 이로인해 여러 문제가 발생할 수 있음. I/O블록인 경우 시간이 지나면 해제되거나, IOException이 발생하고 스레드는 동기화된 영역이나 메소드를 빠져나오고 락을 해제하기 때문에 문제가 되지 않음. 하지만 자신이 소유하지 않은 Lock에 의한 블로킹이면 서로 자원을 점유중인데 서로 상대가 소유한 락이 걸린 자원을 기다릴 때 데드락이 발생한다.(circular)
양보(yield)
스레드는 명시적인 양보(yield)를 하여 CPU제어를 포기할 수 있음. Thread.yield()정적 메서드를 호출하여 일을 하는데, 이 메서드는 가상 머신이 다른 스레드를 실행할 수 있도록 신호를 보냄.
슬립(sleep)
yeild(양보)보다 강력한 형태로 제어를 넘겨줄 다른 스레드의 존재 유무에 상관없이 호출한 스레드가 중지된다. 같은 우선순위의 스레드뿐만 아니라 더 낮은 우선순위의 스레드에게도 실행의 기회를 준다. 하지만 슬립상태의 스레드 역시 락을 반환하지 않는다. 그러므로 슬립 상태의 스레드가 붙잡고 있는 락이 필요한 다른 스레드는(sleep이후) CPU를 이용할 수 있는 상황이지만 블로킹이 유지된 상태이므로 동기화된 메소드나 영역 안에서 스레드가 슬립되지 않도록 해야한다.(위험위험..)
interrupt()메서드를 호출해서 슬립중인 스레드를 다른 스레드에서 깨울 수 있다. 슬립 중인 스레드는 InterruptedException을 받고 일반적인 스레드처럼 동작하게 된다.
스레드 조인(join)
객체 대기(wait)
스레드는 락이 걸린 객체를 기다릴 수 있음. 객체를 기다리는 동안 객체의 락을 해제하고, 객체를 사용중인 다른 스레드에 의해 알림(notify)을 받을 때까지 실행이 중지(pause)된다. join과의 차이점은 join은 한 스레드가 종료될 때까지 실행을 중지하는 것이고, wait는 특정 조건이 충족되어 상태가 변경될 때까지 실행을 중지한다는 점에서 차이가 있다.
때문에 wait()메서드는 Thread클래스에서 제공하지 않는다. wait()메서드는 java.lang.Object 클래스에서 제공하며 모든 클래스의 객체로부터 wait() 메소드를 호출할 수 있다.
타임아웃(time-out)
sleep()과 join() 메서드에서의 의미와 같다. 지정된 시간이 흐른 뒤에 스레드가 깨어남. 타임아웃이 발생하면 wait()가 호출된 코드부터 스레드의 실행이 재개됨.
스레드 인터럽트(interrupt)
sleep()과 join() 메서드에서의 의미와 같다. 다른 스레드에서 wait()을 호출하여 대기중인 스레드의 interrupt()를 호출할 수 있다.
객체 알림(notify)
스레드가 기다리고 있는 객체의 notify(), notifyAll()메서드를 호출할 때 발생한다. java.lang.Object클래스에 존재하며 스레드가 대기하고 있는 객체에 대해 호출한다. notify()는 해당 객체를 대기하고 있는 스레드 목록에서 랜덤으로 하나를 깨우고, notifyAll()은 해당 객체를 기다리고 있는 모든 스레드를 깨운다.
스레드 종료(finish)
run() 메서드가 반환될 때 스레드는 죽고 다른 스레드가 CPU 제어를 이어받을 수 있따. 이 방법은 파일 다운로드등과 같은 단일 블로킹 동작을 수행하는 스레드에서 종종 사용되며, 애플리케이션의 나머지 부분이 블록되지 않도록 한다. 그리고 가상머신이 스레드를 생성하고 종료하는데는 적지 않은 오버헤드가 발생한다. 따라서 매우 짧은 시간에 종료되는 것이라면 별도의 스레드보다는 메서드를 호출하는 편이 더 나을 것이다.
프로그램에 멀티스레드 구조를 추가하면 입출력이 많은 네트워크 프로그램에 효과가 좋다. 스레드를 시작 및 종료하는데에 오버헤드가 있고 객체를 할당하는 것과 같은 성능에 좋지 않은 영향을 준다. 컨텍스트 스위칭(context-switching)에도 오버헤드가 발생한다.
위에선 4개의 스레드풀을 만들었고 결국 4개의 스레드 중 하나를 사용해 작업들이 처리된다. pool.shutdown(); 은 풀에 여전히 처리해야 할 작업들이 남아있을 때 호출되지만 대기중인 작업들을 중지시키진 않는다. 대기중인 작업들이 모두 끝난 후에 종료할 것임을 알린다. pool.shutdownNow(); 메서드를 호출하면 현재 처리 중인 작업을 중단하고 대기중인 작업을 건너 뛸 수가 있다.(즉시종료)
=====================
TODO more
멀티스레드 동기화 영역/ JVM 데이터 영역
데몬스레드
code :