스레드(Thread)

주8·2023년 1월 26일
0

Process(공장) & Thread(일꾼)

  • 프로세스는 프로그램 수행하는 데 필요한 데이터, 메모리 등의 자원 그리고 스레드로 구성된다.
  • 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 스레드이다.
  • 모든 프로세스에는 최소한 하나 이상의 스레드가 존재하며, 둘 이상의 스레드를 가진 프로세스를 멀티스레드 프로세스(multi-thread process)라고 한다. (하나일 경우 single-threaded process)
  • 하나의 프로세스가 가질 수 있는 스레드의 개수는 제한 없으나 스레드가 작업을 수행하는데 개별적인 메모리공간(호출스택)을 필요로 하기 때문에 프로세스 메모리 한계(호출스택의 크기)에 따라 생성할 수 있는 스레드의 수가 결정된다.
  • 실제로는 한 개의 CPU는 한 번에 단 한가지 작업만 수행할 수 있기 때문에 아주 짧은 시간동안 여러 작업을 번갈아 가며 수행함으로써 동시에 여러 작업이 수행되는 것처럼 보이게 하는 것이다.

멀티프로세스 vs 멀티스레드

  • 하나의 새로운 프로세스를 생성하는 것보다 하나의 새로운 스레드를 생성하는 것이 더 적은 비용이 든다.
  • 스레드는 Virtual CPU다: 스레드의 데이터 구조는 인스트럭션들에 대한 순서를 기록하고 있다.
    • 현재 컴퓨터의 레지스터 내용과 현재 실행되는 인스트럭션의 위치, 메소드에 의해 사용되는 런타임 스택(지역변수와 인자들을 담고 있다)이 저장된다.
  • 자바 프로세서는 중앙처리장치 내에 스케쥴러에서 실행 시간을 배분받아 자바 프로그램을 실행시킨다.
  • 멀티스레드의 장점
    • CPU의 사용률을 향상시킨다.
    • CPU자원을 보다 효율적으로 사용할 수 있다.
    • 사용자에 대한 응답성이 향상된다.
    • 작업이 분리되어 코드가 간결해진다.
  • 멀티스레드의 예: 채팅의 경우 파일을 다운로드 하면서 음성대화를 나눌 수 있다.
  • 서버 프로그램은 멀티스레드가 필수적이다.

멀티스레드의 장/단점

  • 프로세스를 생성하는 것이 스레드를 생성하는 것에 비해 훨씬 더 많은 시간과 메모리 공간을 필요하기 때문에 많은 수의 사용자 요청을 서비스하기 어렵다.
    • 스레드를 가벼운 프로세스, 즉 경량 프로세스(LWP, light-weight process)라고 부르기도 한다.
    • 스레드는 공유 메모리 영역을 사용하기 때문에 멀티프로세싱보다 멀티쓰레딩을 사용한다. 별도의 메모리 영역을 할당하지 않으므로 메모리가 절약되고 스레드 간의 컨텍스트 전환이 프로세스보다 시간이 덜 걸린다.
  • 교착상태(deadlock): 두 스레드가 자원을 점유한 상태에서 서로 상대편이 점유한 자원을 사용하려고 대기하며 진행이 멈추어 있는 상태
장점단점
- 자원을 보도 효율적으로 사용할 수 있다.
- 사용자에 대한 응답성(responseness)이 향상된다.
- 작업이 분리되어 코드가 간결해진다.
- 개발시 고려할 사항들이 많다.
- 동기화(synchronization)에 주의해야 한다.
- 교착상태(dead-lock)가 발생하지 않도록 주의해야 한다.
- 각 스레드가 효율적으로 고르게 실행될 수 있게 해야 한다.

스레드의 구현과 실행

  • 구현방법은 두 가지(어느 방법을 사용하더라도 별 차이는 없다)
    • Thread 클래스를 상속: 이 클래스를 상속 받으면 다른 클래스를 상속 받을 수 없다.
    • Runnable 인터페이스를 구현: 일반적인 방법으로 재사용성(Resuability)이 높고 코드의 일관성(consistency)을 유지할 수 있다는 장점이 있기 때문에 보다 객체지향적인 방법이라고 할 수 있다.
  • Thread 클래스를 상속: Thread 클래스의 run() 메서드를 오버라이딩 해서 Thread가 처리할 Task를 구현한다.
class JobThread extends Thread{
	public void run(){/* Task 구현 */}
}
  • Runnable 인터페이스를 구현: Runnable 인터페이스의 run() 메서드를 구현한다.
class JobThread implements Runnable{
	public void run(){/* Task 구현 */}
}
  • Runnable interface는 추상메서드 run()만 있다.
@FunctionalInterface
public interface Runnable{
	public abstract void run();
}
  • Runnable 인터페이스를 구현한 경우의 인스턴스 생성방법이 다르다.
    • Runnable 인터페이스 구현 클래스의 인스턴스 생성
    • 인스턴스를 Thread 클래스의 인스턴스 생성시 Thread(Runnable target) 생성자의 매개변수로 넘겨줘야 한다.
  • Thread를 상속했을 때 현재의 스레드 이름 얻기 방법: String getName();
System.out.println(getName());
  • Runnable을 구현했을 때 현재의 스레드 이름 얻기 방법: Thread.currentThread();
System.out.prinln(Thread.currentThread.getName());
=>Thread t = Thread.currentThread();
	String name = t.getName();
Runnable r = new ThreadEx02();
//생성자 Thread(Runnable target)
Thread t2 = new Thread(r);
  • 쓰레드를 생성 후 start() 메소드를 호출해야만 작업을 시작한다.
  • 한 번 사용한 쓰레드는 다시 재사용할 수 없다. → start() 메소드는 한 번만 호출될 수 있다.
  • 한 번 더 수행되기를 원한다면 새로운 쓰레드를 생성한 다음 start()를 호출한다.
ThreadEx new ThreadEX();
t.start();
t.start(); //java.lang.IllegalThreadStateException 발생
public class ThreadTester{
	public static void main(String[] args){
		HelloRunner r = new HelloRunner();
		Thread t = new Thread(r);
		t.start();
	}
}

class HelloRunner implments Runnable{
	int i;
	@Override
	public void run(){
		i = 0;
		while(true){
			System.out.println("Hello " + i++);
			if(i == 30) break;
		}
	}
}

