본문 바로가기

프로그래밍/Java

[Java 요약 정리] 9. 쓰레드(Thread)

9. 쓰레드(Thread)


1) 프로세스와 쓰레드

- 프로그램: 실행 가능한 파일(HDD)

- 프로세스: 실행 중인 프로그램(메모리) -> 자원과 쓰레드로 구성

- 쓰레드: 프로세스 내 실제 작업 수행, 모든 프로세스는 하나 이상의 쓰레드 보유

- 싱글 쓰레드 프로세스: 자원+쓰레드

- 멀티 쓰레드 프로세스: 자원+N개의 쓰레드


2) 멀티 쓰레드

- 멀티쓰레딩: 하나의 프로세스 내에 여러 개의 쓰레드를 사용하는 것

- 장점: 효율적인 자원 사용, 사용자에 대한 응답성 향상, 작업 분리로 인해 간결한 코드

- 주의사항: 동기화에 의한 문제 발생, 교착 상태(Dead-Lock), 기아 상태

*프로그래밍 고려사항이 많은 게 단점으로 작용할 수 있음

- 싱글코어: 순차실행, 병행(Concurrent) // 병행 -> 번갈아가며 작업

- 멀티코어: 병행(Concurrent), 병렬(Parallel) // 병렬 -> 동시에 작업

병행: 작업을 번갈아가며 수행

병렬: 한 작업을 나눠서 수행


3) 쓰레드의 구현과 실행

- Thread 클래스 상속: 쓰레드의 메소드 오버라이딩 필요

- Runnable 인터페이스 구현: Thread 클래스의 메소드를 가져와서 사용해야 함

- 클래스의 다른 상속을 위해 인터페이스를 구현하는 방향이 낫다.

- public void run() {/*작업내용*/} // 쓰레드 생성(별도의 스택 생성)

- start() // 쓰레드 생성 후 start()를 호출해서 실행


4) 쓰레드의 우선순위

- 작업의 중요도에 따라 우선순위 조정 가능(처리 시간 조정/ 순서는 스케쥴러가 조정)

- void setPriority(int newPriority) // 쓰레드의 우선순위를 지정값으로 변경(10높음~1낮음)

- int getPriority() // 쓰레드의 우선순위 반환


5) 쓰레드 그룹

- 관련 쓰레드를 묶어서 다루기 위한 것

- 모든 쓰레드는 반드시 그룹에 포함(미지정시 main쓰레드 그룹에 포함)

- 조상 쓰레드의 그룹과 우선순위 상속


6) 데몬 쓰레드(Daemon Thread)

- 일반 쓰레드의 작업을 돕는 보조 역할 수행

- 일반 쓰레드가 모두 종료되면 자동 종료

- 가비지 컬렉터, 자동저장, 화면 자동갱신 등에 사용

- 무한루프와 조건문 이용 실행 후 대기하다가 특정 조건 만족시 작업 수행 후 다시 대기


7) 쓰레드의 상태

- NEW : 쓰레드가 생성되고 아직 START()가 호출되지 않은 상태

- RUNNABLE : 실행 중 또는 실행 가능 상태

- BLOCKED : 동기화 블럭에 의해 일시정지 상태

- WAITING : 작업이 종료되지는 않았지만 실행 불가(unrunnalbe) 일시정지 상태

- TIMED_WAITING : 일시정지 시간이 지정된 경우

- TERMINATED : 쓰레드의 작업이 종료된 상태


8) 쓰레드 실행 제어

- static void sleep(long millis, int nanos) : 1/1000초 단위로 쓰레드 일시 정지

*InterruptedException이 발생하면 깨어나기 때문에 예외처리 필수

- void join(long millis, int nanos) : 지정시간 동안 쓰레드 실행

- void interrupt() : 일시정지 상태의 쓰레드를 실행대기 상태로 변경

- boolean isInterrupted() : 쓰레드의 interrupted상태를 반환

- static boolean interrupted() : 현재 쓰레드의 interrupted 상태 반환 후 false로 변경

- void stop() : 쓰레드 즉시 종료

- void suspend() : 쓰레드 일시 정지

- void resume() :  일시 정지 상태의 쓰레드를 실행 대기 상태로 변경

- static void yield() : 실행 중 주어진 실행시간을 다른 쓰레드에게 양보 후 실행대기

*deprecated Method: stop, suspend, resume // Dead-Lock(교착 상태) 방지 목적


9) 쓰레드의 동기화

- 객체에 Lock을 걸어서 한 번에 하나의 쓰레드만 객체에 접근할 수 있도록 제한

- 목적: 데이터의 일관성 유지

- 특정 객체 대상 양식: synchronized(객체의 참조변수) {}

- 메소드 대상 양식: public synchronized void 메소드명() {}

- 동기화블럭 : synchronized(this) {연산내용} // 메소드 내에서 동기화시킬 구간 설정


10) wait()와 notify()

- 동기화된 영역의 코드를 수행 중에 더 이상 진행할 상황이 아니면 잠시 Lock을 반납해서 다른 작업을 수행 할 수 있도록 한다.

