[Java] #13. 쓰레드(Thread)

bien·2024년 2월 15일
0

java

목록 보기
8/11

1. 프로세스와 쓰레드

프로세스(process)

  • 프로세스(process)
    • 실행중인 프로그램(program). 프로그램 실행 시 OS로부터 실행에 필요한 자원(메모리)을 할당 받아 프로세스가 된다.
    • 이때 자원에는 메모리, CPU 등이 있다.
  • 쓰레드(thread)
    • 프로세스 내에서 실제 작업을 수행한다.
    • 모든 프로세스는 최소한 하나의 쓰레드를 가지고 있다.

프로세스 : 쓰레드 = 공장 : 일꾼

  • 싱글 쓰레드 프로세스 = 자원 + 쓰레드
  • 멀티 쓰레드 프로세스 = 자원 + 쓰레드 + 쓰레드 + 쓰레드 + 쓰레드 ..

하나의 새로운 프로세스를 생성하는 것보다 하나의 새로운 쓰레드를 생성하는 것이 더 적은 비용이 든다.

멀티쓰레딩(multithreading)

  • 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것.
    • CPU의 코어(core)가 한 번에 단 하나의 작업만 수행할 수 있으므로, 실제로 동시에 처리되는 작업의 개수는 코어의 개수와 일치한다.
    • 처리해야 하는 쓰레드의 수는 언제나 코어의 개수보다 훨씬 많기 때문에 코어가 아주 짧은 시간 동안 여러 작업을 번갈아가며 수행함으로써 여러 작업들이 모두 동시에 수행되는 것처럼 보이게 한다.
  • 여러 사용자에게 서비스를 해주는 서버 프로그램의 경우 멀티쓰레드로 작성하는 것이 필수적이다.
    • 하나의 서버 프로세스가 여러 개의 쓰레드를 생성해 쓰레드와 사용자의 요청이 일대일로 처리되도록 프로그래밍해야 한다.
    • 싱글 쓰레드로 서버 프로그램을 작성하면 사용자의 요청마다 새로운 프로세스를 생성해야 하는데, 프로세스의 생성은 쓰레드 생성보다 더 많은 시간과 메모리 공간을 요구하므로 많은 수의 사용자 요청을 서비스하기 어렵다.

장점

  • CPU의 사용률을 향상시킨다.
  • 자원을 보다 효율적으로 사용할 수 있다.
  • 사용자에 대한 응답성이 향상된다.
  • 작업이 분리되어 코드가 간결해진다.

단점

  • 코드 작성에 유의해야할 부분이 많다.
    • 동기화(synchronization)에 주의해야 한다.
    • 교착상태(deadlock)가 발생하지 않도록 주의해야 한다.
      • 교착상태란 두 쓰레드가 자원을 점유한 상태에서 서로 상대편이 점유한 자원을 사용하려고 기다리느라 진행이 멈춰있는 상태를 말한다.
    • 기아(굶어죽는)에 주의해야 한다.
      • 특정 쓰레드만 활동하고, 활동하지 않는 쓰레드가 생기지 않도록 주의해야 한다.

2. 쓰레드의 구현과 실행

  • 쓰레드를 구현한다는 것은 그저 쓰레드를 통해 작업하고자 하는 내용으로 run()의 몸통{}을 채우는 것이다.
    • run()의 몸통을 채우는 방식으로 2가지 방법이 있는데 Thread 클래스 상속과 Runnable 인터페이스 구현이 그것이다.

1) Thread 클래스 상속

  • 자바는 단일상속만 가능하므로 Thread를 상속받으면 다른 클래스를 상속받을 수 없다.
class MyThread extends Thread {
	public void run() { //Thread 클래스의 run()을 오버라이딩
    	/* 작업내용 */
    }
}

// 쓰레드의 생성
MyThread t1 = new Thread();
t1.start();

2) Rnanble 인터페이스 구현 (추천)

  • 인터페이스를 상속받아도 다른 클래스를 추가로 더 상속받을 수 있으므로 더 유연해 사용이 추천된다.
class MyThread2 implements Runnable {
	public void run() { // Runnable 인터페이스의 추상 메서드 run()을 구현
    	/* 작업 내용 */
	}
}    

// 쓰레드의 생성
Runnable r = new MyThread2();
Thread t2 = new Thread(r); // Thread(Runnable r)
// Thread t2 = new Thread(new MyThread2());
t2.satrt();

3) 쓰레드 생성 및 호출 예시

public class ThreadEx1 {

	public static void  main(String args[]) {
		ThreadEx1_1 t1 = new ThreadEx1_1();
		
		Runnable r = new ThreadEx1_2();
		Thread t2 = new Thread(r); // 생성자 Thread(Runnable target)
		
		t1.start();
		t2.start();
	}
}

class ThreadEx1_1 extends Thread {
	public void run() {
		for (int i = 0; i < 500; i++) {
			// 조상인 Thread의 getName() 호출
			System.out.print(0);
		}
	}
}

class ThreadEx1_2 implements Runnable {
	public void run() {
		for (int i = 0; i < 500; i++) {
			// Thread.currentThread() - 현재 실행중 인 Thread를 반환한다.
			System.out.print(1);
		}
	}
}
  • static Thread currentThread() 현재 실행중인 쓰레드의 참조를 반환한다.
    • Thread 클래스를 상속받으면 자손 클래스에서 조상인 Thread클래스의 메서드를 직접 호출할 수 있지만, Runnable을 구현한 경우 Thread 클래스의 statc 메서드인 currentThread()를 호출해 쓰레드에 대한 참조를 얻어와야만 호출이 가능하다.
  • String getName() 쓰레드의 이름을 반환한다.

4) 쓰레드의 실행: start()

  • 쓰레드를 생성한 후에 start()를 호출해야 쓰레드가 작업을 실행한다.
    • 어느 쓰레드가 먼저 실행될지 알 수 없다. 예시에서 t1이 t2보다 먼저 start되었다고 해서 t1쓰레드가 먼저 실행되는 것은 아니다. OS 스케줄러가 어떤 순서로 누가 얼마나 실행될지를 결정한다. 한낱 OS에서 돌아가는 하나의 프로세스에 불과한 JAVA Vartual Machine에서 순서를 결정할 수 없으며, OS가 정해주는 순서를 따라야 한다.
  • 결론
    • start()했다고 해서 즉시 실행되는것이 아니다.
    • 먼저 start()했다고 해서 먼저 실행되는 것이 아니다.
ThreadEx1_1 t1 = new ThreadEx1_1(); // 쓰레드가 t1을 생성한다.
ThreadEx1_1 t2 = new ThreadEx1_1(); // 쓰레드가 t2을 생성한다.

t1.start(); // 쓰레드 t1을 실행시킨다.
t2.start(); // 쓰레드 t2을 실행시킨다.

한 번 실행된 쓰레드는 다시 실행할 수 없다. 즉, 하나의 쓰레드에 대해 start()가 한 번만 호출될 수 있다. 만일 쓰레드의 작업을 한번 더 수행해야 한다면 새로운 쓰레드를 생성한 다음에 satrt()를 실행해야 한다.

ThreadEx1_1 t1 = new ThreadEx1_1();
t1.start();
t1.satrt(); // IllegarThreadStateException 발생
ThreadEx1_1 t1 = new ThreadEx1_1();
t1.start();
t1 = new ThreadEx1_1(); // 다시 생성
t1.start(); // OK

3. start()와 run()

왜 쓰레드를 실행시킬때 run()이 아닌 start()를 호출할까?

  • run() : 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메서드를 호출하는 것이다.
  • start() : 새로운 쓰레드가 작업을 수행하는데 필요한 호출스택(call stack)을 생성한 다음 run()을 호출해 생성된 호출스택에 run()이 첫번째로 올라가게 한다.

start()

class ThreadTest {
	public static void main(String args[]) {
    	MyThread t1 = new MyThread();
        t1.start();
    }
}

run()

class MyThread extends Thread {
	public void run() {
    	// ...
    }
}

  1. main메서드에서 쓰레드의 start()를 호출한다.
  2. start()는 새로운 쓰레드를 생성하고, 쓰레드가 작업하는데 사용될 호출스택을 생성한다.
  3. 새로 생성된 호출 스택에 run()이 호출되어, 쓰레드가 독립적 공간에서 작업을 수행한다.
  4. 이제는 호출 스택이 2개 이므로 스케줄러가 정한 순서에 의해서 번갈아 가면서 실행된다.

main쓰레드

  • main메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main 쓰레드라고 한다.
    • 프로그램을 실행하면 기본적으로 하나의 쓰레드(일꾼)를 생성하고, 그 쓰레드가 main 메서드를 호출해서 작업이 수행되도록 한다.
  • 실행중인 사용자 쓰레드가 하나도 없을 때에 프로그램이 종료된다.
    • 따라서 한 쓰레드가 예외가 발생해 종료되어도 다른 쓰레드의 실행에는 영향을 미치지 않는다.

4. 싱글쓰레드와 멀티쓰레드

싱글쓰레드

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


작업 A와 B를 하나의 쓰레드가 실행한다.

멀티쓰레드

class ThreadTest {
	public static void main(String args[]) {
    	MyTread1 th1 = new MyThread1();
        MyThread th2 = new MyThread2();
        th1.start();
        th2.start();
    }
}