start()와 run()

  • run() 메서드를 호출하는 것은 생성된 스레드를 실행시키는 것이 아니라 단순히 클래스에 속한 메서드를 호출한 것이다.
  • 아래의 순서대로 실행이 되었을 때 호출스택의 최상위에 있는 메소드일지라도 대기상태일 수 있다.
    → 스케줄러는 스레드들의 우선순위를 고려하여 실행순서와 실행시간을 결정한다. (OS 스케줄러의 영향을 받는다.)
  • run()의 작업이 종료된 스레드는 호출스택이 모두 비어지고 사라진다.
  1. main()메서 스레드의 start() 호출
  2. start()는 스레드가 작업을 수행하는데 필요한 Call stack 생성
  3. 생성된 Call stack에 run()메서드를 호출해서 스레드가 작업을 수행하도록 한다.
  4. Call stack이 2개이기 때문에 스케줄러가 정한 순서에 의해서 번갈아 가면서 실행된다.

main() Thread

  • main()의 작업을 수행하는 것도 스레드이다.
  • 프로그램이 실행되기 위해서는 최소한 하나의 스레드가 필요하다.
  • main()의 작업이 완료되었어도 다른 스레드가 작업중이면 프로그램이 종료되지 않는다.
  • 실행 중인 사용자 스레드가 하나도 없을 때 프로그램은 종료된다.
  • 스레드는 사용자 스레드(user thread: non-daemon thread)와 데몬 스레드(daemon thread) 두 종류가 있다.

Single thread와 Multi thread

  • Single thread: 하나의 작업이 완료된 후 다른 작업이 시작된다.
  • Multi thread: 짧은 시간동안 2개 이상의 스레드가 번갈아 가면서 작업을 수행
  • Context switching: single thread보다 multi thread가 작업시간 더 걸리게 되는 이유는 스레드간의 작업전환에 시간이 걸리기 때문이다. 프로세스간 또는 스레드간의 전환을 스위칭이라고 하는데 현재 진행중인 작업의 상태, 다음에 실행해야할 위치(프로그램 카운터:PC) 등의 정보를 저장하고 읽어오는 시간이 소요된다.
  • 단순히 CPU만을 사용하는 계산작업: Single thread로 프로그래밍 하는 게 효율적이다.
  • CPU의 자원을 사용하는 작업: 멀티스레드 프로세스가 더 효율적이다.
    예) 데이터를 입력받는 작업, 네트워크로부터 파일을 주고 받는 작업, 프린터 출력 작업과 같은 외부기기와의 입출력 작업 등

예제

public class SingleThreadTest{
	public static void main(String[] args){
		for(int i=0; i<50; i++){
			System.out.println("-");
		}
		for(int i=0; i<50; i++){
			System.out.println("|");
		}
	}
}

[결과]
--------------------------------------------------||||||||||||||||||||||||||||||||||||||||||||||||||
public class MultiThreadTest {
	public static void main(String args[]){
		MyThread1 t1 = new MyThread1();
		MyThread2 t2 = new MyThread2();
		t1.start();
		t2.start();
	}
}
class MyThread1 extends Thread {
	@Override
	public void run() {
		for(int i=0; i<50; i++) {
			System.out.print("-");
		}
	}
}
class MyThread2 extends Thread {
	@Override
	public void run() {
		for(int i=0; i<50; i++) {
			System.out.print("|");
		}
	}
}

[결과]
————||||||||||||||||||||||||||||||||||||||||||||||||||-----------------------------------------
-||||||||-------------------------------------------------||||||||||||||||||||||||||||||||||||||||||

OS에 종속적인 스레드

  • 앞의 예제에서 실행시마다 다른 결과가 나온느 것은 OS의 스케줄러의 영향을 받기 때문이다.
  • JVM의 스레드 스케줄러에 의해서 어떤 스레드가 얼마동안 실행될 것인지 결정되는 것과 같이 프로세스도 프로세스 스케줄러에 의해서 실행순서와 실행시간이 결정되기 때문에 매 순간 상황에 따라 프로세스에게 할당되는 실행시간이 일정하지 않고 스레드에게 할당되는 시간 역시 일정하지 않게 된다.
  • 자바 프로세서는 CPU 내에 스케줄러에서 실행 시간을 배분받아 자바 프로그램을 실행시킨다. - JVM의 종류에 따라 스레드 스케줄러의 구현방법이 다를 수 있기 때문에 멀티 스레드로 작성된 프로그램을 다른 종류의 OS에서도 충분히 테스트해 볼 필요가 있다.

Thread Priority (스레드 우선순위)

  • 스레드는 우선순위(Priority)라는 멤버변수를 가지고 있다.
  • 작업의 중요도에 따라 우선순위를 달리 지정하여 특정 스레드가 더 많은 작업시간을 갖도록 할 수 있다. (선점형 스케줄링-Preemptive scheduling이라고 한다.)
  • 메신저의 경우 채팅내용 전송 보다 파일다운로드(I/O)를 처리하는 스레드의 우선순위가 높아야 한다.
  • 우선순위의 범위는 1~10로 높은 숫자일수록 순위가 높다. I/O작업이 최우선 순위이다.
  • 우선순위는 스레드를 생성한 스레드로부터 상속이므로 main()에서 생성한 스레드는 우선순위가 자동적으로 5이다.
  • 우선순위 Setter, Getter 메서드를 통해 우선순위 값을 핸들링할 수 있다.
  • public final int getPriority(): 주어진 스레드의 우선 순위를 반환한다.
  • public final void setPriority(int newPriority): 스레드의 우선 순위를 newPriority에 업데이트하거나 할당한다.
  • newPriority 값이 1(최소)에서 10(최대) 사이의 범위를 벗어나면 메서드에서 illegalArgumentException이 발생한다.
  • Thread 클래스에 정의된 3개의 상수: MIN_PRIORITY(1), NORM_PRIORITY(5), MAX_PRIORITY(10)이다.
  • 동일한 우선 순위를 가진 두 개의 스레드가 있는 경우 어떤 스레드가 먼저 실행할 기회를 얻을 것인지 예측할 수 없다.
  • 다음 실행은 스레드 스케줄러의 알고리즘(선착순, 라운드 로빈 등)에 따라 달라진다.

예제

public class ThreadPriorityEx extends Thread{
	public void run(){
		System.out.println("in run() method");
	}
	public static void main(String argvs[]){
		System.out.println("메인 스레드 현재 우선순위:"+
						Thread.currentThread().getPriority()); // 5
		// 메인 스레드의 우선순위를 7로 설정한다
		Thread.currentThread().setPriority(7);
		// currentThread() 메서드를 사용해서
		// 현재 스레드를 찾아서 getPriority()로 우선순위를 얻는다
		System.out.println("메인 스레드 변경된 우선순위:"+
						Thread.currentThread().getPriority());

		ThreadPriorityEx t = new ThreadPriorityEx();
		// t 스레드는 메인스레드의 child 스레드이기에 우선순위도 7이다
		System.out.println("스레드 t의 우선순위:"+
						t.getPriority());
		t.start();
	}
}

