JAVA 10 : 스레드

LeeWonjin·2022년 7월 25일

2022 백엔드스터디

목록 보기
10/20

남궁성의 정석코딩 : 자바의 정석 기초편(2020최신) ch13

개요

개념

  • 프로세스 : 실행중인 프로그램. 최소한 하나의 스레드 가짐
  • 스레드 : 프로세스 내에서 실제 작업을 수행하는 CPU작업단위
    • 사용자 스레드 : 사용자가 생성하는 스레드 (메인스레드 포함)
    • 데몬 스레드 : 사용자 스레드를 보조하는 역할

사용자 스레드가 0개일 때 프로그램 종료
스레드 도대체 무엇이길래 나를 힘들게 하나?(JAVA)

고려사항

  • 장점 : 시스템 자원 효율적 사용, 응답형 향상
  • 단점 : 동기화, 교착상태, 공평한 태스크 배분 문제해결 필요

구현

  • Thread클래스를 상속받아 run() 오버라이드
  • Runnable인터페이스를 구현하고 run() 구현

실행

  1. 어떤 스레드 T의 메소드start()를 메인스레드의 호출스택에 올림
  2. T를 실행가능한 상태로 전환
  3. T의 start() 호출
  4. T를 위한 새로운 호출 스택에 T의 run()을 올리고 호출
  • 실행순서는 OS 스케쥴러가 결정 (시작과 끝 시점을 예상할 수 없음)
class GoraniExt extends Thread {
	@Override
	public void run() {
		System.out.println(getName());
		System.out.println("RUN : I extended Thread");
	}
}

class GoraniImpl implements Runnable {
	@Override
	public void run() {
		System.out.println(Thread.currentThread());
		System.out.println("RUN : I implemented Runnable");
	}
}

public class Test {
	public static void main(String[] args) {
		GoraniExt ge = new GoraniExt();
		ge.start(); 
		// Thread-0
		// RUN : I extended Thread
		
		GoraniImpl gi = new GoraniImpl();
		Thread giThread = new Thread(gi);
		giThread.start(); 
		// Thread[Thread-1,5,main]
		// RUN : I implemented Runnable
	}
}

join() 메소드로 생성한 스레드의 종료를 기다릴 수 있음

public class Test {
	public static void main(String[] args) {
		GoraniExt ge = new GoraniExt();
		ge.start();
		
		Thread giThread = new Thread(new GoraniImpl());
		giThread.start(); 
		
		try {
			ge.join();
			giThread.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		System.out.println("Main : End");
		// ge, giThread가 종료된 다음 "Main : End"출력
	}
}

스레드

I/O Blocking

입출력시 작업을 중단하는 것
한 스레드 내에서 외부장치와 입출력을 수행하는 동안 아무 작업도 하지 않음

아래 코드는 다음과 같이 동작

  • t1의 run() 실행, 그 동시에 사용자 입력 받음
  • 사용자 입력이 끝난 뒤 t2의 run() 실행
class GoraniExt extends Thread {
	@Override
	public void run() {
		System.out.println(getName() + " : start");
		
		for(int i=0; i<5; i++)
			System.out.println(getName() + " " + i);
		
		System.out.println(getName() + " : end");
	}
}

public class Test {
	public static void main(String[] args) throws InterruptedException {
		 
		GoraniExt t1 = new GoraniExt();
		GoraniExt t2 = new GoraniExt();

		t1.start();
		
		Scanner sc = new Scanner(System.in);
		System.out.println(sc.next());
		
		t2.start();
		
		// join
		t1.join();
		t2.join();
		System.out.println("main thread : end");
	}
}

스레드 우선순위

JVM에서 스레드 우선순위를 1~10까지 설정가능
숫자가 높은(우선순위가 높은) 스레드에 많은 작업시간 할당

메인스레드 그룹은 아래와 같이 설정되어있음

  • 최대 우선순위(MAX_PRIORITY) : 10
  • 보통 우선순위(NORM_PRIORITY) : 5
  • 최소 우선순위(MIN_PRIORITY) : 1

변경과 조회 가능

  • 우선순위 변경 : void setPriority(int)
  • 우선순위 조회 : int getPriority()

JVM단에서 우선순위를 설정했다고 하여도 실제로 OS단에서 스케쥴링이 어떻게 될지는 알 수 없음
아래 코드는 더 낮은 우선순위 스레드가 먼저 끝날 수도 있음을 보임

class Gorani extends Thread {
	private char ch;
	public Gorani(char ch) { this.ch = ch; }
	