class MyThread1 extends Thread {
	public void run() {
    	for (int i = 0; i < 300; i++) {
        	System.out.println("-");
        }
    }    // run()
}    

class MyThread2 extends Thread {
	public void run() {
    	for (int i = 0; i < 300; i++) {
        	System.out.println("|");
        }
    }    // run()
}

  • 작업 A, B를 분리된 2개의 쓰레드가 각각 실행한다.
    • 두 쓰레드가 실행되는 순서와 시간은 OS 스케쥴러의 알고리즘에 따라 결정된다.
    • A와 B 작업이 교체되는데 'Context Switching' 시간이 소요되게 된다. 따라서 싱글쓰레드의 작업시간을 t1, 멀티쓰레드의 작업시간을 t2라 할때 t2가 t1보다 더 길다.
    • 시간이 조금 더 소요되더라도 두가지 작업을 동시에 시행할 수 있다는 장점이 있어 멀티쓰레드 사용이 선호된다.
  • 위 결과는 코드 실행시 마다 다른 결과를 얻을 수 있는데, 이는 실행중인 예제프로그램(프로세스)이 OS의 프로세스 스케줄러의 영향을 받기 때문이다.
    • 프로세스의 실행 순서와 시간은 OS 스케줄러에 의해 결정되므로 일정하지 않다는 점을 염두에 두어야한다.
    • 자바가 OS(플랫폼) 독립적이지만 OS 종속적인 부분이 몇 가지 있는데, 쓰레드도 그 중 하나이다.

JVM 종류에 따라 쓰레드 스케줄러의 구현방법이 다를 수 있기 때문에 멀티쓰레드로 작성도니 프로그램을 다른 종류의 OS에서도 테스트해 볼 필요가 있다.

싱글코어 vs 멀티코어

  • 싱글코어
    • 멀티쓰레드라도 하나의 코어가 번갈아가면서 작업을 수행하는 것이므로 두 작업이 절대 겹치지 않는다.
  • 멀티코어
    • 멀티쓰레드로 두 작업을 수행하는 경우 동시에 두 쓰레드가 수행될 수 있다.
    • 따라서 A와 B가 겹치는 부분이 발생하고, 이때 화면(console)이라는 자원을 놓고 두 쓰레드가 경쟁하게 된다.

여러 쓰레드가 여러 작업을 동시에 진행하는 것을 병행(concurrent)라고 하고, 하나의 작업을 여러 쓰레드가 나눠서 처리하는 것을 병렬(parallel)이라고 한다.

쓰레드의 I/O 블락킹 (blocking)

  • I/O (Input과 Output, 입출력) 블락킹 (작업중단)
    • "입출력 시 작업 중단"을 의미한다.
public class ThreadEx6 {

	public static void main(String[] args) throws Exception {
		String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
		System.out.println("입력하시신 값은 " + input + "입니다.");
		
		for (int i = 10; i > 0; i--) {
			System.out.println(i);
			try {
				Thread.sleep(1000); // 1초간 시간을 지연한다.
			} catch(Exception e) {}
		}
	}
}
  • 입력을 받는 작업과 카운트 다운(10부터 0까지)작업을 진행한다.
    • 이때, 사용자가 입력을 마칠때까지 작업을 중단한다. (사용자가 값을 입력한 이후에야 카운트다운을 시작한다.)
package _ch12;

import javax.swing.JOptionPane;

public class ThreadEx7 {
	
	public static void main(String[] args) {
		ThreadEx7_1 th1 = new ThreadEx7_1();
		th1.start();
		
		String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
		System.out.println("입력하신 값은 " + input + " 입니다.");
	}

}

class ThreadEx7_1 extends Thread {
	public void run() {
		for (int i = 10; i > 0; i--) {
			System.out.println(i);
			try {
				sleep(1000);
			} catch(Exception e) {}
		}
	} // run()
}
  • 입력을 받는 작업과 카운트 다운 작업을 별도의 쓰레드로 진행한다.
    • A쓰레드가 작업을 하는 동안 B쓰레드가 작업을 수행한다.
    • 동시에 시행되므로 이 경우에 훨씬 작업이 빨리 끝나게 된다.
  • 입출력(파일작성, 프린트 등) 작업에 비해 cpu의 작업속도가 훨씬 빠르므로, 멀티쓰레드로 프로그래밍을 수행하면 I/O에 소요되는 시간동안 다른 작업을 수행할 수 있어 더 효율적이다.

5. 쓰레드의 우선순위

  • 쓰레드가 수행하는 작업의 중요도에 따라 우선순위를 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다.
    • ex. 파일전송 기능이 있는 메신저의 경우 파일 다운로드를 처리하는 쓰레드보다 채팅 내용을 전송하는 쓰레드의 우선순위를 더 높여 채팅의 불편함을 예방할 수 있다.
      • 대신 파일 다운로드 작업에 걸리는 시간은 더 길어질 것이다.
    • 시각적인 부분이나 사용자에게 빠르게 반응해야하는 작업을 하는 쓰레드의 우선순위는 다른 작업을 수행하는 쓰레드에 비해 높아야 한다.

쓰레드의 우선순위 지정하기

void setPriority(int newPriority) 쓰레드의 우선순위를 지정한 값으로 변경한다.
int getPriority() 쓰레드의 우선순위를 반환한다.

pubilc static final int MAX_PRIORITY = 10 // 최대 우선 순위
pubilc static final int MIN_PRIORITY = 1 // 최소 우선순위
pubilc static final int NORM_PRIORITY = 5 // 보통 우선순위
  • 쓰레드가 가질 수 있는 우선순위의 범위는 1~10이며 숫자가 높을수록 우선순위가 높다.
  • 쓰레드의 우선 순위는 쓰레드를 생성한 쓰레드로부터 상속 받는다.
    • main()를 수행하는 쓰레드는 우선순위가 5이므로 main 메서드 내에서 생성하는 쓰레드의 우선 순위는 자동으로 5가 된다.

  • 우선순위가 같은 경우 각 쓰레드에게 거의 같은 양의 실행시간이 주어지진다.
  • 우선순위가 다른 경우 우선순위가 높은 쓰레드에게 상대적으로 더 많은 양의 실행시간이 주어지고, 결과적으로 더 빨리 완료될 수 있다.
    • 다만, 이는 확률이 높아지는 것일 뿐 쓰레드 우선 순위가 정확한 실행 순서를 의미하는 것은 아니다.
      • 쓰레드에 높은 우선순위를 주면 더 많은 실행시간과 실행 기회를 갖게 될 것이라고 기대할 뿐, 결국 스케쥴링 자체는 OS에서 결정하므로 정확히 알 수는 없다.
    • 차라리 쓰레드의 우선순위를 부여하는 대신 작업에 우선순위를 두어 PriorityQueue에 저장해 놓고, 우선순위가 높은 작업이 먼저 처리되도록 하는 것이 나을 수 있다.

예제

package _ch12;

public class ThreadEx8 {

	public static void main(String[] args) {
		ThreadEx8_1 th1 = new ThreadEx8_1();
		ThreadEx8_2 th2 = new ThreadEx8_2();
		
		th2.setPriority(7);
		
		System.out.println("Priority of th1(-) : " + th1.getPriority());
		System.out.println("Priority of th2(|) : " + th2.getPriority());
		th1.start();
		th2.start();
	}
}

class ThreadEx8_1 extends Thread {
	public void run() {
		for (int i = 0; i < 300; i++ ) {
			System.out.print("-");
			for(int x = 0; x < 10000000; x++);
		}
	}
}

class ThreadEx8_2 extends Thread {
	public void run() {
		for (int i = 0; i < 300; i++) {
			System.out.print("|");
			for (int x = 0; x < 10000000; x++);
		}
	}
}
  • th1과 th2 모두 main()에서 생성했으므로 main 메서드를 실행하는 쓰레드의 우선순위인 5를 상속받았다.
    • th2.setPriority(7)로 th2의 우선순위를 7로 변경했다.
    • 이처럼 쓰레드를 실행하기 전에만 우선 순위를 변경할 수 있다.
  • 우선 순위가 높아지면 한 번에 작업이 끝나버릴 수 있어 아무일도 하지 않는 반복문을 추가하여 작업을 지연시켰다.
		for (int i = 0; i < 300; i++ ) {
			System.out.print("-");
			for(int x = 0; x < 10000000; x++); // 작업을 지연시키기 위한 for문
		} 

실행결과

Priority of th1(-) : 5
Priority of th2(|) : 7
-||-
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||-------------------------------------------------------------------
-------------------------------------------------------------------------
-------------------------------------------------------------------------
-------------------------------------------------------------------------
------------
  • 우선순위가 높은 th2의 실행시간이 th1에 비해 훨씬 증가한 것을 확인할 수 있다.

