Java

[Java] 스레드

삶_ 2022. 6. 21. 17:07

 

 

쓰레드

  • 프로세스 : 실행중인 프로그램
  • 쓰레드 : 프로세스 내에서 실제 작업을 수행하는 주체 (코드의 실행 흐름)
    • 모든 프로세스는 최소 하나의 쓰레드를 가짐
    • Thread.currentThread(); 
      • 스레드의 인스턴스 메서드를 쓰려면 객체의 참조가 있어야 아래를 쓸수있음!
      • currentThread() : 스레드의 주소를 알려줌
    • 스레드의 이름
      • Thread-n(n은 숫자) 이라는 이름으로 설정됨
      • thread.setName("이름") : 스레드 이름 -> 이름으로 설정
      • thread.getName()  : 스레드의 이름 반환(= Thread-n)
    • 멀티 스레드 : 하나의 프로세스로 두가지 이상의 작업을 실행함
      • 따라서 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있음
      • 메인 스레드 + 스레드1 + 스레드2 ... (작업 스레드들)
      • 멀티 스레드는 동시성/병렬성으로 실행된다
        • 스레드 스케줄링 : 스레드들을 어떤 순서에 의해 동시성으로 실행할건지 결정함
        • 스레드들이 아주 짧은 시간에 번갈아가면서 실행 메서드들을 조금씩 실햄함
    • 메인 스레드 : main() 메서드의 첫코드부터 끝코드까지
      • (어떤 자바 프로그램이든 메인스레드는 반드시 존재)
      • 도중에 필요에 따라 작업용 멀티스레드를 생성해 병렬로 코드를 실행시킴
    • 쓰레드의 생성
      • 메인메서드에서 스레드 객체(매개변수는 실행할 클래스) 생성
      • thread.start() 메서드를 호출 (클래스 내의 메서드(작업스레드) 실행)

 

쓰레드 생성 방법 (1) - Runnable 인터페이스 구현

  • 스레드를 관리하기 쉽고, 다른 인터페이스와 함께 사용하기 편리함
  • 구현할 메서드가 run() 하나 뿐인 함수형 인터페이스
//1번째 방법
//Runnable을 매개값으로 갖는 생성자 호출(1. 스레드 객체 생성)
Thread thread = new Thread(Runnable Task);

//"string" 이름으로 스레드 저장 및 생성
Thread thread = new Thread(Runnable Task, "string");

//Runnable 구현 클래스
class Task implements Runnable {
	public void run(){  //2. run()메서드 호출
    	스레드가 실행할 코드;
    }
}

//메인 메서드라고 칠때 그 안에 넣기
thread.start(); //3.start() 로 스레드 실행



//2번째 방법 - 이게 많이 쓰임
// new Runnable() -> 익명 구현클래스. Task 클래스 이름 생략
Thread thread = new Thread(new Runnable() {
	public void run(){
    	스레드가 실행할 코드;
    }
} );

 

 

쓰레드 생성 방법 (2) - Thread 클래스 상속 

// 1번째 방법
public class WorkerThread extends Thread {
	public void run(){
    	//스레드가 실행할 코드 //run() 오버라이딩
    }
}
Thread thread = new WorkerThread();


// 2번째 방법
Thread thread = new Thread(){
	public void run(){
    	//스레드가 실행할 코드 //run() 오버라이딩
    }
};

 

 

쓰레드의 우선순위

thread.setPriority(우선순위) : 우선순위를 지정한 값으로 변경

  • setMaxPriority(우선순위) : 스레드 그룹의 최대 우선순위를 설정

thread.getPriority() : 우선순위를 반환

  • 스레드 객체에 우선순위 번호를 부여해서 개발자가 코드로 제어하는법
  • 숫자가 높을수록 우선순위가 높음
    • 하지만 스레드 5개 이상이 아니면 별 영향을 못끼친다
      • 멀티코어에서는 쓰레드의 우선순위에 따른 차이가 없다
      • 싱글코어일 때, 우선순위가 높은 스레드가 더 많은 양의 실행시간이 주어지고 빨리 완료함
      • 1부터 10까지의 범위. 기본적으로 5를 할당받음
        • 우선순위 안에 1~10을 넣어도 되고, (예시. Thread.MAX_PRIORITY)같은 상수를 넣어도 됨
        • Thread.MAX_PRIORITY : 10
        • Thread.MIN_PRIORITY : 1
        • Thread.NORM_PRIORITY : 5  *(main()을 실행하는 스레드의 우선순위 이기도 하다)

 

 

 