[결과]
메인 스레드 현재 우선순위: 5
메인 스레드의 우선순위: 7
스레드 t의 우선순위 : 7
in run() method
public class ThreadPriorityTest extends Thread{
	public void run(){
		System.out.println("in run() method");
	}
	public static void main(String[] args){
		ThreadPriorityEx t1 = new ThreadPriorityEx();
		ThreadPriorityEx t2 = new ThreadPriorityEx();
		ThreadPriorityEx t3 = new ThreadPriorityEx();

		//스레드의 우선순위는 기본값인 5이다
		System.out.println("t1의 우선순위 : " + t1.getPriority());
		System.out.println("t2의 우선순위 : " + t2.getPriority());
		System.out.println("t3의 우선순위 : " + t3.getPriority());

		//우선순위 변경
		t1.setPriority(6);
		t2.setPriority(3);
		t3.setPriority(11);

		System.out.println("t1 스레드 우선순위 : " + t1.getPriority());
		System.out.println("t2 스레드 우선순위 : " + t2.getPriority());
		System.out.println("t3 스레드 우선순위 : " + t3.getPriority());
		System.out.println("현재 실행중인 스레드 : " + Thread.currentThread().getName());
		System.out.println("메인 스레드의 우선순위 : "
					+ Thread.currentThread().getPriority());
		//메인스레드 우선순위 10으로 변경
		Thread.currentThread().setPriority(10);
		System.out.println("메인 스레드의 우선순위 : "
					+ Thread.currentThread().getPriority());
	}
}

[결과]
t1의 우선순위 : 5
t2의 우선순위 : 5
t3의 우선순위 : 5
t1 스레드 우선순위 : 6
t2 스레드 우선순위 : 3
t3 스레드 우선순위 : 9
현재 실행중인 스레드 : main
메인 스레드의 우선순위 : 5
메인 스레드의 우선순위 : 10

Thread Group

  • 서로 관련된 스레드를 그룹으로 묶어서 다루기 위한 것
  • 모든 스레드는 반드시 하나의 스레드 그룹에 포함되어 있어야 한다.
  • 스레드 그룹을 지정하지 않고 생성한 스레드는 ‘main 스레드 그룹’에 속한다.
  • Garbage Collection을 수행하는 Finalizer 스레드는 System스레드 그룹에 속한다.
  • 자신을 생성한 스레드(상위 스레드)의 그룹과 우선순위를 상속받는다.
  • 보안상의 이유로 도입된 개념으로 자신이 속한 스레드 그룹 이하나 하위 스레드 그룹은 변경할 수 있지만 다른 스레드 그룹의 스레드를 변경할 수는 없다.
  • ThreadGroup 클래스로 생성할 수 있다.
생성자/메서드설명
ThreadGroup(String name)주어진 이름으로 새로운 스레드 그룹 생성
ThreadGroup(ThreadGroup parent, String name)주어진 부모그룹과 이름으로 새로운 스레드 그룹 생성
int activeCount()스레드 그룹 및 하위 그룹의 활성 스레드 수 반환
void destroy()스레드 그룹 및 하위 그룹까지 모두 삭제
int getMaxPriority()스레드 그룹의 최대 우선 순위를 반환
void setMaxPriority(int pri)스레드 그룹의 최대 우선 순위를 설정
String getName()스레드 그룹의 이름을 반환
ThreadGroup getParent()스레드 그룹의 상위 스레드 그룹을 반환
void list()스레드 그룹, 하위 그룹 및 스레드에 대한 정보를 출력
boolean isDaemon()스레드 그룹이 데몬 스레드 그룹인지 확인
void setDaemon(boolean daemon)스레드 그룹을 데몬 스레드 그룹으로 설정 또는 해제
boolean isDestroyed()스레드 그룹이 삭제되었는지 확인
void interrup()스레드 그룹에 속한 모든 스레드를 중단

예제

public class ThreadGroupEx01 implements Runnable{
	public void run(){
		System.out.println(Thread.currentThread().getName());
	}
	public static void main(String[] args){
		ThreadGroupEx01 runnable = new ThreadGroupEx01();
		ThreadGroup tg1 = new ThreadGroup("Parent ThreadGroup");

		Thread t1 = new Thread(tg1, runnable, "one");
		t1.start();
		Thread t2 = new Thread(tg1, runnable, "two");
		t2.start();
		Thread t3 = new Thread(tg1, runnable, "three");
		t3.start();
		System.out.println("Thread Group Name: "+tg1.getName());
		tg1.list();
	}
}

[결과]
one
two
three
Thread Group Name: Parent ThreadGroup
java.lang.ThreadGroup[name=Parent ThreadGroup,maxpri=10]

Daemon Thread(데몬 스레드)

  • 일반 스레드(non-daemon thread)의 작업을 돕는 보조적인 역할을 수행
  • 애플리케이션 실행되면 JVM은 Garbage Collection, 이벤트처리, 그래픽처리 등 프로그램이 실행되는데 필요한 보조작업을 수행하는 데몬 스레드들을 자동적으로 생성해서 실행시킨다.
  • 일반 스레드가 모두 종료되면 자동적으로 종료되며, 우선 순위가 낮은 스레드이다.
  • boolean isDaemon(): 스레드가 데몬스레드인지 확인하여 데몬스레드일 경우 true 반환
  • void setDaemon(boolean on): 스레드를 데몬스레드(true)로 또는 사용자스레드(false)로 변경한다.
  • setDaemon(boolean on)은 반드시 start()를 호출하기 전에 실행되어야 한다. 그렇지 않으면 IllegalThreadStateException이 발생한다.
public class DaemonThreadEx01 extends Thread{
	public void run(){
		if(Thread.currentThread().isDaemon()){
			System.out.println("daemon thread work");
		}else{
			System.out.println("user thread work");
		}
	}

	public static void main(String[] args){
		DaemonThreadEx01 t1 = new DaemonThreadEx01();
		DaemonThreadEx01 t2 = new DaemonThreadEx01();
		DaemonThreadEx01 t3 = new DaemonThreadEx01();

		//Daemon thread로 설정
		t1.setDaemon(true);

		t1.start();
		t2.start();
		t3.start();
	}
}

[결과]
daemon thread work
user thread work
user thread work

Thread Control

  • 효율적인 멀티쓰레드 처리를 위해서는 정교한 스케쥴링을 통해 프로세스에게 주어진 자원과 시간을 여러 쓰레드가 낭비없이 잘 사용하도록 프로그래밍 해야 한다.
  • 스케쥴링을 잘 하기 위해서는 쓰레드의 상태와 관련 메서드를 잘 알아야 한다.
  • resume(), stop(), suspend()는 쓰레드를 교착상태(dead-lock)로 만들기 쉽기 때문에 deprecated되었다.
  • stop()의 경우 동기화가 걸려있을 경우 lock을 반환하지 않고 종료시키는 치명적 오류가 발견되었다.