6. 쓰레드 그룹(thread group)

  • 쓰레드 그룹
    • 서로 관련된 쓰레드를 그룹으로 다루기 위한 개념
    • 폴더를 생성해 관련된 파일들을 함께 넣어 관리하는 것 처럼, 쓰레드 그룹을 생성해 쓰데를 그룹으로 묶어서 관리할 수 있다.
    • 폴더 안에 폴더를 생성할 수 있듯이, 쓰레드 그룹에 다른 쓰레드 그룹을 포함시킬 수 있다.
  • 쓰레드 그룹은 보안상의 이유로 도입된 개념이다.
    • 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경할 수 있지만, 다른 쓰레드 그룹의 쓰레드를 변경할 수는 없다.
  • 쓰레드를 쓰레드 그룹에 포함시키려면 Thread의 생성자를 이용해야 한다.
Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
  • 모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 한다.
    • 생성자로 쓰레드 그룹을 부여하지 않은 쓰레드는 기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 된다.
  • 자바 애플리케이션 실행 시, JVM은 main과 system이라는 쓰레드 그룹을 생성한다.
    • main()을 수행하는 main쓰레드는 main 쓰레드 그룹에 속한다.
    • 가비지 컬렉션을 수행하는 Finalizer 쓰레드는 system 그룹에 속한다.
    • 우리가 생성하는 모든 쓰레드 그룹은 main 쓰레드 그룹의 하위 쓰레드 그룹이 된다.
    • 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 자동적으로 main 쓰레드 그룹에 속하게 된다.
  • 쓰레드 그룹 관련 메서드
    • ThreadGroup getThreadGroup() : 쓰레드 자신이 속한 쓰레드 그룹을 반환한다.
    • void uncaughException(Thread t, Throwable e): 쓰레드 그룹의 쓰레드가 처리되지 않은 예외에 의해 실행이 종료되었을 때, JVM에 의해 이 메서드가 자동적으로 호출된다.

TreadEx9.java

public class ThreadEx9 {

	public static void main(String[] args) {

		ThreadGroup main = Thread.currentThread().getThreadGroup();
		ThreadGroup grp1 = new ThreadGroup("Group1");
		ThreadGroup grp2 = new ThreadGroup("Group2");

		// ThreadGroup(ThreadGroup parent, String name)
		ThreadGroup subGrp1 = new ThreadGroup(grp1, "subGroup1");

		grp1.setMaxPriority(3); // 쓰레드 그룹 grp1의 최대 우선순위를 3으로 변경

		Runnable r = new Runnable() {

			@Override
			public void run() {
				try {
					Thread.sleep(1000); // 쓰레드를 1초간 멈추게 한다.
				} catch (InterruptedException e) {
				}
			}
		};

		// Thread(ThreadGroup parent, Runnable r, String name)
		new Thread(grp1, r, "th1").start();
		new Thread(subGrp1, r, "th2").start();
		new Thread(grp2, r, "th3").start();

		System.out.println(">>List of ThreadGroup : " + main.getName() 
							+ ", Active threadGroup: "+ main.activeGroupCount() 
							+ ", Active Thread: " + main.activeCount());
		main.list();
	}
}

결과

>>List of ThreadGroup : main, Active threadGroup: 3, Active Thread: 4
java.lang.ThreadGroup[name=main,maxpri=10]
    Thread[main,5,main]
    java.lang.ThreadGroup[name=Group1,maxpri=3]
        Thread[th1,3,Group1]
        java.lang.ThreadGroup[name=subGroup1,maxpri=3]
            Thread[th2,3,subGroup1]
    java.lang.ThreadGroup[name=Group2,maxpri=10]
        Thread[th3,5,Group2]
  • 쓰레드 그룹에 포함된 하위 쓰레드 그룹이나 하위 쓰레드는 들여쓰기를 이용해 구분되게 했다.
  • 새로 생성한 모든 쓰레드 그룹은 main 쓰레드 그룹의 하위 쓰레드 그룹으로 포함되어 있다.
  • setMaxPriority()는 쓰레드가 쓰레드 그룹에 추가되기 이전에 호출되어야 한다.
  • 쓰레드 그룹 grp1의 최대 우선순위를 3으로 했기 때문에, 후에 여기에 속하게 된 쓰레드 그룹과 쓰레드가 영향을 받았다.

ThreadGroup과 Thread의 계층도

  • main (maxPriority : 10) : th0(main, (쓰레드의 우선순위 5))
    • grp1 (maxPriority: 3) : th1 (쓰레드의 우선순위 3:쓰레드 그룹에서 제한하는 최대 우선순위)
      • subgrp1 (maxPriority: 3) : th2 (쓰레드의 우선순위 3: 쓰레드 그룹에서 제한하는 최대 우선순위)
    • grp2 (maxPriorityL 10) : th3 (쓰레드의 우선순위 5: main으로부터 상속받음.)

7. 데몬 쓰레드 (daemon thread)

  • 데몬 쓰레드
    • 일반 쓰레드(non-daemon thread)의 작업을 돕는 보조적인 역할을 수행하는 쓰레드
    • 일반 쓰레드가 모두 종료되면 자동적으로 종료된다.
      • 데몬 쓰레드는 보조적인 역할을 수행하므로 일반 쓰레드가 모두 종료되고 나면 데몬 쓰레드의 존재 의미가 없기 때문이다.
    • 예: 가비지 컬렉터, 자동저장, 화면 자동 갱신
  • 무한 루프조건문을 이용하여 실행 후 대기하다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.
  • 일반 쓰레드의 작성방법과 실행방법이 동일하다.
    • 쓰레드를 생성한 다음 실행하기 전에 setDaemon(true)를 호출하기만 하면 된다.
      • setDaemon(true)는 반드시 start()호출 전에 실행되어야 한다. 그렇지 않으면 IllegalThreadStateException이 발생한다..
    • 데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다.
  • 관련 메서드
    • boolean isDaemon()
      • 쓰레드가 데몬 쓰레드인지 확인한다. 데몬 쓰레드이면 true를 반환한다.
    • void setDeaemon(boolean on)
      • 쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경한다. 매개변수 on의 값을 true로 지정하면 데몬 쓰레드가 된다.

자동 저장 예시

public class ThreadEx10 implements Runnable {
	
	static boolean autoSave = false;
	
	public static void main(String[] args) {
		Thread t = new Thread(new ThreadEx10());
		t.setDaemon(true);
		t.start();
		
		for (int i = 1; i <= 10; i++) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {}
			System.out.println(i);
			
			if (i == 5) {
				autoSave = true;
			}
		}
		
		System.out.println("프로그램을 종료합니다.");
	}

	public void run() {
		while(true) {
			try {
				Thread.sleep(3 * 1000); // 3초마다
			} catch(InterruptedException e) {}
			
			// autoSave의 값이 true이면 autoSave()를 호출한다.
			if(autoSave) {
				autoSave();
			}
		}
		
	}
	
	public void autoSave() {
		System.out.println("작업 파일이 자동 저장 되었습니다.");
	}
}
  • 5초부터 3초마다 변수 autoSave값을 확인해 그 값이 true인 경우 autoSave()를 호출하는 일을 무한히 반복하도록 쓰레드를 생성했다.
    • 만일 이 쓰레드를 데몬 쓰레드로 설정하지 않았다면, 이프로그램은 강제종료하지 않는 한 영원히 종료되지 않는다.
Thread t = new Thread(new ThreadEx10());

t.setDaemon(true); // 이 부분이 없으면 종료되지 않는다.
t.start();

데몬 쓰레드 조회 예시

package _ch12;

import java.util.Iterator;
import java.util.Map;

public class TreadEx11 {
	
	public static void main(String[] args) {
		ThreadEx11_1 t1 = new ThreadEx11_1("Thread1");
		ThreadEx11_2 t2 = new ThreadEx11_2("Thread2");
		t1.start();
		t2.start();
	}

}

class ThreadEx11_1 extends Thread {
	ThreadEx11_1(String name) {
		super(name);
	}
	
	public void run() {
		try {
			sleep(5 * 1000); // 5초동안 기다린다.
		} catch(InterruptedException e) {}
	}
}

class ThreadEx11_2 extends Thread {
	ThreadEx11_2(String name) {
		super(name);
	}
	
	public void run() {
		Map map = getAllStackTraces();
		Iterator it = map.keySet().iterator();
		
		int x = 0;
		while(it.hasNext()) {
			Object obj = it.next();
			Thread t = (Thread) obj;
			StackTraceElement[] ste = (StackTraceElement[]) (map.get(obj));
			
			System.out.println("[" + ++x + "] name: " + t.getName()
						+ ", group :" + t.getThreadGroup().getName()
						+ ", daemon : " + t.isDaemon());
			
			for (int i = 0; i < ste.length; i++) {
				System.out.println(ste[i]);
			}
			
			System.out.println();
		}
	}
}

결과

[1] name: DestroyJavaVM, group :main, daemon : false

[2] name: Attach Listener, group :system, daemon : true

[3] name: Signal Dispatcher, group :system, daemon : true

[4] name: Thread1, group :main, daemon : false

[5] name: Thread2, group :main, daemon : false
java.base@17.0.7/java.lang.Thread.dumpThreads(Native Method)
java.base@17.0.7/java.lang.Thread.getAllStackTraces(Thread.java:1662)
app//_ch12.ThreadEx11_2.run(TreadEx11.java:35)

[6] name: Common-Cleaner, group :InnocuousThreadGroup, daemon : true
java.base@17.0.7/java.lang.Object.wait(Native Method)
java.base@17.0.7/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:155)
java.base@17.0.7/jdk.internal.ref.CleanerImpl.run(CleanerImpl.java:140)
java.base@17.0.7/java.lang.Thread.run(Thread.java:833)
java.base@17.0.7/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:162)