- Object에 정의되어 있음

- 동기화 블록 내에서만 사용 가능

- 기아(Starvation)현상: wait 이후 notify를 받지 못한 채 오랫동안 기다리는 상태

- wait() : Lock을 반납하고 기다림

- notify() : 수행을 중단했던 쓰레드를 임의호출해서 재진입(Reentrance)하도록 한다.

- notifyAll() : 수행을 중단했던 모든 쓰레드를 호출해서 재진입하도록 한다.(기아 방지)


11) Lock과 Condition을 이용한 동기화

- java.util.concurrent.locks패키지에서 제공하는 Lock클래스들을 이용한 동기화(JDK1.5)

- wait()와 notify()로는 불가능한 선별적인 통지 가능(경쟁상태 방지)

*경쟁 상태(Race Condition): 여러 쓰레드가 lock을 얻기 위해 서로 경쟁하는 것

- ReentrantLock: 재진입이 가능한 lock, 가장 일반적인 배타적인 lock

=> 무조건 lock이 있어야만 임계영역의 코드 수행 가능

- ReentrantReadWriteLock: 읽기에는 공유적, 쓰기에는 배타적인 lock

=> 중복해서 읽기 가능(읽기 lock이 걸린 상태에서 쓰기 lock 걸기 불가)

- stampedLock: ReentrantReadWriteLock에 낙관적 읽기의 기능을 추가(JDK1.8)

=> lock을 걸거나 해지할 때 스탬프(long type)를 사용,

*낙관적 읽기(Optimistic Reading Lock): 무조건 읽기 Lock을 걸지 않고 쓰기와 읽기가 충돌할때만 사용(쓰기가 끝난 후 읽기 Lock을 걸어줌)


12) ReentrantLock 클래스 사용

- 생성자: ReentrantLock() or ReentrantLock(boolean fair): 매개변수로 true 입력시 가장 오래된 쓰레드가 lock을 획득 할 수 있게 공정한 처리 => 확인 과정에서 성능이 떨어지기 때문에 대부분의 경우 공정함보다 성능을 선택

- Sychronized 대신 lock()과 unlock()을 사용

void lock(): 잠금

void unlock() : 잠금해제

boolean isLocked() : 잠겼는지 확인

- tryLock(): Lock을 얻기 위해 기다리지 않음

tryLock(): Lock을 얻으면 true, 얻지 못하면 false 반환

tryLock(long timeout, timeUnit unit) throws InterruptedException: 정해진 시간만큼만 기다렸다가  Lock을 얻으면 true, 얻지 못하면 false 반환 (생성자: 시간, 시간 타입)


13) Condition 클래스 사용

- 클래스에서 쓰레드 각각의 객체를 얻어 쓰레드 구분 통지에 사용

- Condition은 이미 생성된 lock으로부터 newCondition()을 호출해서 생성

ex)

private ReentrantLock lock = new ReentrantLock(); // 락 생성

private Condition forCook = lock.newCondition(); // 요리사를 위한 객체

private Condition forCust = lock.newCondition(); // 손님을 위한 객체


14) volatile

- CPU는 성능 향상을 위해 변수값을 코어의 cache에 저장해놓고 작업 -> 각 코어의 캐시와 메모리의 값이 불일치하는 현상이 발생

- 변수 앞에 volatile를 붙이면 캐시가 아닌 메모리에서 값을 읽기 때문에 불일치 해소

- volatile대신 synchronized블럭을 사용해도 동기화로 인해 같은 효과를 얻을 수 있음

- long과 double 타입의 변수는 하나의 명령어로 값을 읽거나 쓸 수 없음

-> 변수값을 읽는 과정에서 다른 쓰레드가 개입하지 못하게 하기 위해 volatile 사용


15) fork & join 프레임워크

- 멀티 쓰레드 프로그래밍을 위한 프레임워크(JDK1.7)

- RecursiveAction 또는 RecursiveTask를 상속받아서 구현

- RecursiveAction : 반환값이 없는 작업을 구현할 때 사용

- RecursiveTask : 반환값이 있는 작업을 구현할 때 사용

- 이용 방법: 위 두 클래스의 compute()라는 추상메소드 구현 후 invoke()로 구동

- ForkJoinPool은 fork & join프레임워크에서 제공하는 쓰레드 풀(Thread Pool)

특징: 각 쓰레드가 작업큐를 제공받아 각자에게 담긴 작업을 순서대로 처리

장점: 쓰레드 반복생성 or 과다 생성 방지, 작업 큐가 비어있는 쓰레드는 다른 쓰레드의 작업 큐를 가져와서 수행

- fork(): 해당 작업을 쓰레드 풀의 작업 큐에 넣는다(비동기 메소드)

- join(): 해당 작업의 수행이 끝나면 결과를 반환(동기 메소드)

- 과정: compute() 작업 분리 -> fork() 작업큐에 제공 -> join() 작업 결과 반환