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()을 실행하는 스레드의 우선순위 이기도 하다)
- 하지만 스레드 5개 이상이 아니면 별 영향을 못끼친다
쓰레드의 동기화
- 한 스레드가 다른 스레드에게 작업을 방해받지 않도록 막는 것
- 멀티 스레드는 객체를 공유해서 작업하는 경우가 많음
- 스레드가 작업 도중 다른 스레드에게 제어권이 넘어가면 문제를 일으킴
- 임계영역 설정 : 공유데이터를 사용 시 스레드가 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.sleep()
- 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 상태를 반환
- interrupt() : 해당 스레드에 인터럽트를 검
- 스레드는 run() 실행 후 자동으로 종료됨
- 스레드 호출
- 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=스레드 상태를 기록하고 있는 호출스택의 정보를 담은 배열
- uncaughtException(Thread t, Throwable e)
스레드 그룹의 일괄 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 반환
- shutdown()