생성자/메서드설명
void interrupt()sleep(), join(), wait()에 의해 일시정지 상태인 스레드르 꺠워서 실행대기 상태로 만든다. (Thread state가 WAITING → RUNNABLE) 해당 스레드에서는 interruptedException이 발생되면서 WAITING 상태를 벗어나게 된다.
void join()
void join(long millis)
join()을 호출한 스레드가 종료될 때까지 기다리게 한다. 즉 스레드를 Running → Waiting 상태로 만든다.
파라미터가 있는 join()은 대기 시간을 지정할 수 있고, 대기 시간이 끝나면 실행을 계속한다.
void resume()suspend()에 의해 일시정지 상태에 있는 스레드를 실행대기 상태로 만든다.
static void sleep(long millis)
static void sleep(long millis, int nanos)
지정된 시간(천분의 일초 단위)동안 스레드를 일시정지시킨다. 지정한 시간이 지나고 나면, 자동적으로 다시 실행대기 상태가 된다.
void stop()스레드를 즉시 종료시킨다. 교착상태(dead-lock)에 빠지기 쉽기 때문에 deprecated 되었다.
boolean isAlive()스레드가 시작되었고 아직 끝나지 않았으면 true, 끝났으면 false를 반환한다.
void suspend()스레드를 일시정지시킨다. resume()에 의해 다시 실행대기 상태가 된다.
static void yield()다른 스레드에게 양보(yield)하고 자신은 실행대기 상태가 된다.

Thread State

  • 스레드 상태는 Thread의 getStatus() 메서드를 호출해서 확인할 수 있다. (1.5부터 추가)
  • Java의 스레드는 아래의 상태 중 하나만 가질 수 있으며, JVM의 상태이다.
상태설명
NEW스레드가 생성되었지만 스레드가 아직 실행할 준비가 되지 않았다.
RUNNABLE실행할 준비가 된 스레드가 실행 가능한 상태로 스케줄링을 기다리는 상태
BLOCKED동기화블럭이나 I/O 작업에 의해서 일시정지된 상태(lock이 풀릴 때까지 기다리는 상태)
WAITING다른 스레드가 특정 작업을 완료하기를 기다리며, 다른 스레드가 notify(), notifyAll()을 호출하기를 기다리고 있는 상태
TIME_WAITINGTIME_WAITING은 일시정지 시간이 지정된 경우의 상태로 스레드가 sleep() 메서드 호출로 지정한 밀리초 동안 Sleep하고 있는 상태
TERMINATED스레드가 종료된 상태

  • New: 새 스레드가 생성될 때의 상태이다. New 상태 스레드의 경우 코드가 아직 실행 안 된 상태로 스레드가 생성되었지만 스레드가 아직 실행할 준비가 되지 않은 상태
  • Active: 스레드가 start() 메서드를 호출하면 Active 상태로 이동한다. Active 상태는 그 안에 두 가지(runnable, running) 상태를 포함한다.
    • Runnable: 실행할 준비가 된 스레드가 실행 가능한 상태로 이동한다. 실행 가능한 상태에서 스레드는 실행 중이거나 주어진 시간에 실행할 준비가 될 수 있다. 스레드가 실행 중인 상태로 이동하는 것이 스레드 스케줄러의 역할이다.
      • 멀티스레딩을 구현하는 프로그램은 각 개별 스레드에 대해 고정된 시간의 부분(slice)을 얻는다.
      • 각각의 모든 스레드는 짧은 시간 동안 실행되며 할당된 타임 슬라이스가 끝나면 스레드가 자발적으로 CPU를 다른 스레드에 양보하므로 다른 스레드도 해당 시간 동안 실행할 수 있다. 이러한 시나리오가 발생할 때마다 실행하려는 모든 스레드가 실행되기를 기자리는 실행 가능한 상태로 놓이며, 이 상태에는 스레드의 큐에 있다.
    • Running: 스레드가 CPU를 가져오면 실행 가능한 상태에서 실행 중 상태로 이동한다. 일반적으로 스레드 상태의 가장 일반적인 변경은 실행 가능에서 실행 중으로, 다시 실행 가능으로 돌아가는 것이다.
  • Blocked / Waiting: 스레드가 일정 시간 동안 비활성 상태일 때마다 스레드는 차단된 상태이거나 대기 중인 상태이다.
    • 예를 들어, 스레드 A가 프린터에서 일부 데이터를 인쇄하려고 할 수 있다. 그러나 동시에 다른 스레드 B가 프린터를 사용하여 일부 데이터를 인쇄하고 있다.
    • 스레드 A는 스레드 B가 프린터를 사용할 때까지 기다려야 한다. 따라서 스레드 A는 blocked 상태이다. blocked 상태의 스레드는 실행할 수 없으므로 CPU의 사이클을 소비하지 않는다. 따라서 스레드 스케줄러가 waiting 또는 blocked 상태의 스레드 A를 다시 호라성화할 때까지 스레드 A가 idle 상태로 유지된다고 할 수 있다.
    • 메인 스레드가 join() 메서드를 호출하며 메인 스레드가 대기 상태가 된다. 그런 다음 메인 스레드는 child 스레드가 작업을 완료할 때까지 기다린다. child 스레드가 작업을 완료하면 알림이 메인 스레드로 전송되어 스레드가 waiting 상태에서 active 상태로 다시 이동한다.
    • waiting 중이거나 blocked 상태의 스레드가 많은 경우 스레드 스케줄러는 선택할 스레드와 거부할 스레드를 결정하고 선택한 스레드에 실행할 기회가 주어진다.
  • Timed Waiting: waiting 상태가 영원해지지 않도록 스레드에 time limit waiting 상태가 제공된다. 따라서 스레드는 특정 시간 동안 waiting 상태에 있게 된다.
  • Terminated: 다음과 같은 이유로 스레드가 terminated 상태가 된다. 종료된 스레드는 해당 스레드가 시스템에 더 이상 존재하지 않음을 의미한다.
    • 스레드가 작업을 완료하면 정상적으로 존재하거나 종료된다.
    • 비정상 종료: 처리되지 않은 예외 또는 분할 오류과 같은 일부 비정상 이벤트가 생기는 경우 발생한다.
  • Thread.getState() 메서드를 사용하면 스레드의 현재 상태를 얻을 수 있다. java.lang.Thread.State 클래스는 스레드 상태를 나타내는 상수 ENUM을 제공한다.
  • 스레드는 특정 시점에 하나의 상태만 가질 수 있다.
public enum State{
	NEW,
	RUNNABLE,
	BLOCKED,
	WAITING,
	TIMED_WAITING,
	TERMINATED;
}

