-
🌑 프로세스 (Process) & 쓰레드 (Thread)
-
✔️ 프로세스 (Process)
-
✔️ 멀티태스킹(Multi-tasking) & 멀티쓰레딩(Multi-threading)
-
🌒 쓰레드의 구현과 실행
-
✔️ 쓰레드 생성 (Thread class 상속 / Runnable interface 구현)
-
✔️ Thread class 상속
-
✔️ Runnable interface 구현
-
✔️ 쓰레드의 실행 방법
-
🌓 쓰레드(Thread)와 호출스택(Call stack)과의 관계
-
✔️ 쓰레드를 실행시킬 때 ' run() ' 이 아닌 ' start() ' 를 호출하는 이유?
-
🌔 싱글 쓰레드 & 멀티 쓰레드
-
✔️ 싱글 쓰레드 프로세스 & 멀티 쓰레드 프로세스
-
🌕 쓰레드의 우선순위
-
✔️ 쓰레드의 우선순위 ?
-
✔️ 싱글 코어와 멀티 코어에서의 우선순위 효과
-
🌖 쓰레드 그룹 (Thead Group)
-
✔️ 쓰레드 그룹 ?
-
✔️ 쓰레드 그룹 생성 & 지정
-
🌗 데몬 쓰레드 (Daemon Thread)
-
✔️ 데몬 쓰레드 ?
-
✔️ 데몬 쓰레드의 생성
-
🌘 쓰레드의 실행제어
-
✔️ 쓰레드의 스케줄링
-
🌚 쓰레드의 동기화
-
✔️ 쓰레드의 동기화(Synchronization) ?
-
✔️ synchronized를 이용한 동기화
-
✔️ 특정 쓰레드가 lock을 반납하지 않은 경우 - wait() & notify()
-
✔️ Lock과 Condition을 이용한 동기화 ( java.util.concurrent.locks )
-
✔️ volatile (휘발성)
-
✔️ fork & join 프레임워크
-
▼ Study📋
🌑 프로세스 (Process) & 쓰레드 (Thread)
✔️ 프로세스 (Process)
🔹 프로세스 (Process) ?
- 프로그램을 실행하면 OS로부터 실행에 필요한 자원(데이터, 메모리)을 할당받아 프로세스가 된다
- 이 프로세스의 자원을 이용해서 작업을 수행하는 것이 쓰레드(Tread) !
- 프로세스 ➜ 작업공간 / 쓰레드 ➜ 노동자
- 하나의 프로세스가 가질 수 있는 쓰레드의 개수는 제한 X
➜ 단, 쓰레드가 작업을 수행하는데 개별적인 메모리 공간(호출스택)을 필요로 하기 때문에, 프로세스의 메모리 한계에 따라 생성할 수 있는 쓰레드의 수가 결정
(실제로 메모리 한계에 다다를 일은 거의 없니 이 부분은 걱정할 필요 X)

