프로세스 : 실행 중인 프로그램. 자원(resources)과 쓰레드로 구성
쓰레드 : 프로세스 내에서 실제 작업을 수행. 모든 프로세스는 최소한 하나의 쓰레드를 가지고 있다.
프로세스 : 쓰레드 = 공장 : 일꾼
하나의 새로운 프로세스를 생성하는 것보다 하나의 새로운 쓰레드를 생성하는 것이 더 적은 비용이 든다.
대부분의 프로그램이 멀티쓰레드로 작성되어 있다. 하지만 멀티쓰레드 프로그래밍이 장점만 있는 것은 아니다.
동기화(synchronization)에 주의해야 한다.
쓰레드 동기화는 여러 쓰레드가 동일한 리소스를 공유하여 사용하게 되면 서로의 결과에 영향을 주기 때문에 이를 방지하는 기법이다.
교착상태(dead-lock)가 발생하지 않도록 주의해야 한다.
교착상태(dead-lock) : 둘 이상의 작업이 서로 상대방 작업이 끝나기만 기다리고 있어서 서로 다음 단계로 진행하는 못하는 상태로 서로 무한 대기 상태로 빠지게 된다.
기아가 발생하지 않도록 각 쓰레드가 효율적으로 고르게 실행될 수 있도록 해야 한다.
기아 : 특정 쓰레드보다 우선순위가 높은 쓰레드만 계속 실행되면서 특정 쓰레드가 실행될 기회를 얻지 못하는 상태
쓰레드를 구현하는 방법은 2가지가 있다.
Thread 클래스를 상속
Runnable 인터페이스를 구현
상속으로 구현을 하게 되면 자바는 단일 상속이기 때문에 다른 클래스를 상속 못한다는 단점이 있다.
그래서 일반적으로 Runnable 인터페이스를 구현
방법을 사용한다.
class Thread1 extends Thread {
public void run() {
/*작업내용*/
}
}
run()
메서드는 Thread 클래스의 run()
메서드를 오버라이딩한 것이다.class Thread2 implements Runnable {
public void run() {
/*작업내용*/
}
}
run()
메서드는 Runnable 인터페이스의 run()
추상 메서드를 구현한 것이다.class Ex13_1 {
public static void main(String[] args) {
Thread1 t1 = new Thread1();
Runnable r = new Thread2();
Thread t2 = new Thread(r); // 생성자 Thread(Runnable target)
t1.start();
t2.start();
}
}
class Thread1 extends Thread {
public void run() { // 쓰레드가 수행할 작업을 작성
for (int i = 0; i < 5; i++) {
// 조상인 Thread의 getName()을 호출
System.out.printf("(%s)%n", getName());
}
}
}
class Thread2 implements Runnable {
public void run() {
for (int i = 0; i < 5; i++) {
// 현재 실행중인 Thread의 이름을 반환
System.out.printf("(%s)%n", Thread.currentThread().getName());
}
}
}
(Thread-0)
(Thread-0)
(Thread-0)
(Thread-1)
(Thread-1)
(Thread-1)
(Thread-1)
(Thread-1)
(Thread-0)
(Thread-0)
Runnable r = new Thread2();
Thread t2 = new Thread(r); // 생성자 Thread(Runnable target)
Thread(Runnable target)
를 이용해줘야 한다.Thread t2 = new Thread(new Thread2());
: 이렇게 한 줄로 바꿔줄 수도 있다.// 조상인 Thread의 getName()을 호출
System.out.printf("(%s)%n", getName());
...
// 현재 실행중인 Thread의 이름을 반환
System.out.printf("(%s)%n", Thread.currentThread().getName());
getName()
을 호출해서 쓰레드의 이름을 출력할 수 있다.Thread.currentThread().getName());
메서드를 통해서 현재 쓰레드의 이름을 반환할 수 있다.쓰레드를 생성한 후에 start()를 호출해야 쓰레드가 작업 가능한 상태가 된다.
t1.start();
t2.start();
start()를 호출하면 쓰레드가 실행 가능한 상태가 되는 것이지 실행이 되는 것이 아니다.
실제로 언제 실행이 될지는 OS의 스케쥴러가 실행 순서를 결정한다.
t1.start();
먼저 적는다고 해도 먼저 호출되는 것이 아니다.
run()
메서드를 통해 수행할 작업을 작성했지만 호출은 start()
메서드로 하는 이유
호출 스택에서 start()
메서드가 새로운 호출 스택 생성
start()
메서드는 종료되고 새로운 호출 스택에서 run()
메서드가 동작함으로써 서로 독립적인 작업 수행이 가능(멀티쓰레드)
t1.start();
가 아닌 t1.run();
을 하게될 경우
: main메서드의 코드를 수행하는 쓰레드
쓰레드는 "사용자 쓰레드"와 "데몬 쓰레드" 두 종류가 있다.
public class MainThread {
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread1());
Thread t2 = new Thread(new MyThread2());
t1.start();
t2.start();
for (int i = 0; i < 100; i++) {
System.out.print(0);
}
}
}
class MyThread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print(1);
}
}
}
class MyThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print(2);
}
}
}
00002112222222222222222222222220000000000000000000000000000000000000000000000022222222222222222221111111111111222222222222222222220000000000000000000000000000000000000000000000000
main쓰레드 종료시점
222222222222222222222222222222222222MyThread2 종료시점
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111MyThread1 종료시점
main
, MyThread2
, MyThread1
총 3개의 "사용자 쓰레드"가 모두 종료될 때까지 프로그램은 종료되지 않는다.싱글쓰레드
public class MainThread {
for (int i = 0; i < 100; i++) {
System.out.print(0);
}
for (int i = 0; i < 100; i++) {
System.out.print(1);
}
}
}
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
멀티쓰레드
public class MainThread {
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread1());
t1.start();
for (int i = 0; i < 100; i++) {
System.out.print(0);
}
}
}
class MyThread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print(1);
}
}
}
01000000000000000000000000000000111111111111110000000000000000000000000000000000000000000000000000010011111111111111111111111111111111111111111111111111111111111111100000000000000111111111111111111111
싱글쓰레드와 멀티쓰레드를 그래프로 그려보면 다음과 같다.
가로축을 보게 되면 멀티쓰레드가 시간이 약간 더 걸린다는 것을 알 수 있다.
그 이유는 main쓰레드가 실행되다가 t1쓰레드가 실행되면 쓰레드 간의 작업 전환(context switching)이 발생하면서 시간이 조금씩 소모하기 때문이다.
사실 위의 멀티쓰레드 그래프는 이상적인 그래프이다. 해당 쓰레드가 언제 얼마만큼 실행될지는 OS의 스케쥴러가 결정하기 때문에 실제로는 위의 그래프처럼 일정한 시간동안 두 쓰레드가 번갈아가며 실행되지 않는다.
OS 스케쥴러는 OS 전체의 프로세스와 쓰레드를 총괄하기 때문에 시시각각 변하는 모든 프로레스와 쓰레드의 상황을 고려해서 실행순서와 실행기간을 정해 스케쥴링을 한다.
따라서, 우리가 작성한 쓰레드는 실행할 때마다 결과가 달라지는 것이다.
싱글쓰레드로 작성
import javax.swing.JOptionPane;
class Ex13_4 {
public static void main(String[] args) throws Exception {
String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
System.out.println("입력하신 값은 " + input + "입니다.");
for (int i = 5; i > 0; i--) {
System.out.println(i);
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
I/O Blocking
이라고 한다.멀티쓰레드로 작성
import javax.swing.JOptionPane;
class Ex13_4 {
public static void main(String[] args) throws Exception {
Thread t1 = new ThreadEx5();
t1.start();
String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
System.out.println("입력하신 값은 " + input + "입니다.");
}
}
class ThreadEx5 extends Thread {
public void run() {
for (int i = 5; i > 0; i--) {
System.out.println(i);
try {
sleep(500);
} catch (Exception e) {
}
}
}
}
결론 : 멀티쓰레드를 통해서 I/O Blocking을 해결할 수 있다.
: 작업의 중요도에 따라 쓰레드의 우선순위를 다르게 하여 특정 쓰레드가 더 많은 작업 시간을 갖게 할 수 있다.
void setPriority(int newPriority)
: 지정한 값으로 우선순위 변경
int getPriority()
: 우선순위 반환
우선순위 범위 : 1 (최소) ~ 10 (최대)
우선순위 default 값 : 5
main쓰레드 우선순위 : 5
JVM의 우선순위는 범위가 1 ~ 10으로 지정할 수 있도록 정해져있고 Windows OS의 경우에는 우선순위 범위가 32단계로 나뉘어져 있다.
JVM에서 설정된 쓰레드들의 우선순위를 OS 스케줄러에 전달하는 것이다.
그러나 우리가 쓰레드들의 우선순위를 각자 지정한다고 현재 OS 전체 작업을 다 제끼고 우선적으로 설정되는 것이 아니라 그저 희망 사항을 스케쥴러에 전달하는 것 뿐이다.
OS 스케쥴러는 우리가 전달한 희망 사항을 참고할 뿐 결국 OS 내에서 돌아가는 전체 프로그램들의 작업 효율을 따져 실행 순서를 정한다.
서로 관련된 쓰레드를 그룹으로 묶어서 다루기 위한 것
모든 쓰레드는 반드시 하나의 쓰레드 그룹에 포함되어 있어야 하고 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 자동으로 main쓰레드 그룹
에 속한다.
자신을 생성한 쓰레드(부모 쓰레드)의 그룹과 우선순위를 상속받는다.
Thread 생성자
Thread(ThreadGroup group, String name) // Thread 클래스 상속받아서 만들어진 경우
Thread(ThreadGroup group, Runnable target) // Runnable 인터페이스 구현해서 만들어진 경우
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name , long stackSize) // 생성할 호출스택 사이즈 설정
Method | 설명 |
---|---|
생성자 | |
ThreadGroup getThreadGroup() | 자신이 속한 쓰레드 그룹 반환 |
ThreadGroup(String name) | 지정된 이름의 새로운 쓰레드 그룹 생성 |
ThreadGroup(ThreadGroup parent, String name) | 지정된 쓰레드 그룹에 속하는 지정된 이름의 새로운 쓰레드 그룹 생성 |
반환 | |
int activeCount() | 쓰레드 그룹에 포함된 활성상태 쓰레드 수 반환 |
int activeGroupCount() | 쓰레드 그룹에 포함된 활성상태 쓰레드 그룹 수 반환 |
int getMaxPriority() | 쓰레드 그룹의 최대 우선순위 반환 |
String getName() | 쓰레드 그룹의 이름 반환 |
ThreadGroup getParent() | 쓰레드 그룹의 상위 쓰레드 그룹 반환 |
void list() | 소속 쓰레드와 하위 쓰레드 그룹 정보 출력 |
조회/확인 | |
boolean isDaemon() | 해당 쓰레드 그룹이 데몬 쓰레드 그룹인지 확인 |
boolean isDestroyed() | 해당 쓰레드 그룹이 삭제되었는지 확인 |
boolean parentOf(ThreadGroup g) | 해당 쓰레드 그룹이 지정된 쓰레드 그룹의 상위 쓰레드 그룹인지 확인 |
void checkAccess() | 현재 실행 중인 쓰레드가 쓰레드 그룹을 변경할 권한이 있는지 체크 |
삭제 | |
void destroy() | 쓰레드 그룹과 하위 쓰레드 그룹까지 전부 삭제 |
int enumerate(Thread[] list) int enumerate( Thread[] list, boolean recurse) int enumerate(ThreadGroup[] list) int enumerate(ThreadGroup[] list, boolean recurse) | 그룹에 속한 쓰레드와하위 쓰레드 그룹의 목록을 지정된 배열에 담고 개수 반환 |
설정 | |
void setDaemon(boolean daemon) | 쓰레드 그룹을 데몬 쓰레드 그룹으로 설정/해제 |
void setMaxPriority(int Priority) | 그룹의 최대 우선순위 설정 |
void interrupt() | 그룹에 속한 모든 쓰레드를 interrupt() |
void uncaughtException(Thread th, Throwable e) 메서드
class Demo implements Runnable {
@Override
public void run() {
int x = 10 / 0; // ArithmeticException 발생
}
}
class MyHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("쓰레드 " + t.getName() + "에서 " + e.getMessage() + " 발생");
// 로깅 작업 등을 할 수 있다.
}
}
public class GFG {
public static void main(String[] args) {
Thread t = new Thread(new Demo());
t.setUncaughtExceptionHandler(new MyHandler()); // uncaughtException 설정
t.start();
}
}
쓰레드 Thread-0에서 / by zero 발생
public class MainThread {
public static void main(String[] args) {
ThreadGroup root = new ThreadGroup("rootGroup"); // 루트 그룹 생성
ThreadGroup childGroup = new ThreadGroup(root, "childGroup"); // 루트 그룹에 속하는 자식 그룹 생성
rootThread rt = new rootThread(root, "rt"); // 루트 그룹에 속하는 쓰레드 생성
Thread ch1 = new Thread(childGroup, new childThread1(), "ch1"); // 자식 그룹에 속하는 쓰레드 생성
Thread ch2 = new Thread(new childThread2(), "ch2"); // 쓰레드 그룹 미지정 -> 자동으로 main쓰레드 그룹에 속함
rt.start(); // 루트 그룹의 쓰레드 시작
ch1.start(); // 자식 그룹의 쓰레드 시작
ch2.start(); // main쓰레드 그룹의 쓰레드 시작
root.list(); // 루트 그룹과 하위 그룹들의 정보 출력
System.out.println();
}
}
class childThread1 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "가 속한 쓰레드 그룹 : " + Thread.currentThread().getThreadGroup().getName());
}
}
class childThread2 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "가 속한 쓰레드 그룹 : " + Thread.currentThread().getThreadGroup().getName());
}
}
class rootThread extends Thread {
public rootThread(ThreadGroup group, String name) {
super(group, name);
}
@Override
public void run() {
System.out.println(getName() + "가 속한 쓰레드 그룹 : " + getThreadGroup().getName()); // rt의 쓰레드 그룹 이름 출력
}
}
쓰레드에 어떤 명령을 내릴 때 쓰레드 그룹에 한번에 명령을 내릴 수 있다.
rootThread rt = new rootThread(root, "rt");
: Thread(ThreadGroup group, String name)
생성자 사용
Thread ch1 = new Thread(childGroup, new childThread1(), "ch1");
: Thread(ThreadGroup group, Runnable target, String name)
생성자 사용
쓰레드를 생성할 땐 생성자를 통해 쓰레드 그룹을 지정해줘야 하지만 지정해주지 않으면 자동으로 main쓰레드 그룹
에 속하게 된다. -> Thread ch2 = new Thread(new childThread2(), "ch2");
: 일반 쓰레드의 작업을 돕는 보조적인 역할을 수행하는 쓰레드
일반 쓰레드가 모두 종료되면 자동적으로 종료된다.(일반 쓰레드의 보조적인 역할로써 일반 쓰레드가 종료되면 존재의 의미가 없어지기 때문에)
데몬 쓰레드 예시 : 가비지 컬렉터(GC), 자동 저장, 화면 자동갱신 등
: 무한루프와 조건문을 이용해서 실행 후 대기 상태에 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성
boolean isDaemon()
: 쓰레드가 데몬 쓰레드인지 확인. 데몬 쓰레드면 true 반환
void setDaemon(boolean on)
: 쓰레드를 데몬 쓰레드로 또는 일반 쓰레드로 변경. 매개변수로 true를 지정하면 데몬 쓰레드가 된다.
void setDaemon(boolean on)
는 반드시 start()
메서드를 호출하기 전에 실행되어야 한다. 그렇지 않으면 IllegalThreadStateException
발생
class Ex13_7 implements Runnable {
static boolean autoSave = false;
public static void main(String[] args) {
Thread t = new Thread(new Ex13_7());
t.setDaemon(true); // 쓰레드 t를 데몬 쓰레드로 변경
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; // 5초가 지나면 autoSave를 true로 설정
}
System.out.println("프로그램을 종료합니다.");
}
public void run() {
while (true) { // 무한루프로 작성
try {
Thread.sleep(3 * 1000);
} catch (InterruptedException e) {
}
if (autoSave) autoSave(); // autoSave가 true 조건을 만족할 때만 작업을 수행
}
}
public void autoSave() {
System.out.println("작업 파일이 자동 저장 되었습니다.");
}
}
1
2
3
4
5
작업 파일이 자동 저장 되었습니다.
6
7
8
작업 파일이 자동 저장 되었습니다.
9
10
프로그램을 종료합니다.
5초 이후 autoSave를 true로 변경하고 이 때부터 데몬 쓰레드의 if 조건문이 true가 되면서 3초마다 자동 저장하는 데몬 쓰레드가 동작한다.
일반 쓰레드가 10초 후 프로그램 종료가 되도록 설정되었기 때문에 10초 후 데몬 쓰레드도 자동적으로 종료된다.
t.setDaemon(true);
: 쓰레드 t를 데몬 쓰레드로 변경. 반드시 start()
메서드 전에 실행되어야 한다.
상태 | 설명 |
---|---|
NEW | 생성은 되었지만 아직 start() 호출이 안된 상태 |
RUNNABLE | 실행 중 또는 실행 가능한 상태 |
BLOCKED | 동기화 블럭에 의해 일시 정지된 상태(풀릴 때까지 기다리는 상태) |
WAITING / TIMED_WAITING | 종료는 아니지만 실행 불가능한 일시 정지 상태 (TIMED_WAITING은 시간이 지정된 일시정지) |
TERMINATED | 쓰레드의 작업이 종료된 상태 -> 소멸 |
쓰레드가 생성되었지만 아직 start()
메서드가 호출되기 전을 NEW
상태라고 한다.
실행 뿐만 아니라 실행 후에 다시 자신 차례가 올 때까지 실행 대기를 하는 상태 또한 RUNNABLE
상태라고 한다.
작업을 다 마치거나 stop()
메서드가 호출되면 TERMINATED
상태가 된다.
suspend()
↔ resume()
suspend()
메서드가 호출되면 쓰레드가 WAITING / BLOCKED
상태가 된다.resume()
메서드를 호출하면 다시 RUNNALBLE
상태가 된다.sleep()
↔ time-out
, interrupt()
sleep()
메서드가 호출되면 쓰레드가 지정한 시간동안 WAITING / BLOCKED
상태가 된다. time-out
이 되고 다시 RUNNALBLE
상태가 된다.interrupt()
메서드를 호출하면 중간에 바로 RUNNABLE
상태가 된다.wait()
↔ notify()
wait()
메서드가 호출되면 쓰레드가 WAITING / BLOCKED
상태가 된다.notify()
메서드를 호출하면 다시 RUNNALBLE
상태가 된다.join()
: 지정한 다른 쓰레드가 종료될 때까지 기다리는 메서드
t1.join()
을 호출하면 main 쓰레드는 t1 쓰레드가 종료될 때까지 일시정지 상태에 있게 된다.I/O block
: 입출력이 완료될 때까지 대기
suspend()
와 wait()
는 자바에서 쓰레드를 일시 정지시키는 메서드이다.
그러나 두 메소드는 다음과 같은 차이점이 있습니다.
suspend()
는 쓰레드가 속한 Thread 클래스의 메서드이고,wait()
는 모든 객체가 상속하는 Object 클래스의 메서드이다.
suspend()
는 쓰레드를 일시 정지시키면서 락을 해제하지 않는다. 이로 인해 다른 쓰레드가 공유 자원에 접근할 수 없게 되어 교착 상태(dead-lock)가 발생할 수 있다.
반면 wait()
는 쓰레드를 일시 정지시키면서 락을 해제한다. 이로 인해 다른 쓰레드가 공유 자원에 접근하고 통지(notify)를 보낼 수 있다.
suspend()
는 resume()
메서드로만 재개될 수 있다. 이 때 resume()
메서드가 호출되기 전에 suspend()
된 쓰레드가 종료되면 문제가 발생할 수 있다.
반면 wait()
는 notify()
, notifyAll()
, interrupt()
, time-out
등의 방법으로 재개될 수 있다.
따라서 suspend()
와 resume()
은 권장되지 않는 방법이며, 대신 wait()/notify()
, sleep()/interrupt()
등의 방법을 사용하는 것이 더 좋다.