오늘 한일 자바강의 5주차
수업 목록
프로세스와 쓰레드
싱글 쓰레드와 멀티 쓰레드
구현방법 3가지
싱글 쓰레드와 멀티 쓰레드 실습
데몬, 운선순위, 쓰레드 그룹
쓰레드 상태
sleep, interrupt,
join, yield, synchronized
wait, notify
Lock, Condition
프로세스 & 쓰레드
📌 프로세스 vs 쓰레드
- 프로세스 : 운영체제로부터 자원을 할당받는 작업의 단위
- 프로세스 : 실행중인 프로그램
- 간단하게 작업관리자에서 떠있는 것
- OS 위에서 실행 되는 모든 프로그램

- 쓰레드 : 프로세스가 할당받은 자원을 이용하는 실행의 단위
- 쓰레드 : 프로세스 내에서 일하는 일꾼(코드의 흐름)
- 쓰레드의 생성 : 프로세스가 작업 중인 프로그램에서 실행 요청이 들어오면 쓰레드를 만들어 명령
- 쓰레드의 자원 : 쓰레드는 프로세스 내 주소 공간이나 메모리 공간(Heap)을 공유 받고 자신만을 위한 공간(Stack)도 공유받음

- Java 쓰레드
- Java 프로그램을 실행하면 앞서 배운 JVM 프로세스 위에서 실행됩니다.
- Java 프로그램 쓰레드는 Java Main 쓰레드부터 실행되며 JVM에 의해 실행됩니다.
- 📌 일반 쓰레드와 동일하며 JVM 프로세스 안에서 실행되는 쓰레드를 말합니다.
싱글 쓰레드 & 멀티 쓰레드
싱글 쓰레드
- 프로세스 안에서 하나의 쓰레드만 실행
- Java 프로그램 main() 메서드만 실행했을때 이것을 싱글 쓰레드라고 함
멀티 쓰레드 : 작업에 쓸 쓰레드를 병렬로 실행
- 프로세스 안에서 여러개의 쓰레드가 실행되는 것
- 메인 쓰레드외에 다른 자겁 쓰레드들을 생성하여 여러개의 실행흐름 만듬
- 여러개의 쓰레드를 쓰면 여러개의 작업을 동시에 할 수 있어서 성능이 좋아집니다.
- 응답 쓰레드와 작업 쓰레드를 분리하여 빠르게 응답을 줄 수 있습니다
멀티 쓰레드 단점 :
- 동기화 문제가 발생
- 프로세스의 자원을 공유하면서 작업을 처리하기 때문에 자원을 서로 사용하려고 충돌이 발생 -> 데드락
구현방법 3가지
1. Thread 클래스를 상속받아서 구현
public class Main {
public static void main(String[] args) {
TestThread thread = new TestThread();
thread.start();
}
}
class TestThread extends Thread {
@Override
public void run() {
for (int i = 0; i <100; i++) {
System.out.print("*");
}
}
}
- run() 메서드에 작성된 코드가 쓰레드가 수행할 작업입니다.
2. Runnable : Java에서 제공하는 Runnable 인터페이스 사용해서 구현
public class Main {
public static void main(String[] args) {
Runnable run = new TestRunnable();
Thread thread = new Thread(run);
thread.start();
}
}
class TestRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i <100; i++) {
System.out.print("$");
}
}
}
- run() 메서드에 작성된 코드가 쓰레드가 수행할 작업입니다.(Thread 클래스 방식과 동일)
- 이렇게 사용하는 이유는 인터페이스는 다중 상속이 가능하기 때문에 확장성이 높다
3. 람다식 : Runnable 인터페이스에 람다식을 사용 가장 많이 사용하는 방식
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
int sum = 0;
for (int i = 0; i < 50; i++) {
sum += i;
System.out.println(sum);
}
System.out.println(Thread.currentThread().getName() + " 최종 합 : " + sum);
};
Thread thread1 = new Thread(task);
thread1.setName("thread1");
Thread thread2 = new Thread(task);
thread2.setName("thread2");
thread1.start();
thread2.start();
}
}
- run() 메서드에 작성했던 쓰레드가 수행할 작업을 실행 블록 { } 안에 작성하시면 됩니다.
- setName() 메서드는 쓰레드에 이름을 부여할 수 있습니다.
- Thread.currentThread().getName() 은 현재 실행 중인 쓰레드의 이름을 반환합니다.
싱글 쓰레드와 멀티 쓰레드 실습
1. 싱글 스레드
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
System.out.print("$");
}
};
Thread thread1 = new Thread(task);
thread1.setName("thread1");
thread1.start();
}
}
- 메인 쓰레드에서 싱글 쓰레드로 하나의 작업을 실행합니다.
- $ 표시가 순서대로 출력되는 모습을 볼 수 있습니다.
2. 멀티 스레드
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
System.out.print("$");
}
};
Runnable task2 = () -> {
for (int i = 0; i < 100; i++) {
System.out.print("*");
}
};
Thread thread1 = new Thread(task);
thread1.setName("thread1");
Thread thread2 = new Thread(task2);
thread2.setName("thread2");
thread1.start();
thread2.start();
}
}
- $ 와 * 를 출력하는 두가지 쓰레드를 병렬로 사용
- 순서는 정해져 있지 않음(OS 스케줄러에 달려 있음)
데몬, 운선순위, 쓰레드 그룹
1. 데몬 쓰레드
📌 보이지 않는 곳(background) 에서 실행되는 낮은 우선순위를 가진 쓰레드를 말합니다.
- 보조적인 역할을 담당하며 대표적인 데몬 쓰레드로는 메모리 영역을 정리해 주는 가비지 컬렉터(GC)가 있습니다.
[ Garbage Collection(가비지 컬렉션)이란? ]
프로그램을 개발 하다 보면 유효하지 않은 메모리인 가바지(Garbage)가 발생하게 된다. C언어를 이용하면 free()라는 함수를 통해 직접 메모리를 해제해주어야 한다. 하지만 Java나 Kotlin을 이용해 개발을 하다 보면 개발자가 메모리를 직접 해제해주는 일이 없다. 그 이유는 JVM의 가비지 컬렉터가 불필요한 메모리를 알아서 정리
출처: https://mangkyu.tistory.com/118 [MangKyu's Diary:티스토리]
- 데몬 쓰레드 설정 방법
public class Main {
public static void main(String[] args) {
Runnable demon = () -> {
for (int i = 0; i < 1000000; i++) {
System.out.println("demon");
}
};
Thread thread = new Thread(demon);
thread.setDaemon(true); // true로 설정시 데몬스레드로 실행됨
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println("task");
}
}
}
demon 쓰레드는 우선순위가 낮고 다른 쓰레드가 모두 종료되면 강제 종료 당하기 때문에 main() 쓰레드의 task가 100번이 먼저 찍히면 종료되어 1000000번 수행이 되지 않고 종료됩니다.
2. 쓰레드 우선 순위
📌 쓰레드 작업의 중요도에 따라서 쓰레드의 우선순위를 부여할 수 있습니다.
- 작업의 중요도가 높을 때 우선순위를 높게 지정하면 더 많은 작업시간을 부여받아 빠르게 처리될 수 있습니다.
- 쓰레드는 생성될 때 우선순위가 정해집니다.
- 이 우선순위는 우리가 직접 지정하거나 JVM에 의해 지정될 수 있습니다.
- 우선순위는 아래와 같이 3가지 (최대/최소/보통) 우선순위로 나뉩니다.
- 최대 우선순위 (MAX_PRIORITY) = 10
- 최소 우선순위 (MIN_PRIORITY) = 1
- 보통 우선순위 (NROM_PRIORITY) = 5
- 기본 값이 보통 우선순위입니다. 수치가 클수록 우선 순위 높음
- ⚠️우선순위가 높다고 반드시 쓰레드가 먼저 종료되는 것은 아님(확률이 높음)
- 더 자세하게 나눈다면 1~10 사이의 숫자로 지정 가능합니다.
- 이 우선순위의 범위는 OS가 아니라 JVM에서 설정한 우선순위입니다.
- 쓰래드 우선 순위 설정(setPriority())
Thread thread1 = new Thread(task1);
thread1.setPriority(8);
- 쓰레드 우선순위 반환(getPriority())
int threadPriority = thread1.getPriority();
System.out.println("threadPriority = " + threadPriority);
3. 쓰레드 그룹 생성
📌 서로 관련이 있는 쓰레드들을 그룹으로 묶어서 다룰 수 있습니다.
- 쓰레드들은 기본적으로 그룹에 포함되어 있습니다.
- JVM 이 시작되면 system 그룹이 생성되고 쓰레드들은 기본적으로 system 그룹에 포함됩니다.
- 메인 쓰레드는 system 그룹 하위에 있는 main 그룹에 포함됩니다.
- 모든 쓰레드들은 반드시 하나의 그룹에 포함되어 있어야 합니다.(설정 안하면 main쓰레드 하위)
쓰레드 그룹 생성 코드 : ThreadGroup 클래스로 객체를 만들어서 Thread 객체 생성 시 첫 번째 매개변수로 넣어주면 됨
public class Main {
public static void main(String[] args) {
Runnable task = () -> {//runnable 메소드 방식(1초마다 스레드 이름 출력)
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
break;
}
}
System.out.println(Thread.currentThread().getName() + " Interrupted");
};
// ThreadGroup 클래스로 객체를 만듭니다.(그룹객채)
ThreadGroup group1 = new ThreadGroup("Group1");
// Thread 객체 생성시 첫번째 매개변수로 넣어줍니다.
// Thread(ThreadGroup group, Runnable target, String name)
Thread thread1 = new Thread(group1, task, "Thread 1");
Thread thread2 = new Thread(group1, task, "Thread 2");
// Thread에 ThreadGroup 이 할당된것을 확인할 수 있습니다.
System.out.println("Group of thread1 : " + thread1.getThreadGroup().getName());
System.out.println("Group of thread2 : " + thread2.getThreadGroup().getName());
thread1.start();
thread2.start();
try {
// 현재 쓰레드를 지정된 시간동안 멈추게 합니다.
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// interrupt()는 일시정지 상태인 쓰레드를 실행대기 상태로 만듭니다.
group1.interrupt();
}
}
쓰레드 상태와 제어
쓰레드 상태: 쓰레드는 실행과 대기를 반복하면 run()메서드 수행/run()메서드 종료 되면 멈춤