스레드 스케줄러(Thread Scheduler)

  • 실행하거나 실행할 스레드와 대기할 스레드를 결정하는 Java 구성요소를 스레드 스케줄러라고 한다.
  • 스레드는 실행 가능한 상태인 경우에만 스레드 스케줄러에 의해 선택된다.
  • Time to Arrival(도착시간): 우선순위가 같은 두 스레드가 실행 가능한 상태에 들어간다고 가정하면 우선순위는 이 두 스레드의 선택 요소가 되는데, 이 경우 스케줄러는 스레드 도착 시간을 고려하여 먼저 도착한 스레드가 우선된다.
  • 도착시간과 우선순위가 다른 5개의 스레드가 있다고 가정하면 어떤 스레드가 CPU를 먼저 차지할지 결정하는 것은 스레드 스케줄러의 책임이다.

스레드 스케줄링

  1. 스레드 생성후 start() 호출 → 실행대기열에 저장되며 큐(queue)와 같은 구조 → FIFO구조로 먼저 들어온 스레드가 먼저 실행된다.
  2. 실행대기 상태에 있다가 차례가 되면 실행된다.
  3. 스케쥴러에 의해 할당된 실행시간이 다 되거나 yeild()를 만나면 다시 실행대기 상태가 되고, 다음 스레드가 실행된다.
  4. 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지 상태가 될 수 있다.
    I/O block은 입출력 작업에 발생하는 지연상태이다.
    예) 사용자의 입력을 기다리는 경우 → 일시정지 → 사용자 입력마침 → 다시 실행대기 상태가 실행된다.
  5. 지정된 일시정지 시간이 다 되거나(time-out), notify(), resume(), interrupt()가 호출되면 일시정지 상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다린다.
  6. 실행을 모두 완료하면 스레드가 소멸된다.

suspend(), resume(), stop()은 쓰레드를 교착상태(dead-lock)로 빠뜨릴 가능성이 있기 때문에 deprecated 되었으므로 사용하지 않는 것이 좋으며, 아래의 코드처럼 stopped와 suspended라는 boolean 타입의 변수를 선언하고 이 변수의 값을 변경함으로써 작업이 중지되고 종료되도록 변경할 수 있다.

public class ThreadSuspendResumeTest{
	public static void main(String[] args){
		ThreadSuspendResume ts1 = new ThreadSuspendResume();
		ThreadSuspendResume ts2 = new ThreadSuspendResume();
		t1.start();
		t2.start();

		try{
			Thread.sleep(2000);
			ts1.suspend();
			Thread.sleep(2000);
			ts2.suspend();
			Thread.sleep(2000);
			ts1.resume();
			Thread.sleep(2000);
			ts1.stop();
			ts2.stop();
		}catch(InterruptedException e){}
	}
}
public class ThreadSuspendResumeTest implements Runnable{
	boolean suspended = false;
	boolean stopped = false;

	Thread t;

	@Override
	public void run(){
		while(!stopped){
			if(!suspended){
				//구현부
				try{
					Thread.sleep(1000);
				}catch(InterruptedException e){}
			}
		}
	}

	public void suspend(){
		suspended = true;
	}
	public void resume(){
		suspended = false;
	}
	public void stop(){
		stopped = true;
	}
	public void start(){
		t.start();
	}
}

  • join()을 사용해서 main 스레드가 작업이 끝날 때까지 기다리도록 할 수 있다.
  • join()을 사용하지 않으면 main 스레드가 바로 종료된다.
  • 여러 개의 서버나 Job의 실행 결과를 가지고 최종결과를 얻어야 할 경우 등에 사용한다.
  • yeild()는 양보를 통해 실행시간을 낭비시키지 않는다.
  • 밑의 예제에서 suspended가 true여서 잠시 실행을 멈추게 된다면 쓰레드는 단순히 while문을 Looping 하면서 낭비를 하게 된다.
  • 하지만 yield()를 호출해서 남은 실행시간을 while문에서 낭비하지 않고 다른 쓰레드에게 양보할 수 있도록 처리하여 효율적으로 쓰레드를 운영할 수 있다.
while(!stopped){
	if(!suspended){
		System.out.println(Thread.currentThread().getName());

		try{
			Thread.sleep(1000);
		}catch(InterruptedException e){}
	}
}

while(!stopped){
	if(!suspended){
		System.out.println(name);

		try{
			Thread.sleep(1000);
		}catch(InterruptedException e){
			System.out.println(name + " - interrupted");
		}
	}else{
		Thread.yield();
	}
}

  • interrupt()는 InterruptedException을 발생시켜서 sleep(), join(), wait()에 의해 일시정지 상태인 쓰레드를 실행대기 상태(RUNNABLE)로 만든다.
  • 그러나 interrupt()가 호출되었을 때 sleep(), join(), wait()에 의해 일시정지 상태가 아니라면 아무 일도 일어나지 않는다.

스레드 풀(Thread Pool)

  • Java 스레드 풀은 작업을 기다리고 여러번 재사용되는 Worker Thread Group을 의미한다.
  • 스레드 풀은 고정 사이즈 스레드 그룹이 생성된다. 그 스레드 풀에서 유휴 스레드(idle thread)를 가져와 서비스 작업에 할당한다.
  • 작업이 완료되면 스레드는 다시 스레드 풀에 포함된다.
  • java.util.concurrent.Executors, ExecutorService를 이용하여 스레드풀을 생성하여 병렬처리를 할 수 있다.
  • Executors는 ExecutorService 객체를 생성하며, 스레드 풀 개수 및 종류를 생성할 수 있다.
  • Executors 메서드
    • newFixedThreadPool(int s): 이 메서드는 고정 크기 s의 스레드 풀을 생성한다.
      실제 생성되는 객체는 ThreadPoolExecutor이다.
    • newCachedThreadPool(): 필요할 때마다 스레드를 생성하는 스레드 풀을 생성한다. 이미 생성된 스레드의 경우 재사용된다.
    • newSingleThreadExecutor(): 하나의 스레드만 사용하는 ExecutorService를 생성하며 싱글 스레드에서 동작해야 하는 작업을 처리할 때 사용하며, 스레드가 1개이기 때문에 작업을 예약한 순서대로 처리하며 동시성(Concurrency)를 고려할 필요가 없다.
  • ExecutorService 작업을 처리할 수 있다.
    • submit(): 멀티스레드로 처리할 작업을 추가한다.
    • shutdown(): 더 이상 스레드풀에 작업을 추가하지 못하며, 처리 중인 task가 모두 완료되면 스레드 풀을 종료시킨다.
    • awaitTermination(): 이미 수행 중인 task가 지정된 시간동안 끝나기를 기다리며, 지정된 시간내에 종료되지 않으면 false를 리턴한다.
      이 때 shutdownNow()를 호출하면 실행 중인 task를 모두 강제 종료시킬 수 있다.
  • 장점: 필요시에 새로운 스레드를 생성할 필요가 없기 때문에 스레드 생성 시간이 절약되고 바로 사용할 수 있어서 더 나은 성능을 기대할 수 있다.
  • Real-time usage: 컨테이너가 요청을 처리하기 위해 스레드 풀을 생성하는 서블릿 및 JSP에서 사용된다.