쓰레드의 동기화

  • 한 스레드가 다른 스레드에게 작업을 방해받지 않도록 막는 것
    • 멀티 스레드는 객체를 공유해서 작업하는 경우가 많음
    • 스레드가 작업 도중 다른 스레드에게 제어권이 넘어가면 문제를 일으킴
    • 임계영역 설정 : 공유데이터를 사용 시 스레드가 lock을 얻어 방해받지 않고 쓰는 곳
      • 영역을 벗어나면 lock을 반납

 

synchronized 를 이용한 동기화

  • 임계영역 설정방법
// (1) 메서드 전체를 임계영역으로 만듬
public synchronized void method(){
	//단 하나의 스레드만 실행
}

// (2) 특정한 영역을 임계영역으로 만듬 - 추천(성능 효율화)
public void method(){
	//<--여러 스레드가 실행 가능한 영역-->
    synchronized(객체의 참조변수){
    	//단 하나의 스레드만 실행
    }
    //<--여러 스레드가 실행 가능한 영역-->
}

 

 

wait(), notify()

  • 동기화 블록 내에서만 사용 가능 (다른 스레드의 개입을 막기위해)
  • 특정 스레드가 lock을 너무 오랫동안 갖고 있다면, wait() 으로 lock을 반납.
    • wait() : 갖고있던 lock을 해제. 현재 실행중인 스레드를 잠재움.
    • 다른 스레드에서 notify를 호출해주면 깨어난다
  • 그리고 다시 작업을 진행할 수 있게 되면 notify() 로 lock을 얻기.
    • notify() : 잠들어있는 다른 스레드 중 임의로 하나를 깨움. lock을 얻게함.
    • notifyAll() : waiting pool에 대기중인 스레드만 lock을 얻음.
    • 운이 나쁘면 깨워야 하는 스레드가 영영 깨어나지 못하므로,
    • notifyAll 을 사용해 모두 깨는방법을 애용한다

 

 

 

Lock, Condition을 이용한 동기화 - lock 클래스들

  • 수동으로 lock을 잠그거나 해제
  • 메서드
    • lock() : lock 을 잠금
    • unlock() : lock을 해지함
      • try-finally 문 이용.
      • (임계 영역 내에서 예외발생 할수도 있으니)
    • isLocked() : lock 이 잠겼는지 확인
    • thread.trylock() : 해당 스레드가 trylock() 실행 시점에 lock을 취득할 수 있는지 파악

 

ReenTrantLock

  • 재진입이 가능한 lock. 가장 일반적인 배타 lock
  • 무조건 lock이 있어야만 임계영역의 코드 수행 가능
  • 생성자
    • ReenTrentLock()
    • ReenTrentLock(boolean fair) : fair=true 이면, 가장 오래 기다린 쓰레드가 lock을 획득할 수 있음
  • Condition()
    • producer & consumer 패턴에서 잘 쓰임
    • Condition forcook = lock.newCondition();
    • wait(), notify() 대신 await() => 기다리기, signal() => 깨움 을 사용

 

ReentrantReadWriteLock

  • 읽기에는 공유적이고, 쓰기에는 배타적인 lock
  • 읽기 lock이 걸려있으면, 다른 쓰레드가 읽기 lock을 중복해서 걸고 읽기를 수행 가능 (쓰기 lock은 불가능)

 

StampedLock

  • ReentrantReadWriteLock 에 낙관적인 lock 기능 추가
  • lock을 걸기/해지 시, 스탬프(long 타입)를 사용
  • 읽기/쓰기 lock + 낙관적 읽기 lock
    • StampedLock 외엔 읽기 lock이 끝나야 비로소 쓰기 lock 사용 가능한데
    • 낙관적 읽기 lock은 쓰기 lock을 쓰자마자 풀림
    • 낙관적 읽기 lock이 풀릴 시, 읽기 lock을 얻어서 다시 읽어와야 함

 

 

 

 

 

 