- 음악을 듣다 일시정지를 하는 것과 마찬가지로 쓰레드도 일시정지 상태를 만들 수 있습니다. (2)
- 일시정지 상태에서는 쓰레드가 실행을 할 수 없는 상태가 됩니다.
- 쓰레드가 다시 실행 상태로 넘어가기 위해서는 우선 일시정지 상태에서 실행 대기 상태로 넘어가야 합니다. (3)
|
상태
|
Enum
|
설명
|
|
객체생성
|
NEW
|
쓰레드 객체 생성, 아직 start() 메서드 호출 전의 상태
|
|
실행대기
|
RUNNABLE
|
실행 상태로 언제든지 갈 수 있는 상태
|
|
일시정지
|
WAITING
|
다른 쓰레드가 통지(notify) 할 때까지 기다리는 상태
|
|
일시정지
|
TIMED_WAITING
|
주어진 시간 동안 기다리는 상태
|
|
일시정지
|
BLOCKED
|
사용하고자 하는 객체의 Lock이 풀릴 때까지 기다리는 상태
|
|
종료
|
TERMINATED
|
쓰레드의 작업이 종료된 상태
|
쓰레드의 제어

- sleep() :
📌 현재 쓰레드를 지정된 시간 동안 멈추게 합니다.
- sleep()은 쓰레드 자기 자신에 대해서만 멈추게 할 수 있습니다
- 예외 처리 필요 sleep 상태에 있는 동안 interrupt()를 만나면 다시 실행되기 때문에 InterruptedException이 발생
- 특정 쓰레드를 지목해서 멈추게 하는 것은 불가능합니다.
- interrupt()
📌 일시정지 상태인 쓰레드를 실행 대기 상태로 만듭니다.
- Thread 클래스 내부에 interrupted 되었는지를 체크하는 boolean 변수가 존재
- 쓰레드가 start() 된 후 동작하다 interrupt()를 만나 실행하면 interrupted 상태가 true
- join()
📌 정해진 시간 동안 지정한 쓰레드가 작업하는 것을 기다립니다.
- 시간을 지정하지 않았을 때는 지정한 쓰레드의 작업이 끝날 때까지 기다립니다.
- yield()
📌 남은 시간을 다음 쓰레드에게 양보하고 쓰레드 자신은 실행 대기 상태가 됩니다.
- synchronized
📌 멀티 쓰레드의 경우 여러 쓰레드가 한 프로세스의 자원을 공유해서 작업하기 때문에 서로에게 영향을 줄 수 있습니다. 이로 인해서 장애나 버그가 발생할 수 있습니다.
- 이러한 일을 방지하기 위해 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 침범하지 못하도록 막는 것을 '쓰레드 동기화(Synchronization)'라고 합니다.
- 동기화를 하려면 다른 쓰레드의 침범을 막아야 하는 코드들을 ‘임계 영역’으로 설정하면 됩니다.
- 임계 영역에는 Lock을 가진 단 하나의 쓰레드만 출입이 가능합니다.
- 즉, 임계 영역은 한 번에 한 쓰레드만 사용이 가능합니다.
synchronized 사용 방법( none synchronized)
public class Main {
public static void main(String[] args) {
AppleStore appleStore = new AppleStore();
Runnable task = () -> {
while (appleStore.getStoredApple() > 0) {
appleStore.eatApple();
System.out.println("남은 사과의 수 = " + appleStore.getStoredApple());
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
}
}
class AppleStore {
private int storedApple = 10;
public int getStoredApple() {
return storedApple;
}
public void eatApple() {
if (storedApple > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
storedApple -= 1;
}
}
}
synchronized 사용 방법( synchronized)
public class Main {
public static void main(String[] args) {
AppleStore appleStore = new AppleStore();
Runnable task = () -> {
while (appleStore.getStoredApple() > 0) {
appleStore.eatApple();
System.out.println("남은 사과의 수 = " + appleStore.getStoredApple());
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
}
}
class AppleStore {
private int storedApple = 10;
public int getStoredApple() {
return storedApple;
}
public void eatApple() {
synchronized (this) {
if(storedApple > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
storedApple -= 1;
}
}
}
}
- wait() : 실행 중이던 쓰레드는 해당 객체의 대기실(waiting pool)에서 통지를 기다립니다.
- notify() : 해당 객체의 대기실(waiting pool)에 있는 모든 쓰레드 중에서 임의의 쓰레드만 통지를 받습니다.
Lock
📌 synchronized 블럭으로 동기화하면 자동적으로 Lock이 걸리고 풀리지만, 같은 메서드 내에서만 Lock을 걸 수 있다는 제약이 있습니다.
이런 제약을 해결하기 위해 Lock 클래스를 사용합니다.
Condition
📌 wait() & notify()의 문제점인 waiting pool 내 쓰레드를 구분하지 못한다는 것을 해결한 것이 Condition입니다.
wait()과 notify()는 객체에 대한 모니터링 락(lock)을 이용하여 스레드를 대기시키고 깨웁니다. 그러나 wait()과 notify()는 waiting pool 내에 대기 중인 스레드를 구분하지 못하므로, 특정 조건을 만족하는 스레드만 깨우기가 어렵습니다.
이러한 문제를 해결하기 위해 JDK 5에서는 java.util.concurrent.locks 패키지에서 Condition 인터페이스를 제공합니다. Condition은 waiting pool 내의 스레드를 분리하여 특정 조건이 만족될 때만 깨우도록 할 수 있으며, ReentrantLock 클래스와 함께 사용됩니다. 따라서 Condition을 사용하면 wait()과 notify()의 문제점을 보완할 수 있습니다.