본문 바로가기

Java

[Java] # Thread, 쓰레드

반응형

 

 Java Thread

  • 프로세스는 '실행중인 프로그램'
    • 데이터와 메모리, 자원, 쓰레드로 구성
  • 실제로 작업을 수행하는 것이 쓰레드
  • OS는 멀티 태스킹(다중 작업)
  • 하나의 프로세스는 멀티 쓰레드

멀티 쓰레드 장점

  • CPU 사용률 향상
  • 사용자 응답성 향상
  • 작업리 분리되어 코드 간결
  • 자원을 공유하므로 동기화, 교착상태와 같은 문제 발생할 수 있음.

쓰레드 구현 방법

  • Thread 상속 받는 방법
  • class MyThread extends Thread {
    public void run() {
    }
    }

    MyThread th1 = new MyThread();
    th1.start();
  • Runnable 인터페이스 구현하는 방법
  • class MyThread Implements Runnable {
    public void run() {
    }
    }

    Runnable r = new MyThread();
    Thread th1 = new Thread(r);
    th1.start();
  • Runnable 인터페이스 구현하는 방법이 일반적, 다른 클래스 상속 받을수 없기 때문에

쓰레드 이름 가져오기

  • Thread 클래스를 상속 받으면 조상인 Thread 클래스의 메서드 직접 호출 → getName()
  • Runnable 인터페이스로 구현하면 static 메서드인 currentThread() 호출 후 getName()

쓰레드 이름 지정

  • 생성자나 메서드 통해 지정
  • //생성자
    Thread(Runnable r, String name)
    Thread(String name)

    //메서드
    void setName(String name)

쓰레드 실행 → start();

  • 한번 실행이 종료된 쓰레드는 다시 실행할 수 없다
  • 2번이상 호출하면 IllegalThreadStateException 발생

START()와 RUN()

  • main메서드에서 run()을 호출 하는 것은 쓰레드를 실행 시키는 것이 아니라 단순히 메서드 호출한 것
  • start() 호출은 호출스택 생성 → run() 호출하여 호출 스택에 run()이 첫번째로 올라감
  • main메서드의 작업도 하나의 쓰레드이다.
  • 실행중인 쓰레드가 없을 경우 프로그램은 종료된다.

싱글쓰레드, 멀티쓰레드

  • 프로세스 또는 쓰레드 간의 작업 전화을 컨텍스트 스위칭이라고 한다
  • 싱글 코어에서 단순히 CPU만을 사용하는 계산 작업이면 오히려 멀티쓰레드보다 싱글쓰레드가 더 효율적
  • 여러 쓰레드가 번갈아가면서 작업을 처리하면 컨텍스트 스위칭 시간이 소요되서 오히려 안좋을수 있음
  • 자바가 OS 독립적이라고 하지만 OS 종속적인 부분이 몇가지 있는데 쓰레드도 그중 하나

쓰레드의 우선순위

  • 우선순위 속성을 가지고 있음
  • void setPriority(int priority) // 우선 순위 지정
    int getPriority() // 우선 순위 반환
    //최대 우선 순위 = 10
    //최소 우선 순위 = 1
    //기본 우선 순위 = 5

쓰레드 그룹

  • 쓰레드 그룹을 생성하여 묶어서 관리할 수 있음.
  • 그룹에 포함 시키는 방법은 생성자를 이용
  • Thread(ThreadGroup group, String name)
    Thread(ThreadGroup group, Runnable target)
    Thread(ThreadGroup group, Runnable target, String name)
    Thread(ThreadGroup group, Runnable target, String name, long stackSize)
  • 모든 쓰레드는 반드시 그룹에 포함 되어야 함
  • 기본적으로 자신을 생성한 쓰레드와 같은 그룹으로 지정
  • 자바 어플리케이션이 실행되면, JVM은 main과 system 쓰레드 그룹을 생성
  • 우리가 생성하는 모든 쓰레드 그룹은 main쓰레드 그룹의 하위 쓰레드 그룹이 됨
  • 그룹을 지정하지 않으면 main쓰레드 그룹에 속함
ThreadGroup getThreadGroup();   //자신이 속한 쓰레드 그룹 반환