스레드 상태

  • 스레드 실행과정 : 생성 (start() 를 실행) -> 실행대기상태 -> 실행 -> 소멸
  • 실행 -> 일시정지상태 가 되면, 다시 실행하기위해 실행대기상태로 보내야한다.
    • 생성(start()) -> 실행대기(yield()) -> 실행(suspend(), sleep(), wait(), join(), I/O block)
    • -> 일시정지(WAITING, BLOCKED) -> time-out, resume(), notify(), interrupt() -> 실행대기(yield())
    • -> 일시정지(WAITING, BLOCKED) -> stop() -> 소멸
  • 스레드 상태 확인법
    • Thread.State state = targetThread.getState();
    • if (state == Thread.State.NEW) {...}
    • 위의 객체의 참조가 있어야 아래를 쓸 수 있다!
      • 객체생성
        • Thread.State.NEW : 스레드 객체가 생성. start()가 호출되지 않은상태
      • 실행대기
        • Thread.State.RUNNABLE : 실행 상태로 언제든지 갈 수 있는 상태
      • 일시정지
        • Thread.State.WAITING : 다른 스레드가 통지할 때까지 기다리는 상태
        • Thread.State.TIMED_WAITING : 주어진 시간 동안 기다리는 상태
        • Thread.State.BLOCKED : 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태
      • 종료
        • Thread.State.TERMINATED : 실행을 마친 상태

 

 

스레드 실행 제어 메서드

  • 항상 현재 실행중인 스레드에 대해 작동함
  • 정지
    • Thread.sleep()
      • 주어진 시간동안 스레드 일시정지 (1/1000 단위)
      • 스레드 실행 자체를 일시정지시킴
      • Thread.sleep(1000) : 1/1000*1000초 동안 일시정지함
      • InterruptedException 예외처리
        • sleep(), wait() 등으로 스레드에서 하던 일을 멈추라는 신호를 보낼 때, InterruptedException 예외가 발생
        • -> try-catch 문으로 예외처리하기
  • Thread.yield()
    • 호출한 메서드는 실행 대기로 돌아가고, 다음 차례의 스레드에게 실행을 양보한다
  • thread2.join()
    • 현재 실행중인 스레드가 thread2가 다 끝날때까지 기다림
    • main 내에서 실행됬다면 main 스레드가 기다림
  • wait() notify() notifyAll()
    • 동기화 메서드/동기화 블록 내에서만 사용 가능하다
    • 스레드가 작업완료시 notify() 호출 : 일시정지된 다른 스레드를 실행대기상태로 만듬
    • -> 그 후 본인 스레드는 wait() 호출해서 일시정지상태로 만듬
      • notify() : wait()에 의해 일시 정지된 스레드중 하나만 실행대기상태로 만듬
      • notifyAll() : 위와 같지만 모든 스레드를 실행대기상태로 만듬
  • 스레드의 안전한 종료
    • 스레드는 run() 실행 후 자동으로 종료됨
      • 하지만 개발자가 실행중인 스레드를 즉시 종료할 때가 필요함
      • (사용자가 종료를 요구할 경우 => 안전하게 종료해야 함)
    • stop(), suspend(), resume()
      • 데드락을 일으키기 쉽게 작성되어 있어, 사용 권장 X 
      • stop()
        • 실행해야 할 메서드 내에
        • while(!stop){} 문을 만들어서 stop()이 true가 되면 안전하게 종료될수 있도록 한다
      • suspend()
        • 스레드를 정지하고, resume()을 호출해야 실행대기상태가 됨
    • Interrupt()
      • interrupt() : 해당 스레드에 인터럽트를 검
        • 스레드의 interrupted상태를 false -> true로 변경
        • 일시정지 상태의 스레드에서 InterruptedException 예외를 발생시킴
        • 일시정지상태가 아니면 실행되지 않는다
        • 예외 처리코드에서 실행대기/종료상태 중에 갈수있도록 한다
      • interruped() : 현재 스레드의 interrupted 상태 반환 후 false로 변경
        • 호출될시 interrupted()는 true, 호출되지 않으면 false
      •  isinterrupted() : 현재 스레드의 인터럽트 상태 해제. 해제하기 전 interrupted 상태를 반환
  • 스레드 호출
    • start() : 새로운 쓰레드가 작업을 실행하는 데 필요한 호출스택을 생성한 다음 run() 실행
    • run() : 단순히 생성된 스레드의 run() 메서드를 실행하는 것

 

 

 

 