예제: ExecutorService 및 Executors를 사용하는 Java 스레드 풀의 사용 예

public class ThreadPoolWorkerThread implements Runnable{
 private String message;
 public ThreadPoolWorkerThread(String s){
	 this.message=s;
 }
 public void run() {
		System.out.println(Thread.currentThread().getName() +
						" (Start) message = "+message);
		// 스레드를 1초간 sleep하는 processmessage 메소드 호출
		processmessage();
		System.out.println(Thread.currentThread().getName()+" (End)");
	}
	private void processmessage() {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

public class ThreadPoolTest {
	public static void main(String[] args) {
		// 스레드 풀을 생성 (5 스레드)
		ExecutorService executor = Executors.newFixedThreadPool(5);
		for (int i = 0; i < 10; i++) {
			Runnable worker = new ThreadPoolWorkerThread("" + i);
			// ExecutorService의 execute 메소드 호출
			executor.execute(worker);
		}
		// shutdown한다. 이미 executor에 제공된 task는 실행되지만
		// 새로운 작업은 수용 안한다
		executor.shutdown();
		// shutdown후 모든 작업이 종료되었는지 여부를 확인한다
		while (!executor.isTerminated()) {}
		
		System.out.println("모든 소레드 종료");
	}
}

[결과]
pool-1-thread-4 (Start) message = 3
pool-1-thread-1 (Start) message = 0
pool-1-thread-2 (Start) message = 1
pool-1-thread-3 (Start) message = 2
pool-1-thread-5 (Start) message = 4
pool-1-thread-5 (End)
pool-1-thread-3 (End)
pool-1-thread-1 (End)
pool-1-thread-3 (Start) message = 6
pool-1-thread-2 (End)
pool-1-thread-2 (Start) message = 8
pool-1-thread-5 (Start) message = 5
pool-1-thread-4 (End)
pool-1-thread-4 (Start) message = 9
pool-1-thread-1 (Start) message = 7
pool-1-thread-3 (End)
pool-1-thread-4 (End)
pool-1-thread-2 (End)
pool-1-thread-1 (End)
pool-1-thread-5 (End)
모든 소레드 종료

  • 스레드 풀을 사용하면 비용적인 측면이나 컨텍스트 스위칭이 발생하는 상황에서 딜레이를 줄일 수 있는 장점이 있다.
  • 단점
    • 스레드 풀에 너무 많은 양의 스레드를 만들어둔다면 메모리 낭비가 심해질 수 있다. 필요한 스레드 수를 예측하고 할당해서 사용해야 한다.
    • long task에 스레드를 사용할 때마다 주의해야 한다. 스레드가 영원히 대기하는 결과를 초래할 수 있으며 결국 리소스 누출로 이어진다.

스레드 동기화

  • 클래스에 대한 객체가 생성될 때마다 Object lock이 생성되어 객체 내부에 저장된다.
  • 한 번에 하나의 스레드만 객체에 접근할 수 있도록 객체에 락(lock)을 걸어서 데이터의 일관성을 유지하는 것으로 공유 리소스에 대한 여러 스레드의 접근 제어 기능을 한다.
  • Java 1.5부터는 java.util.concurrent.locks, java.util.concurrent.atomic 패키지를 통해서 다양한 방식의 동기화를 구현할 수 있도록 지원하고 있다.
  • 사용 이유
    • 스레드의 간섭을 방지한다.
    • 정볼르 처리하는데 있어 일관성 문제를 방지한다. (예를 들면 하나의 계좌에 입/출금을 동시에 여러 스레드가 접근하는 것을 방지해야 한다.)
    • 사용법
      • 동기화 블럭(Synchronized Block): 특정 객체에 lock을 걸어서 사용하고자 할 경우
        synchronized(객체 참조 변수){
        //구현 내용
        }
      • 메서드에 lock을 걸어 사용하고자 할 경우 (메서드 전체를 동기화하는 것보다 메서드의 일부 영역을 동기화하는 것이 성능 향상에 더 좋다)
        public synchronized void CalcPay(){
        //구현 내용
        }
  • 만일 스레드 A가 작업하던 도중 다른 스레드 B에게 제어권이 넘어갔을 때 스레드 A가 작업하던 공유 데이터를 스레드 B가 임의로 변경하였다면 → 스레드 A는 의도하지 않은 다른 결과를 얻게 된다.
  • 동기화는 lock 또는 Monitor로 알려진 내부 Entity를 중심으로 구축되며, 모든 객체에는 lock과 연결되어 있다.
  • 규칙에 따라 객체의 필드에 대한 접근이 필요한 스레드는 접근 전에 객체의 lock을 획득해야 하고 작업이 완료되면 lock을 해제(반납)해야 한다.
  • java 5 버전의 java.util.concurrent.locks 패키지에 여러 lock 관련 구현이 포함되어 있다.
  • 동기화는 공유 리소스에 대한 객체를 잠그는데 사용되기에 공유리소스에 접근하는 스레드들은 lock을 얻기 위해 대기하게 되며 그렇기 떄문에 시스템 성능이 저하될 수 있다.
  • 교착상태(Dead lock): 두 스레드가 lock을 건 상태에서 서로 lock이 풀리기를 기다리는 상황으로 작업이 진행되지 않고 영원히 기다리는 상황으로 교착상태에 빠지지 않도록 개발해야 한다.
  • stop(), suspend(), resume()과 같이 스레드의 상태를 변경하는 메서드들은 교착상태를 일으킬 가능성이 높다는 이유로 deprecated 되었다.
  • 인스턴스의 변수 Reference 변수인 this를 통해 Thread들에 의해 공유되지만 지역변수는 각 스레드의 스택 내에 생성되므로 같은 프로세스 내의 스레드일지라도 공유되지 않는다. (CASE 1)
  • 아래 예제처럼 Thread 생성시 공유할 객체를 전달하지 않으면 각각의 객체로 Thread별로 처리된다. (CASE 2)
//CASE 1) T1, T2에 객체가 공유됨
RunnableImpl r = new RunnableImpl();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);

//CASE 2) T1, T2가 공유하는 객체 없음
MyThread t1 = new MyThread();
MyTHread t2 = new MyThread();

t1.start();
t2.start();
  • Account의 출금메소드인 withdraw에 synchronized 키워드를 붙이기만 하면 한 스레드에 의해서 먼저 withdraw()가 호출되면, 종료될 때까지 다른 스레드는 호출하더라도 대기상태에 머물게 된다.
public class SynchronizedTest{
	public static void main(String[] args){
		Runnable r = new RunnableSyncEx();
		new Thread(r).start();
		new Thread(r).start();
	}
}

class Account{
	private int balance = 1000;