[7] name: Notification Thread, group :system, daemon : true

[8] name: Reference Handler, group :system, daemon : true
java.base@17.0.7/java.lang.ref.Reference.waitForReferencePendingList(Native Method)
java.base@17.0.7/java.lang.ref.Reference.processPendingReferences(Reference.java:253)
java.base@17.0.7/java.lang.ref.Reference$ReferenceHandler.run(Reference.java:215)

[9] name: Finalizer, group :system, daemon : true
java.base@17.0.7/java.lang.Object.wait(Native Method)
java.base@17.0.7/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:155)
java.base@17.0.7/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:176)
java.base@17.0.7/java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:172)
  • getAllStackTraces()
    • 실행 중 또는 대기상태, 즉 작업이 완료되지 않은 모든 쓰레드의 호출스택을 출력할 수 있다.
    • 새로 생성한 Thread1, Thread 를 포함해 다양한 쓰레드가 실행중, 혹은 대기상태에 있다.
  • 프로그램 실행 시, JVM은 가비지컬렉션, 이벤트처리, 그래픽 처리와 같이 프로그램이 실행되는데 필요한 보조작업을 수행하는 데몬 쓰레드들을 자동으로 생성해 실행시킨다.
    • 이들은 'system 쓰레드 그룹' 또는 'main 쓰레드 그룹'에 속한다.
    • AWT나 Swing과 같이 GUI를 가진 프로그램을 실행하는 경우엔느 이벤트와 그래픽 처리를 위해 더 많은 수의 데몬 쓰레드가 생성된다.

8. 쓰레드의 실행제어

쓰레드의 상태

  • NEW: 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
  • RUNNABLE: 실행 중 또는 실행 가능한 상태
  • BLOCKED: 동기화블럭에 의해서 일시정지된 상태(lock이 풀릴 때까지 기다리는 상태)
  • WAITING, TIMED_WAITING
    • 쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은(unrrunnable) 일시정지상태
    • TIMED_WAITING은 일시정지 시간이 지정된 경우를 의미
  • TERMINATED: 쓰레드의 작업이 종료된 상태

쓰레드의 상태 변화

  1. 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행되기열에 저장되어 자신의 차례가 될때까지 기다려야 한다. 실행대기열은 큐(Queue)와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행된다.
    • start()를 호출하면 실행대기(RUNNABLE)상태가 된다. 먼저온 쓰레드 뒤에 줄을 선다.
  2. 실행대기상태에 있다가 자신의 차례가 되면 실행상태가 된다.
  3. 주어진 실행시간이 다 되거나 yield()를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 된다.
  4. 실행중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지 상태가 될 수 있다.
    • I/O block은 입출력작업에서 발생하는 지연상태를 말한다. 사용자의 입력을 기다리는 경우를 예로 들 수 있는데, 이런 경우 일시정지 상태에 있다가 사용자가 입력을 마치면 다시 실행대기 상태가 된다.
  5. 지정된 일시정지 시간이 다 되거나(time-out), notify(), resume(), interrupted()가 호출되면 일시정지 상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 된다.
  6. 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸된다.

쓰레드를 생성해 start()를 호출하면 줄을 서고, 자기 차례가 되면 실행되고, 시간이 끝나면 다시 줄을 서는 것을 반복한다. 그러다가 자신의 작업이 종료되면 소멸된다. 이게 기본 프로세스이고, 중간중간 스레드가 멈추는 경우가 있는데, suspend, sleep, wait, join, I/O block으로 일시정지 상태가 될 수 있다.

설명을 위해 1에서 6까지 번호를 붙이긴 했지만 번호의 순서대로 쓰레드가 수행되는 것은 아니다.

쓰레드 실행제어 메서드

쓰레드의 실행을 제어할 수 있는 메서드가 제공된다. 이들을 활용해서 효율적인 프로그램을 작성할 수 있다.

  • static void sleep(long millis)
  • static void sleep(long millis, int nanos)
    • 지정된 시간(천분의 일초 단위)동안 쓰레드를 일시정지시킨다. 지정한 시간이 지나고 나면, 자동적으로 다시 실행 대기 상태가 된다.
  • void join() : 다른 쓰레드 기다리기
  • void join(long millis)
  • void join(long millis, int nanos)
    • 지정된 시간 동안 쓰레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다.
  • void interrupt(): 자거나 기다리는 쓰레드 깨우기
    • sleep()이나 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기 상태로 만든다. 해당 쓰레드에서는 InterruptedException이 발생함으로써 일시 정지 상태를 벗어나게 된다.
  • void stop()
    • 쓰레드를 즉시 종료시킨다.
  • void suspend() 일시정지
    • 쓰레드를 일시정지시킨다. resume()을 호출하면 다시 실행 대기 상태가 된다.
  • void resume() 재개
    • suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기상태로 만든다.
  • static void yield() 양보
    • 실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보(yield)하고 자신은 실행 대기 상태가 된다.

static이 붙은 메서드들은 쓰레드 자기 자신에게만 호출이 가능하다.

resume(), stop(), suspend()는 쓰레드를 교착상태(dead-lock)로 만들기 쉽기 때문에 deprecated 되었다.

sleep(long millis)

  • 현재 쓰레드를 지정된 시간동안 멈추게 한다.
    • static 메서드로 쓰레드 자기 자신에게만 적용 가능하다. (yield와 함께)
      • 특정 쓰레드를 지정해서 멈추게하는 것은 불가능하다.
static void sleep(long millis) // 천분의 일초 단위
static void sleep(long millis, int nanos) // 천분의 일초 + 나노초
  • 예외처리를 해야 한다. (InterruptedException이 발생하면 깨어난다)
    • tiem-up(시간종료)나 누군가 깨워 InterruptedException 예외를 발생시키면 깨어난다.
try {
	Thread.slee(1, 500000); // 쓰레드를 0.0015초동안 멈추게 한다.
} catch(InterruptedException e) {}
  • 매번 예외처리하는 것이 번거로워 try-catch문 까지 포함하는 새로운 메서드를 만들어 사용하기도 한다.