	@Override
	public void run() {
		System.out.println("\n" + getName() + " : start");
		for(int i=0; i<100; i++)
			System.out.print(ch);
		System.out.println("\n" + getName() + " : end");
	}
}

public class Test {
	public static void main(String[] args) throws InterruptedException {
		Gorani t1 = new Gorani('O');
		Gorani t2 = new Gorani('X');

		t1.setPriority(1);
		t2.setPriority(10);
		System.out.println(t1.getPriority() + " " + t2.getPriority()); // 1 10
		
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		
		System.out.println("main thread : end");
	}
	
	// < t2 (Thread-1)가 빨리 종료되는 예시 >
	//	Thread-0 : start
	//	Thread-1 : start
	//	XOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXOOOOOOOOXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXOOOOOOOXXXXXXXXXXXXXXXXXXOOOOOOOOOOOOOOOOOOOO
	//	Thread-1 : end
	//	OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
	//	Thread-0 : end
	//	main thread : end
	
	// < t1 (Thread-0)가 발리 종료되는 예시 >
	//	Thread-0 : start
	//	Thread-1 : start
	//	XOOOOOOOOOOOOOOOOOOOOXXXOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
	//	Thread-0 : end
	//	XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
	//	Thread-1 : end
	//	main thread : end
}

스레드 그룹

모든 스레드는 반드시 하나의 스레드 그룹에 속함 (지정하지 않는 경우 메인스레드 그룹)

Thread생성자의 첫 번째 인수는 스레드 그룹 식별자
어떤 스레드가 속한 그룹은 ThreadGroup getThreadGroup()으로 알 수 있음

  • 특징

    • 그룹이 그룹 포함 가능
    • 생성된 스레드는 자신이 속한 그룹의 우선순위를 상속받음
  • 생성자

    • 부모 없는 그룹 생성자 ThreadGroup(String name)
    • 부모 있는 그룹 생성자 ThreadGroup(ThreadGroup parent, String name)
  • 대표적 메소드 (대체로 그룹내 스레드에 일괄적으로 적용되는 동작)

    메소드동작
    int activeCount()그룹 내 활성상태 스레드 개수 반환
    int activeGroupCount()그룹 내 활성상태 그룹 개수 반환
    void destroy()해당 그룹과 하위 그룹들을 삭제(비어있어야 동작)
    int enumerate(Thread[], boolean)그룹 내 스레드를 배열에 추가. bool값이 true이면 하위그룹의 스레드도 추가. 추가한 스레드 개수 반환
    int enumerate(ThreadGroup[], boolean)그룹 내 그룹을 배열에 추가. bool값이 true이면 하위그룹의 그룹도 추가. 추가한 그룹 개수 반환
    void uncaughtException(Thread, Throwable)그룹 내 스레드가 JVM에 에러를 던졌을 때 호출
    String getName()그룹 이름 반환
    ThreadGroup getParent()부모 그룹 반환
    void interupt()그룹 내 스레드 인터럽트
    void setDaemon(boolean)그룹을 데몬 스레드 그룹으로 설정(true)/해제(false)

데몬 스레드

일반 스레드의 보조역할
일반스레드가 모두 종료되면 데몬스레드도 자동종료됨

일반적으로 무한루프를 돌고 특정 조건이 만족되면 작업을 수행하도록 작성

while(true){
  Thread.sleep(1000);
  if(exp){
    // .. Do something
  }
}

아래 과정에 따라 데몬스레드 설정 및 실행
1. 스레드 선언, 생성
2. 데몬 스레드로 지정 setDaemon(true)
3. 실행 start()

class Gorani extends Thread {
	public boolean condition = false;
	DaemonGorani daemon = null;
	
	public Gorani() {
		daemon = new DaemonGorani();
		daemon.setDaemon(true);
		
	}
	
	@Override
	public void run() {
		daemon.start();
		
		for(int i=0; i<10; i++) {
			try { Thread.sleep(500); } 
			catch (InterruptedException e) { e.printStackTrace(); }
			
			System.out.println(i);
			if(i>3)
				condition = true;
		}
	}
	