데몬 스레드

  • 일반 스레드의 작업을 돕는 보조적인 역할 수행
  • 주 스레드가 종료시 데몬 스레드도 강제적으로 종료된다
    • ex. 자동저장기능/음악재생/가비지 컬렉터
    • 무한루프문에 주로 사용됨
  • 스레드 -> 데몬 스레드 만들기
    • setDaemon(true)  //true면 데몬스레드가 됨
    • IllegalThreadStateException 에러 발생을 막기위해 start() 호출 전에 실행하기
    • Boolean isDaeMon() : 쓰레드가 데몬 쓰레드인지 확인하기
public static void main(String[] args){
	AutoSaveThread thread = new AutoSaveThread();
    thread.setDaemon(true); //start() 호출전에 얘 먼저 호출해야함! 에러생김.
    thread.start();
    ...
}

 

 

스레드 그룹

  • 서로 관련된 스레드를 묶어 관리하는 것
  • 스레드는 반드시 하나의 스레드 그룹에 포함
  • 스레드 그룹을 지정안하고 생성할 시, main스레드 그룹에 속한다

 

스레드 그룹 이름 얻기

//getThreadGroup() : 쓰레드 자신이 속한 쓰레드 그룹을 반환함
ThreadGroup group = Thread.currentThread().getThreadGroup();
String groupName = group.getName();

 

스레드 그룹 생성

  • 스레드그룹 이름만 주거나, 부모 스레드그룹과 이름을 매개값으로 줌
  • 부모 스레드그룹을 지정하지 않으면, 현재 스레드가 속한 그룹의 하위스레드그룹으로 생성된다
ThreadGroup tg = new ThreadGroup(String name)
ThreadGroup tg = new ThreadGroup(ThreadGroup parent, String name)

 

새 스레드 그룹에 스레드 넣기

  • Thread 객체를 생성할때 매개값에 넣어주면 됨
  • ThreadGroup group = new Thread(스레드그룹, ...)
    • Runnable target = Runnable 구현객체 target
    • String name = 스레드 이름 name
    • long stackSize = JVM이 이 스레드에 할당할 stack 크기
Thread t = new Thread(ThreadGroup group, Runnable target);
Thread t = new Thread(ThreadGroup group, Runnable target, String name);
Thread t = new Thread(ThreadGroup group, Runnable target, String name, long stackSize);
Thread t = new Thread(ThreadGroup group, String name);

 

메서드

  • 생성
    • ThreadGroup(이름) : 지정된 이름의 새로운 스레드그룹 생성
    • ThreadGroup(스레드그룹, 이름) : 지정된 스레드그룹에 포함되는 새로운 스레드그룹 생성
  • 스레드 수
    • activeCount() : 스레드 그룹에 포함된 활성상태의 스레드 수 반환
    • activeGroupCount() : 스레드 그룹에 포함된 활성상태의 스레드 그룹의 수 반환
  • 삭제
    • destory() : 스레드 그룹과 하위 스레드 그룹까지 모드 삭제. 단 스레드 그룹은 비워져 있어야 함.
  • 반환
    • getMaxPriority(우선순위) : 스레드 그룹의 최대우선순위를 반환
    • setMaxPriority(우선순위) : 스레드 그룹의 최대우선순위를 설정
    • getName() : 스레드 그룹의 이름 반환
    • list() : 스레드 그룹에 속한 스레드와 하위 스레드그룹에 대한 정보를 출력
  • 확인
    • isDaemon() : 스레드 그룹이 데몬스레드 그룹인지 확인
    • isDestroyed() : 스레드 그룹이 삭제되었는지 확인
  • 그외
    • uncaughtException(Thread t, Throwable e)
      • 스레드 그룹의 스레드가 처리되지 않은 예외에 의해 실행이 종료될 시, JVM에 의해 이 메서드가 자동 호출됨
    • getAllStackTraces()
      • map 타입의 객체 반환
      • 모든 스레드의 호출스택을 출력할 수 있음 (실행중/대기)
      • key=스레드 인스턴스, value=스레드 상태를 기록하고 있는 호출스택의 정보를 담은 배열

 

 

 