데몬 쓰레드

  • 작업을 돋는 보조적인 역할을 수행하는 쓰레드
  • 일반 쓰레드가 종료되면 데몬쓰레드는 강제적으로 자동 종료
  • 데몬쓰레드 예로는 가비지 컬렉션, 워드프로세서로 치면 자동 저장, 화면 갱신 등...
  • 쓰레드 생성 및 실행방법은 같으며 실행 전에 setDaemon(true) 호출 하면 됨.
  • setDaemon 메서드는 반드시 start()를 호출하기 전에 실행 되어야 한다.
    • 아닐 경우 IllegalThreadStateException 발생
  • getAllStackTraces() 실행 중 또는 대기상태, 즉 작업 완료되지 않은 모든 쓰레드의 호출 스택을 출력 할 수 있다.

쓰레드 실행 제어

  • 쓰레드 프로그래밍이 어려운 이유는 동기화와 스케쥴링 때문
  • 스케쥴링 메서드
  • //지정된 시간 일시정지
    sleep(long mills)
    sleep(long mills, int nanos)

    //지정된 시간 동안 실행
    join()
    join(long mills)
    join(long mills, int nanos)

    //일시정지된 쓰레드 깨워서 실행 대기상태로 만듦
    interrupt()

    //즉시 종료
    stop()

    //일시 정지
    suspend()

    //일시 정지 -> 실행 대기 상태
    resume()

    //다른 쓰레드에게 양보 -> 자신은 실행 대기 상태
    yield()
  • 쓰레드 상태
    • NEW : 아직 start() 호출되지 않은 상태
    • RUNNABLE : 실행 중 또는 실행 가능 상태
    • BLOCKED : 동기화 블럭에 의해 일시 정지 상태
    • WAITING, TIMED_WAITING : 종료되진 않았지만 실행 가능하지 않은 상태, 일시 정지 상태
    • TERMINATED : 작업이 종료된 상태

Sleep()

  • 참조변수를 이용해서 호출해도 현재 실행 중인 쓰레드가 영향을 받음.
  • Thread.sleep 으로 호출 하는게 좋음

interrupt(), interrupted()

  • interrupt() : 쓰레드에게 작업을 멈추라고 요청, 종료시키지는 못함
    • 쓰레드의 interrupted 상태(인스턴스 변수)를 바꾸는 역할
  • interrupted() : interrupt() 호출 여부 반환 후 false로 초기화
    • 호출되었으면 true
    • 호출되지 않았으면 false
    • 쓰레드 클래스 안에서 확인해보면 isInterrupted가 true 일 때, interrupted() 호출하면 true를 반환한다. 다시한번 isInterrupted를 호출하면 그때는 false를 반환한다. 초기화 시켜서
  • isInterrupted() : interrupt() 호출 여부 반환

suspend(), resume(), stop()

  • suspend()와 stop() 교착상태를 일이크기 쉽게 작성되어 사용 권장되지 않음

yield()

  • 다음 차례 쓰레드에게 양보
  • yield(), interrupt() 적절히 사용하면 응답성을 높일 수 있음.

JOIN()

  • 자신 쓰레드를 잠시 멈추고 다른 쓰레드가 작업을 수행하도록 할 때 JOIN 사용
  • 시간을 지정하지 않으면 해당 쓰레드가 모두 마칠 때까지 기다림

쓰레드 동기화

  • 멀티 쓰레드 프로세서의 경우 같은 프로세스 내의 자원을 공유해서 서로 작업에 영향을 주게 됨
  • 한 쓰레드가 특정 작업을 마치기 전까지 방해 받지 않도록 하는것이 필요하여 도입된 개념이 임계 영역(ciritical section), 잠금(lock)
  • java에서는 synchronized 블럭을 이용하는 방법으로 지원했었지만 JDK 1.5부터는 locks, atomic 패키지를 지원
  • synchronized 이용
  • //메서드 전체를 임계영역으로 지정
    public synchronized void mathod() {

    }

    //특정 영역 지정
    synchronized(객체의 참조 변수) {

    }
  • 모든 객체는 lock을 하나씩 가지고 있음.
  • critical section은 프로그램의 성능을 좌우하기 때문에 최소화하는 것이 중요