	class DaemonGorani extends Thread {
		@Override
		public void run() {
			while(true) {
				try { Thread.sleep(500); } 
				catch (InterruptedException e) { e.printStackTrace(); }
				
				if(condition) 
					System.out.println("GORANI IS CRYING");
			}
		}
	}
}

public class Test {
	public static void main(String[] args) throws InterruptedException {
		Gorani t = new Gorani();
		t.start();
		t.join();
		
		System.out.println("main thread : end");
	}
	//	0
	//	1
	//	2
	//	3
	//	4
	//	5
	//	GORANI IS CRYING
	//	6
	//	GORANI IS CRYING
	//	7
	//	GORANI IS CRYING
	//	8
	//	GORANI IS CRYING
	//	GORANI IS CRYING
	//	9
	//	main thread : end
}

스레드 실행제어

스레드의 상태

  • NEW : 생성 되고 start()가 호출되기 전의 상태
  • RUNNABLE : 실행 중/실행 가능한 상태
  • BLOCKED : 동기화 블럭에 의해 lock이 풀리기를 기다리는 상태
  • WAITING, TIMED_WAITING : 실행 가능하지 않은 일시정지 상태
  • TERMINATED : 종료된 상태

sleep() / interrupt()

  • sleep() : 일정시간동안 작업 중단
    • static void sleep(long millies)
    • static void sleep(long millies, int nanos)
  • interrupt 동작 : WAITING상태의 스레드를 RUNNABLE상태로 전환
    • void interrupt() : 인터럽트 수행, 인터럽트 상태변수 변경(true)
    • boolean isInterrupted() : 인터럽트 되었는지 여부 반환
    • static boolean interrupted() : 인터럽트 되었는지 여부 반환하고 인터럽트 상태변수 초기화(false)
class Gorani extends Thread {
	@Override
	public void run() {
		try {
			for(int i=0; i<1000; i++) {	 
				Thread.sleep(500, 0); 
				System.out.println("i : "+i+" / interrupted : "+this.isInterrupted());
			}
		} catch (InterruptedException e) { 
			System.out.println("[Gorani] : I am interrupted");
		}
	}
}

public class Test {
	public static void main(String[] args) throws InterruptedException {
		Gorani t = new Gorani();
		t.start();
		
		Scanner sc = new Scanner(System.in);
		System.out.println(sc.next());
		t.interrupt();
		
		t.join();
		
		System.out.println(t.isInterrupted());
		System.out.println("main thread : end");
	}
	//	i : 0 / interrupted : false
	//	i : 1 / interrupted : false
	//	i : 2 / interrupted : false
	//	i : 3 / interrupted : false
	//	asdasdsadas
	//	asdasdsadas
	//	[Gorani] : I am interrupted
	//	false
	//	main thread : end
}

suspend() / resume() / stop()

세 메소드 모두 교착상태를 유발할 가능성이 있어 deprecated됨
Java Thread Primitive Deprecation

  • void suspend() : 스레드 실행 일시정지 (RUNNABLE-->WAITING 상태 전환)
  • void resume() : WAITING --> RUNNABLE 상태 전환
  • void stop() : 스레드 강제 종료
    IT Story : Thread의 interrupt()와 stop()
class Gorani implements Runnable {
	
	@Override
	public void run() {
		for(int i=0; i<100; i++) {
			try {
				Thread.sleep(400);
				System.out.println(Thread.currentThread());
			} catch (InterruptedException e) { }
		}
	}
}

public class Test {
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(new Gorani());
		Thread t2 = new Thread(new Gorani());
		t1.start();
		t2.start();
		
		Thread.sleep(1000); t1.suspend();
		Thread.sleep(1000); t1.resume();
		Thread.sleep(1000); t2.stop();
		Thread.sleep(2000); t1.stop();
	
		t1.join();
		t2.join();
		
		System.out.println("main thread : end");
	}
	//Thread[Thread-1,5,main]
	//Thread[Thread-0,5,main]
	//Thread[Thread-1,5,main]
	//Thread[Thread-0,5,main]
	//Thread[Thread-1,5,main]
	//Thread[Thread-1,5,main]
	//Thread[Thread-0,5,main]
	//Thread[Thread-1,5,main]
	//Thread[Thread-0,5,main]
	//Thread[Thread-1,5,main]
	//Thread[Thread-0,5,main]
	//Thread[Thread-1,5,main]
	//Thread[Thread-0,5,main]
	//Thread[Thread-0,5,main]
	//Thread[Thread-0,5,main]
	//Thread[Thread-0,5,main]
	//Thread[Thread-0,5,main]
	//main thread : end
}