void delay(long millis) {
	try {
    	Thread.sleep(millis);
    } catch(InterruptedException e) {}

예제

public class ThreaEx12 {

	public static void main(String[] args) {
		ThreadEX12_1 th1 = new ThreadEX12_1();
		ThreadEx12_2 th2 = new ThreadEx12_2();
		th1.start();
		th2.start();

		try {
			th1.sleep(2000);
		} catch (InterruptedException e) {
		}
		
		System.out.print("<<main 종료>>");
	} // main
}

class ThreadEX12_1 extends Thread {
	public void run() {
		for (int i = 0; i < 300; i++) {
			System.out.print("-");
		}
		System.out.print("<<th1 종료>>");
	} // run()
}

class ThreadEx12_2 extends Thread {
	public void run() {
		for (int i = 0; i < 300; i++) {
			System.out.print("|");
		}
		System.out.print("<<th2 종료>>");
	} // run()
}

결과

||-----------||||--------------------------------------------------
|||||||||||||||||||||||||||||||||||||||--|||||||||||||||||||||-----------
-------------------------------------------------------------------------
-----------------------------------------||||||||||||||||||||||--------
||---------------------
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||------------------------|||||||||||||||----------------
-------------------||||||||||||------||-----|||||||||||||-------------
<<th1 종료>>|||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||<<th2 종료>><<main 종료>>
  • th1.sleep(2000); 코드를 실행했음에도 th1이 먼저 종료되었다.
    • 그 이유는 sleep()이 항상 현재 실행중인 쓰레드에 작동하기 때문에 th1.sleep(2000);와 같은 형태로 호출하였어도 실제로 영향을 받는 것은 main메서드를 실행하는 main 쓰레드이기 때문이다.
  • 그래서 sleep()은 static으로 선언되어 있으며 참조변수를 이용해 호출하기 보다는 Thread.sleep(2000);과 같이 호출되어야 한다.
    • yield()의 경우에도 동일하다.

interrupt() & interrupted()

  • interrupt()
    • 쓰레드에게 작업을 멈추라고 요청하는 것.
      • 대기상태(WAITING)인 쓰레드를 실행대기 상태(RUNNABLE)로 만든다.
    • 진행 중인 쓰레드의 작업이 끝나기 전에 취소시켜야 하는 경우 사용한다.
      • ex. 큰 파일을 다운로드 받을 때 시간이 너무 오래 걸리면 중간에 다운로드를 포기하고 취소할 수 있다.
      • 파일 다운로드를 시행하다가 취소버튼을 클릭하면 interrupt()를 실행할 수 있다.
    • 쓰레드의 인스턴스 변수인 interrupted상태(인스턴스 변수)를 변경하는 것이다.
      • 쓰레드를 강제로 종료시키는 것이 아니다.
void interrupt() 쓰레드의 interrupted 상태를 false에서 true로 변경.
boolean isInterrupted() 쓰레드의 interrupted 상태를 반환.
static boolean interrupted() 현재 쓰레드의 interrupted 상태를 알려주고, false로 초기화
  • interrupted()
    • 쓰레드에 대해 interrupt()가 호출되었는지 알려준다.
      • interrupt()가 호출된 경우 true, 호출되지 않은 경우 false를 반환한다.
      • 다시 interrupt()가 실행될 수 있도록 다시 false로 초가화하는 기능을 수행한다.
Thead th = new Thread();
th.start();
...
th.interrupt(); // 쓰레드 th에 interrupt()를 호출한다.

class MyThread extends Thread {
	public void run() {
    	while(!interrupted()) { // interrupted()의 결과가 false인 동안 반복
        	...
        }
    }
}

예제: for 루프 지연

public class ThreadEx13 {

	public static void main(String[] args) throws Exception {
		
		ThreadEx13_1 th1 = new ThreadEx13_1();
		th1.start();
		
		String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
		System.out.println("입력하신 값은 " + input + "입니다.");
		th1.interrupt(); // interrupt()를 호출하면, interrupted 상태가 true가 된다.
		System.out.println("isInterrupted(): " + th1.isInterrupted()); // true
	}
}

class ThreadEx13_1 extends Thread {
	public void run() {
		int i = 10;
		
		while(i != 0 && !isInterrupted()) {
			System.out.println(i--);
			int count = 0;
			for (long x = 0; x < 2500000000L; x++) {
				count += x;// 시간 지연
			}
		}
		
		System.out.println("카운트가 종료되었습니다.");
	}
}
  • 카운트 다운 도중에 사용자의 입력이 들어오면 카운트다운을 종료한다.
    • for문을 통해 카운트를 지연시켰다.

결과

10
9
8
7
입력하신 값은 123456입니다.
isInterrupted(): true
카운트가 종료되었습니다.

예제: Thread.sleep 지연

public class ThreadEx14 {
	
	public static void main(String[] args) throws Exception {
		ThreadEx14_1 th1 = new ThreadEx14_1();
		th1.start();
		
		String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
		System.out.println("입력하신 값은 " + input + "입니다.");
		th1.interrupt(); // interrupt()를 호출하면, interrupted 상태가 true가 된다.
		System.out.println("isInterrupted(): " + th1.isInterrupted());
	}

}

class ThreadEx14_1 extends Thread {
	public void run() {
		int i = 10;
		
		while(i != 0 && !isInterrupted()) {
			System.out.println(i--);
			try {
				Thread.sleep(1000); // 1초 지연
			} catch(InterruptedException e) {}
		}
		
		System.out.println("카운트가 종료되었습니다.");
	}
}

결과

10
9
8
입력하신 값은 123456789입니다.
isInterrupted(): flase // <- true일때도 있음
7
6
5
4
3
2
1
카운트가 종료되었습니다.
  • for문 대신 Thread.sleep(1000);로 1초동안 지연되도록 변경하자 카운트가 종료되지 않았다.
    • Thread.sleep(1000);에서 InterruptedException이 발생했기 때문이다.
  • sleep()에 의해 쓰레드가 잠시 멈춰있을 때, interrupt()를 호출하면 InterruptedException이 발생되고 쓰레드의 interrupted 상태는 false로 자동 초기화된다.
    • 그런 경우 catch 블럭에 interrupted()를 추가로 넣어줘서 쓰레드의 interrupted상태를 true로 다시 바꿔줘야 한다.

suspend(), resume(), stop()

  • suspend, resume, stop읜 교착상태에 빠지기 쉬워서 deprecated 되었다.
    • suspend(): 쓰레드를 일시정지시킨다.
      • 이 메서드를 호출한 쓰레드가 공유 객체를 잠궈두고 일시정지 상태가 되면 다른 쓰레드들이 그 객체를 사용할 수 없어 프로그램이 무한 대기 상태에 빠질 수 있다. 이 상태를 데드락(Deadlock)이라고 한다.
    • resume(): suspend()에 의해 일시정지된 쓰레드를 실행대기상태로 만든다.
      • suspend()보다 먼저 호출되면 쓰레드는 영원히 실행 가능 상태가 되지 않는다.
    • stop(): 쓰레드를 즉시 종료시킨다.
      • 쓰레드가 처리하던 작업이 중간에 끊기게 되면 객체들이 일관성 없는 상태로 남을 수 있다.
  • 위와 같은 문제를 해결하기 위해 별도의 인스턴스 변수를 만들어 쓰레드를 제어하는 방식이 권장된다. 이렇게 하면 쓰레드가 실행 중인지, 일시정지 상태인지, 종료해야 하는지를 안전하게 제어할 수 있다.
    • 또한 이러한 방식을 사용하는 경우 쓰레드가 공유하는 객체의 상태를 안전하게 유지할 수 있다.

예제

public class ThreadEx15 {
	
	public static void main(String[] args) {

		RunImplEx15 r = new RunImplEx15();
		Thread th1 = new Thread(r, "*");
		Thread th2 = new Thread(r, "**");
		Thread th3 = new Thread(r, "***");
		th1.start();
		th2.start();
		th3.start();
		
		try {
			Thread.sleep(2000);
			th1.suspend(); // 쓰레드 th1을 잠시 중단시킨다.
			Thread.sleep(2000);;
			th2.suspend();
			Thread.sleep(3000);
			th1.resume(); // 쓰레드 th1이 다시 동작하도록 한다.
			Thread.sleep(3000);
			th1.stop(); // 쓰레드 th1을 강제종료시킨다.
			th2.stop();
			Thread.sleep(2000);
			th3.stop();
		} catch (InterruptedException e) {};
	}
}

class RunImplEx15 implements Runnable {
		public void run() {
			while(true) {
				System.out.println(Thread.currentThread().getName());
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {}
			}
		} //run()
}

결과

***
**
*
***
**
*
*** // th1이 suspend되었다.
**
***
**
***
***
***
* // th1이 resume되었다.
***
*
***
* // th1이 stop되었다.
***
***
***

예제

package _ch12;

public class ThreadEx16 {
	
	public static void main(String[] args) {
		RunImplEx16 r1 = new RunImplEx16();
		RunImplEx16 r2 = new RunImplEx16();
		RunImplEx16 r3 = new RunImplEx16();
		
		Thread th1 = new Thread(r1, "*");
		Thread th2 = new Thread(r2, "**");
		Thread th3 = new Thread(r3, "***");
		
		th1.start();
		th2.start();
		th3.start();
		
		try {
			Thread.sleep(2000);
			r1.suspend();
			Thread.sleep(2000);
			r2.suspend();
			Thread.sleep(3000);
			r1.resume();
			Thread.sleep(3000);
			r1.stop();
			r2.stop();
			Thread.sleep(2000);
			r3.stop();
		} catch (InterruptedException e) {}
	}

}

class RunImplEx16 implements Runnable {
	boolean suspended = false;
	boolean stopped = false;
	
	public void run() {
		while(!stopped) {
			if(!suspended) {
				System.out.println(Thread.currentThread().getName());
				try {
					Thread.sleep(1000);
				} catch(InterruptedException e) {}
			}
		}
		System.out.println(Thread.currentThread().getName() + " - stopped");
	}
	
	public void suspend() {
		suspended = true;
	}
	
	public void resume() {
		suspended = false;
	}
	
	public void stop() {
		stopped = true;
	}
	
}
  • stoppedsuspended라는 boolean 타입의 두 변수를 인스턴스 변수로 선언하고, 이 변수를 사용해 반복문과 조건문의 조건식으로 사용한다.
    • 이 변수의 값을 변경함으로써 쓰레드 작업의 중지 및 재개, 종료가 가능하다.
  • 만일 예제가 잘 작동하지 않는다면 suspended와 stopped의 선언문 앞에 volatile을 붙이자.
volatile boolean suspedned = false;
volatile boolean stopped = false;
  • 위의 코드를 더 객체지향적으로 변경하면 아래와 같다.
package _ch12;

public class ThreadEx17 {
	
	public static void main(String[] args) {
		ThreadEx17_1 th1 = new ThreadEx17_1("*");
		ThreadEx17_1 th2 = new ThreadEx17_1("**");
		ThreadEx17_1 th3 = new ThreadEx17_1("***");
		th1.start();
		th2.start();
		th3.start();
		
		try {
			Thread.sleep(2000);
			th1.suspend();
			Thread.sleep(2000);
			th2.suspend();
			Thread.sleep(3000);
			th1.resume();
			Thread.sleep(3000);
			th1.stop();
			th2.stop();
			Thread.sleep(2000);
			th3.stop();
		} catch (InterruptedException e) {}
	}	
}

class ThreadEx17_1 implements Runnable {
	boolean suspended = false;
	boolean stopped = false;
	
	Thread th;
	
	ThreadEx17_1(String name) {
		th = new Thread(this, name); // Thread(Runnable r, String name)
	}
	
	public void run() {
		while(!stopped) {
			if(!suspended) {
				System.out.println(Thread.currentThread().getName());
				try {
					Thread.sleep(1000);
				}  catch (InterruptedException e) {
				}
			}
		}
		System.out.println(Thread.currentThread().getName() + " - stopped");
	}
	
	public void suspend() { 
		suspended = true;
	}
	
	public void resume() {
		suspended = false;
	}
	
	public void stop() {
		stopped = true;
	}
	
	public void start() {
		th.start();
	}
}

결과

*
**
***
**
*
***
**
***
**
***
**
***
***
***
*
***
*
***
*
***
** - stopped
* - stopped
***
***
*** - stopped

join

  • 지정된 시간동안 특정 쓰레드가 작업하는 것을 기다린다.
    • 작업 중에 다른 쓰레드의 작업이 먼저 수행되어야 할 필요가 있을때 join()을 사용한다.
    • 시간을 지정하지 않으면 해당 쓰레드가 작업을 모두 마칠때까지 기다리게 된다.
void join() // 작업이 모두 끝날때까지
void join(long millis) // 천분의 일초 동안
void join(long millis, int nanos) // 천분의 일초 + 나노초 동안
  • 예외처리를 해야한다. (InterruptedException이 발생 시 작업 재개)
try {
	th1.join(); // 현재 실행중인 쓰레드가 쓰레드 th1의 작업이 끝날때까지 기다린다.
} catch (InterruptedException e) {
}

예제

public class ThreadEx19 {
	static long startTime = 0;
	
	public static void main(String[] args) {
		
		ThreadEx19_1 th1 = new ThreadEx19_1();
		ThreadEx19_2 th2 = new ThreadEx19_2();
		
		th1.start();
		th2.start();
		startTime = System.currentTimeMillis();
		
		try {
			th1.join(); // main 쓰레드가 th1의 작업이 끝날때까지 기다린다.
			th2.join(); // main 쓰레드가 th2의 작업이 끝날때까지 기다린다.
		} catch (InterruptedException e) {
		}
		
		System.out.println("소요시간: " + (System.currentTimeMillis() - ThreadEx19.startTime));
	}

}

class ThreadEx19_1 extends Thread {
	public void run() {
		for (int i = 0; i < 300; i++) {
			System.out.print(new String("-"));
		}
	} // run()
}

class ThreadEx19_2 extends Thread {
	public void run() {
		for (int i = 0; i < 300; i++)  {
			System.out.print(new String("|"));
		}
	} // run()
}
  • join()으로 쓰레드 th1과 th2의 작업이 마칠때까지 main쓰레드가 기다리도록 했다.
    • 그래서 main 쓰레드가 두 쓰레드의 작업에 소요된 시간을 출력할 수 있다.

예제: 가비지 컬렉터

public class ThreadEx20 {

	public static void main(String[] args) {
		ThreadEx20_1 gc = new ThreadEx20_1();
		gc.setDaemon(true);
		gc.start();
		
		int requiredMemory = 0;
		
		for (int i = 0; i < 20; i++) {
			requiredMemory = (int)(Math.random() * 10) * 20;
			
			// 필요한 메모리가 사용할 수 있는 양보다 크거나 전체 메모리의 60% 이상을
			// 사용했을 경우 gc를 깨운다.
			if (gc.freeMemory() < requiredMemory 
					|| gc.freeMemory() < gc.totalMemory() * 0.4) {
				gc.interrupt(); // 잠자고 있는 쓰레드 gc를 깨운다.
			}
			
			gc.usedMemory += requiredMemory;
			System.out.println("usedMemory: " + gc.usedMemory);
				
		}
	}
}

class ThreadEx20_1 extends Thread {
	final static int MAX_MEMORY = 1000;
	int usedMemory = 0;
	
	public void run() {
		while(true) {
			try {
				Thread.sleep(10 * 1000); // 10초를 기다린다.
			} catch (InterruptedException e) {
				System.out.println("Awaken by interrupt().");
			}
			
			gc(); // garbage collection을 수행한다.
			System.out.println("Garbage Collected. Free Memory :" + freeMemory());
		}
	}
	
	public void gc() {
		usedMemory -= 300;
		if(usedMemory < 0) { 
			usedMemory = 0;
		}
	}
	
	public int totalMemory() {
		return MAX_MEMORY;
	}
	
	public int freeMemory() {
		return MAX_MEMORY - usedMemory;
	}
}
  • JVM의 가비지 컬렉터(garbage colelctor)를 흉내내어 간단히 구현해본 예제이다.
    • random()을 사용했기 때문에 실행할 때마다 결과가 다를 수 있다.
  • sleep()을 이용해 10초마다 한 번씩 가비지 컬렉션을 수행하는 쓰레드를 만든 다음, 쓰레드를 생성해 데몬 쓰레드로 설정했다.
  • 반복문을 사용해 메모리의 양을 계속 감소시키도록 했고, 반복문마다 if문을호출해 메모리의 남은양을 확인하고 가비지 컬렉터 쓰레드를 깨워 gc()를 수행했다.

결과

usedMemory: 160
usedMemory: 260
usedMemory: 360
usedMemory: 440
usedMemory: 620
usedMemory: 700
usedMemory: 780
usedMemory: 860
usedMemory: 920
usedMemory: 980
usedMemory: 1040
usedMemory: 1140
usedMemory: 1180
usedMemory: 1340
usedMemory: 1360
usedMemory: 1480
usedMemory: 1640
usedMemory: 1640
usedMemory: 1760
usedMemory: 1820
Awaken by interrupt().
Garbage Collected. Free Memory :-520
  • 문제상황: MAX_MEMORY가 1000임에도 불구하고 중간에 값이 1000을 넘는 경우가 발생한다.
    • 이는 gc()쓰레드가 interupt()에 의해 깨어난 이후 gc() 수행 이전 main쓰레드의 작업이 수행되어 메모리를 사용(gc.usedMemory += requiredMemory;)했기 때문이다.
    • 그래서 join()을 호출해 쓰레드 gc가 작업할 시간을 어느 정도 주고 main쓰레드가 기다리도록 해서, 사용할 수 있는 메모리가 확보된 다음 작업을 계속하는 것이 필요하다.
if(gc.freeMemory() < requiredMemory...
	gc.interrupt();
    try {
    	gc.join(100);
	} catch(InterruptedException e) {}
}
  • 가비지 컬렉터와 같은 데몬 쓰레드의 우선순위를 낮추기 보다는 sleep()을 이용해서 주기적으로 실행되도록 하다가 필요할 때마다 interrupt()를 호출해서 즉시 가비지 컬렉션이 이루어지도록 하는 것이 좋다.
    • 필요하다면 join()도 함께 사용해야 한다는 것을 기억하자.

yield()

  • 남은 시간을 다음 쓰레드에게 양보하고, 자신(현재 쓰레드)은 실행대기한다.
    • 특정 쓰레드가 작업이 이미 끝났거나, 작업을 할 수 없는 상황일 때 남은 시간을 다른 쓰레드에게 양보 할 수 있다.
      • 할당받은 5초의 시간 중 3초만 작업하고 나머지 2초는 다른 쓰레드에게 양보
    • 단, 양보라는 개념이 OS 스케줄러에게 통보하는 방식이다. 반드시 yield()가 발생한다는 보장은 없다. (위에서 언급한 것처럼 초 개념이 명확히 나뉘는게 아니다.)
  • yield()interrupt()를 적절히 사용하면, 응답성과 효율을 높일 수 있다.

  • suspend인 경우, 즉 잠시 실행을 멈추게 한 상태라면, 쓰레드는 주어진 실행 시간을 그저 while문을 의미없이 돌면서 낭비하게 된다. 이런 상황을 '바쁜 대기상태(busy-waiting)'이라고 한다.
    • 이와 같은 경우에 yield()를 호출해 남은 실행시간을 while문에서 낭비하지 않고 다른 쓰레드에게 양보(yield)할 수 있다.
while(!stopped) { // true
	if(!suspended) { // false
    	...
        try {
        	Thread.sleep(1000);
        } catch(InterruptedException e) {}
    } // if문이 생략되고 의미없는 while문의 수행이 반복된다.
} 

while(!stopped) { // true
	if(!suspended) { // false
    	...
        try {
        	Thread.sleep(1000);
        } catch(InterruptedException e) {}
    } else { // suspend인 경우 다른 쓰레드에게 실행을 양보한다.
    	Thread.yield();
    }
} 
  • suspend()stop()interrupt()호출 코드를 추가한다.
    • 만일 stop()이 호출되었을 때 Thread.sleep(1000)에 의해 쓰레드가 일시정지 상태에 머물러있는 상황이라면, stopped의 값이 true로 바뀌었어도 쓰레드가 정지될 때까지 최대 1초의 시간지연이 생기게 된다.
      • 그러나 같은 상황에서 interrupt()를 호출하면, sleep()에서 InterruptedException이 발생해 즉시 일시정지 상태에서 벗어나게 되므로 응답성이 좋아진다.
public void suspend() {
	suspended = true;
    th.interrupt();
}

public void stop() {
	stopped = true;
    th.interrupt();
}

예제

public class ThreadEx18 {
	
	public static void main(String[] args) {

		ThreadEx18_1 th1 = new ThreadEx18_1("*");
		ThreadEx18_1 th2 = new ThreadEx18_1("**");
		ThreadEx18_1 th3 = new ThreadEx18_1("***");
		th1.start();
		th2.start();
		th3.start();
		
		try {
			Thread.sleep(2000);
			th1.suspend();
			Thread.sleep(2000);
			th2.suspend();
			Thread.sleep(3000);
			th1.resume();
			Thread.sleep(3000);
			th1.stop();
			th2.stop();
			Thread.sleep(2000);
			th3.stop();
		} catch(InterruptedException e) {}
	}

}

class ThreadEx18_1 implements Runnable {
	boolean suspended = false;
	boolean stopped = false;
	
	Thread th;
	
	ThreadEx18_1(String name) {
		th = new Thread(this, name); // Thread(Runnable r, String name)
	}
	
	public void run() {
		String name = th.getName();
		
		while(!stopped) {
			if(!suspended) {
				System.out.println(name);
				try {
					Thread.sleep(1000);
				} catch(InterruptedException e) {
					System.out.println(name + " - interuupted");
				}
			} else {
				Thread.yield();
			}
		}
		System.out.println(name + " - stopped");
	}
	
	public void suspend() {
		suspended = true;
		th.interrupt();
		System.out.println(th.getName() + " - interrupted() by suspended()");
	}
	
	public void stop() {
		stopped = true;
		th.interrupt();
		System.out.println(th.getName() + " - interrupted() by stop()");
	}
	
	public void resume() {
		suspended = false;
	}
	
	public void start() {
		th.start();
	}
	
}

9. 쓰레드의 동기화

  • 멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유하기 때문에 서로의 작업에 영향을 주게 된다.
    • 쓰레드 A가 작업하던 도중에 다른 쓰레드 B에게 제어권이 넘어가고, 쓰레드 A가 작업하던 공유 데이터를 쓰레드 B가 임의로 변경했다면, 다시 쓰레드 A가 제어권을 받아 나머지 작업을 마쳤을 때 원래 의도했던 것과는 다른 결과를 얻을 수 있다.
  • 이와 같은 일을 방지하기 위해 한 쓰레드가 특정 작업을 끝마치기 전까지 다른쓰레드에 의해 방해받지 않도록 하는 것이 필요하다.
    • 이를 위해 도입된 개념이 임계영역(critical section)잠금(락, lock)이다.
  • 쓰레드의 동기화(synchronization)
    • 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것
    • 공유 데이터를 사용하는 코드 영역을 임계영역으로 정해놓고, 공유 데이터(객체)가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다.
    • 해당 쓰레드가 임계영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행
    • 자바에서는 synchronized 블럭을 이용해 쓰레드의 동기화를 지원했지만, JDK 1.5부터 java.util.concurrent.locskjava.util.concurrent.atomic 패키지를 통해 다양한 방식으로 동기화 구현을 지원한다.

1) synchronized를 이용한 동기화

  • synchronized로 임계영역(lock이 걸리는 영역)을 설정하는 방법 2가지
    • 두 방법 모두 lock의 획득과 반납이 자동적으로 이루어지므로 우리가 해야할 일은 그저 임계 영역만 설정하는 것이다.
    • 한번에 한개의 쓰레드만 입장 가능하므로 최소화하는 것이 좋다.
  • 모든 객체는 lock을 하나씩 가지고 있으며, 해당 객체의 lock을 가지고 있는 쓰레드만 임계 영역의 코드를 수행할 수 있다.
    • 다른 쓰레드들은 lock을 얻을 때까지 기다리게 된다.

