Recent Posts
Recent Comments
Link
06-30 12:53
Today
Total
관리 메뉴

삶 가운데 남긴 기록 AACII.TISTORY.COM

JAVA Thread 본문

DEV&OPS/Java

JAVA Thread

ALEPH.GEM 2022. 4. 19. 14:32

자바 어플리케이션은 main()스레드 외에 병렬로 처리하는 스레드를 같이 실행할 수 있습니다.

보통은 멀티 스레드로 병렬로 작업하는 것이 효율적이지만, 너무 많은 스레드를 실행하면 각 스레드간 context switching 에 시간을 더 소비하게 되어 오히려 효율이 떨어지게 됩니다.

java.lang.Thread 클래스로부터 extends 하거나 Runnable 인터페이스로부터 implements 해서 스레드를 생성할 수 있습니다.

 

Runnable 구현 방법

import java.awt.Toolkit;

public class BeepTask implements Runnable {

	@Override
	public void run() {
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		for(int i=0; i<5;i++) {
			toolkit.beep();
			System.out.println("beep");
			try {
				Thread.sleep(1000);
			}catch(Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) {
		Runnable beepTask = new BeepTask();
		Thread thread = new Thread(beepTask);
		thread.start();
	}
}

Thread클래스로부터 상속받아 실행하는 방법(main 스레드 동시 실행)

import java.awt.Toolkit;

public class BeepThread extends Thread{
	@Override
	public void run() {
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		for(int i=0; i<5;i++) {
			toolkit.beep();
			System.out.println("beep");
			try {
				Thread.sleep(1000);
			}catch(Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) {
		Thread thread = new BeepThread();
		thread.start();
		//메인스레드 0.5초간격 동시 진행
		for(int i=0;i<5;i++) {
			System.out.println("빞");
			try {
				Thread.sleep(500);
			}catch(Exception e){
				e.printStackTrace();
			}
		}
	}
}

 

스레드의 이름

스레드의 이름은 크게 의미는 없지만 디버깅할 때 스레드를 구분하는 용도로 가끔 사용합니다.

public class ThreadNameEx {

	public static void main(String[] args) {
		Thread mainThread = Thread.currentThread();
		System.out.println("시작 스레드: "+mainThread.getName());
		
		ThreadA threadA = new ThreadA();
		System.out.println("작업 스레드: "+threadA.getName());
		threadA.start();
		
		ThreadB threadB = new ThreadB();
		System.out.println("작업 스레드: "+threadB.getName());
		threadB.start();
	}

}

class ThreadA extends Thread{
	public ThreadA() {
		setName("ThreadA");	//스레드 이름 설정
	}
	
	public void run() {
		for(int i=0;i<2;i++) {
			System.out.println(getName() + "가 실행");
		}
	}
}

class ThreadB extends Thread{
	public ThreadB() {
		setName("ThreadB");	//스레드 이름 설정
	}
	
	public void run() {
		for(int i=0;i<2;i++) {
			System.out.println(getName() + "가 실행");
		}
	}
}

 

스레드 우선순위

스레드를 병렬로 실행하다보면 우선순위를 지정해줘야 할 필요가 생깁니다.

우선 순위는 1부터 10까지 부여되는데 숫자가 클수록 우선순위가 높습니다.

우선 순위의 기본값은 5입니다.

주의할 점은 우선 순위가 높다고 반드시 작업이 먼저 끝나는 것은 아닙니다.

스레드의 개수가 많을 수록 우선 순위대로 작업을 완료할 확률이 높아집니다.

public class PriorityEX {

	public static void main(String[] args) {
		for(int i=1; i<=10; i++) {
			Thread thread = new CalcThread("thread"+i);
			if(i != 10) {
				thread.setPriority(Thread.MIN_PRIORITY); 	//가장 낮은 우선순위: 1
			}else {
				thread.setPriority(Thread.MAX_PRIORITY);	//가장 높은 우선순위: 10
			}
			thread.start();
		}
	}

}

class CalcThread extends Thread{
	public CalcThread(String name) {
		setName(name);
	}
	public void run() {
		long sum = 0;
		for(int i=0;i<10000000;i++) {
			sum += i;
		}
		System.out.println(getName() +":"+sum);
	}
}

 

동기화 블록

시스템 자원(메모리)를 여러 스레드가 공유해서 진행하다보면 결과 값이 엉터리가 될 수도 있기 때문에 각 스레드들을 동기화가 필요할 수도 있습니다.

스레드가 사용중인 자원을 작업이 끝날때까지 잠금을 걸어서 다른 스레드가 사용할 수 없도록 해야 합니다.

public class MainThreadEx {

	public static void main(String[] args) {
		Calculator calculator = new Calculator();
		
		User1 user1 = new User1();
		user1.setCalculator(calculator);
		user1.start();
		
		User2 user2 = new User2();
		user2.setCalculator(calculator);
		user2.start();
	}

}

class Calculator{
	private int memory;
	public int getMemory() {
		return memory;
	}
	public void setMemory(int memory) {
		this.memory = memory;
		try {
			Thread.sleep(2000);	//스레드를 2초 정지
		}catch(InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+": "+this.memory);
		
	}
}

class User1 extends Thread{
	private Calculator calculator;
	public void setCalculator(Calculator calculator) {
		this.setName("User1");
		this.calculator = calculator;
	}
	public void run() {
		calculator.setMemory(100); 	//공유객체 메모리에 100을 저장
	}
}

class User2 extends Thread{
	private Calculator calculator;
	public void setCalculator(Calculator calculator) {
		this.setName("User2");
		this.calculator = calculator;
	}
	public void run() {
		calculator.setMemory(40); 	//공유객체 메모리에 40을 저장
	}
}

User1스레드가 Calculator객체의 memory 필드에 100을 저장하고 2초간 일시정지 되며, 그동안 User2 스레드가 memory필드 값을 40으로 변경합니다. 

2초가 지나 정지상태에 있던 User1 스레드가 memory필드값을 출력하면 40이 나오게 됩니다.

의도대로 였다면 User1은 100을 출력하고 User2는 40을 출력했어야하지만 결과는 User1, Use2둘다 40을 출력하게 됩니다.

 

동기화 블록

스레드가 객체 내부 동기화 블록에 들어가면 객체에 잠금을 걸어 다른 스레드가 실행하지 못하도록 해야하는데, synchronized 키워드를 이용하면 됩니다.

위 예제에서 setMemory()메서드를 동기화 하려면 아래와 같이 하면됩니다.

public class MainThreadEx {

	public static void main(String[] args) {
		Calculator calculator = new Calculator();
		
		User1 user1 = new User1();
		user1.setCalculator(calculator);
		user1.start();
		
		User2 user2 = new User2();
		user2.setCalculator(calculator);
		user2.start();
	}

}

class Calculator{
	private int memory;
	public int getMemory() {
		return memory;
	}
	public synchronized void setMemory(int memory) {
		this.memory = memory;
		try {
			Thread.sleep(2000);	//스레드를 2초 정지
		}catch(InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+": "+this.memory);
		
	}
}

class User1 extends Thread{
	private Calculator calculator;
	public void setCalculator(Calculator calculator) {
		this.setName("User1");
		this.calculator = calculator;
	}
	public void run() {
		calculator.setMemory(100); 	//공유객체 메모리에 100을 저장
	}
}

class User2 extends Thread{
	private Calculator calculator;
	public void setCalculator(Calculator calculator) {
		this.setName("User2");
		this.calculator = calculator;
	}
	public void run() {
		calculator.setMemory(40); 	//공유객체 메모리에 40을 저장
	}
}

setMemory()에 synchronized를 걸어주면 User1스레드가 setMemory() 실행을 마칠 때까지 User2스레드는 기다려야 합니다.

위 예제처럼 메소드에 직접 잠금을 하는 방법 말고도 아래처럼 synchronized블록으로 잠금 대상인 Calulator 인스턴스 객체를 잠글 수 있습니다.

public synchronized void setMemory(int memory) {
	synchronized(this){
		this.memory = memory;
		try {
			Thread.sleep(2000);	//스레드를 2초 정지
		}catch(InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+": "+this.memory);
	}
}

 

스레드 상태

스레드를 start()하면 바로 스레드가 시작되는 것처럼 보이지만 사실은 실행 대기(RUNNABLE) 상태가 됩니다.

이러한 실행 대기 상태의 스레드들 중에서 우선순위 스케줄링으로 선택된 스레드가 실행됩니다.

실행중인 스레드가 할당된 시간을 다 사용하면 작업이 마무리되지 않아도 다시 대기 상태로 돌아가고 스케줄링에 의해  다른 스레드가 선택되어 실행상태가 됩니다. 

이렇게 스레드들이 번갈아가면서 실행되다가 작업을 모두 완료하면 스레드가 종료(Terminated) 상태가 됩니다.

경우에 따라 스레드가 일시 정지(WAITING:다른스레드응답대기, TIMED_WAITING(일정시간대기), BLOCKED(락이 플릴때까지대기)) 상태가 되기도 합니다.

일시 정지된 스레드들이 다시 실행 상태가 되기 위해선 먼저 실행 대기 상태로 돌아가야 합니다.

public class TargetThreadEx {

	public static void main(String[] args) {
		StatePrintThread statePrintThread = new StatePrintThread(new TargetThread());
		statePrintThread.start();
	}

}

class StatePrintThread extends Thread{
	private Thread targetThread;
	public StatePrintThread(Thread targetThread) {
		this.targetThread = targetThread;
	}
	public void run() {
		while(true) {
			Thread.State state = targetThread.getState();
			System.out.println("타겟 스레드 상태: "+state);
			if(state == Thread.State.NEW) {
				targetThread.start();
			}
			if(state == Thread.State.TERMINATED) {
				break;
			}
			try {
				Thread.sleep(300);
			}catch(Exception e) {
				e.printStackTrace();
			}
		}
	}
}

class TargetThread extends Thread{
	public void run() {
		for(long i = 0; i<1000000000; i++) {
		}
		
		try {
			Thread.sleep(1500);
		}catch(Exception e) {
			e.printStackTrace();
		}
		
		for(long i = 0; i<1000000000; i++) {
		}
	}
}

 

다른 스레드에게 실행 양보 yield

동시에 여러 스레드가 실행중일 때, 무의미한 대기 시간을 줄이기 위해 다른 스레드에게 실행을 양보할 수 있습니다.

public class YieldEx {

	public static void main(String[] args) {
		ThreadA1 threadA1 = new ThreadA1();
		ThreadB1 threadB1 = new ThreadB1();
		threadA1.start();
		threadB1.start();
		
		try {
			Thread.sleep(3000);
		}catch(Exception e) {
			e.printStackTrace();
		}
		threadA1.work = false;
		
		try {
			Thread.sleep(3000);
		}catch(Exception e) {
			e.printStackTrace();
		}
		threadA1.work =true;
		
		try {
			Thread.sleep(3000);
		}catch(Exception e) {
			e.printStackTrace();
		}
		threadA1.stop = true;
		threadB1.stop = true;
	}

}

class ThreadA1 extends Thread{
	public boolean stop = false;
	public boolean work = true;
	public void run() {
		while(!stop) {
			if(work) {
				System.out.println("ThreadA1 작업 내용");
			}else {
				Thread.yield(); //work가 false면 양보
			}
		}
		System.out.println("ThreadA1 종료");
	}
}

class ThreadB1 extends Thread{
	public boolean stop = false;
	public boolean work = true;
	
	public void run() {
		while(!stop) {
			if(work) {
				System.out.println("ThreadB1 작업 내용");
			}else {
				Thread.yield();
			}
		}
		System.out.println("ThreadB1 종료");
	}
}

 

다른 스레드의 종료를 기다림 join

어느 한 스레드가 어떠한 다른 작업을 요청 한 뒤 스레드를 일시 정지 했다가 요청한 결과값을 받으면 다시 실행해야 하는 경우가 있습니다.

예를 들어, ThreadA가 ThreadB의 join()메서드를 호출하면 ThreadA는 ThreadB가 종료될 때 까지 일시정지가 되고 ThreadB가 종료되어야 ThreadA가 비로소 일시정지에서 풀려 이후의 코드가 실행하게 되는 것입니다.

public class JoinEX {

	public static void main(String[] args) {
		SumThread sumThread = new SumThread();
		sumThread.start();
		try {
			sumThread.join();
		}catch(Exception e) {
			e.printStackTrace();
		}
		System.out.println("합:"+sumThread.getSum());
	}

}

class SumThread extends Thread{
	private long sum;
	public long getSum() {
		return sum;
	}
	public void setSum(long sum) {
		this.sum = sum;
	}
	public void run() {
		for(int i = 1; i <= 100; i++) {
			sum += i;
		}
	}
}

 

스레드 간 협업: wait, notify, notifyAll

경우에 따라, 두개의 스레드가 교대로 번갈아가면서 실행해야 하는 경우도 있습니다.

아래 예제는 두 스레드의 작업을 WorkObject의 methodA()아 methodB()에 정의 하고, 두 스레드 ThreadA2와 ThreadB2가 번갈아가면서 methodA()와 methodB()를 호출합니다.

public class WaitNotifyEx {

	public static void main(String[] args) {
		WorkObject sharedObject = new WorkObject();	//공유객체 생성
		ThreadA2 threadA2 = new ThreadA2(sharedObject);
		ThreadB2 threadB2 = new ThreadB2(sharedObject);
		threadA2.start();
		threadB2.start();
	}
}

class WorkObject{
	public synchronized void methodA() {
		System.out.println("ThreadA2의 methodA() 실행");
		notify();	//일시 정지한 ThreadB2를 실행 대기 상태로 만듦
		try {
			wait();
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	public synchronized void methodB() {
		System.out.println("ThreadB2의 methodB() 실행");
		notify(); 	//일시 정지한 ThreadA2를 실행 대기 상태로 만듦
		try {
			wait();
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
}

class ThreadA2 extends Thread{
	private WorkObject workObject;
	public ThreadA2(WorkObject workObject) {
		this.workObject = workObject;
	}
	public void run() {
		for(int i=0;i<10;i++) {
			workObject.methodA();
		}
	}
}

class ThreadB2 extends Thread{
	private WorkObject workObject;
	public ThreadB2(WorkObject workObject) {
		this.workObject = workObject;
	}
	public void run() {
		for(int i=0;i<10;i++) {
			workObject.methodB();
		}
	}
}

생산자 스레드가 생성한 데이터를 소비자 스레드가 읽는 작업을 교대로 실행하는 예제

public class WaitNotify {

	public static void main(String[] args) {
		DataBox dataBox = new DataBox();
		
		ProducerThread producerThread = new ProducerThread(dataBox);
		ConsumerThread consumerThread = new ConsumerThread(dataBox);
		producerThread.start();
		consumerThread.start();
	}

}

class DataBox{
	private String data;
	public synchronized String getData() {
		if(this.data == null) {
			try {
				wait(); 	//소비자 스레드를 일시 정지 상태로 
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
		String returnValue = data;
		System.out.println("소비자 스레드가 읽은 데이터:"+returnValue);
		data = null;	//data필드를 null로
		notify();		//생산자 스레드를 실행 대기 상태로
		return returnValue;
	}
	public synchronized void setData(String data) {
		if(this.data != null) {
			try {
				wait(); 	//생산자 스레드를 일시 정지 상태로 
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.data = data;	//data 필드를 세팅
		System.out.println("생산자 스레드가 생성한 데이터:"+data);
		notify();	//소비자 스레드를 실행 대기 상태로
	}
}

//생산자(저장) 스레드
class ProducerThread extends Thread{
	private DataBox dataBox;
	public ProducerThread(DataBox dataBox) {
		this.dataBox = dataBox;
	}
	public void run() {
		for(int i = 1; i <= 3; i++) {
			String data = "Data-" + i;
			dataBox.setData(data);
		}
	}
}

//소비자(읽는) 스레드
class ConsumerThread extends Thread{
	private DataBox dataBox;
	public ConsumerThread(DataBox dataBox) {
		this.dataBox = dataBox;
	}
	public void run() {
		for(int i=0;i<=3;i++) {
			String data = dataBox.getData();
		}
	}
}

 

스레드의 안전한 종료 stop, interrupt

stop() 메소드는 deprecated되었는데 사용중이던 자원들이 종료되지 않고 남겨지기 때문입니다.

그래서 stop 플래그를 통해서 run() 메소드의 종료를 유도합니다.

public class StopFlagEx {

	public static void main(String[] args) {
		PrintThread1 printThread = new PrintThread1();
		printThread.start();
		try {
			Thread.sleep(1000);
		}catch(Exception e) {
			e.printStackTrace();
		}
		printThread.setStop(true);
	}

}

class PrintThread1 extends Thread{
	private boolean stop;
	public void setStop(boolean stop) {
		this.stop = stop;
	}
	public void run() {
		while(!stop) {
			System.out.println("실행 중");
		}
		System.out.println("자원 정리");
		System.out.println("실행 종료");
	}
}

interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterrruptedException을 발생시켜 스레드를 종료시킬 수 있습니다.

주의할 점은 일시 정지 상태에 있을 때만 예외를 발생시킨다는 점입니다.

public class InterruptEx {

	public static void main(String[] args) {
		Thread thread = new PrintThread2();
		thread.start();
		try {
			Thread.sleep(1000);
		}catch(Exception e) {
			e.printStackTrace();
		}
		thread.interrupt(); //interruptedException 발생
	}

}

class PrintThread2 extends Thread{
	public void run() {
		while(true) {
			System.out.println("실행 중");
			if(Thread.interrupted()) {
				break;
			}
		}
		
		System.out.println("자원 정리");
		System.out.println("실행 종료");
	}
}

 

데몬 스레드

데몬 스레드는 주 스레드의 보조적인 역할을 수행하는 스레드를 말합니다.

주 스레드가 종료되면 데몬 스레드도 강제적으로 종료됩니다.

이 점을 제외하면 일반 스레드와 다르지 않습니다.

스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드에 setDaemon(true)를 호출하면 됩니다.

주의할 점은 start() 호출되고 나서 setDaemon(true)를 호출하면 IllegalThreadStateException이 발생하기 때문에 start()하기 전에 setDaemon(true)를 호출해야 합니다.

아래 예제는 자동 저장하는 데몬 스레드 예제 입니다.

public class DaemonEx {

	public static void main(String[] args) {
		AutoSaveThread autoSaveThread = new AutoSaveThread();
		autoSaveThread.setDaemon(true);
		autoSaveThread.start();
		try {
			Thread.sleep(3000);
		}catch(Exception e) {
			e.printStackTrace();
		}
		System.out.println("메인 스레드 종료");
	}

}

class AutoSaveThread extends Thread{
	public void save() {
		System.out.println("작업 내용 저장.");
	}
	
	public void run() {
		while(true) {
			try {
				Thread.sleep(1000);
			}catch(Exception e) {
				break;
			}
			save();
		}
	}
}

 

스레드 그룹

스레드 그룹은 관련된 스레드끼리 묶어서 관리할 목적으로 이용합니다.

JVM이 실행되면 system스레드 그룹을 만들고 하위 스레드 그룹으로 main을 만들고 메인 스레드를 mian 그룹에 포함시킵니다.

스레드는 명시적으로 그룹을 지정하지 않으면 자신을 생성한 스레드와 같은 그룹에 속하게 됩니다.

아래 예제의 AutoSaveThread는 위의 예제의 AutoSaveThread 입니다.

import java.util.Map;
import java.util.Set;

public class ThreadInfoEx {

	public static void main(String[] args) {
		AutoSaveThread autoSaveThread = new AutoSaveThread();
		autoSaveThread.setName("AutoSaveThread");
		autoSaveThread.setDaemon(true);
		autoSaveThread.start();
		
		Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
		Set<Thread> threads = map.keySet();
		for(Thread thread : threads) {
			System.out.println("Name: "+thread.getName() + ((thread.isDaemon()) ? "(데몬)" : "(주)"));
			System.out.println("\t" + "소속그룹: "+ thread.getThreadGroup().getName());
			System.out.println();
		}
	}

}

스레드를 그룹으로 지정하면 interrupt()를 이용해서 일괄 종료 시킬 수 있습니다.

 

 

 

 

 

 

 

728x90

'DEV&OPS > Java' 카테고리의 다른 글

Generic  (0) 2022.04.21
Thread Pool  (0) 2022.04.20
Format 클래스  (0) 2022.04.12
자바 날짜, 시간, 달력 다루기  (0) 2022.04.12
Math, Random 클래스  (0) 2022.04.12