join() / yield()

  • join : 어떤 스레드를 기다림
    • void join() : 스레드가 종료될 때 까지 기다림
    • void join(long millies [, int nanos]) : 일정 시간동안 기다림 (sleep()과 유사)
  • yield : 스레드 상태를 WAITING으로 만들고 다른 스레드에게 CPU양보 (양보할지는 OS스케쥴러가 결정)
    • public static void yield()
    • 바쁜대기의 대안으로 사용될 수 있는 경우 있음
class Gorani implements Runnable {
	@Override
	public void run() {
		for(int i=0; i<15; i++) {
			try {
				if(i<10) {
					Thread.sleep(200);
					System.out.print('#');					
				} else {
					Thread.yield();
				}
			} catch (InterruptedException e) { }
		}
	}
}

public class Test {
	public static void main(String[] args) throws InterruptedException {
		Thread t = new Thread(new Gorani());
		t.start();
		
		t.join(1000);
		System.out.println("Main thread waited for 1 sec");
		
		t.join();
		System.out.println("Thread t is terminated");
		
		t = new Thread(new Gorani());
		t.start();
	}
	//	####Main thread waited for 1 sec
	//	######Thread t is terminated
}

스레드 동기화

임계영역 설정을 통해 한 스레드가 진행중인 작업이 다른 스레드에 의해 방해되지 않도록 하는 것
임계영역은 lock을 얻은 하나의 스레드만 진입 가능

synchronized

블록 또는 메소드를 임계영역으로 지정하는 키워드

class Gorani implements Runnable {
	private static int n = 1000;
	
	private boolean mode; // false : 빼기, true : 더하기
	private int reps;
	
	public Gorani(boolean m, int r) {
		mode = m;
		reps = r;
	}
	
	public static int getN() {
		synchronized (this) {
			return n;	
		}
	}
	
	@Override
	public synchronized void run() {
		if(mode) {
			for(int i=0; i<reps; i++)
				n += 1;
		} else {
			for(int i=0; i<reps; i++)
				n -= 1;
		}
	}
}

public class Test {
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(new Gorani(false, 1000)); // 1씩 1000번 빼기
		Thread t2 = new Thread(new Gorani(true, 500)); // 1씩 500번 더하기
		t1.start();
		t2.start();
		
		t1.join();
		t2.join();
		
		System.out.println("Thread t is terminated");
		System.out.println("n : " + Gorani.getN());
		// 동기화 하면 500, 동기화가 없으면 결과 알 수 없음
	}
}

wait() + notify()

synchronized 키워드만 사용했을 때 한 스레드가 lock을 계속 쥐고있어 무한루프가 발생할 수 있음
이를 해결하기 위해 추가로 wait, notify를 사용

  • wait() : 해당 스레드의 lock을 풀고 WAITING상태로 전환
  • notify() : WAITING상태인 스레드 중 하나를 RUNNABLE로 전환
  • notifyAll() : WAITING상태의 스레드를 모두 RUNNABLE로 전환

예시 시나리오

  • 논밭(Field) 객체는 작물의 개수 CropsNum을 가지고, 최대로 자랄 수 있는 작물은 10개임
  • 농부(Farmer) 객체는 논밭에 대해 makeCrop() 행위를 함. 작물이 10개이면 대기.
  • 고라니(Gorani) 객체는 논밭에 대해 eatCrop() 행위를 함. 작물이 0개이면 대기.
class Field {
	private int cropsNum = 5;	
	public synchronized int getNum () { return cropsNum; }
	
	public synchronized void eatCrop(String name) {
		while (cropsNum <= 0) {
			System.out.println(name + " : There is no crop");
			try { 
				wait();
				Thread.sleep(500);
			} catch (InterruptedException e) { }
		}
		
		cropsNum--;
		notify();
	}
	
	public synchronized void makeCrop(String name) {
		while (cropsNum >= 10) {
			System.out.println(name + " : There is no room for new crops");
			try { 
				wait();
			} catch (InterruptedException e) { }
		}
		
		cropsNum++;
		notify();
	}
}