	public int getBalance(){
		return balance;
	}

	public synchronized void withdraw(int money){
		if(balance >= money){
			try{
				Thread.sleep(1000);
			}catch(InterruptedException e){}
			balance -= money;
		}
	} //withdraw
}

class RunnableSyncEx implements Runnable{
	Account acc = new Account();

	@Override
	public void run(){
		while(acc.getBalance() > 0){
			//임의의 값으로 출금(withdraw) 처리
			int money = (int)(Math.random() * 3 + 1) * 100;
			acc.withdraw(money);
			System.out.println(Thread.currentThread().getName()
							+" => 잔액 :"+ acc.getBalance());
		}
	}
}

[결과]
//Synchronized 미사용 결과
Thread-1 => 잔액 :600
Thread-0 => 잔액 :800
Thread-1 => 잔액 :400
Thread-0 => 잔액 :400
Thread-1 => 잔액 :300
Thread-0 => 잔액 :0
Thread-1 => 잔액 :-200

//Synchronized 사용 결과
Thread-1 => 잔액 :700
Thread-0 => 잔액 :500
Thread-1 => 잔액 :300
Thread-1 => 잔액 :0
Thread-0 => 잔액 :0

  • wait(), notify(), notifyAll() 메서드는 동기화의 효율을 높이기 위해 사용한다.
  • 한 스레드가 무한정 객체에 lock을 걸고 점유하게 되면 다른 스레드들이 lock이 풀릴 때까지 기다려야 하는 상황이 발생한다. (비효율적)
    • 한 스레드가 객체에 lock을 걸고 오래 기다리는 대신 wait()을 호출해서 다른 스레드에게 제어권을 넘겨주고 대기상태로 기다린다.
    • 다른 스레드에 의해 notify()가 호출되면 다시 실행대기열 상태가 되도록 하는 것이다.
  • 관련 메서드들은 Object클래스에 정의되어 있어서 모든 객체에서 호출가능하며 동기화 블록 내에서만 사용할 수 있다.
  • wait(): 객체의 lock을 풀고 해당 객체의 스레드를 waiting pool에 넣는다.
  • notify(): waiting pool에서 대기중인 스레드 중의 하나를 깨운다.
  • notifyAll(): waiting pool에서 대기중인 모든 스레드를 깨운다.
  • notify()에 의해 어떤 스레드가 깨워지게 될지 알 수 없고, 특정 스레드가 오랫동안 객체의 waiting pool에 머물 수 있기 때문에 다시 waiting pool에 들어가더라도 nofityAll()을 호출해서 모든 스레드를 깨워놓고 JVM의 스케쥴링에 의해서 처리되도록 하는 것이 안전하다.
  • 잔고가 부족할 경우 wait()을 호출해서 스레드가 객체의 lock을 풀고 waiting pool에 들어가면서 다른 스레드에게 제어권을 넘긴다.
  • 다른 스레드에 의해서 deposit() 메소드가 호출되고 잔고가 증가하면서 notify()를 호출하면 객체의 waiting pool에서 기다리고 있던 스레드를 깨우게 된다.
class AccountEx{
	private int balance = 1000;

	public int getBalance(){
		return balance;
	}	

	public synchronized void withdraw(int money){
		while(balance < money){
			try{
				//잔고 부족으로 출고 job 스레드는 lock 해제 후
				//waiting pool로 들어가며 제어권을 넘긴다
				wait();
			}catch(InterruptedException e){}
		}
		balance -= money;
	} //withdraw

	public synchronized void deposit(int money){
		balance += money;
		//잔고가 증가되면 notify()를 호출해서 waiting pool의
		//스레드를 깨워서 job을 이어서 처리하도록 한다
		notify();
	}
}

실행 중인 모든 JVM 스레드 정보

  • Thread 클래스의 getAllStackTrace() 메서드는 실행 중인 모든 스레드의 스택 추적을 제공한다.
  • Key가 Thread 객체인 Map을 반환하므로 Key Set을 얻어와서 해당 요소를 반복하여 스레드에 대한 정보를 얻을 수 있다.
  • Main thread외에 다른 스레드가 있고, Java 버전에 따라 다를 수 있다.
  • Single Dispatcher: 이 스레드는 운영 체제에서 JVM으로 보내는 신호를 처리한다.
  • Finalizer: 이 스레드는 더 이상 시스템 리소스를 해제할 필요가 없는 객체에 대한 종료를 수행한다.
  • Reference Handler: 더 이상 필요하지 않는 객체를 Finalizer 스레드에서 처리할 대기열에 넣는다.
  • Spring boot tomcat thread 수: Minimum worker thread -10 / max - 200

Common Application Properties

Set<Thread> threads = Thread.getAllStackTraces().keySet();
System.out.printf("%-15s \t %-15s \t %-15s \t %s\n", "Name", "State", "Priority", "isDaemon");
for (Thread t : threads) {
	System.out.printf("%-15s \t %-15s \t %-15d \t %s\n", t.getName(), t.getState(), t.getPriority(), t.isDaemon());
}

[결과]

NameStatePriorityisDeamon
mainRUNNABLE5false
Thread-0TERMINATED5false
Reference HandlerRUNNABLE10true
Monitor Ctrl-BreakRUNNABLE5true
FinalizerWAITING8true
Thread-2TERMINATED5false
Thread-1BLOCKED5false
Common-CleanerTIMED_WAITING8true

Dead Lock(교착 상태)