1. 메서드 전체를 임계영역으로 지정

public synchronized void calcSum() {
	// ...
}
  • synchronized 메서드가 호출된 시점부터 해당 메서드가 포함된 객체의 lock을 얻어 작업을 수행하다가 메서드가 종료되면 lock을 반납한다.

2. 특정한 영역을 임계영역으로 지정

synchronized(객체의 참조변수) {
	// ...
}
  • 메서드 내의 코드 일부를 블럭{}으로 감싸고 블럭 앞에 synchronized(참조변수)를 붙인다.
    • 이때 참조변수는 락을 걸고자 하는 객체를 참조하는 것이어야 한다.
  • 이 블럭을 synchronized 블럭이라고 부르며, 이 블럭의 영역 안으로 들어가면서부터 쓰레드는 지정된 객체의 lock을 얻게되고, 이 블럭을 벗어나면 lock을 반납한다.

3. 출금 기능

public class ThreadEx21 {
	
	public static void main(String[] args) {
		Runnable r = new RunnableEx21();
		new Thread(r).start(); // TreadGroup에 의해 참조되므로 gc 대상이 아니다.
		new Thread(r).start(); // TreadGroup에 의해 참조되므로 gc 대상이 아니다.
	}

}

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

class RunnableEx21 implements Runnable {
	Account acc = new Account();
	