class Farmer implements Runnable{
	Field field = null;
	String name;
	
	public Farmer(Field field, String name) {
		this.field = field;
		this.name = name;
	}
	
	@Override
	public void run() {
		while(true) {
			try { Thread.sleep(150); } catch (InterruptedException e) { }
			field.makeCrop(name);
			System.out.println(name + " : grow grow / cropsNum : " + field.getNum());
		}
	}
}

class Gorani implements Runnable {
	Field field = null;
	String name;
	
	public Gorani(Field field, String name) {
		this.field = field;
		this.name = name;
	}
	
	@Override
	public void run() {
		while(true) {
			try { Thread.sleep(200); } catch (InterruptedException e) { }
			field.eatCrop(name);
			System.out.println(name + " : nyam nyam. / cropsNum : " + field.getNum());
		}
	}
}


public class Test {
	public static void main(String[] args) throws InterruptedException {
		Field field = new Field();
		
		Thread farmer = new Thread(new Farmer(field, "farmer"));
		Thread gorani1 = new Thread(new Gorani(field, "Wonjin"));
		Thread gorani2 = new Thread(new Gorani(field, "Java"));
		
		farmer.start();
		gorani1.start();
		gorani2.start();
		
		
		farmer.join();
		gorani1.join();
		gorani2.join();
		
		System.out.println("All child threads are terminated");
	}
	//	farmer : grow grow / cropsNum : 6
	//	Wonjin : nyam nyam. / cropsNum : 4
	//	Java : nyam nyam. / cropsNum : 4
	//	farmer : grow grow / cropsNum : 5
	//	Wonjin : nyam nyam. / cropsNum : 4
	//	Java : nyam nyam. / cropsNum : 3
	//	farmer : grow grow / cropsNum : 4
	//	farmer : grow grow / cropsNum : 4
	//	Wonjin : nyam nyam. / cropsNum : 4
	//	Java : nyam nyam. / cropsNum : 3
	//	farmer : grow grow / cropsNum : 4
	//	Wonjin : nyam nyam. / cropsNum : 3
	//	Java : nyam nyam. / cropsNum : 2
	//	farmer : grow grow / cropsNum : 3
	//	Wonjin : nyam nyam. / cropsNum : 2
	//	Java : nyam nyam. / cropsNum : 1
	//	farmer : grow grow / cropsNum : 2
	//	Wonjin : nyam nyam. / cropsNum : 1
	//	Java : nyam nyam. / cropsNum : 1
	//	farmer : grow grow / cropsNum : 1
	//	farmer : grow grow / cropsNum : 2
	//	Wonjin : nyam nyam. / cropsNum : 1
	//	Java : nyam nyam. / cropsNum : 0
	//	farmer : grow grow / cropsNum : 1
	//	Wonjin : nyam nyam. / cropsNum : 0
	//	Java : There is no crop
	//	Java : nyam nyam. / cropsNum : 0
	//	Wonjin : There is no crop
	//	farmer : grow grow / cropsNum : 0
	//	farmer : grow grow / cropsNum : 1
	//	Wonjin : nyam nyam. / cropsNum : 0
	//	Java : nyam nyam. / cropsNum : 0
	//	farmer : grow grow / cropsNum : 1
	//	farmer : grow grow / cropsNum : 1
	//	Wonjin : nyam nyam. / cropsNum : 0
	//	Java : There is no crop
	//	farmer : grow grow / cropsNum : 1
	//	Java : nyam nyam. / cropsNum : 1
	// .................. 계속
}

Lock & Condition

윗 절에서 사용하는 notify()는 특정한 스레드가 아닌 랜덤한 스레드를 깨움.
따라서 불필요하게 깨어나는 스레드가 생겨 성능상 손해가 있을 수 있음
e.g. 작물이 없어서 notify()했는데 farmer가 아니라 gorani가 깨어나는 경우

이를 해결하기 위해 waiting pool을 분리하고, 특정 조건에 맞는 스레드를 깨우는 방법을 사용

Hello, Dev World! : Lock 클래스 / Condtion 클래스
그릿 속의 해빗 : 멀티쓰레드(Multi Thread)의 동기화 2 - Lock과 Condition을 이용한 동기화

profile
노는게 제일 좋습니다.

0개의 댓글