스레드 그룹의 일괄 interrupt()

  • 새 스레드 그룹에 스레드를 넣으면, 그룹 내의 모든 스레드들을 일괄 interrupt() 할 수 있음
  • 한번만 interrupt() 호출해주면 됨. 대신 예외처리를 해줘야함
  • 스레드그룹의 메서드
    • activeCount() : 현재 그룹 및 하위 그룹에서 활동중인 스레드 수를 반환
    • activeGroupCount() : 현재 그룹에서 활동중인 모든 하위 그룹의 수를 반환
    • checkAccess() : 현재 스레드가 스레드 그룹을 변경할 권한이 있는지 체크
    • destroy() : 현재 스레드 그룹과 하위 스레드 그룹까지 모두 삭제
    • getMaxPriority() : 현재 그룹에 포함된 스레드가 가질수있는 최대우선순위를 반환

 

스레드풀

  • 작업 큐에 들어오는 작업들을 제한된 갯수만큼 하나씩 스레드가 맡아 처리하게 하는 것
    • 병렬 작업 처리가 많아지면 스레드 개수 증가/CPU가 바빠져 메모리 사용량이 늘어남
    • 따라서 위의 스레드 폭증을 막기 위해 스레드풀이 필요함
    • 요청이 폭증해도 스레드풀 내에 스레드의 전체 갯수를 늘어나지 않게 하므로

 

스레드 풀 생성

  • = ExecutorService 구현객체
  • ExecutorService는 Executors 클래스의 메서드들을 통해 생성 가능
    • 초기 스레드수: 객체가 생성될때 기본적으로 생성되는 스레드수
    • 코어 스레드수 : 스레드 수가 증가된 후 사용되지 않는 스레드를 제거시, 최소한 유지해야할 스레드 수
    • newCachedThreadPool()
      • 초기스레드/코어스레드 갯수 0, 최대스레드수 Integer.MAX_VALUE
      • 스레드 개수 > 작업개수면 새 스레드를 생성시켜 작업을 처리함
      • 1개 이상의 스레드가 추가됬을 때, 해당 스레드가 60초 동안 아무 작업을 하지 않으면 스레드를 종료하고 풀에서 제거함
      • ExecutorService executorService = Executors.newCashedThreadPool();
    • newFixedThreadPool(int nThreads)
      • 초기스레드 수 0, 코어/최대스레드수 nThreads
      • 스레드 개수 > 작업개수면 새 스레드를 생성시켜 작업을 처리함
      • 스레드가 작업을 안하고 놀고있어도 스레드 개수는 줄지 않는다
      • ExecutorService executorService = Executors.newCashedThreadPool(
      • Runtime.getRuntime().availableProcessors() );

 

스레드풀 종료

  • 스레드풀은 main()이 끝나도 프로세스가 종료되지않음
  • 따라서 ExecutorService는 종료와 관련된 메서드를 가짐
    • shutdown()
      • 현재 처리 중인 작업, 작업 큐에 대기하고 있는 모든 작업을 처리한 뒤에 스레드풀 종료
    • shutdownNow()
      • 현재 작업처리중인 스레드를 interrupt 해서 작업중지 하고 스레드풀을 종료시킴
      • shutdown()에 비해 강제종료 느낌임
    • awaitTermination(long timeOut, TimeUnit unit)
      • shutdown() 호출 이후, 모든 작업처리를 timeOut 시간 내에 완료하면 true 반환
      • 아니면 작업 처리중인 스레드를 interrupt하고 false 반환