wait(), notify()

  • 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는 것이 중요
  • 작업을 더 이상 진행할 상황이 아니면 일단 wait() 호출
    • 매개변수가 있을 경우 지정된 시간 동안만 대기
  • 다시 진행할 상황이 되면 notify()를 호출
  • void wait()
    void wait(long timeout)
    void wait(long timeout, int nanos)
    void notify()
    void notifyAll()
  • wating pool은 객체마다 존재
  • notifyAll() 호출 된다고 모든 객체의 waiting pool에 있는 쓰레드가 깨워지는건 아님
  • 호출된 객체의 waiting pool에 있는 쓰레드만 해당

기아 현상, 경쟁 상태

  • 쓰레드 통지를 계속 받지 못하는 상황을 기아(starvation) 현상이라 함
    • 이 현상을 막으려면 notify() 대신 notifyAll()을 사용
  • 여러 쓰레드가 lock을 엳기 위해 서로 경쟁하는 것을 경쟁 상태(race condition)
    • 쓰레드를 구별해서 통지하는 것이 필요

lock과 condition

  • synchronized을 이용한 동기화 방법은 같은 메서드 내에서만 lock을 걸 수 있는 제약이 불편
  • lock클래스를 사용하면 됨
    • ReentrantLock → 재진입이 가능한 lock, 일반적인 배타적인 lock
    • ReentrantReadWriteLock → 읽기에는 공유적, 쓰기에는 배타적
      • 다른 쓰레드가 읽기 lock을 중복해서 걸고 읽기를 수행할 수 있다.
      • 대신 읽기 lock은 중복으로 허용되지 않는다.
      • 내용은 변경되지 않으므로 문제 되지 않는다
    • StampedLock → ReentrantReadWriteLock에 낙관적인 lock의 기능 추가
      • 읽기 lock이 걸려 있으면 쓰기 lock을 얻기 위해 읽기 lock이 풀릴 때까지 대기
      • 읽기 lock은 쓰기 lock에 의해 바로 풀림.
      • 쓰기가 끝난 후에 읽기 lock을 거는 것

ReentrantLock

ReentrantLock()
ReentrantLock(boolean fair) //가장 오래 기다린 쓰레드가 lock 획득

ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
  //임계 영역
} finally {
  lock.unlock();
}
  • tryLock() → 다른 쓰레드에 의해 lock이 걸려 있으면 얻으려고 기다리지 않음
    • 지정된 시간만 기다림.
    • 얻지 못하면 false 반환

condition

  • await() → wait()라 생각
  • signal() → notify()
  • signalAll() → notifyAll()
ReentrantLock lock = new ReentrantLock();

private Condition forlock1 = lock.newCondition();
private Condition forlock2 = lock.newCondition();

volatile

  • 멀티 코어 프로세서는 코어마다 별도의 캐시를 가지고 있다.
  • 메모리에서 읽어온 값을 캐시에 저장하고 캐시에서 값을 읽어서 작업한다
  • 메모리에 저장된 변수의 값이 변경되었는데도 캐시에 저장된 값이 갱신 되지 않아서 문제가 발생
  • 이럴 때 이렇게
  • boolean stopped = false; -> volatile boolean stopped = false;
  • synchronized 블럭 사용해도 같은 효과
    • 쓰레드가 synchronized블럭으로 들어갈때와 나올때 캐시와 메모리간 동기화가 이루어지기 때문
  • volatile long sharedVal; // long 타입의 변수(8byte)를 원자화

fork, join 프레임웍

  • JDK 1.7부터 추가
  • 하나의 작업을 작은 단위로 나눠서 여러 쓰레드가 동시에 처리하는 것을 쉽게 만들어줌
  • RecursiveAction, RecursiveTask 두 클래스 중에서 하나 상속 받아 구현
    • RecursiveAction → 반환값이 없는 작업을 구현할 때 사용
    • RecursiveTask → 반환값이 있는 작업을 구현할 때 사용
    • 두 클래스는 cimpute()라는 추상 메서드가 있고 추상 메서드를 구현하기만 하면됨
class testTask extends RecursiveTask<Long> {
    public Long compute() {
        // 로직 구현
    }
}

참고

  • Java의 정석 3rd Edition

 

반응형

'Java' 카테고리의 다른 글

[Java] #Formating, 포맷  (0) 2018.04.16
[Java] #Time, Date, 시간, 날짜  (2) 2018.04.16