✔️ 멀티태스킹(Multi-tasking) & 멀티쓰레딩(Multi-threading)
🔹 멀티태스킹 & 멀티쓰레딩
- 여러 개의 프로세스가 동시에 실행되는 것 ➜ 멀티태스킹 (Multi-tasking)
(대부분의 OS (Windows, Unix, etc)가 지원) - 하나의 프로세스 내에서 여러 쓰레드가 동시에 실행되는 것 ➜ 멀티쓰레딩 (Multi-threading)
- CPU의 사용률 증가
- 자원을 효율적으로 사용 가능
- 사용자에 대한 응답성 향상
- 작업이 분리되어 코드가 간결해진다
- 우리가 여러 작업을 동시에 수행할 수 있는 것은 멀티쓰레딩 덕분이다
(메신저 채팅, 파일 다운로드, 게임 등을 동시에 하는 것)
- 따라서, 여러 사용자에게 서비스를 해주는 서버 프로그램의 경우 멀티쓰레드로 작성하는 것은 필수 !
➜ 하나의 서버 프로세스가 여러 개의 쓰레드를 생성해서 쓰레드와 사용자의 요청이 일대일로 처리되도록 한다 - 단, 여러 개의 쓰레드가 하나 프로세스의 자원을 공유하며 작업하기 때문에, 동기화(Synchronization), 교착상태(Deadlock)와 같은 문제를 고려해야 한다
🌒 쓰레드의 구현과 실행
✔️ 쓰레드 생성 (Thread class 상속 / Runnable interface 구현)
🔹Thread class를 상속받아 쓰레드를 구현하는 방법
- 다른 class를 상속받을 수 없다
- Thread class의 메서드 ' run() ' 을 Overriding 해주면 된다
🔹 Runnable interface를 구현하는 쓰레드를 구현하는 방법
- 재사용성(Reusability)이 높고 코드의 일관성(Consistency) 유지 가능
➜ 일반적이고 객체지향적인 방법 - 추상메서드인 ' run() ' 을 구현(implements)해주면 된다
(Runnable interface엔 오로지 run()만 정의되어 있다)
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
🔹 두 방법의 성능 차이
- ' run() ' 메서드 코드의 성능은 동일하다
- Thread class의 ' run() ' 메서드 하나 때문에 상속 기능을 사용하는 것은 비효율
- 만약, Thread class의 또다른 기능을 확장하거나 재정의해야 할 경우엔 Runnable interface를 구현하는 것보다 Thread class을 상속받는 방법이 더 좋을 수 있다
➜ Thread class 기능의 확장 여부에 따라 Thread class를 상속받을 것인지, Runnable interface를 구현할 것인지 선택하자 !
✔️ Thread class 상속
🔹 instance 생성 방법
- Thread class를 상속받은 Sub class의 intance를 생성해주면 된다
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println("main end");
}
}
public class MyThread extends Thread{
int num;
public MyThread() {
this.num = 0;
}
public MyThread(int num) {
this.num = num;
}
@Override
public void run() {
System.out.println(this.num + " thread start");
try{
Thread.sleep(1000); // 1초간 시간을 지연
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(getName());
System.out.println(this.num +" thread end");
}
}
main end
0 thread start
Thread-0
0 thread main
🔻"main end" 가 먼저 출력된 것을 보면,
main 쓰레드와 다른 쓰레드(' myThread ')가 생성되어 동시에 실행된 것을 알 수 있다
( " 쓰레드와 호출스택 관계 " 참고 )
✔️ Runnable interface 구현
🔹 instance 생성 방법
- Runnable interface를 구현한 class의 intance를 생성한다
- 생성한 instance를 Thread class의 생성자의 매개변수로 제공하여 Thread class의 객체를 생성한다
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable(i));
thread.start();
System.out.println("main end");
}
}
public class MyRunnable implements Runnable {
int num;
public MyRunnable() {
this.num = 0;
}
public MyRunnable(int num) {
this.num = num;
}
@Override
public void run() {
System.out.println(this.num + " thread start");
try {
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
System.out.println(this.num +" thread end");
}
}
main end
0 thread start
Thread-0
0 thread main
🔻Runnable interface를 구현하여 쓰레드를 생성한 경우엔 Thread class의 static메서드인
' currentThread() ' 를 호출하여 현재 실행중인 쓰레드의 참조를 얻어 와야만
' getName() ' 과 같은 Thread class의 메서드를 호출(사용) 가능
🔻쓰레드의 이름을 아래와 같은 생성자나 메서드를 통해서 지정해주지 않으면
"Thread-번호" 형식으로 이름이 정해진다
Thread(Runnable target, String name)
Thread(String name)
void setName(String name)
🔹 Thread class의 객체가 Thread class의 instance가 아닌 Runnable interface를 구현한 class의 instance를 참조하는 이유?
- Thread class의 생성자가 Runnable interface를 구현한 inatance를 참조하도록 되어 있기 때문이다 (아래 코드 참고)
- Thread class를 상속받아 ' run() ' 메서드를 Overriding 하지 않고도 Runnable interface를 구현한 class(외부)로부터 ' run() ' 을 제공받을 수 있는 이유이기도 하다
public class Thread {
private Runnable r;
public Thread(Runnable r) {
this.r = r; // Runnable interface를 구현한 instance를 참조
}
public void run() {
if(r! = null)
r.run(); // Runnable interface를 구현한 instance의 run()을 호출
}
...
}
✔️ 쓰레드의 실행 방법
🔹 start()
- 쓰레드를 생성하고 ' start() ' 를 호출해야만 쓰레드가 실행된다
(바로 실행되는 것이 아닌 실행대기 상태에 있다가 차례가 되면 실행)
➜ 하나의 쓰레드에 대해 ' start() ' 는 한 번만 호출될 수 있다
ExtendsThread t1 = new ExtendsThread((); // 쓰레드 객체 생성
t1.start(); // 쓰레드 생성 및 실행
// t1.start(); // IllegalThreadStateException 발생
t1 = new ExtendsThread(); // 다시 생성
t1.start(); // OK
🌓 쓰레드(Thread)와 호출스택(Call stack)과의 관계

✔️ 쓰레드를 실행시킬 때 ' run() ' 이 아닌 ' start() ' 를 호출하는 이유?
🔹 새로운 쓰레드를 생성하고 ' start() ' 메서드를 호출한 후 호출스택의 변화

- main메서드에서 쓰레드의 ' start() ' 메서드를 호출
➜ main메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main쓰레드라고 한다 - ' start() ' 메서드는 새로운 쓰레드가 작업하는데 사용될 호출스택을 생성
- 새로 생성된 호출스택에 ' run() ' 메서드가 호출되어, 쓰레드가 독립된 공간에서 작업을 수행
- 이제 호출스택이 2개이므로 스케줄러가 정한 순서에 의해 번갈아 가면서 실행된다
- 스케줄러는 실행 대기중인 쓰레드들의 우선순위를 고려하여 실행 순서와 실행 시간을 결정
➜ 각 쓰레드들은 스케줄링한 것에 따라 자신의 순서가 되면 지정된 시간동안 작업을 수행한다 - 작업을 마친 쓰레드(위에선 ' run() ' 의 수행이 종료된 쓰레드)는 호출스택이 모두 비워지면서 이 쓰레드가 사용하던 호출스택은 사라진다 (이 경우에는 쓰레드도 함께 사라진다)
➜ 실행 중인 '사용자 쓰레드 (User Thread)'가 하나도 없을 때 프로그램은 종료된다 - ' start() ' 메서드가 아닌 ' run() ' 메서드를 호출하면 쓰레드와 해당 쓰레드가 작업하는데 사용될 호출스택이 생성되지 않는다
🌔 싱글 쓰레드 & 멀티 쓰레드
✔️ 싱글 쓰레드 프로세스 & 멀티 쓰레드 프로세스
🔹 싱글 쓰레드 프로세스와 멀티 쓰레드 프로세스의 비교 (싱글 코어 & 멀티 코어)

- 멀티쓰레드는 두 개의 쓰레드로 두 작업을 동시에 처리하는 것처럼 보이지만, 사실 짧은 시간동안 2개의 쓰레드(th1, th2)가 번갈아 가면서 작업을 수행하는 것이다
- 그렇기 때문에, 싱글 쓰레드로 작업을 수행한 시간과 멀티 쓰레드로 작업을 수행한 시간은 거의 같다
(오히려 쓰레드간의 작업 전환(context switching)에 시간이 더 걸려 멀티 쓰레드가 시간이 더 걸리는 경우도 있다)
➜ 싱글 코어에서 단순히 CPU만을 사용하는 계산작업이라면 오히려 멀티 쓰레드보다 싱글 쓰레드로 프로그래밍하는 것이 효율적

- 위와 같이 하나의 작업을 여러 쓰레드가 나눠서 처리하는 것이 병렬(parallel) 실행이라 한다
- 여러 쓰레드가 여러 작업을 동시에 진행하는 것을 병행(concurrent) 실행이라 한다
➜ 즉, 멀티 코어에서 멀티 쓰레드가 작업을 수행한다면 여러 작업을 동시에 진행 가능
➜ 두 작업이 겹치는 경우가 발생 (같은 자원을 두고 경쟁)

- 이와 달리 쓰레드들이 서로 다른 자원을 사용하는 작업의 경우에는 싱글 쓰레드 프로세스보다 멀티 쓰레드 프로세스가 더 효율적이다 !
ex) 사용자로부터 데이터를 입력받고 화면에 출력하는 작업
class MultiThreadEx {
// main 쓰레드
public static void main(String[] args) throws Exception {
// 화면에 숫자를 출력하는 쓰레드
NumPrintThread th2 = new NumPrintThread();
th2.start();
String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
System.out.println("입력하신 값은 " input + "입니다.");
}
}
class NumPrintThread extends Thread {
public void run() {
System.out.println(i);
try {
sleep(1000);
} catch(Exception e) {}
}
}
- Java가 OS(플랫폼) 독립적이라고 하지만 실제로는 OS 종속적인 부분이 몇 가지 있는데 쓰레드가 그 중의 하나이다
- JVM의 쓰레드 스케줄러에 의해 매 순간 상황에 따라 프로세스와 쓰레드에게 에게 할당되는 실행 시간이 일정하지 않다
➜ 쓰레드가 이러한 불확실성을 갖고 있는 것을 알아두자
🌕 쓰레드의 우선순위
✔️ 쓰레드의 우선순위 ?
🔹 우선순위 (Priority)
- 우선순위는 속성, 즉 멤버변수이다
- 이 우선수위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다
- 쓰레드가 수행하는 작업의 중요도에 따라 우선순위를 지정
(파일 전송 기능이 있는 메신저 ➜ 채팅을 전송하는 쓰레드 > 파일 다운로드를 처리하는 쓰레드)
🔹 쓰레드의 우선순위 지정하기
- 관련 메서드
void setPriority(int newPriority) // 쓰레드의 우선순위를 지정한 값으로 변경
int getPriority() // 쓰레드의 우선순위를 반환
public static final int MAX_PRIORITY = 10 // 최대우선순위
public static final int MIN_PRIORITY = 1 // 최소우선순위
public static final int NORM_PRIORITY = 5 // 보통우선순위
- 쓰레드가 가질 수 있는 우선순위의 범위는 1 ~ 10이다
(숫자가 높을수록 우선순위가 높은 것) - 쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받는다
- 쓰레드의 우선순위는 실행(' start() ' 호출)하기 전에만 변경 가능하다
class ThreadPriorityEx {
public static void main(String args[]) {
ExtendsThread th = new ExtendsThread();
th.setPriority(7); // th의 우선순위를 7로 변경
th.start(); // 쓰레드 실행
}
}
🔻쓰레드 th는 main 메서드에서 생성했기 때문에
main 메서드를 실행하는 쓰레드의 우선순위인 5를 상속받아 자동적으로 5가 된다
✔️ 싱글 코어와 멀티 코어에서의 우선순위 효과
🔹 싱글 코어
- 우선순위가 높은 쓰레드에게 상대적으로 우선순위가 낮은 쓰레드보다 더 많은 양의 실행시간이 주어진다
➜ 결과적으로 우선순위가 높은 쓰레드의 작업이 더 빨리 완료될 수 있다

🔹 멀티 코어
- 쓰레드의 우선순위에 따른 차이 X
- 굳이 우선수위에 차등을 주어 쓰레드를 실행하려면, 해당 프로그램이 실행되는 OS의 스케쥴링 정책과 JVM의 구현을 확인해야 한다
➜ JVM의 구현을 확인하더라도 OS의 스케쥴러에 종속적이기에 예측 정도만 가능 - 쓰레드에 우선순위를 부여하는 것보다 작업에 우선순위를 두어 PriorityQueue에 저장하고,
우선순위가 높은 작업이 먼저 처리되도록 하는 것이 나을 수 있다
🌖 쓰레드 그룹 (Thead Group)
✔️ 쓰레드 그룹 ?
🔹 쓰레드 그룹
- 서로 관련된 쓰레드를 그룹으로 묶어서 관리하기 위함 (폴더와 비슷한 개념)
- 쓰레드 그룹에 다른 쓰레드 그룹을 포함시킬 수 있다
- 사실 보안상의 이유로 도입된 개념
- 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경 O
- 다른 쓰레드 그룹의 쓰레드는 변경 X
✔️ 쓰레드 그룹 생성 & 지정
🔹 쓰레드 그룹의 생성
- 모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 한다
- 쓰레드 그룹의 생성 과정
- Java 어플리케이션 실행
- JVM이 main과 system이라는 쓰레드 그룹 생성
- JVM 운영에 필요한 쓰레드들을 생성해서 이 쓰레드 그룹에 포함시킨다
(main 쓰레드 ➜ main 쓰레드 그룹 / Finalize 쓰레드(가비지컬렉션(GC) 수행) ➜ system 쓰레드 그룹
- 보통 우리가 생성하는 쓰레드 그룹 ➜ main 쓰레드 그룹의 하위 쓰레드 그룹
🔹 쓰레드 그룹 관련 생성자 & 메서드
- 쓰레드 그룹을 지정하는 생성자를 사용하지 않은 쓰레드도 자동적으로 main 쓰레드 그룹에 속하게 된다
(기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 되는 특징)
https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html
Thread (Java Platform SE 8 )
Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group, and has the specified stack size. This constructor is identical to Thread(ThreadGroup,Runnable,
docs.oracle.com
🔹 참조변수 없이 쓰레드를 생성하고 실행시킨 경우
- 그래도 해당 쓰레드의 참조는 ThreadGroup에 저장된다
➜ 따라서, 이 쓰레드는 가비지 컬렉터(Garbage Collector)의 제거 대상이 되지 않는다
// Thread(ThreadGroup group, Runnable target, String name)
new Thread(grp, r, "th").start();
// Thread th = new Thread(grp, r, "th");
// th.start();
🌗 데몬 쓰레드 (Daemon Thread)
✔️ 데몬 쓰레드 ?
🔹 데몬 쓰레드
- 데몬 쓰레드가 아닌 쓰레드의 작업을 돋는 쓰레드 (보조적인 역할)
ex) 가비지 컬렉터, 워드 프로세서의 자동 저장, 화면 자동 갱신
➜ JVM이 필요한 보조작업을 수행하는 데몬 쓰레드들을 자동적으로 생성해서 실행시킨다 - 일반 쓰레드가 모두 종료되면, 데몬 쓰레드도 강제적으로 종료된다
- GUI를 가진 프로그램(AWT, Swing)을 실행하는 경우에는 이벤트와 그래픽처리를 위해 더 많은 수의 데몬 쓰레드가 생성된다
✔️ 데몬 쓰레드의 생성
🔹 데몬 쓰레드의 작성 · 실행방법
- 쓰레드를 생성한 다음 실행하기 전에 ' setDaemon(true) ' 메서드를 호출해야 한다
- 나머지는 일반 쓰레드의 작성 · 실행방법과 동일 !
- 데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다
boolean isDaemon() // 쓰레드가 데몬 쓰레드인지 확인한다
// 데몬 쓰레드이면 true를 반환한다
void setDaemon(boolean on) // 쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경한다
// 매개변수 on의 값을 true로 지정하면 데몬 쓰레드가 된다
- 무한 루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성
class Main implements Runnable {
static boolean autoSave = false;
public void main(String[] args) {
Thread t = new Thread(new Main());
t.setDaemon(true); // 이 부분이 없으면 종료되지 않는다
t.start();
for(int i = 1; i <= 8; i++) {
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
System.out.println(i);
if(i == 2)
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("작업파일 자동 저장 완료");
}
}
1
2
3
작업파일 자동 저장 완료
4
5
작업파일 자동 저장 완료
6
7
8
프로그램 종료
🔻main 쓰레드가 종료되니 데몬 쓰레드도 강제 종료
🌘 쓰레드의 실행제어
✔️ 쓰레드의 스케줄링
🔹 쓰레드의 상태
- NEW
➜ 쓰레드가 생성되고 아직 ' start() ' 가 호출되지 않은 상태 - RUNNABLE
➜ 실행 중 또는 실행 가능한 상태 - BLOCKED
➜ 동기화블럭에 의해서 일시정지된 상태 (lock이 풀릴 때까지 기다리는 상태) - WAITING, TIMED_WAITED
➜ 쓰레드의 작업이 종료되지는 않았지만 일시정지 상태.
TIMED_WAITED는 일시정지 시간이 지정된 경우를 의미 - TERMINATED
➜ 쓰레드의 작업이 종료된 상태

- 쓰레드 생성 후 ' start() ' 메서드 호출
- 실행대기 상태에 있다가 자신의 차례가 되면 실행상태가 된다
- 주어진 실행시간이 다되거나 ' yield() ' 메서드를 만나면 다시 실행대기 상태가 되고 다음 차례의 쓰레드가 실행상태가 된다
- 실행 중에 ' suspend() ', ' sleep() ', ' join() ', ' I/O block ' 에 의해 일시정지 상태가 될 수 있다
( I/O block ➜ 입출력작업에서 발생하는 지연상태, 사용자의 입력을 기다리는 경우) - 지정된 일시정지시간이 다되거나(time-out), notify(), ' resume() ', ' interrupt() '가 호출되면 일시정지 상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다린다
- 실행을 모두 마치거나 ' stop() ' 메서드가 호출되면 쓰레드는 소멸한다
🔹 쓰레드의 스케줄링 관련 메서드
sleep(long millis) - 일정시간동안 쓰레드를 멈추게 한다
➜ 지정된 시간(일정시간)동안 쓰레드를 일시정지시킨다 (쓰레드가 잠에 든 상태)
지정한 시간이 지나거나 ' interrupt() ' 가 호출되면 다시 실행 대기 상태가 된다
➜ ' sleep() ' 메서드를 호출할 때는 항상 try-catch문으로 예외 처리해주기
( ' interrupt() ' 가 호출될 경우 예외가 발생해서)
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
🔻매번 예외 처리하는 것이 번거로워 아예 메서드를 만들어 사용하기도 한다
class MyThread {
public static void main(String args[]) {
ExtendsThread_1 th1 = new ExtendsThread_1();
ExtendsThread_2 th2 = new ExtendsThread_2();
try {
th1.sleep(1000);
} catch(InterruptedException e) {}
System.out.println("main 종료");
}
}
class ExtendsThread_1 extends Thread {
public void run() {
...
System.out.println("th1 종료");
}
}
class ExtendsThread_2 extends Thread {
public void run() {
...
System.out.println("th2 종료");
}
}
(생략)
th1 종료
th2 종료
main 종료
🔻 쓰레드 th1이 가장 늦게 종료될 것 같지만 가장 빨리 종료되었다
➜ 'th1.sleep(1000);'과 같이 ' sleep() ' 메서드를 호출했어도
실제로 영향을 받는 것은 main 쓰레드 !
🔻 ' sleep() ' 메서드는 static으로 선언되어 있다
➜ 따라서, 참조변수로 호출하기 보다는 'Thread.sleep(1000);' 처럼 호출하자
interrupt() - 쓰레드의 작업을 취소한다
➜ 쓰레드를 강제로 종료시키진 못하고, 작업을 멈추라고 요청만 하는 것이다
( 쓰레드의 interrupted상태(instance 변수)를 true로 변경하는 것뿐이다 )
void interrupt() // 쓰레드의 interrupted상태를 false에서 true로 변경
boolean isInterrupted() // 쓰레드의 interrupted상태를 반환
static boolean interrupted() // 현재 쓰레드의 interrupted상태를 반환 후, false로 변경
➜ ' sleep() ', ' wait() ', ' join() ' 메서드에 의해 ' 일시정지 상태(WAITING) '인 쓰레드에 대해 ' interrupt() ' 를 호출했을 땐, ' 실행대기 상태(RUNNABLE) '로 만든다
( 해당 쓰레드에선 InterruptedException이 발생함으로써 일시정지 상태를 벗어나게 하는 것)
class MyThread {
public static void main(String[] args) throws Exception {
ExtendsThread th = new ExtendsThread();
th.start();
String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
System.out.println("입력하신 값은 " input + "입니다.");
th.interrupt();
System.out.println("isInterrupted() : " + th.isInterrupted());
}
}
class ExtendsThread extend Thread {
public void run() {
int i = 5;
while(i!=0 && !isInterrupted()) {
System.ou.println(i--);
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
// interrupt();
}
}
System.out.println("쓰레드 th 종료");
}
}
5
4
입력하신 값은 abcd입니다.
isInterrupted() : false
3
2
1
🔻 메서드 ' interrupt() ' 가 호출되면서 ' Thread.sleep(1000) ' 에서 InterruptedException이 발생
되고 쓰레드의 interrupted상태를 false로 자동 초기화되면서 카운트가 중단되지 않은 것이다
🔻 catch블럭에 메서드 ' interrupt() ' 를 추가해줘 쓰레드의 interrupted의 값을
true로 다시 바꿔주면 카운트다운이 중단될 것이다
stop() - 쓰레드를 즉시 종료시킨다
suspend() - 쓰레드를 일시정지시킨다
resume() - ' suspend() ' 메서드에 의해 정지된 쓰레드를 다시 실행대기 상태로 만든다
➜ 쓰레드의 실행을 제어하는 가장 손쉬운 방법
➜ 하지만, ' suspend() ' 와 ' stop() ' 메서드가 교착상태 (Deadlock)을 일으키기 쉽게 작성되어 있어 모두 'Deprecated (하위 호완성을 위해 삭제하지 않았다는 의미)'로 되어있다
➜ 이 문제를 해결하는 방법
class MyThread {
public static void main(String args[]) {
// 객체지향적이지 않은 방식
// ThreadImplements r1 = new ThreadImplements();
// ThreadImplements r2 = new ThreadImplements();
// Thread th1 = new Thread(r1, "*");
// Thread th2 = new Thread(r2, "**");
ThreadImplements th1 = new ThreadImplments("*");
ThreadImplements th2 = new ThreadImplments("**");
th1.start();
th2.start();
try {
Thread.sleep(2000);
th1.suspend();
Thread.sleep(2000);
th2.suspend();
Thread.sleep(2000);
th1.resume();
Thread.sleep(2000);
th1.stop();
th2.stop();
} catch(InterruptedException e) {}
}
}
class ThreadImplements implements Runnable {
volatile boolean suspended = false;
volatile boolean stopped = false;
Thread th;
ThreadImplements(String name) {
th = new Thread(this, name); // Thread(Runnable r, String name)
}
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() { th.start(); }
}
🔻 각 쓰레드 th1, th2가 다른 실행상태를 가지도록
하나의 ThreadImplements class의 객체를 공유하지 않는다
🔻 객체지향적으로 정리된 코드이며, 쓰레드를 생성하는 부분을 주목하자
yield() - 다른 쓰레드에게 양보한다
➜ 실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보(yield)하고 자신은 실행대기 상태가 된다
➜ ' yield() ' 와 ' interrupt() ' 메서드를 적절히 사용하면, 프로그램의 응답성을 높이고 보다 효율적인 실행을 가능하게 해준다
- 효율적인 코드
while(!stopped) {
if(!suspended) {
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
}
}
🔻 만약 변수 ' supended ' 의 값이 true라면, 즉 ' suspend() ' 메서드를 호출해 잠시 실행을 멈추게
한 상태라면, 해당 쓰레드는 주어진 실행시간을 그저 while문을 의미없이 돌면서 낭비하게 된다
⭣ ⭣
while(!stopped) {
if(!suspended) {
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
} else {
Thread.yield();
}
}
🔻 ' yield() ' 메서드를 호출해서 남은 실행시간을 while문에서 낭비하지 않고
다른 쓰레드에 양보하게 되어 효율적이다
- 응답성이 좋아지는 코드
public void suspend() {
suspended = true;
}
public void stop() {
stopped = true;
}
🔻 ' stop() ' 메서드가 호출됐을 때 ' Thread.sleep(1000) ' 에 의해 쓰레드가 일시
정지 상태에 머물러 있는 상황이라면, 변수 ' stopped ' 의 값이 true로 바뀌었어도
쓰레드가 정지될 때까지 최대 1초의 시간지연이 생기는 것이다
⭣ ⭣
public void suspend() {
suspended = true;
th.interrupt();
}
public void stop() {
stopped = true;
th.interrupt();
}
🔻 이렇게 하면, ' Thread.sleep(1000) ' 에서 InterruptedException이 발생하여
즉시 일시정지 상태에서 벗어나게 되므로 응답성이 좋아진다 !
join() - 다른 쓰레드의 작업을 기다린다
➜ 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 한다.
➜ 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속 한다
( 시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다리게 된다 )
➜ ' sleep() ' 처럼 ' interrupt() ' 메서드에 의해 대기상태에서 벗어날 수 있으며, ' join() ' 메서드가 호출되는 부분을 try - catch 문으로 감싸야 한다
➜ ' sleep() ' 메서드와 달리 현재 쓰레드가 아닌 특정 쓰레드에 대해 동작하므로 static 메서드가 아니다
void join()
void join(long millis)
void join(long millis, int nanos)
➜ ' join() ' 메서드를 언제 사용해야 할까?
ex) 가비지 컬렉터(GC)를 간단히 구현한 코드
if(gc.freeMemory() < requiredMemory) { // 필요한 메모리가 사용할 수 있는 양보다 클 경우
gc.interrupt(); // 가비지컬렉터('gc')를 깨운다
}
🔻 쓰레드 gc가 ' interrupt() ' 메서드에 의해 실행대기 상태가 됐음에도,
쓰레드 gc가 수행되기 이전에 main 쓰레드의 작업이 수행되어 메모리가 초과될 수 있다
⭣ ⭣
if(gc.freeMemory() < requiredMemory) {
gc.interrupt();
try {
gc.join(100); // 쓰레드 gc에 0.1초의 시간동안 작업하도록 해준다
} catch(InterruptedException e) {}
}
🔻 쓰레드 gc가 작업할 시간을 어느 정도 주고 main 쓰레드가 기다리도록 해서,
사용할 수 있는 메모리가 확보된 다음 작업을 수행하도록 할 수 있다
🌚 쓰레드의 동기화
✔️ 쓰레드의 동기화(Synchronization) ?
🔹 쓰레드의 동기화
- 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하게 막는 것
➜ 쓰레드의 동기화(Syschronization)
- 멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유하여 작업하기 때문에 서로의 작업에 영향을 주게 된다
➜ ATM 출금을 예로 들면, 출금 프로그램에서 쓰레드 A가 200만원이 있는 계좌에서 출금을 하는 중에 쓰레드 A에게 주어진 실행시간이 끝나고, 다른 쓰레드 B에게 제어권이 넘어가 해당 계좌에서 50만원을 출금했다면?
다시 쓰레드 A의 차례가 됐을 때 끝내지 못한 출금 작업을 수행하게 되고 해당 계좌의 잔액이 -50만원이 돼있을 수 있다
➜ 이런 상황을 방지하기 위해 도입된 개념이 '임계 영역(Critical section)'과 '잠금(Lock)' 이다
- 공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정
- 공유 데이터(객체)가 갖고 있는 lock을 획득한 단 하나의 쓰레드만 이 임계 영역 내의 코드를 수행 가능
(하나의 객체는 lock을 하나만 갖고 있다) - 해당 쓰레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납하면, 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드 수행 가능
- 공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정
🔹 쓰레드의 동기화 구현 방법
- ' synchronized ' 키워드를 이용한 동기화
- ' java.util.concurrent.locks ' & ' java.util.concurrent.atomic' package를 통한 동기화 (JDK1.5 ~)
✔️ synchronized를 이용한 동기화
🔹 synchronized ?
- 임계 영역을 설정하는데 사용
🔹 동기화가 필요한 예시
- 한 쓰레드가 if문을 통과하고 ' Thread.sleep(1000) ' 로 인해 출금하기(' balance -= money ') 직전에 다른 쓰레드에게 제어권이 넘어간다
- 즉, 다른 쓰레드가 끼어들어 출금을 먼저 하게 되고, 다시 이전의 쓰레드에게 제어권이 넘어오면 if문 다음부터 수행하게 된다
- 잔고에 남아있는 금액보다 많은 금액을 출금하게 되는 문제 발생
- if문과 출금하는 코드(' balance -= money ')는 하나의 임계 영역으로 묶어져야 한다
( ' Thread.sleep(1000) ' 가 없더라도 쓰레드들 서로 영향을 줄 수 있기에 동기화가 반드시 필요 )
public void withdraw(int money) { // 출금 메서드
// balance : 잔고
// money : 출금하려는 금액
if(balance >= money) {
try { Thread.sleep(1000); catch(Exception e) {}
balance -= money;
}
}
🔹 동기화 방법
1. 메서드 전체를 임계 영역으로 지정
public synchronized void withdraw(int money) { ... }
🔻 쓰레드는 synchronized 메서드(' withdraw() ')가 호출된 시점부터 이 메서드가 종료되어
lock를 반환할 때까지 다른 쓰레드는 해당 메서드를 호출하더라도 실행대기 상태에 머문다
2. 특정한 영역을 임계 영역으로 지정 (synchronized 블럭)
➜ 효율적인 프로그램을 작성하고자 한다면, 이 방식을 이용하여 임계 영역을 최소화하자
( 메서드 전체에 임계 영역 지정 X )
// synchronized(객체의 참조변수) { ... } // lock을 걸고자 하는 객체를 참조하는 참조변수
public void withdraw(int money) {
synchronized(this) {
if(balance >= money) {
try { Thread.sleep(1000); catch(Exception e) {}
balance -= money;
}
}
}
🔻 쓰레드는 이 블럭의 영역에 들어가면서부터 지정된 객체(' this ')의 lock을 얻게 되고
이 블럭을 벗어나면 lock을 반납
- 두 방법 모두 lock의 획득과 반납이 자동적으로 이루어진다
- 주의해야할 점
➜ ' synchronized ' 를 이용한 동기화는 지정된 영역의 코드를 한 번에 하나의 쓰레드가 수행하도록 보장하는 것일 뿐, 영역 밖의 코드까지 동기화를 보장해준다는 생각 X
class Account {
private int balance = 1000;
public void withdraw(int money) {
synchronized(this) { ... }
}
}
🔻 instance 변수인 ' balance ' 의 접근 제어자는 private으로 선언해주지 않으면,
아무리 동기화를 해줘도 이 값의 변경(외부에서의 접근)을 막을 방법이 없다 !
✔️ 특정 쓰레드가 lock을 반납하지 않은 경우 - wait() & notify()
🔹 wait() & notify()
- synchronized 된 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, ' wait() ' 메서드를 호출
➜ wait()을 호출한 쓰레드는 lock을 반납하고 기다린다
( 객체의 대기실 (waiting pool) 에서 통지를 기다린다) - 나중에 작업을 진행할 수 있는 상황이 되면 ' notify() ' 메서드를 호출해서 작업을 중단했던 쓰레드가 다시 lock을 얻어 작업을 재진행한다
- notifyAll - waiting pool에서 기다리고 있는 모든 쓰레드에게 통보
( waiting pool은 객체마다 존재하기 때문에, ' notifyAll() ' 이 호출된 객체의 waiting pool에 대기 중인 쓰레드만 해당된다 )
그래도 lock은 하나의 쓰레드만 받을 수 있어 나머지는 다시 lock을 기다리는 상태가 된다
void wait()
void wait(long timeout) // 지정된 시간동안만 기다린다. 즉, 지정된 시간이 지난 후에 자동적으로 notify()를 호출하는 느낌
void wait(long timeout, int nanos)
void notify()
void notifyAll()
🔻 특정 객체에 대한 것 ➜ Object class에 정의되어 있다
🔻 synchronized 블럭 내에서만 사용 가능
🔹 특정 쓰레드(Customer 쓰레드)가 lock을 계속해서 쥐고 기다리는 문제
- 식당에서 음식(dish)을 만들어서 Table에 서빙(serve)하는 요리사(Cook) 쓰레드와 Table의 음식을 먹는(eat) 손님(Customer) 쓰레드로 구현
- 여러 쓰레드(손님, 요리사)가 Table class의 객체 하나를 공유하는데 동기화 X
- CUST1 쓰레드(손님1)가 COOK1 쓰레드(요리사)가 테이블에 음식을 놓는 중(' serve() ' 메서드 )에 음식을 가져가려는(' eat() ' 메서드) 문제 (ConcurrentModificationException 발생)
- CUST1 쓰레드가 마지막 음식을 가져가는 도중(' eat() ')에 CUST2 쓰레드가 해당 음식을 낚아채가는(' eat() ') 문제 (IndexOutOfBoundsException 발생)
➜ 여러 쓰레드가 공유하는 객체인 Table의 ' serve() ' 와 ' eat() ' 메서드를 동기화(synchronized)했다
class Table {
...
public synchronized void serve(String dish) {
...
}
public void eat(String dishName) {
synchronized(this) {
...
}
...
}
...
}
- 손님 쓰레드가 Table class의 객체의 lock을 반납하지 않고 계속 쥐고 있어, 요리사 쓰레드가 음식을 서빙(' serve() ')하려 해도 lock을 얻을 수 없어 정상적으로 작업이 수행되지 않는 문제 발생
// eat() 메서드 내 일부 코드
synchronized(this) {
while(dishes.size() == 0) { // 무한 반복
try { Thread.sleep(500); } catch(InterruptedException e) {}
}
...
}
➜ ' wait() ' 과 ' notify() ' 메서드 추가
class Main() {
public static void main(String[] args) throws Exception {
Table table = new Table(); // 테이블 객체 생성
new Thread(new Cook(table), "Cook").start(); // 해당 테이블 담당 요리사
new Thread(new Customer(table, "pizza"), "Customer1").start(); // 피자 주문 손님
new Thread(new Customer(table, "burger"), "Customer2").start(); // 버거 주문 손님
Thread.sleep(2000);
System.exit(0);
}
}
class Customer implements Runnable {
// 생성자 Customer(), run() 구현한 내용 (eat() 메서드 수행)
}
class Cook implements Runnable {
// 생성자 Cook(), run() 구현한 내용 (serve() 메서드 수행)
}
class Table {
String[] menu = { "pizza", "burger" };
final int MAX_FOOD = 3;
private ArrayList<String> dishes = new ArrayList<>();
// Cook 쓰레드가 호출
public synchronized void serve(String dish) {
while(dishes.size() >= MAX_FOOD) { // 테이블에 음식이 가득 찼다
wait(); // 요리사 쓰레드를 기다리게 한다 (요리사 쓰레드의 lock 반납)
// waiting pool에서 기다리고 있는 다음 쓰레드가 lock을 얻고 작업 수행
Thrad.sleep(500);
}
// 요리사 쓰레드가 깨어나면(lock을 얻으면) 음식을 추가한다
dishes.add(dish);
notify(); // 잠자고 있는 손님 쓰레드를 깨운다
}
// Customer1, Customer2 쓰레드가 호출
public eat(String dish) {
synchronized(this) {
while(dishes.size() == 0) { // 음식이 하나도 없다
wait(); // 음식이 없으니 손님 쓰레드를 기다리게 한다 (손님 쓰레드의 lock 반납)
// waiting pool에서 기다리고 있는 다음 쓰레드가 lock을 얻고 작업 수행
Thread.sleep(500);
}
// 손님 쓰레드가 깨어나면(lock을 얻으면) while문 수행
while(true) {
for(int i = 0; i < dishes.size(); ++i) {
if(dish.equals(dishes.get(i))) { // 원하는 음식이 있다면
// 음식을 먹는다
dishes.remove(i);
notify(); // 잠자고 있는 쓰레드(요리사 or 다른 손님)를 깨운다
return;
}
}
try { // 현재 lock을 쥐고 있는 손님 쓰레드가 원하는 음식이 없다
wait(); // 손님 쓰레드를 다시 기다리게 한다 (손님 쓰레드의 lock 반납)
// waiting pool에서 기다리고 있는 다음 쓰레드가 lock을 얻고 작업 수행
Thread.sleep(500);
} catch(InterruptedExceptione) {}
} // while(true)
} // syschronized
}
public int menCount() { ... }
}
🔻 하지만, Table class의 객체의 waiting pool에서
요리사 쓰레드와 손님 쓰레드가 같이 대기한다는 것에 문제가 있다
➜ 기아 현상 & 경쟁 상태
🔹 기아 현상과 경쟁 상태
- 위의 코드를 예시로 들면,
피자 주문 손님(쓰레드 "Customer1")이 피자를 만들어 달라고 ' notify() ' 를 호출해도, 요리사(쓰레드 "Cook")이 아닌 햄버거 주문 손님(쓰레드 "Customer2)가 통지를 받고 lock을 얻는 경우가 발생 !
( ' notify() ' 는 Table class의 객체의 wating pool에서 대기중인 쓰레드들 중에 하나를 임의로 선택해서 통지하기 때문이다 )
➜ 최악의 경우 요리사 쓰레드는 계속 통지를 받지 못하고 오랫동안 기다리는 "기아(starvation) 현상" 발생
➜ 이 문제는 ' notifyAll() ' 을 사용해 해결 가능 (요리사 쓰레드도 결국 통지를 받아 lock을 얻고 작업 진행) - 하지만, 불필요하게 요리사가 아닌 손님 쓰레드까지 통지를 받아, 통지를 받은 모든 쓰레드들이 lock을 얻기 위해 경쟁하게 된다
" 경쟁 상태 (race condition) "
➜ 쓰레드들(요리사 쓰레드, 손님 쓰레드)을 구별해서 통지하는 것이 필요
➜ Lock과 Condition을 이용하면, 이러한 선별적인 통지도 가능하다
✔️ Lock과 Condition을 이용한 동기화 ( java.util.concurrent.locks )
🔹 Lock class의 종류
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/package-summary.html
java.util.concurrent.locks (Java Platform SE 8 )
Package java.util.concurrent.locks Description Interfaces and classes providing a framework for locking and waiting for conditions that is distinct from built-in synchronization and monitors. The framework permits much greater flexibility in the use of loc
docs.oracle.com
ReentrantLock - 재진입이 가능(?)한 가장 일반적인 배타 lock
- wait() & notify() 처럼, 특정 조건에서 lock을 풀고 나중에 다시 lock을 얻고 임계영역으로 들어와서 이후의 작업을 수행
- ReentrantLock의 생성자
ReentrantLock()
ReentrantLock(boolean fair)
🔻 매개변수로 true를 주면, lock이 풀렸을 때 가장 오래 기다린 쓰레드가 lock을 획득
(공정한 방법) ➜ 그만큼 성능이 떨어지기에 대부분 이렇게 처리 X
- lock() & unlock()
void lock() // lock을 잠근다
void unlock() // lock을 해지
boolean isLocked() // lock이 잠겼는지 확인
🔻 synchronized 블럭과 달리, lock class들은 수동으로 lock을 잠그고 해제해야 한다
lock.lock(); // ReentrantLock lock = new ReentrantLock();
try {
// 임계 영역
} finally {
lock.unlock(); // 무조건 수행
}
🔻 임계 영역 내에서 예외가 발생하거나 return문으로 빠져 나가게 되면
lock이 풀리지 않을 수 있어, unlock() 메서드는 try - finally 문으로 감싸자
➜ 대부분의 경우 lock() & unlock() 메서드 대신 synchronized 블럭 사용 가능해서
synchronized 블럭을 이용하는 것이 날 수 있다
- tryLock() - lock을 얻을 때까지 기다리지 않는다
boolean tryLock()
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
🔻 ' lock() ' 메서드는 lock을 얻을 때까지 쓰레드를 block 시키므로 쓰레드의 응답성 ⭣⭣
➜ 이때, ' tryLock() ' 메서드를 이용해서 지정된 시간동안 lock을 얻지 못했을 경우
작업 중단 여부를 결정하도록 코드 작성 가능
🔻 InterruptedException을 발생시켜 지정된 시간동안 lock을 얻으려고 기다리던 중에
' interrupt() ' 메서드로 작업을 취소할 수 있도록 코드 작성 가능
ReentrantReadWriteLock - 읽기에는 공유적이고, 쓰기에는 배타적인 lock
- 읽기 lock과 쓰기 lock을 따로 제공하며, 읽기 lock만 여러 쓰레드가 중복해서 걸 수 있다
StampedLock - ReentrantReadWriteLock + 낙관적인 lock 가능 ( by Stamp(long타입의 정수값) )
- 일반적인 lock ➜ 읽기 lock이 걸려있으면, 쓰기 lock은 읽기 lock이 풀려야만 얻을 수 있다
낙관적인 lock ➜ ' 낙관적 읽기 lock ' 은 쓰기 lock에 의해 바로 풀린다
➜ 따라서 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 걸어야 한다
// StampedLock을 이용한 낙관적 읽기의 예
int getBalanced() {
long stamp = lock.tryOptimisticRead(); // 낙관적 읽기 lock을 건다
int curBalance = this.balance; // 공유 데이터인 balance를 읽어온다
if(!lock.validate(stamp)) { // 쓰기 lock에 의해 낙관적 읽기 lock이 풀렸는지 확인
stamp = lock.readLock(); // 낙관적 읽기 lock이 풀렸으면, 읽기 lock을 얻으려고 기다린다
try {
curBalance = this.balance; // 공유 데이터를 다시 읽어온다
} finally {
lock.unlockRead(stamp); // 읽기 lock을 푼다
}
}
return curBalance; // 낙관적 읽기 lock이 풀리지 않았으면 곧바로 읽어온 값을 반환
}
🔹 ReentrantLock & Condition
- 쓰레드의 종류를 구분해 선별적인 통지를 가능하게 한 것이 Condition이다
➜ 공유 객체의 waiting pool에 몰아넣는 것이 아닌,
쓰레드의 종류별로 Condition을 만들어서 waiting pool를 분리 ! - Condition은 어떻게 쓰는 것인가?
- 이미 생성된 lock으로부터 Condition을 생성
- wait() & notify() 대신 await() & signal()을 사용 !
class Main() {
public static void main(String[] args) throws Exception {
Table table = new Table(); // 테이블 객체 생성
new Thread(new Cook(table), "Cook").start(); // 해당 테이블 담당 요리사
new Thread(new Customer(table, "pizza"), "Customer1").start(); // 피자 주문 손님
new Thread(new Customer(table, "burger"), "Customer2").start(); // 버거 주문 손님
Thread.sleep(2000);
System.exit(0);
}
}
class Customer implements Runnable {
// 생성자 Customer(), run() 구현한 내용 (eat() 메서드 수행)
}
class Cook implements Runnable {
// 생성자 Cook(), run() 구현한 내용 (serve() 메서드 수행)
}
class Table {
String[] menu = { "pizza", "burger" };
final int MAX_FOOD = 3;
private ArrayList<String> dishes = new ArrayList<>();
private ReentrantLock lock = new ReentrantLock(); // lock 생성
// lock으로 condition 생성
private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondtion();
// Cook 쓰레드가 호출
public void serve(String dish) {
lock.lock(); // lock 걸기
try { // 임계영역
while(dishes.size() >= MAX_FOOD) { // 테이블에 음식이 가득 찼다
try {
forCook.await(); // 요리사 쓰레드를 기다리게 한다
Thrad.sleep(500);
} catch(InterrutedException e) {}
}
// 요리사 쓰레드가 깨어나면 음식을 추가한다
dishes.add(dish);
forCust.signal(); // 잠자고 있는 손님 쓰레드를 깨운다
} finally {
lock.unlock(); // lock 해지
}
}
// Customer1, Customer2 쓰레드가 호출
public eat(String dish) {
lock.lock(); // lock 걸기
try {
while(dishes.size() == 0) { // 음식이 하나도 없다
forCust.await(); // 손님 쓰레드를 기다리게 한다
Thread.sleep(500);
}
// 손님 쓰레드이 깨어나면 while문 수행
while(true) {
for(int i = 0; i < dishes.size(); ++i) {
if(dish.equals(dishes.get(i))) { // 원하는 음식이 있다면
// 음식을 먹는다
dishes.remove(i);
forCook.signal // 잠자고 있는 요리사 쓰레드를 깨운다
return;
}
}
try { // 현재 lock을 쥐고 있는 손님 쓰레드가 원하는 음식이 없다
forCust.await(); // 손님 쓰레드를 다시 기다리게 한다
Thread.sleep(500);
} catch(InterruptedExceptione) {}
} // while(true)
} finally {
lock.unlock(); // lock 해지
}
}
public int menCount() { ... }
}
🔻 '기아 현상'이나 '경쟁 상태'는 확실히 개선됐다
🔻 그래도 쓰레드의 종류에 따라 구분하여 통지할 수 있게 된 것일 뿐
( 요리사 쓰레드가 통지를 받아야 할 상황에서 손님 쓰레드가 통지를 받지 않는 것 ),
여전히 특정 쓰레드를 선택할 수 없기 떄문에 같은 종류의 쓰레드(Customer1, Customer2)간의
'기아 현상'이나 '경쟁 상태'가 발생할 가능성은 남아 있다
➜ Condition을 더 세분화하면 더 개선 가능하다 !
✔️ volatile (휘발성)
🔹 volatile ?
- 코어(Core)는 메모리에서 읽어온 값을 캐시(cache)에서 저장하고, 메모리가 아닌 캐시에서 먼저 값을 읽어서 작업하기 때문에, 도중에 메모리에 저장된 변수의 값이 변경돼도 캐시에 저장된 값이 갱신되지 않아서 캐시와 메모리에 저장된 값이 다른 경우가 발생한다

- volatile을 변수 앞에 붙이면,
코어(Core)가 변수의 값을 읽어올 때 캐시(cache)가 아닌 메모리(Memory)에서 읽어오기 때문에,
멀티 코어 프로세서에서의 캐시와 메모리간의 값의 불일치를 해결 가능
🔹 volatile로 long과 double을 원자화
- 원자화 ?
: 작업을 더 이상 나눌 수 없게 한다는 의미
➜ volatile은 해당 변수에 대한 읽기나 쓰기가 원자화되는 것
(단, 동기화하는 것은 아니다 ! )
➜ synchronized 블럭은 여러 문장을 원자화함으로써 쓰레드의 동기화를 구현한 것 - JVM은 데이터를 4 byte(=32bit) 단위로 처리하기 때문에, int보다 크기가 큰 long과 double타입의 변수는 하나의 명령어로 값을 읽거나 쓸 수 없어, 작업의 중간에 다른 쓰레드가 끼어들 여지가 있다
- 이를 방지하기 위해 변수를 읽고 쓰는 모든 문장을 synchronized 블럭으로 감쌀 수도 있지만,
변수를 선언할 때 volatile을 붙여 원자화하는 것이 훨씬 간단하다
( 상수(final)는 멀티쓰레드에 안전하기 때문에 volatile을 붙일 수 없다 )
➜ 단, 동기화가 필요한 경우엔 깜빡하지 말고 동기화 처리를 해줘야 한다 !
✔️ fork & join 프레임워크
🔹 fork & join 프레임워크
- 하나의 작업을 작은 단위로 나눠서 여러 쓰레드가 동시에 처리하기 쉽게 만들어주는 것
🔹 fork & join 프레임워크 구현 및 사용
- 수행할 작업에 따라 두 class 중 하나를 상속받아 구현해야 한다
- RecursiveAction : 반환값 X 작업을 구현할 때 상속
- RecursiveTask : 반환값 O 작업을 구현할 때 상속
- ' run() ' 을 구현했던 것처럼 ' compute() ' 라는 추상 메서드를 구현한다
- 쓰레드 풀(Thread pool)을 생성하고, 쓰레드풀과 수행할 작업을 생성한다
- 쓰레드 풀
➜ 쓰레드가 수행해야 하는 작업이 담긴 큐를 제공, 각 쓰레드는 자신의 작업 큐에 담긴 작업을 순서대로 처리
장점
➜ 지정된 수의 쓰레드를 생성해서 미리 마들어 놓고 반복해서 재사용 가능
➜ 반복 작업이 줄고, 너무 많은 쓰레드가 생성되어 성이 저하되는 것을 막아준다
- 쓰레드 풀
- 쓰레드를 시작할 때 ' start() ' 를 호출했던 것처럼, fork&join 프레임워크로 수행할 작업도 ' invoke() ' 를 호출해서 작업을 시작한다
fork()
: 해당 작업을 쓰레드 풀의 작업 큐에 넣는다 (비동기 메서드; 결과를 기다리지 X)
join()
: 해당 작업의 수행이 끝날 때까지 기다렸다가, 수행이 끝나면 그 결과를 반환한다 (동기 메서드; 결과를 기다린다)
import java.util.concurrent.*;
class Main {
static final ForkJoinPool pool = new ForkJoinPool(); // 쓰레드 풀 생성
public static void main(String[] args) {
long from = 1L, to = 100_000_000L;
SumTask task = new SumTask(from, to); // 수행할 작업을 생성
Long result = pool.invoke(task); // 작업 시작
}
}
class SumTask extends RecursiveTask<Long> { // 반환값이 있어야 하기 때문에 RecursiveTask 상속
long from, tol
SumTask(long from, long to) { // 생성자
this.from = from;
this.to = to;
}
public Long compute() { // 추상 메서드 compute() 구현
long size = to - from + 1;
if(size <= 2)
return sum();
long half = (from + to)/2;
SumTask leftSum = new SumTask(from, half);
SumTask rightSum = new SumTask(half + 1, to);
leftSum.fork(); // 작업 큐에 저장 (작업 큐에 넣으면 다른 쓰레드가 가져가서 처리할 수 있다)
//compute()의 재귀호출이 종료된 후, join()의 결과를 기다렸다가 리턴!
return rightSum.compute() + leftSum.join();
}
long sum() {
// from ~ to의 모든 숫자를 더한 결과를 반환
}
}
🔻 ' fork() ' 메서드로 작업 큐에 저장하면,
자신의 작업 큐가 비어있는 쓰레드가 이 작업을 가져가서 처리한다
➜ " 작업 훔쳐오기 (work stealing) " - 쓰레드풀에 의해 자동적으로 이뤄진다 !
( 이 덕분에, 여러 쓰레드가 골고루 작업을 나눠 처리 가능한 것 )
🔻 ' compute() ' 가 재귀호출될 떄, ' join() ' 은 호출되지 않는다
🔻 fork&join 프레임워크로 계산한 결과보다 for문으로 계산한 결과가 시간이 덜 걸린다
➜ 항상 멀티쓰레드로 처리하는 것이 빠르다고 생각 X (테스트 해보고 이득이 있을 때만)

▼ Study📋
☑️ 쓰레드들은 항상 일정하게 번걸아가면서 작업을 수행하지는 않는다
☑️ Thread class의 생성자가 Runnable interface를 구현한 inatance를 참조하도록 되어 있어서, Thread class를 상속받아 ' run() ' 을 Overriding 하지 않고도 Runnable interface를 구현한 class(외부)로부터 ' run() ' 을 제공받을 수 있다
☑️ Thread class 기능의 확장 여부에 따라 Thread 클래스를 상속받을 것인지, Runnable interface를 구현할 것인지 선택하자 !
☑️ 가비지 컬렉터(GC)는 객체를 참조하는 참조 변수가 없으면 메모리에서 제거하는 기능을 하는데, 참조변수 없이 쓰레드를 생성해도 해당 쓰레드의 참조가 ' ThreadGroup ' 에 저장되어 있기 때문에 가비지 컬렉터에 제거되지 않는다
☑️ 데드락(Deadlock)을 방지하기 위해 나온 것이 " waiting pool " 이다
🌑 프로세스 (Process) & 쓰레드 (Thread)
✔️ 프로세스 (Process)
🔹 프로세스 (Process) ?
- 프로그램을 실행하면 OS로부터 실행에 필요한 자원(데이터, 메모리)을 할당받아 프로세스가 된다
- 이 프로세스의 자원을 이용해서 작업을 수행하는 것이 쓰레드(Tread) !
- 프로세스 ➜ 작업공간 / 쓰레드 ➜ 노동자
- 하나의 프로세스가 가질 수 있는 쓰레드의 개수는 제한 X
➜ 단, 쓰레드가 작업을 수행하는데 개별적인 메모리 공간(호출스택)을 필요로 하기 때문에, 프로세스의 메모리 한계에 따라 생성할 수 있는 쓰레드의 수가 결정
(실제로 메모리 한계에 다다를 일은 거의 없니 이 부분은 걱정할 필요 X)

✔️ 멀티태스킹(Multi-tasking) & 멀티쓰레딩(Multi-threading)
🔹 멀티태스킹 & 멀티쓰레딩
- 여러 개의 프로세스가 동시에 실행되는 것 ➜ 멀티태스킹 (Multi-tasking)
(대부분의 OS (Windows, Unix, etc)가 지원) - 하나의 프로세스 내에서 여러 쓰레드가 동시에 실행되는 것 ➜ 멀티쓰레딩 (Multi-threading)
- CPU의 사용률 증가
- 자원을 효율적으로 사용 가능
- 사용자에 대한 응답성 향상
- 작업이 분리되어 코드가 간결해진다
- 우리가 여러 작업을 동시에 수행할 수 있는 것은 멀티쓰레딩 덕분이다
(메신저 채팅, 파일 다운로드, 게임 등을 동시에 하는 것)
- 따라서, 여러 사용자에게 서비스를 해주는 서버 프로그램의 경우 멀티쓰레드로 작성하는 것은 필수 !
➜ 하나의 서버 프로세스가 여러 개의 쓰레드를 생성해서 쓰레드와 사용자의 요청이 일대일로 처리되도록 한다 - 단, 여러 개의 쓰레드가 하나 프로세스의 자원을 공유하며 작업하기 때문에, 동기화(Synchronization), 교착상태(Deadlock)와 같은 문제를 고려해야 한다
🌒 쓰레드의 구현과 실행
✔️ 쓰레드 생성 (Thread class 상속 / Runnable interface 구현)
🔹Thread class를 상속받아 쓰레드를 구현하는 방법
- 다른 class를 상속받을 수 없다
- Thread class의 메서드 ' run() ' 을 Overriding 해주면 된다
🔹 Runnable interface를 구현하는 쓰레드를 구현하는 방법
- 재사용성(Reusability)이 높고 코드의 일관성(Consistency) 유지 가능
➜ 일반적이고 객체지향적인 방법 - 추상메서드인 ' run() ' 을 구현(implements)해주면 된다
(Runnable interface엔 오로지 run()만 정의되어 있다)
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
🔹 두 방법의 성능 차이
- ' run() ' 메서드 코드의 성능은 동일하다
- Thread class의 ' run() ' 메서드 하나 때문에 상속 기능을 사용하는 것은 비효율
- 만약, Thread class의 또다른 기능을 확장하거나 재정의해야 할 경우엔 Runnable interface를 구현하는 것보다 Thread class을 상속받는 방법이 더 좋을 수 있다
➜ Thread class 기능의 확장 여부에 따라 Thread class를 상속받을 것인지, Runnable interface를 구현할 것인지 선택하자 !
✔️ Thread class 상속
🔹 instance 생성 방법
- Thread class를 상속받은 Sub class의 intance를 생성해주면 된다
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println("main end");
}
}
public class MyThread extends Thread{
int num;
public MyThread() {
this.num = 0;
}
public MyThread(int num) {
this.num = num;
}
@Override
public void run() {
System.out.println(this.num + " thread start");
try{
Thread.sleep(1000); // 1초간 시간을 지연
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(getName());
System.out.println(this.num +" thread end");
}
}
main end
0 thread start
Thread-0
0 thread main
🔻"main end" 가 먼저 출력된 것을 보면,
main 쓰레드와 다른 쓰레드(' myThread ')가 생성되어 동시에 실행된 것을 알 수 있다
( " 쓰레드와 호출스택 관계 " 참고 )
✔️ Runnable interface 구현
🔹 instance 생성 방법
- Runnable interface를 구현한 class의 intance를 생성한다
- 생성한 instance를 Thread class의 생성자의 매개변수로 제공하여 Thread class의 객체를 생성한다
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable(i));
thread.start();
System.out.println("main end");
}
}
public class MyRunnable implements Runnable {
int num;
public MyRunnable() {
this.num = 0;
}
public MyRunnable(int num) {
this.num = num;
}
@Override
public void run() {
System.out.println(this.num + " thread start");
try {
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
System.out.println(this.num +" thread end");
}
}
main end
0 thread start
Thread-0
0 thread main
🔻Runnable interface를 구현하여 쓰레드를 생성한 경우엔 Thread class의 static메서드인
' currentThread() ' 를 호출하여 현재 실행중인 쓰레드의 참조를 얻어 와야만
' getName() ' 과 같은 Thread class의 메서드를 호출(사용) 가능
🔻쓰레드의 이름을 아래와 같은 생성자나 메서드를 통해서 지정해주지 않으면
"Thread-번호" 형식으로 이름이 정해진다
Thread(Runnable target, String name)
Thread(String name)
void setName(String name)
🔹 Thread class의 객체가 Thread class의 instance가 아닌 Runnable interface를 구현한 class의 instance를 참조하는 이유?
- Thread class의 생성자가 Runnable interface를 구현한 inatance를 참조하도록 되어 있기 때문이다 (아래 코드 참고)
- Thread class를 상속받아 ' run() ' 메서드를 Overriding 하지 않고도 Runnable interface를 구현한 class(외부)로부터 ' run() ' 을 제공받을 수 있는 이유이기도 하다
public class Thread {
private Runnable r;
public Thread(Runnable r) {
this.r = r; // Runnable interface를 구현한 instance를 참조
}
public void run() {
if(r! = null)
r.run(); // Runnable interface를 구현한 instance의 run()을 호출
}
...
}
✔️ 쓰레드의 실행 방법
🔹 start()
- 쓰레드를 생성하고 ' start() ' 를 호출해야만 쓰레드가 실행된다
(바로 실행되는 것이 아닌 실행대기 상태에 있다가 차례가 되면 실행)
➜ 하나의 쓰레드에 대해 ' start() ' 는 한 번만 호출될 수 있다
ExtendsThread t1 = new ExtendsThread((); // 쓰레드 객체 생성
t1.start(); // 쓰레드 생성 및 실행
// t1.start(); // IllegalThreadStateException 발생
t1 = new ExtendsThread(); // 다시 생성
t1.start(); // OK
🌓 쓰레드(Thread)와 호출스택(Call stack)과의 관계

✔️ 쓰레드를 실행시킬 때 ' run() ' 이 아닌 ' start() ' 를 호출하는 이유?
🔹 새로운 쓰레드를 생성하고 ' start() ' 메서드를 호출한 후 호출스택의 변화

- main메서드에서 쓰레드의 ' start() ' 메서드를 호출
➜ main메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main쓰레드라고 한다 - ' start() ' 메서드는 새로운 쓰레드가 작업하는데 사용될 호출스택을 생성
- 새로 생성된 호출스택에 ' run() ' 메서드가 호출되어, 쓰레드가 독립된 공간에서 작업을 수행
- 이제 호출스택이 2개이므로 스케줄러가 정한 순서에 의해 번갈아 가면서 실행된다
- 스케줄러는 실행 대기중인 쓰레드들의 우선순위를 고려하여 실행 순서와 실행 시간을 결정
➜ 각 쓰레드들은 스케줄링한 것에 따라 자신의 순서가 되면 지정된 시간동안 작업을 수행한다 - 작업을 마친 쓰레드(위에선 ' run() ' 의 수행이 종료된 쓰레드)는 호출스택이 모두 비워지면서 이 쓰레드가 사용하던 호출스택은 사라진다 (이 경우에는 쓰레드도 함께 사라진다)
➜ 실행 중인 '사용자 쓰레드 (User Thread)'가 하나도 없을 때 프로그램은 종료된다 - ' start() ' 메서드가 아닌 ' run() ' 메서드를 호출하면 쓰레드와 해당 쓰레드가 작업하는데 사용될 호출스택이 생성되지 않는다
🌔 싱글 쓰레드 & 멀티 쓰레드
✔️ 싱글 쓰레드 프로세스 & 멀티 쓰레드 프로세스
🔹 싱글 쓰레드 프로세스와 멀티 쓰레드 프로세스의 비교 (싱글 코어 & 멀티 코어)

- 멀티쓰레드는 두 개의 쓰레드로 두 작업을 동시에 처리하는 것처럼 보이지만, 사실 짧은 시간동안 2개의 쓰레드(th1, th2)가 번갈아 가면서 작업을 수행하는 것이다
- 그렇기 때문에, 싱글 쓰레드로 작업을 수행한 시간과 멀티 쓰레드로 작업을 수행한 시간은 거의 같다
(오히려 쓰레드간의 작업 전환(context switching)에 시간이 더 걸려 멀티 쓰레드가 시간이 더 걸리는 경우도 있다)
➜ 싱글 코어에서 단순히 CPU만을 사용하는 계산작업이라면 오히려 멀티 쓰레드보다 싱글 쓰레드로 프로그래밍하는 것이 효율적

- 위와 같이 하나의 작업을 여러 쓰레드가 나눠서 처리하는 것이 병렬(parallel) 실행이라 한다
- 여러 쓰레드가 여러 작업을 동시에 진행하는 것을 병행(concurrent) 실행이라 한다
➜ 즉, 멀티 코어에서 멀티 쓰레드가 작업을 수행한다면 여러 작업을 동시에 진행 가능
➜ 두 작업이 겹치는 경우가 발생 (같은 자원을 두고 경쟁)

- 이와 달리 쓰레드들이 서로 다른 자원을 사용하는 작업의 경우에는 싱글 쓰레드 프로세스보다 멀티 쓰레드 프로세스가 더 효율적이다 !
ex) 사용자로부터 데이터를 입력받고 화면에 출력하는 작업
class MultiThreadEx {
// main 쓰레드
public static void main(String[] args) throws Exception {
// 화면에 숫자를 출력하는 쓰레드
NumPrintThread th2 = new NumPrintThread();
th2.start();
String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
System.out.println("입력하신 값은 " input + "입니다.");
}
}
class NumPrintThread extends Thread {
public void run() {
System.out.println(i);
try {
sleep(1000);
} catch(Exception e) {}
}
}
- Java가 OS(플랫폼) 독립적이라고 하지만 실제로는 OS 종속적인 부분이 몇 가지 있는데 쓰레드가 그 중의 하나이다
- JVM의 쓰레드 스케줄러에 의해 매 순간 상황에 따라 프로세스와 쓰레드에게 에게 할당되는 실행 시간이 일정하지 않다
➜ 쓰레드가 이러한 불확실성을 갖고 있는 것을 알아두자
🌕 쓰레드의 우선순위
✔️ 쓰레드의 우선순위 ?
🔹 우선순위 (Priority)
- 우선순위는 속성, 즉 멤버변수이다
- 이 우선수위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다
- 쓰레드가 수행하는 작업의 중요도에 따라 우선순위를 지정
(파일 전송 기능이 있는 메신저 ➜ 채팅을 전송하는 쓰레드 > 파일 다운로드를 처리하는 쓰레드)
🔹 쓰레드의 우선순위 지정하기
- 관련 메서드
void setPriority(int newPriority) // 쓰레드의 우선순위를 지정한 값으로 변경
int getPriority() // 쓰레드의 우선순위를 반환
public static final int MAX_PRIORITY = 10 // 최대우선순위
public static final int MIN_PRIORITY = 1 // 최소우선순위
public static final int NORM_PRIORITY = 5 // 보통우선순위
- 쓰레드가 가질 수 있는 우선순위의 범위는 1 ~ 10이다
(숫자가 높을수록 우선순위가 높은 것) - 쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받는다
- 쓰레드의 우선순위는 실행(' start() ' 호출)하기 전에만 변경 가능하다
class ThreadPriorityEx {
public static void main(String args[]) {
ExtendsThread th = new ExtendsThread();
th.setPriority(7); // th의 우선순위를 7로 변경
th.start(); // 쓰레드 실행
}
}
🔻쓰레드 th는 main 메서드에서 생성했기 때문에
main 메서드를 실행하는 쓰레드의 우선순위인 5를 상속받아 자동적으로 5가 된다
✔️ 싱글 코어와 멀티 코어에서의 우선순위 효과
🔹 싱글 코어
- 우선순위가 높은 쓰레드에게 상대적으로 우선순위가 낮은 쓰레드보다 더 많은 양의 실행시간이 주어진다
➜ 결과적으로 우선순위가 높은 쓰레드의 작업이 더 빨리 완료될 수 있다

🔹 멀티 코어
- 쓰레드의 우선순위에 따른 차이 X
- 굳이 우선수위에 차등을 주어 쓰레드를 실행하려면, 해당 프로그램이 실행되는 OS의 스케쥴링 정책과 JVM의 구현을 확인해야 한다
➜ JVM의 구현을 확인하더라도 OS의 스케쥴러에 종속적이기에 예측 정도만 가능 - 쓰레드에 우선순위를 부여하는 것보다 작업에 우선순위를 두어 PriorityQueue에 저장하고,
우선순위가 높은 작업이 먼저 처리되도록 하는 것이 나을 수 있다
🌖 쓰레드 그룹 (Thead Group)
✔️ 쓰레드 그룹 ?
🔹 쓰레드 그룹
- 서로 관련된 쓰레드를 그룹으로 묶어서 관리하기 위함 (폴더와 비슷한 개념)
- 쓰레드 그룹에 다른 쓰레드 그룹을 포함시킬 수 있다
- 사실 보안상의 이유로 도입된 개념
- 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경 O
- 다른 쓰레드 그룹의 쓰레드는 변경 X
✔️ 쓰레드 그룹 생성 & 지정
🔹 쓰레드 그룹의 생성
- 모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 한다
- 쓰레드 그룹의 생성 과정
- Java 어플리케이션 실행
- JVM이 main과 system이라는 쓰레드 그룹 생성
- JVM 운영에 필요한 쓰레드들을 생성해서 이 쓰레드 그룹에 포함시킨다
(main 쓰레드 ➜ main 쓰레드 그룹 / Finalize 쓰레드(가비지컬렉션(GC) 수행) ➜ system 쓰레드 그룹
- 보통 우리가 생성하는 쓰레드 그룹 ➜ main 쓰레드 그룹의 하위 쓰레드 그룹
🔹 쓰레드 그룹 관련 생성자 & 메서드
- 쓰레드 그룹을 지정하는 생성자를 사용하지 않은 쓰레드도 자동적으로 main 쓰레드 그룹에 속하게 된다
(기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 되는 특징)
https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html
Thread (Java Platform SE 8 )
Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group, and has the specified stack size. This constructor is identical to Thread(ThreadGroup,Runnable,
docs.oracle.com
🔹 참조변수 없이 쓰레드를 생성하고 실행시킨 경우
- 그래도 해당 쓰레드의 참조는 ThreadGroup에 저장된다
➜ 따라서, 이 쓰레드는 가비지 컬렉터(Garbage Collector)의 제거 대상이 되지 않는다
// Thread(ThreadGroup group, Runnable target, String name)
new Thread(grp, r, "th").start();
// Thread th = new Thread(grp, r, "th");
// th.start();
🌗 데몬 쓰레드 (Daemon Thread)
✔️ 데몬 쓰레드 ?
🔹 데몬 쓰레드
- 데몬 쓰레드가 아닌 쓰레드의 작업을 돋는 쓰레드 (보조적인 역할)
ex) 가비지 컬렉터, 워드 프로세서의 자동 저장, 화면 자동 갱신
➜ JVM이 필요한 보조작업을 수행하는 데몬 쓰레드들을 자동적으로 생성해서 실행시킨다 - 일반 쓰레드가 모두 종료되면, 데몬 쓰레드도 강제적으로 종료된다
- GUI를 가진 프로그램(AWT, Swing)을 실행하는 경우에는 이벤트와 그래픽처리를 위해 더 많은 수의 데몬 쓰레드가 생성된다
✔️ 데몬 쓰레드의 생성
🔹 데몬 쓰레드의 작성 · 실행방법
- 쓰레드를 생성한 다음 실행하기 전에 ' setDaemon(true) ' 메서드를 호출해야 한다
- 나머지는 일반 쓰레드의 작성 · 실행방법과 동일 !
- 데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다
boolean isDaemon() // 쓰레드가 데몬 쓰레드인지 확인한다
// 데몬 쓰레드이면 true를 반환한다
void setDaemon(boolean on) // 쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경한다
// 매개변수 on의 값을 true로 지정하면 데몬 쓰레드가 된다
- 무한 루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성
class Main implements Runnable {
static boolean autoSave = false;
public void main(String[] args) {
Thread t = new Thread(new Main());
t.setDaemon(true); // 이 부분이 없으면 종료되지 않는다
t.start();
for(int i = 1; i <= 8; i++) {
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
System.out.println(i);
if(i == 2)
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("작업파일 자동 저장 완료");
}
}
1
2
3
작업파일 자동 저장 완료
4
5
작업파일 자동 저장 완료
6
7
8
프로그램 종료
🔻main 쓰레드가 종료되니 데몬 쓰레드도 강제 종료
🌘 쓰레드의 실행제어
✔️ 쓰레드의 스케줄링
🔹 쓰레드의 상태
- NEW
➜ 쓰레드가 생성되고 아직 ' start() ' 가 호출되지 않은 상태 - RUNNABLE
➜ 실행 중 또는 실행 가능한 상태 - BLOCKED
➜ 동기화블럭에 의해서 일시정지된 상태 (lock이 풀릴 때까지 기다리는 상태) - WAITING, TIMED_WAITED
➜ 쓰레드의 작업이 종료되지는 않았지만 일시정지 상태.
TIMED_WAITED는 일시정지 시간이 지정된 경우를 의미 - TERMINATED
➜ 쓰레드의 작업이 종료된 상태

- 쓰레드 생성 후 ' start() ' 메서드 호출
- 실행대기 상태에 있다가 자신의 차례가 되면 실행상태가 된다
- 주어진 실행시간이 다되거나 ' yield() ' 메서드를 만나면 다시 실행대기 상태가 되고 다음 차례의 쓰레드가 실행상태가 된다
- 실행 중에 ' suspend() ', ' sleep() ', ' join() ', ' I/O block ' 에 의해 일시정지 상태가 될 수 있다
( I/O block ➜ 입출력작업에서 발생하는 지연상태, 사용자의 입력을 기다리는 경우) - 지정된 일시정지시간이 다되거나(time-out), notify(), ' resume() ', ' interrupt() '가 호출되면 일시정지 상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다린다
- 실행을 모두 마치거나 ' stop() ' 메서드가 호출되면 쓰레드는 소멸한다
🔹 쓰레드의 스케줄링 관련 메서드
sleep(long millis) - 일정시간동안 쓰레드를 멈추게 한다
➜ 지정된 시간(일정시간)동안 쓰레드를 일시정지시킨다 (쓰레드가 잠에 든 상태)
지정한 시간이 지나거나 ' interrupt() ' 가 호출되면 다시 실행 대기 상태가 된다
➜ ' sleep() ' 메서드를 호출할 때는 항상 try-catch문으로 예외 처리해주기
( ' interrupt() ' 가 호출될 경우 예외가 발생해서)
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
🔻매번 예외 처리하는 것이 번거로워 아예 메서드를 만들어 사용하기도 한다
class MyThread {
public static void main(String args[]) {
ExtendsThread_1 th1 = new ExtendsThread_1();
ExtendsThread_2 th2 = new ExtendsThread_2();
try {
th1.sleep(1000);
} catch(InterruptedException e) {}
System.out.println("main 종료");
}
}
class ExtendsThread_1 extends Thread {
public void run() {
...
System.out.println("th1 종료");
}
}
class ExtendsThread_2 extends Thread {
public void run() {
...
System.out.println("th2 종료");
}
}
(생략)
th1 종료
th2 종료
main 종료
🔻 쓰레드 th1이 가장 늦게 종료될 것 같지만 가장 빨리 종료되었다
➜ 'th1.sleep(1000);'과 같이 ' sleep() ' 메서드를 호출했어도
실제로 영향을 받는 것은 main 쓰레드 !
🔻 ' sleep() ' 메서드는 static으로 선언되어 있다
➜ 따라서, 참조변수로 호출하기 보다는 'Thread.sleep(1000);' 처럼 호출하자
interrupt() - 쓰레드의 작업을 취소한다
➜ 쓰레드를 강제로 종료시키진 못하고, 작업을 멈추라고 요청만 하는 것이다
( 쓰레드의 interrupted상태(instance 변수)를 true로 변경하는 것뿐이다 )
void interrupt() // 쓰레드의 interrupted상태를 false에서 true로 변경
boolean isInterrupted() // 쓰레드의 interrupted상태를 반환
static boolean interrupted() // 현재 쓰레드의 interrupted상태를 반환 후, false로 변경
➜ ' sleep() ', ' wait() ', ' join() ' 메서드에 의해 ' 일시정지 상태(WAITING) '인 쓰레드에 대해 ' interrupt() ' 를 호출했을 땐, ' 실행대기 상태(RUNNABLE) '로 만든다
( 해당 쓰레드에선 InterruptedException이 발생함으로써 일시정지 상태를 벗어나게 하는 것)
class MyThread {
public static void main(String[] args) throws Exception {
ExtendsThread th = new ExtendsThread();
th.start();
String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
System.out.println("입력하신 값은 " input + "입니다.");
th.interrupt();
System.out.println("isInterrupted() : " + th.isInterrupted());
}
}
class ExtendsThread extend Thread {
public void run() {
int i = 5;
while(i!=0 && !isInterrupted()) {
System.ou.println(i--);
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
// interrupt();
}
}
System.out.println("쓰레드 th 종료");
}
}
5
4
입력하신 값은 abcd입니다.
isInterrupted() : false
3
2
1
🔻 메서드 ' interrupt() ' 가 호출되면서 ' Thread.sleep(1000) ' 에서 InterruptedException이 발생
되고 쓰레드의 interrupted상태를 false로 자동 초기화되면서 카운트가 중단되지 않은 것이다
🔻 catch블럭에 메서드 ' interrupt() ' 를 추가해줘 쓰레드의 interrupted의 값을
true로 다시 바꿔주면 카운트다운이 중단될 것이다
stop() - 쓰레드를 즉시 종료시킨다
suspend() - 쓰레드를 일시정지시킨다
resume() - ' suspend() ' 메서드에 의해 정지된 쓰레드를 다시 실행대기 상태로 만든다
➜ 쓰레드의 실행을 제어하는 가장 손쉬운 방법
➜ 하지만, ' suspend() ' 와 ' stop() ' 메서드가 교착상태 (Deadlock)을 일으키기 쉽게 작성되어 있어 모두 'Deprecated (하위 호완성을 위해 삭제하지 않았다는 의미)'로 되어있다
➜ 이 문제를 해결하는 방법
class MyThread {
public static void main(String args[]) {
// 객체지향적이지 않은 방식
// ThreadImplements r1 = new ThreadImplements();
// ThreadImplements r2 = new ThreadImplements();
// Thread th1 = new Thread(r1, "*");
// Thread th2 = new Thread(r2, "**");
ThreadImplements th1 = new ThreadImplments("*");
ThreadImplements th2 = new ThreadImplments("**");
th1.start();
th2.start();
try {
Thread.sleep(2000);
th1.suspend();
Thread.sleep(2000);
th2.suspend();
Thread.sleep(2000);
th1.resume();
Thread.sleep(2000);
th1.stop();
th2.stop();
} catch(InterruptedException e) {}
}
}
class ThreadImplements implements Runnable {
volatile boolean suspended = false;
volatile boolean stopped = false;
Thread th;
ThreadImplements(String name) {
th = new Thread(this, name); // Thread(Runnable r, String name)
}
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() { th.start(); }
}
🔻 각 쓰레드 th1, th2가 다른 실행상태를 가지도록
하나의 ThreadImplements class의 객체를 공유하지 않는다
🔻 객체지향적으로 정리된 코드이며, 쓰레드를 생성하는 부분을 주목하자
yield() - 다른 쓰레드에게 양보한다
➜ 실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보(yield)하고 자신은 실행대기 상태가 된다
➜ ' yield() ' 와 ' interrupt() ' 메서드를 적절히 사용하면, 프로그램의 응답성을 높이고 보다 효율적인 실행을 가능하게 해준다
- 효율적인 코드
while(!stopped) {
if(!suspended) {
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
}
}
🔻 만약 변수 ' supended ' 의 값이 true라면, 즉 ' suspend() ' 메서드를 호출해 잠시 실행을 멈추게
한 상태라면, 해당 쓰레드는 주어진 실행시간을 그저 while문을 의미없이 돌면서 낭비하게 된다
⭣ ⭣
while(!stopped) {
if(!suspended) {
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
} else {
Thread.yield();
}
}
🔻 ' yield() ' 메서드를 호출해서 남은 실행시간을 while문에서 낭비하지 않고
다른 쓰레드에 양보하게 되어 효율적이다
- 응답성이 좋아지는 코드
public void suspend() {
suspended = true;
}
public void stop() {
stopped = true;
}
🔻 ' stop() ' 메서드가 호출됐을 때 ' Thread.sleep(1000) ' 에 의해 쓰레드가 일시
정지 상태에 머물러 있는 상황이라면, 변수 ' stopped ' 의 값이 true로 바뀌었어도
쓰레드가 정지될 때까지 최대 1초의 시간지연이 생기는 것이다
⭣ ⭣
public void suspend() {
suspended = true;
th.interrupt();
}
public void stop() {
stopped = true;
th.interrupt();
}
🔻 이렇게 하면, ' Thread.sleep(1000) ' 에서 InterruptedException이 발생하여
즉시 일시정지 상태에서 벗어나게 되므로 응답성이 좋아진다 !
join() - 다른 쓰레드의 작업을 기다린다
➜ 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 한다.
➜ 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속 한다
( 시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다리게 된다 )
➜ ' sleep() ' 처럼 ' interrupt() ' 메서드에 의해 대기상태에서 벗어날 수 있으며, ' join() ' 메서드가 호출되는 부분을 try - catch 문으로 감싸야 한다
➜ ' sleep() ' 메서드와 달리 현재 쓰레드가 아닌 특정 쓰레드에 대해 동작하므로 static 메서드가 아니다
void join()
void join(long millis)
void join(long millis, int nanos)
➜ ' join() ' 메서드를 언제 사용해야 할까?
ex) 가비지 컬렉터(GC)를 간단히 구현한 코드
if(gc.freeMemory() < requiredMemory) { // 필요한 메모리가 사용할 수 있는 양보다 클 경우
gc.interrupt(); // 가비지컬렉터('gc')를 깨운다
}
🔻 쓰레드 gc가 ' interrupt() ' 메서드에 의해 실행대기 상태가 됐음에도,
쓰레드 gc가 수행되기 이전에 main 쓰레드의 작업이 수행되어 메모리가 초과될 수 있다
⭣ ⭣
if(gc.freeMemory() < requiredMemory) {
gc.interrupt();
try {
gc.join(100); // 쓰레드 gc에 0.1초의 시간동안 작업하도록 해준다
} catch(InterruptedException e) {}
}
🔻 쓰레드 gc가 작업할 시간을 어느 정도 주고 main 쓰레드가 기다리도록 해서,
사용할 수 있는 메모리가 확보된 다음 작업을 수행하도록 할 수 있다
🌚 쓰레드의 동기화
✔️ 쓰레드의 동기화(Synchronization) ?
🔹 쓰레드의 동기화
- 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하게 막는 것
➜ 쓰레드의 동기화(Syschronization)
- 멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유하여 작업하기 때문에 서로의 작업에 영향을 주게 된다
➜ ATM 출금을 예로 들면, 출금 프로그램에서 쓰레드 A가 200만원이 있는 계좌에서 출금을 하는 중에 쓰레드 A에게 주어진 실행시간이 끝나고, 다른 쓰레드 B에게 제어권이 넘어가 해당 계좌에서 50만원을 출금했다면?
다시 쓰레드 A의 차례가 됐을 때 끝내지 못한 출금 작업을 수행하게 되고 해당 계좌의 잔액이 -50만원이 돼있을 수 있다
➜ 이런 상황을 방지하기 위해 도입된 개념이 '임계 영역(Critical section)'과 '잠금(Lock)' 이다
- 공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정
- 공유 데이터(객체)가 갖고 있는 lock을 획득한 단 하나의 쓰레드만 이 임계 영역 내의 코드를 수행 가능
(하나의 객체는 lock을 하나만 갖고 있다) - 해당 쓰레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납하면, 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드 수행 가능
- 공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정
🔹 쓰레드의 동기화 구현 방법
- ' synchronized ' 키워드를 이용한 동기화
- ' java.util.concurrent.locks ' & ' java.util.concurrent.atomic' package를 통한 동기화 (JDK1.5 ~)
✔️ synchronized를 이용한 동기화
🔹 synchronized ?
- 임계 영역을 설정하는데 사용
🔹 동기화가 필요한 예시
- 한 쓰레드가 if문을 통과하고 ' Thread.sleep(1000) ' 로 인해 출금하기(' balance -= money ') 직전에 다른 쓰레드에게 제어권이 넘어간다
- 즉, 다른 쓰레드가 끼어들어 출금을 먼저 하게 되고, 다시 이전의 쓰레드에게 제어권이 넘어오면 if문 다음부터 수행하게 된다
- 잔고에 남아있는 금액보다 많은 금액을 출금하게 되는 문제 발생
- if문과 출금하는 코드(' balance -= money ')는 하나의 임계 영역으로 묶어져야 한다
( ' Thread.sleep(1000) ' 가 없더라도 쓰레드들 서로 영향을 줄 수 있기에 동기화가 반드시 필요 )
public void withdraw(int money) { // 출금 메서드
// balance : 잔고
// money : 출금하려는 금액
if(balance >= money) {
try { Thread.sleep(1000); catch(Exception e) {}
balance -= money;
}
}
🔹 동기화 방법
1. 메서드 전체를 임계 영역으로 지정
public synchronized void withdraw(int money) { ... }
🔻 쓰레드는 synchronized 메서드(' withdraw() ')가 호출된 시점부터 이 메서드가 종료되어
lock를 반환할 때까지 다른 쓰레드는 해당 메서드를 호출하더라도 실행대기 상태에 머문다
2. 특정한 영역을 임계 영역으로 지정 (synchronized 블럭)
➜ 효율적인 프로그램을 작성하고자 한다면, 이 방식을 이용하여 임계 영역을 최소화하자
( 메서드 전체에 임계 영역 지정 X )
// synchronized(객체의 참조변수) { ... } // lock을 걸고자 하는 객체를 참조하는 참조변수
public void withdraw(int money) {
synchronized(this) {
if(balance >= money) {
try { Thread.sleep(1000); catch(Exception e) {}
balance -= money;
}
}
}
🔻 쓰레드는 이 블럭의 영역에 들어가면서부터 지정된 객체(' this ')의 lock을 얻게 되고
이 블럭을 벗어나면 lock을 반납
- 두 방법 모두 lock의 획득과 반납이 자동적으로 이루어진다
- 주의해야할 점
➜ ' synchronized ' 를 이용한 동기화는 지정된 영역의 코드를 한 번에 하나의 쓰레드가 수행하도록 보장하는 것일 뿐, 영역 밖의 코드까지 동기화를 보장해준다는 생각 X
class Account {
private int balance = 1000;
public void withdraw(int money) {
synchronized(this) { ... }
}
}
🔻 instance 변수인 ' balance ' 의 접근 제어자는 private으로 선언해주지 않으면,
아무리 동기화를 해줘도 이 값의 변경(외부에서의 접근)을 막을 방법이 없다 !
✔️ 특정 쓰레드가 lock을 반납하지 않은 경우 - wait() & notify()
🔹 wait() & notify()
- synchronized 된 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, ' wait() ' 메서드를 호출
➜ wait()을 호출한 쓰레드는 lock을 반납하고 기다린다
( 객체의 대기실 (waiting pool) 에서 통지를 기다린다) - 나중에 작업을 진행할 수 있는 상황이 되면 ' notify() ' 메서드를 호출해서 작업을 중단했던 쓰레드가 다시 lock을 얻어 작업을 재진행한다
- notifyAll - waiting pool에서 기다리고 있는 모든 쓰레드에게 통보
( waiting pool은 객체마다 존재하기 때문에, ' notifyAll() ' 이 호출된 객체의 waiting pool에 대기 중인 쓰레드만 해당된다 )
그래도 lock은 하나의 쓰레드만 받을 수 있어 나머지는 다시 lock을 기다리는 상태가 된다
void wait()
void wait(long timeout) // 지정된 시간동안만 기다린다. 즉, 지정된 시간이 지난 후에 자동적으로 notify()를 호출하는 느낌
void wait(long timeout, int nanos)
void notify()
void notifyAll()
🔻 특정 객체에 대한 것 ➜ Object class에 정의되어 있다
🔻 synchronized 블럭 내에서만 사용 가능
🔹 특정 쓰레드(Customer 쓰레드)가 lock을 계속해서 쥐고 기다리는 문제
- 식당에서 음식(dish)을 만들어서 Table에 서빙(serve)하는 요리사(Cook) 쓰레드와 Table의 음식을 먹는(eat) 손님(Customer) 쓰레드로 구현
- 여러 쓰레드(손님, 요리사)가 Table class의 객체 하나를 공유하는데 동기화 X
- CUST1 쓰레드(손님1)가 COOK1 쓰레드(요리사)가 테이블에 음식을 놓는 중(' serve() ' 메서드 )에 음식을 가져가려는(' eat() ' 메서드) 문제 (ConcurrentModificationException 발생)
- CUST1 쓰레드가 마지막 음식을 가져가는 도중(' eat() ')에 CUST2 쓰레드가 해당 음식을 낚아채가는(' eat() ') 문제 (IndexOutOfBoundsException 발생)
➜ 여러 쓰레드가 공유하는 객체인 Table의 ' serve() ' 와 ' eat() ' 메서드를 동기화(synchronized)했다
class Table {
...
public synchronized void serve(String dish) {
...
}
public void eat(String dishName) {
synchronized(this) {
...
}
...
}
...
}
- 손님 쓰레드가 Table class의 객체의 lock을 반납하지 않고 계속 쥐고 있어, 요리사 쓰레드가 음식을 서빙(' serve() ')하려 해도 lock을 얻을 수 없어 정상적으로 작업이 수행되지 않는 문제 발생
// eat() 메서드 내 일부 코드
synchronized(this) {
while(dishes.size() == 0) { // 무한 반복
try { Thread.sleep(500); } catch(InterruptedException e) {}
}
...
}
➜ ' wait() ' 과 ' notify() ' 메서드 추가
class Main() {
public static void main(String[] args) throws Exception {
Table table = new Table(); // 테이블 객체 생성
new Thread(new Cook(table), "Cook").start(); // 해당 테이블 담당 요리사
new Thread(new Customer(table, "pizza"), "Customer1").start(); // 피자 주문 손님
new Thread(new Customer(table, "burger"), "Customer2").start(); // 버거 주문 손님
Thread.sleep(2000);
System.exit(0);
}
}
class Customer implements Runnable {
// 생성자 Customer(), run() 구현한 내용 (eat() 메서드 수행)
}
class Cook implements Runnable {
// 생성자 Cook(), run() 구현한 내용 (serve() 메서드 수행)
}
class Table {
String[] menu = { "pizza", "burger" };
final int MAX_FOOD = 3;
private ArrayList<String> dishes = new ArrayList<>();
// Cook 쓰레드가 호출
public synchronized void serve(String dish) {
while(dishes.size() >= MAX_FOOD) { // 테이블에 음식이 가득 찼다
wait(); // 요리사 쓰레드를 기다리게 한다 (요리사 쓰레드의 lock 반납)
// waiting pool에서 기다리고 있는 다음 쓰레드가 lock을 얻고 작업 수행
Thrad.sleep(500);
}
// 요리사 쓰레드가 깨어나면(lock을 얻으면) 음식을 추가한다
dishes.add(dish);
notify(); // 잠자고 있는 손님 쓰레드를 깨운다
}
// Customer1, Customer2 쓰레드가 호출
public eat(String dish) {
synchronized(this) {
while(dishes.size() == 0) { // 음식이 하나도 없다
wait(); // 음식이 없으니 손님 쓰레드를 기다리게 한다 (손님 쓰레드의 lock 반납)
// waiting pool에서 기다리고 있는 다음 쓰레드가 lock을 얻고 작업 수행
Thread.sleep(500);
}
// 손님 쓰레드가 깨어나면(lock을 얻으면) while문 수행
while(true) {
for(int i = 0; i < dishes.size(); ++i) {
if(dish.equals(dishes.get(i))) { // 원하는 음식이 있다면
// 음식을 먹는다
dishes.remove(i);
notify(); // 잠자고 있는 쓰레드(요리사 or 다른 손님)를 깨운다
return;
}
}
try { // 현재 lock을 쥐고 있는 손님 쓰레드가 원하는 음식이 없다
wait(); // 손님 쓰레드를 다시 기다리게 한다 (손님 쓰레드의 lock 반납)
// waiting pool에서 기다리고 있는 다음 쓰레드가 lock을 얻고 작업 수행
Thread.sleep(500);
} catch(InterruptedExceptione) {}
} // while(true)
} // syschronized
}
public int menCount() { ... }
}
🔻 하지만, Table class의 객체의 waiting pool에서
요리사 쓰레드와 손님 쓰레드가 같이 대기한다는 것에 문제가 있다
➜ 기아 현상 & 경쟁 상태
🔹 기아 현상과 경쟁 상태
- 위의 코드를 예시로 들면,
피자 주문 손님(쓰레드 "Customer1")이 피자를 만들어 달라고 ' notify() ' 를 호출해도, 요리사(쓰레드 "Cook")이 아닌 햄버거 주문 손님(쓰레드 "Customer2)가 통지를 받고 lock을 얻는 경우가 발생 !
( ' notify() ' 는 Table class의 객체의 wating pool에서 대기중인 쓰레드들 중에 하나를 임의로 선택해서 통지하기 때문이다 )
➜ 최악의 경우 요리사 쓰레드는 계속 통지를 받지 못하고 오랫동안 기다리는 "기아(starvation) 현상" 발생
➜ 이 문제는 ' notifyAll() ' 을 사용해 해결 가능 (요리사 쓰레드도 결국 통지를 받아 lock을 얻고 작업 진행) - 하지만, 불필요하게 요리사가 아닌 손님 쓰레드까지 통지를 받아, 통지를 받은 모든 쓰레드들이 lock을 얻기 위해 경쟁하게 된다
" 경쟁 상태 (race condition) "
➜ 쓰레드들(요리사 쓰레드, 손님 쓰레드)을 구별해서 통지하는 것이 필요
➜ Lock과 Condition을 이용하면, 이러한 선별적인 통지도 가능하다
✔️ Lock과 Condition을 이용한 동기화 ( java.util.concurrent.locks )
🔹 Lock class의 종류
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/package-summary.html
java.util.concurrent.locks (Java Platform SE 8 )
Package java.util.concurrent.locks Description Interfaces and classes providing a framework for locking and waiting for conditions that is distinct from built-in synchronization and monitors. The framework permits much greater flexibility in the use of loc
docs.oracle.com
ReentrantLock - 재진입이 가능(?)한 가장 일반적인 배타 lock
- wait() & notify() 처럼, 특정 조건에서 lock을 풀고 나중에 다시 lock을 얻고 임계영역으로 들어와서 이후의 작업을 수행
- ReentrantLock의 생성자
ReentrantLock()
ReentrantLock(boolean fair)
🔻 매개변수로 true를 주면, lock이 풀렸을 때 가장 오래 기다린 쓰레드가 lock을 획득
(공정한 방법) ➜ 그만큼 성능이 떨어지기에 대부분 이렇게 처리 X
- lock() & unlock()
void lock() // lock을 잠근다
void unlock() // lock을 해지
boolean isLocked() // lock이 잠겼는지 확인
🔻 synchronized 블럭과 달리, lock class들은 수동으로 lock을 잠그고 해제해야 한다
lock.lock(); // ReentrantLock lock = new ReentrantLock();
try {
// 임계 영역
} finally {
lock.unlock(); // 무조건 수행
}
🔻 임계 영역 내에서 예외가 발생하거나 return문으로 빠져 나가게 되면
lock이 풀리지 않을 수 있어, unlock() 메서드는 try - finally 문으로 감싸자
➜ 대부분의 경우 lock() & unlock() 메서드 대신 synchronized 블럭 사용 가능해서
synchronized 블럭을 이용하는 것이 날 수 있다
- tryLock() - lock을 얻을 때까지 기다리지 않는다
boolean tryLock()
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
🔻 ' lock() ' 메서드는 lock을 얻을 때까지 쓰레드를 block 시키므로 쓰레드의 응답성 ⭣⭣
➜ 이때, ' tryLock() ' 메서드를 이용해서 지정된 시간동안 lock을 얻지 못했을 경우
작업 중단 여부를 결정하도록 코드 작성 가능
🔻 InterruptedException을 발생시켜 지정된 시간동안 lock을 얻으려고 기다리던 중에
' interrupt() ' 메서드로 작업을 취소할 수 있도록 코드 작성 가능
ReentrantReadWriteLock - 읽기에는 공유적이고, 쓰기에는 배타적인 lock
- 읽기 lock과 쓰기 lock을 따로 제공하며, 읽기 lock만 여러 쓰레드가 중복해서 걸 수 있다
StampedLock - ReentrantReadWriteLock + 낙관적인 lock 가능 ( by Stamp(long타입의 정수값) )
- 일반적인 lock ➜ 읽기 lock이 걸려있으면, 쓰기 lock은 읽기 lock이 풀려야만 얻을 수 있다
낙관적인 lock ➜ ' 낙관적 읽기 lock ' 은 쓰기 lock에 의해 바로 풀린다
➜ 따라서 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 걸어야 한다
// StampedLock을 이용한 낙관적 읽기의 예
int getBalanced() {
long stamp = lock.tryOptimisticRead(); // 낙관적 읽기 lock을 건다
int curBalance = this.balance; // 공유 데이터인 balance를 읽어온다
if(!lock.validate(stamp)) { // 쓰기 lock에 의해 낙관적 읽기 lock이 풀렸는지 확인
stamp = lock.readLock(); // 낙관적 읽기 lock이 풀렸으면, 읽기 lock을 얻으려고 기다린다
try {
curBalance = this.balance; // 공유 데이터를 다시 읽어온다
} finally {
lock.unlockRead(stamp); // 읽기 lock을 푼다
}
}
return curBalance; // 낙관적 읽기 lock이 풀리지 않았으면 곧바로 읽어온 값을 반환
}
🔹 ReentrantLock & Condition
- 쓰레드의 종류를 구분해 선별적인 통지를 가능하게 한 것이 Condition이다
➜ 공유 객체의 waiting pool에 몰아넣는 것이 아닌,
쓰레드의 종류별로 Condition을 만들어서 waiting pool를 분리 ! - Condition은 어떻게 쓰는 것인가?
- 이미 생성된 lock으로부터 Condition을 생성
- wait() & notify() 대신 await() & signal()을 사용 !
class Main() {
public static void main(String[] args) throws Exception {
Table table = new Table(); // 테이블 객체 생성
new Thread(new Cook(table), "Cook").start(); // 해당 테이블 담당 요리사
new Thread(new Customer(table, "pizza"), "Customer1").start(); // 피자 주문 손님
new Thread(new Customer(table, "burger"), "Customer2").start(); // 버거 주문 손님
Thread.sleep(2000);
System.exit(0);
}
}
class Customer implements Runnable {
// 생성자 Customer(), run() 구현한 내용 (eat() 메서드 수행)
}
class Cook implements Runnable {
// 생성자 Cook(), run() 구현한 내용 (serve() 메서드 수행)
}
class Table {
String[] menu = { "pizza", "burger" };
final int MAX_FOOD = 3;
private ArrayList<String> dishes = new ArrayList<>();
private ReentrantLock lock = new ReentrantLock(); // lock 생성
// lock으로 condition 생성
private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondtion();
// Cook 쓰레드가 호출
public void serve(String dish) {
lock.lock(); // lock 걸기
try { // 임계영역
while(dishes.size() >= MAX_FOOD) { // 테이블에 음식이 가득 찼다
try {
forCook.await(); // 요리사 쓰레드를 기다리게 한다
Thrad.sleep(500);
} catch(InterrutedException e) {}
}
// 요리사 쓰레드가 깨어나면 음식을 추가한다
dishes.add(dish);
forCust.signal(); // 잠자고 있는 손님 쓰레드를 깨운다
} finally {
lock.unlock(); // lock 해지
}
}
// Customer1, Customer2 쓰레드가 호출
public eat(String dish) {
lock.lock(); // lock 걸기
try {
while(dishes.size() == 0) { // 음식이 하나도 없다
forCust.await(); // 손님 쓰레드를 기다리게 한다
Thread.sleep(500);
}
// 손님 쓰레드이 깨어나면 while문 수행
while(true) {
for(int i = 0; i < dishes.size(); ++i) {
if(dish.equals(dishes.get(i))) { // 원하는 음식이 있다면
// 음식을 먹는다
dishes.remove(i);
forCook.signal // 잠자고 있는 요리사 쓰레드를 깨운다
return;
}
}
try { // 현재 lock을 쥐고 있는 손님 쓰레드가 원하는 음식이 없다
forCust.await(); // 손님 쓰레드를 다시 기다리게 한다
Thread.sleep(500);
} catch(InterruptedExceptione) {}
} // while(true)
} finally {
lock.unlock(); // lock 해지
}
}
public int menCount() { ... }
}
🔻 '기아 현상'이나 '경쟁 상태'는 확실히 개선됐다
🔻 그래도 쓰레드의 종류에 따라 구분하여 통지할 수 있게 된 것일 뿐
( 요리사 쓰레드가 통지를 받아야 할 상황에서 손님 쓰레드가 통지를 받지 않는 것 ),
여전히 특정 쓰레드를 선택할 수 없기 떄문에 같은 종류의 쓰레드(Customer1, Customer2)간의
'기아 현상'이나 '경쟁 상태'가 발생할 가능성은 남아 있다
➜ Condition을 더 세분화하면 더 개선 가능하다 !
✔️ volatile (휘발성)
🔹 volatile ?
- 코어(Core)는 메모리에서 읽어온 값을 캐시(cache)에서 저장하고, 메모리가 아닌 캐시에서 먼저 값을 읽어서 작업하기 때문에, 도중에 메모리에 저장된 변수의 값이 변경돼도 캐시에 저장된 값이 갱신되지 않아서 캐시와 메모리에 저장된 값이 다른 경우가 발생한다

- volatile을 변수 앞에 붙이면,
코어(Core)가 변수의 값을 읽어올 때 캐시(cache)가 아닌 메모리(Memory)에서 읽어오기 때문에,
멀티 코어 프로세서에서의 캐시와 메모리간의 값의 불일치를 해결 가능
🔹 volatile로 long과 double을 원자화
- 원자화 ?
: 작업을 더 이상 나눌 수 없게 한다는 의미
➜ volatile은 해당 변수에 대한 읽기나 쓰기가 원자화되는 것
(단, 동기화하는 것은 아니다 ! )
➜ synchronized 블럭은 여러 문장을 원자화함으로써 쓰레드의 동기화를 구현한 것 - JVM은 데이터를 4 byte(=32bit) 단위로 처리하기 때문에, int보다 크기가 큰 long과 double타입의 변수는 하나의 명령어로 값을 읽거나 쓸 수 없어, 작업의 중간에 다른 쓰레드가 끼어들 여지가 있다
- 이를 방지하기 위해 변수를 읽고 쓰는 모든 문장을 synchronized 블럭으로 감쌀 수도 있지만,
변수를 선언할 때 volatile을 붙여 원자화하는 것이 훨씬 간단하다
( 상수(final)는 멀티쓰레드에 안전하기 때문에 volatile을 붙일 수 없다 )
➜ 단, 동기화가 필요한 경우엔 깜빡하지 말고 동기화 처리를 해줘야 한다 !
✔️ fork & join 프레임워크
🔹 fork & join 프레임워크
- 하나의 작업을 작은 단위로 나눠서 여러 쓰레드가 동시에 처리하기 쉽게 만들어주는 것
🔹 fork & join 프레임워크 구현 및 사용
- 수행할 작업에 따라 두 class 중 하나를 상속받아 구현해야 한다
- RecursiveAction : 반환값 X 작업을 구현할 때 상속
- RecursiveTask : 반환값 O 작업을 구현할 때 상속
- ' run() ' 을 구현했던 것처럼 ' compute() ' 라는 추상 메서드를 구현한다
- 쓰레드 풀(Thread pool)을 생성하고, 쓰레드풀과 수행할 작업을 생성한다
- 쓰레드 풀
➜ 쓰레드가 수행해야 하는 작업이 담긴 큐를 제공, 각 쓰레드는 자신의 작업 큐에 담긴 작업을 순서대로 처리
장점
➜ 지정된 수의 쓰레드를 생성해서 미리 마들어 놓고 반복해서 재사용 가능
➜ 반복 작업이 줄고, 너무 많은 쓰레드가 생성되어 성이 저하되는 것을 막아준다
- 쓰레드 풀
- 쓰레드를 시작할 때 ' start() ' 를 호출했던 것처럼, fork&join 프레임워크로 수행할 작업도 ' invoke() ' 를 호출해서 작업을 시작한다
fork()
: 해당 작업을 쓰레드 풀의 작업 큐에 넣는다 (비동기 메서드; 결과를 기다리지 X)
join()
: 해당 작업의 수행이 끝날 때까지 기다렸다가, 수행이 끝나면 그 결과를 반환한다 (동기 메서드; 결과를 기다린다)
import java.util.concurrent.*;
class Main {
static final ForkJoinPool pool = new ForkJoinPool(); // 쓰레드 풀 생성
public static void main(String[] args) {
long from = 1L, to = 100_000_000L;
SumTask task = new SumTask(from, to); // 수행할 작업을 생성
Long result = pool.invoke(task); // 작업 시작
}
}
class SumTask extends RecursiveTask<Long> { // 반환값이 있어야 하기 때문에 RecursiveTask 상속
long from, tol
SumTask(long from, long to) { // 생성자
this.from = from;
this.to = to;
}
public Long compute() { // 추상 메서드 compute() 구현
long size = to - from + 1;
if(size <= 2)
return sum();
long half = (from + to)/2;
SumTask leftSum = new SumTask(from, half);
SumTask rightSum = new SumTask(half + 1, to);
leftSum.fork(); // 작업 큐에 저장 (작업 큐에 넣으면 다른 쓰레드가 가져가서 처리할 수 있다)
//compute()의 재귀호출이 종료된 후, join()의 결과를 기다렸다가 리턴!
return rightSum.compute() + leftSum.join();
}
long sum() {
// from ~ to의 모든 숫자를 더한 결과를 반환
}
}
🔻 ' fork() ' 메서드로 작업 큐에 저장하면,
자신의 작업 큐가 비어있는 쓰레드가 이 작업을 가져가서 처리한다
➜ " 작업 훔쳐오기 (work stealing) " - 쓰레드풀에 의해 자동적으로 이뤄진다 !
( 이 덕분에, 여러 쓰레드가 골고루 작업을 나눠 처리 가능한 것 )
🔻 ' compute() ' 가 재귀호출될 떄, ' join() ' 은 호출되지 않는다
🔻 fork&join 프레임워크로 계산한 결과보다 for문으로 계산한 결과가 시간이 덜 걸린다
➜ 항상 멀티쓰레드로 처리하는 것이 빠르다고 생각 X (테스트 해보고 이득이 있을 때만)

▼ Study📋
☑️ 쓰레드들은 항상 일정하게 번걸아가면서 작업을 수행하지는 않는다
☑️ Thread class의 생성자가 Runnable interface를 구현한 inatance를 참조하도록 되어 있어서, Thread class를 상속받아 ' run() ' 을 Overriding 하지 않고도 Runnable interface를 구현한 class(외부)로부터 ' run() ' 을 제공받을 수 있다
☑️ Thread class 기능의 확장 여부에 따라 Thread 클래스를 상속받을 것인지, Runnable interface를 구현할 것인지 선택하자 !
☑️ 가비지 컬렉터(GC)는 객체를 참조하는 참조 변수가 없으면 메모리에서 제거하는 기능을 하는데, 참조변수 없이 쓰레드를 생성해도 해당 쓰레드의 참조가 ' ThreadGroup ' 에 저장되어 있기 때문에 가비지 컬렉터에 제거되지 않는다
☑️ 데드락(Deadlock)을 방지하기 위해 나온 것이 " waiting pool " 이다