	public void run() {
		while(acc.getBalance() > 0) {
			// 100, 200, 300중의 한 값을 임의로 선택해서 출금(withdraw)
			int money = (int)(Math.random() * 3 + 1) * 100;
			acc.withdraw(money);
			System.out.println("balance: " + acc.getBalance());
		}
	}
}

결과

balance: 500
balance: 500
balance: 200
balance: 200
balance: -100
balance: -200
  • 잔고가 출금하려는 금액보다 더 큰 경우에만 출금(withdraw)이 가능하도록 코드를 작성했다. 그럼에도 불구하고 실행 결과에서 잔고(balance)가 음수인 것을 확인 가능하다.
    • 이는 조건식 balance >=money 이 true여서 출금(balance -= money)을 진행하려던 순간 다른 쓰레드에게 제어권이 넘어가 다른 쓰레드가 300을 출금해 발생한 일이다.
    • 따라서 잔고를 확인하는 if문과 출금하는 문장은 하나의 임계영역으로 묶여져야 한다.
  • 예제에서는 상황을 보여주기 위해 일부로 Thread.sleep(1000)을 사용해 if문을 통과하자마자 다른 쓰레드에게 제어권을 넘기도록 했지만, 굳이 이렇게 하지 않더라도 이처럼 쓰레드의 작업이 다른 쓰레드에 의해 영향을 받는 일이 발생할 수 있다.
    • 따라서 동기화는 반드시 필요하다.
    • 단순히 witdraw 메서드에 synchronized키워드를 붙이기만 하면된다.
	public synchronized void withdraw(int money) {
		if (balance >= money) {
			try {
				Thread.sleep(1000);
			} catch(InterruptedException e) {}
			
			balance -= money;
		}
	} 
    
    public void withdraw(int money) {
    	synchronized(this) {
            if (balance >= money) {
                try {
                    Thread.sleep(1000);
                } catch(InterruptedException e) {}

                balance -= money;
            }
        }
	} // withdraw
  • 한 쓰레드에 의해서 먼저 withdraw()가 호출되면 이 메서드가 종료되어 lock이 반납될 때까지 다른 쓰레드는 withdraw()를 호출하더라도 대기상태에 ㅓ물게 된다.
  • 이때 balance의 접근제어자가 반드시 private여야 한다. private가 아닌 경우 외부에서 직접 접근할 수 있어 아무리 동기화를 해도 이 값의 변경을 막을 길이 없다.
    • synchronized를 이용한 동기화는 지정된 영역의 코드를 한번에 하나의 쓰레드가 수행하는 것을 보장하는 것일 뿐이기 때문이다.

synchronized를 적용한 결과

balance: 800
balance: 600
balance: 500
balance: 400
balance: 100
balance: 0
balance: 0

2) wait()과 notify()

  • synchronized 동기화의 문제점
    • synchronized로 동기화해서 공유 데이터를 보유하는 경우 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는 것이 중요하다.
      • 한 쓰레드가 락을 보유한채로 돈이 입금될 때까지 시간을 보낸다면, 다른 쓰레드들은 모두 해당 객체의 락을 기다리느라 다른 작업들도 원활히 진행되지 않을 것이다.
  • wait(), notify(), notifyAll()
    • 동기화의 효율을 높이기 위해 사용할 수 있다.
    • (특정 객체에 대한 것이므로) Object 클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.
    • wait(): 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다.
    • notify(): waiting pool에서 대기중인 쓰레드 중의 하나를 깨운다.
    • notifyAll(): (특정 객체의) waiting pool에서 대기중인 모든 쓰레드를 깨운다.
class Account {
	int balance = 1000;
    
    public synchronized void withdraw(int money) {
    	while(balance < money) {
        	try {
            	wait(); // 대기 - 락을 풀고 기다린다. 통지를 받으면 락을 재획득(ReEnterance)
            } catch(InterruptedException e) {}
        }
        
        balacne -= money;
    } // withdraw
    
    public synchronized void deposit(int money) {
    	balance += money;
        notify(); // 통지 - 대기중인 쓰레드 중 하나에게 알림.
    } 
    
}    