  • 두 스레드가 서로 lock 해제를 기다리고 있기 때문에 이 상태를 교착 상태라고 한다.
  • Deadlock을 피하는 방법: 교착 상태는 완전히 해결할 수 없다. 그러나 아래의 기본 규칙을 따르면 이러한 문제를 피할 수 있다.
    • 중첩된 lock 피하기: 여러 스레드에 lock을 제공하는 것을 피해야 하며 이것이 교착 상태의 주요 원인이다. 일반적으로 여러 스레드에 lock을 부여할 때 발생한다.
    • 불필요한 lock 피하기: 중요한 스레드에 lock을 제공해야 한다.
    • 스레드 조인 사용: 교착 상태는 일반적으로 한 스레드가 다른 스레드가 완료되기를 기다릴 때 발생한다. 이 경우 스레드가 소요되는 최대 시간으로 조인을 사용할 수 있다.
    • Lock Timeout: 스레드가 lock을 획득하는 시간을 지정하여 주어진 시간 내에 lock을 획득하지 못하면 lock 획득시도를 포기하고 일정 시간 후에 다시 시도하는 방식

예제 - Dead Lock 발생 예제: Dead Lock에서는 resource1, 2에 접근한 패턴이 주요 문제이므로 해결하려면 공유 리소스의 접근 순서만 변경하면 된다.

public class TestDeadlockEx1{
	public static void main(String[] args){
		final String resource1 = "테스트 리소스 1";
		final String resource2 = "테스트 리소스 2";

		//t1은 resource1을 잠그고 resource2를 잠그려고 시도한다
		Thread t1 = new Thread(){
			public void run(){
				synchronized (resource1){
					System.out.println("Thread 1: locked resource 1");
					try{Thread.sleep(100);}catch(Exception e){}
					synchronized(resource2){
						System.out.println("Thread 1: locked resource 2");
					}
				}
			}
		};

		//t2는 resource2를 잠그고 resource1을 잠그려고 시도한다.
		Thread t2 = enw Thread(){
			public void run(){
				synchronized (resource2){
					System.out.println("Thread 2: locked resource 1");
					try{Thread.sleep(100);}catch(Exception e){}
					synchronized(resource1){
						System.out.println("Thread 2: locked resource 2");
					}
				}
			}
		};

		t1.start();
		t2.start();
	}
}

[결과]
//서로 리소스를 잠그고 기다리는 상황으로 스레드가 종료되지 않는다
//Thread t1이 resource1을 잠그고 resource2 사용을 위해 기다린다
//Thread t2가 resource2을 잠그고 resource1 사용을 위해 기다린다
Thread 2: locked resource 2
Thread 1: locked resource 1

Q&A

동기화란 무엇인가?

동기화를 통해 스레드가 특정 메서드 또는 블록을 동시에 실행하지 않고 동기화하도록 만들 수 있다. 메서드 또는 블록이 동기화된 것으로 선언되면 하나의 스레드만 해당 메서드 또는 블록에 들어갈 수 있다. 한 스레드가 동기화된 메서드 또는 블록을 수행 중이면 해당 메서드 또는 블록을 실행하려는 다른 스레드는 첫 번째 스레드가 해당 메서드 또는 블록을 실행할 때까지 기다려야 하며 스레드 간섭을 피하고 스레드 안전성을 달성한다.


객체 잠금 또는 모니터란 무엇인가?

Java의 동기화는 객체 잠금 또는 모니터라는 엔티티를 중심으로 구축된다. 다음은 잠금 또는 모니터에 대한 간략한 설명이다.

  • 어떤 클래스에 대해 객체가 생성될 때마다 object lock이 생성되어 객체 내부에 저장된다.
  • 하나의 객체에는 연결된 객체 잠금이 하나만 있다.
  • 모든 스레드는 객체의 동기화된 메서드 또는 블록에 들어가고자 하며 해당 객체와 연결된 객체 잠금을 획득하고 실행이 완료된 후 잠금을 해제해야 한다.
  • 해당 객체의 동기화된 메서드에 들어가려는 다른 스레드는 현재 실행 중인 스레드가 객체 잠금을 해제할 때까지 기다려야 한다.
  • 정적 동기화 메서드 또는 블록에 들어가려면 정적 멤버가 클래스 메모리 내에 저장되므로 스레드는 해당 클래스와 관련된 클래스 잠금을 획득해야 한다.

뮤텍스(Mutax)란?

동기화된 블록은 하나의 인수를 가지며 이를 뮤텍스라고 한다. 동기화된 블록이 비정적 메서드, 인스턴스 이니셜라이저 또는 생성자와 같은 비정적 정의 블록 내에서 정의된 경우 이 뮤텍스는 해당 클래스의 인스턴스여야 한다. 동기화된 블록이 정적 메서드 또는 정적 초기화 프로그램과 같은 정적 정의 블록 내부에 정의된 경우 이 뮤텍스는 ClassName.class와 같아야 한다.
동기화된 정적 메서드에는 클래스 수준 잠금이 필요하고 동기화된 비정적 메서드에는 객체 수준 잠금이 필요한데 이 두 가지 방법을 동시에 실행할 수 있다.


동기화된 메서드를 실행하는 동안 특정 스레드가 예외로 catch되면 실행 중인 스레드가 잠금을 해제하는가?

스레드는 실행이 정상적으로 완료되었는지 또는 예외로 포착되었는지 여부에 관계없이 잠금을 해제해야 한다.
동기화 메서드보다 동기화 블럭이 메서드의 일부만 동기화하기 때문에 성능이 향상된다.


Java에서 스레드가 서로 통신하는 방법은 무엇인가?

  • wati(), notify() 및 notifyAll() 메소드를 사용하여 서로 통신한다.
  • wait(): 이 메서드는 현재 실행 중인 스레드에게 이 객체의 잠금을 해제하고 다른 스레드가 동일한 잠금을 획득할 때까지 기다렸다가 notify() 또는 notifyAll() 메서드를 사용하여 알릴 것을 지시한다.
  • notify(): 이 메서드는 이 객체에서 wait() 메서드를 호출한 스레드 하나를 임의로 깨운다.
  • notifyAll(): 이 메서드는 이 객체에서 wait() 메서드를 호출한 모든 스레드를 깨운다. 그러나 우선 순위에 따라 하나의 스레드만 이 객체의 잠금을 획득한다.

BLOCKED 상태와 WAITING 상태의 차이점은 무엇인가?

스레드는 다른 스레드의 알림을 기다리는 경우 WAITING 상태가 된다. 다른 스레드가 원하는 lock을 해제하기를 기다리는 경우 스레드는 BLOCKED 상태가 된다.
스레드가 객체에서 wait() 또는 join() 메서드를 호출할 때 WAITING 상태가 된다. 대기 상태로 전환하기 전에 스레드는 보유하고 있는 객체의 lock을 해제한다. 동일한 객체에서 다른 스레드 호출이 notify() 또는 notifyAll()될 때까지 WAITING 상태로 유지된다.
다른 스레드가 동일한 객체에 대하 notify() 또는 notifyAll()을 호출하면 해당 객체의 잠금을 기다리는 스레드 중 하나 또는 모든 것이 통지된다. 알림을 받은 모든 스레드는 객체를 즉시 잠그지 않는다. 현제 스레드가 lock을 해제하면 우선순위에 따라 객체 lock을 받는다. 그 때까지 그들은 차단된 상태에 있을 것이다.

profile
웹퍼블리셔의 백엔드 개발자 도전기

0개의 댓글