1. 식당 예제

  • 다음은 식당의 요리사, 손님, 테이블을 쓰레드로 구현한 예제이다.
    • Table : dishes라는 ArrayList를 가진다. (ArrayList는 동기화가 되어있지 않다.)
      • add(): Table에 음식(dish)를 추가하는 메서드.
      • remove(): Table에 음식({dish)을 제거하는 메서드
    • Cook: Table에 음식(dish)을 추가하는 일을 한다.
    • Customer: Table에 음식(dish)을 먹는 일을 한다.
  • 요리사와 손님이 같은 객체(Table)을 공유하고 있으므로 동기화가 필요하다.
public class ThreadWaitEx1 {
	public static void main(String[] args) throws Exception{
		Table table = new Table(); // 여러 쓰레드가 공유하는 객체
		
		new Thread(new Cook(table), "COOK1").start();
		new Thread(new Customer(table, "donut"), "CUST1").start();
		new Thread(new Customer(table, "burger"), "CUST2").start();
		
		Thread.sleep(100); // 0.1초(100밀리 세컨드) 후에 강제종료 시킨다.
		System.exit(0); // 프로그램 전체 종료 (모든 쓰레드가 종료됨)
	}

}

class Customer implements Runnable {
	private Table table;
	private String food;
	
	Customer(Table table, String food) {
		this.table = table;
		this.food = food;
	}
	
	public void run() {
		while(true) {
			try {
				Thread.sleep(10);
			} catch(InterruptedException e) {}
			String name = Thread.currentThread().getName();
			
			if(eatFood()) {
				System.out.println(name + " ate a " + food);
			} else {
				System.out.println(name + " failed to eat. :(");
			}
		}// while
	}
	
	boolean eatFood() {
		return table.remove(food);
	}
}


class Cook implements Runnable {
	private Table table;
	
	Cook(Table table) {
		this.table = table;
	}
	
	public void run() {
		while(true) {
			// 임의의 요리를 하나 선택해서 table에 추가한다.
			int idx = (int) (Math.random() * table.dishNum());
			table.add(table.dishNames[idx]);
			
			try {
				Thread.sleep(1);
			} catch(InterruptedException e) {}
		}
	} 
}

class Table {
	String[] dishNames = { "donut", "donut", "brger" }; // donut이 더 자주 나온다.
	final int MAX_FOOD = 6; // 테이블에 놓을 수 있는 최대 음식의 수
	
	private ArrayList<String> dishes = new ArrayList<String>();
	
	public void add(String dish) {
		// 테이블에 음식이 가득찼으면, 테이블에 음식을 추가하지 않는다.
		if(dishes.size() >= MAX_FOOD) {
			return;
		}
		dishes.add(dish);
		System.out.println("Dishes: " + dishes.toString());
	}
	
	public boolean remove(String dishName) {
		// 지정된 요리와 일치하는 요리를 테이블에서 치운다.
		for (int i = 0; i < dishes.size(); i++) {
			if (dishName.equals(dishes.get(i))) {
				dishes.remove(i);
				return true;
			}
		}
		
		return false;
	}
	
	public int dishNum() {
		return dishNames.length;
	}
}

동기화 문제 발생

  • [예외1] ConcurrentModificationException
    • 요리사가 Table에 요리를 추가하려는 과정에서 손님이 요리를 먹은 상황.
    • ArrayList 읽기 수행 중 add()remove()등의 변경이 실행되면 발생할 수 있는 예외이다.
  • [예외2] IndexOutOfBoundsException
    • 하나 남은 요리를 손님2가 먹으려하는데, 손님 1이 먹은 상황. 있지도 않은 요리를 먹으려고 노력(remove()시행)해서 예외가 터지게 된다.
  • [문제점] Table을 여러 쓰레드가 공유하기 때문에 작업 중에 끼어들기가 발생한다.
  • [해결책] Table의 add()remove()synchronized로 동기화해야 한다.
    • 손님 쓰레드가 원하는 음식이 테이블에 없으면 'failed to eat'을 출력하고, 테이블에 음식이 하나도 없으면 0.5초마다 음식이 추가되었는지 확인하면서 기다리도록 구현되었다.
	public synchronized void add(String dish) { // synchronized 추가
		if(dishes.size() >= MAX_FOOD) {
			return;
		}
		dishes.add(dish);
		System.out.println("Dishes: " + dishes.toString());
	}
	
	public boolean remove(String dishName) {
		synchronized(this) {
			while(dishes.size() == 0) { // 0.5초마다 음식이 추가되었는지 확인한다.
				String name = Thread.currentThread().getName();
				System.out.println(name + " is waiting.");
				try { Thread.sleep(500); } catch(InterruptedException e) {}
			}
			
			// 지정된 요리와 일치하는 요리를 테이블에서 치운다.
			for (int i = 0; i < dishes.size(); i++) {
				if (dishName.equals(dishes.get(i))) {
					dishes.remove(i);
					return true;
				}
			}
		}// synchronized
		
		return false;
	}

lock 독점 문제 발생

Dishes: [donut]
CUST2 failed to eat. :( // donut이 없어서 먹지 못했다.
CUST1 ate a donut 
CUST1 is waiting. // 음식이 없어서 테이블에 lock을 건 채로 계속 기다리고 있다.
CUST1 is waiting.
CUST1 is waiting.
CUST1 is waiting.
...
  • 결과를 보면 요리사 쓰레드가 음식을 추가하지 않는다.
    • 예외는 발생하지 않지만, 손님(CUST1)이 Table에 lock건 상태를 지속하여 요리사가 Table의 lock을 얻을 수 없어 음식을 추가하지 못하고 있다.
      • 동기화를 수행했지만, 작업진행이 비효율적이다.
      • 이것을 해결하기 위해서 wait() & notify()의 사용이 필요하다.
	public synchronized void add(String dish) {
		while (dishes.size() >= MAX_FOOD) {
			String name = Thread.currentThread().getName();
			System.out.println(name + " is waiting.");
			try {
				wait(); // COOK 쓰레드를 기다리게 한다.
				Thread.sleep(500);
			} catch(InterruptedException e) {}
		}
		dishes.add(dish);
		notify(); // 기다리고 있는 CUST를 깨우기 위함.
		System.out.println("Dishes: " + dishes.toString());
	}
  • 요리사는 테이블이 가득차면 대기(wait())하고, 음식을 추가하고 나면 손님에게 통보(notify())한다.
	public void remove(String dishName) {
		synchronized(this) {
			String name = Thread.currentThread().getName();
			
			while(dishes.size() == 0) {
				System.out.println(name + " is waiting.");
				try {
					wait(); // CUST 쓰레드를 기다리게 한다.
					Thread.sleep(500);
				} catch(InterruptedException e) {}
			}
			
			while(true) {
				for (int i = 0; i < dishes.size(); i++) {
					if (dishName.equals(dishes.get(i))) {
						dishes.remove(i);
						notify(); // 잠자고 있던 COOK을 깨우기 위함
						return;
					}
				} // for문의 끝
				
				try {
					System.out.println(name + " is waiting.");
					wait(); // 원하는 음식이 없는 CUST 쓰레드를 기다리게 한다.
					Thread.sleep(500);
				} catch(InterruptedException e) {}
			} // while(true)			
		} // synchronized
	}
  • 손님은 음식이 없으면 대기(wait())하고, 음식을 먹고나면 요리사에게 통보(notify())한다.

wait()과 notify()의 문제점

  • 전과달리 한 쓰레드가 lock을 오래 쥐는 일이 없어져 코드가 효율적이어 졌다.
  • 그러나 wait()과 notify()의 대상이 불분명하다는 문제점이 발생한다.
    • 손님과 요리사가 모두 waitingpoll에서 대기하고 있어 어느 쓰레드가 호출의 대상이 될 지 알 수 없다. 운좋게 원하는 쓰레드가 통지를 받지 못하면, 쓰레드가 lock을 얻어도 다시 waiting pool에 들어가야 한다.
  • 기아(startvation) 현상
    • 지독히 운이 나빠 특정 쓰레드가 계속 통지를 받지 못하고 오랫동안 기다리게 되는 현상.
    • 이 현상을 막기 위해 notify() 대신 notifyAll()을 사용할 수 있다.
      • 특정 쓰레드가 다시 waiting pool에 들어가더라도 필요한 쓰레드가 결국 lock을 얻어 작업을 진행할 수 있게 된다.
  • 경쟁 상태(race condition)
    • 여러 쓰레드가 lock을 얻기 위해 경쟁하는 상태.
    • 경쟁 상태를 개선하기 위해 Lock과 Condition을 이용해 특정 쓰레드를 구별해 선별적으로 통지할 수 있다.

3) volatile

  • volatile: '이 변수는 자주 바뀌는 값이니 복사본 사용하지말고 원본을 가져와 사용해라'라는 의미의 명령어.

멀티코어 프로세서에서는 코어마다 별도의 캐시를 가지고 있다. 코어는 메모리에서 읽어온 값을 캐시에 저장하고 캐시에서 값을 읽어서 작업한다. 다시 같은 값을 읽어올 때는 먼저 캐시에 값이 있는지 확인하고 없는 경우에만 메모리에서 읽어온다. 그러다보니 도중에 메모리에 저장된 변수의 값이 변경되었는데도 캐시에 저장된 값이 갱신되지 않아서 메모리에 저장된 값이 다른 경우가 발생한다.

volatile boolean suspended = false;
volatile boolean stopped = false;

그러나 변수 앞에 volatile을 붙이면 코어가 변수의 값을 읽어올때 캐시가 아닌 메모리에서 읽어오기 때문에 캐시와 메모리간의 값을 불일치가 해결된다.

변수에 volatile을 붙이는 대신 synchronized블럭을 사용해도 같은 효과를 얻을 수 있다. 쓰레드가 synchronized 블럭으로 들어갈 때와 나올 때, 캐시와 메모리간의 동기화가 이루어지기 때문에 값을 불일치가 해소되기 때문이다.

public synchronized void stop() {
	stopped = true;
}

Reference

  • Java의 정석 (남궁성)
profile
Good Luck!

0개의 댓글