
운영체제는 멀티 프로세스를 생성해서 처리한다. 멀티 프로세스들은 서로 독립적이기 때문에 다른 프로세스에서 오류가 발생해도 현재 프로세스에 영향을 미치지 앟는다. 하지만 멀티 스레드는 하나의 스레드에서 예외를 발생시키면 프로세스가 종료되기에 다른 스레드에게 영향을 끼친다.
모든 자바 프로그램은 메인 스레드가 main() method를 실행하면서 시작된다. 메인 스레드는 main() method의 첫 코드부터 순차적으로 실행된다.

자바 프로그램은 main 스레드에서 실행된다. main 스레드가 종료되면 프로세스도 종료된다. 실행 흐름이 하나뿐이기 때문에 작업을 순차적으로 처리한다.
실행 중인 스레드가 하나라도 존재한다면 프로세스는 종료되지 않는다. 메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않는다.
자바에서는 모든 스레드가 다 끝나야 어플리케이션이 종료된다. 이는 언어마다 다르다.
어떤 스레드가 먼저 실행될지는 JVM이나 OS의 스케줄러가 결정한다. 개발자는 실행 순서를 직접 제어할 수 없으며, 실행 환경에 따라 매번 달라질 수 있다.
멀티 스레드 프로그램을 개발 시 먼저 몇개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해야 한다.
자바는 기본적으로 동기식으로 실행되기 때문에 시간이 오래 걸리는 작업(I/O, network 등)은 별도의 스레드로 분리해서 처리하는 것이 효율적이다. 자바에는 main thread가 반드시 존재하기 때문에 main 작업 이외에 추가적인 작업 수만큼 스레드를 생성하면 된다.
java.lang 패키지에 있는 Thread 클래스로부터 작업 스레드 객체를 직접 생성하는 방법이다.
Thread thread = new Thread(Runnable target);
Runnable은 스레드가 작업을 실행할 때 사용하는 인터페이스이다.
Runnable **task** = new Task();
Thread thread = new Thread(**task**);
Runnable 구현 클래스는 작업 내용을 정의한 것이므로, 스레드에게 전달해야 한다. Runnable 구현 객체를 생성한 후 Thread 생성자 매개값으로 Runnabale 객체를 전달한다.
thread.start();
작업 스레드를 실행하려면 스레드 객체의 start() 메소드를 다음과 같이 호출해야 한다.
package ch14.sec03.exam02;
import java.awt.Toolkit;
public class BeepPrintExample {
public static void main(String[] args) {
// 익명 구현객체로 생성과 동시에 정의했다.
Thread thread = new Thread(new Runnable() { // 작업 스레드 생성
// 작업 스레드가 실행하는 코드
@Override
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
});
**thread.start();** // 작업 스레드 실행
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
start() 실행한다.코드와 스레드 객체를 분리하여 유연한 구조를 제공한다. 다중 상속이 필요한 클래스와 함께 사용이 가능하다. 실무에서 권장되는 방식이다.
Thread 클래스를 상속한 다음 run() 메소드를 재정의해서 스레드가 실행할 코드를 작성하고 객체를 생성한다.
public class WorkerThread extends Thread{
**@Override**
public void run(){
//스레드가 실행할 코드
}
}
//스레드 객체 생성
Thread thread = new WorkerThread();
//스레드 실행
thread.start();
Thread.start()는 실제 새로운 스레드를 생성하여 run()을 비동기로 실행한다. run()만 호출하면 동기 실행이 되어 스레드를 분리하는 의미가 없다.
명시적인 클래스 정의보다, 익명 객체 방식이 일회성 또는 간단한 스레드 작업에서는 더 자주 사용된다.
thread.**setName**("스레드 이름");스레드 이름은 디버깅할 때 어떤 스레드가 작업을 하고 있는지 조사할 목적으로 주로 사용된다.
currentThread(): 현재 코드를 어떤 스레드가 실행하고 있는 지 정적 메소드로 스레드 객체의 참조를 얻는다.getName(): 이름을 가져온다.Thread thread = Thread.currentThread();
System.out.println(thread.getName());

run() 메소드를 실행한다. 스케줄링에 의해 다시 실행 대기 상태로 돌아갔다가 다른 스레드가 실행 상태를 반복한다.run() 메소드가 종료된다.| 구분 | 메소드 | 설명 |
|---|---|---|
| 일시 정지로 보냄 | sleep(long mills) | 주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. |
join() | join() 메소드를 호출한 스레드는 일시 정지 상태가 된다. 실행 대기 상태가 되려면, join() 메소드를 가진 스레드가 종료돼야 한다. 스레드 간의 순서를 부여하기 위해 사용된다. | |
wait() | 동기화 블록 내에서 스레드를 일시 정지 상태로 만든다. | |
| 일시 정지에서 벗어남 | interrupt() | 일시 정지 상태일 경우, InterruptedException을 발생시켜 실행 대기 상태 또는 종료 상태로 만든다. |
| `notify() | ||
| notifyAll()` | wait() 메소드로 인해 일시 정지 상태인 스레드를 실행 대기 상태로 만든다. | |
| 실행 대기로 보냄 | yield() | 실행 상태에서 다른 스레드에게 실행을 양보하고 실행 대기 상태가 된다. |
wait, notify notifyAll은 Object 클래스의 메소드이고, 그 외는 Thread 클래스의 메소드이다.
package ch14.sec05.exam02;
public class JoinExample {
public static void main(String[] args) {
SumThread sumThread = new SumThread();
sumThread.start();
try {
**sumThread.join();**
} catch (InterruptedException e) {
}
System.out.println("1~100 합: " + sumThread.getSum());
}
}
메인스레드가 메인 코드를 실행하다가. sumThread.join()을 만나면 sumThread가 종료될 때까지 실지 정지 상태가 된다. sumThread가 종료되고 나서야 메인 스레드는 다시 실행된다.
멀티 스레드는 하나의 객체를 공유해서 작업할 수도 있다. 하지만 다른 스레드에 의해 객체 내부 데이터가 쉽게 변경될 수 있기 때문에 의도했던 것과는 다른 결과가 나올 수 있다.
이런 사이드 이펙트를 방지하기 위해 스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸면 된다.
이를 위해 자바는 동기화 메소드와 블록을 제공한다. 객체 내부에 동기화 메소드와 동기화 블록이 여러 개가 있다면 스레드가 이들 중 하나를 실행할 때 다른 스레드는 해당 메소드는 물론이고 다른 메소드 및 블록도 실행할 수 없다.
publc **synchronized** void method(){
//단 하나의 스레드만 실행하는 영역
}
스레드가 동기화 메소드를 실행하는 즉시 객체는 잠금이 일어나고, 메소드 실행이 끝나면 잠금이 풀린다.
public void method(){
//여러 스레드가 실행할 수 있는 영역
**synchronized**(공유객체){
//단 하나의 스레드만 실행하는 영역
}
//여러 스레드가 실행할 수 있는 영역
}
메소드 전체가 아닌 일부 영역을 실행할 때만 객체 잠금을 걸고 싶다면 위와 같이 동기화 블록을 생성한다.
경우에 따라서 두개의 스레드를 교대로 번갈아 가며 실행할 때도 있다. 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고 자신은 일시 정지 상태로 만드는 상태를 반복하면 된다.
두 스레드가 작업할 내용을 각각 동기화 메소드로 정해 놓는다. 한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업을 하지 않도록 wait() 메소드를 호출하여 일시 정지 상태로 만든다.

두 메소드 모두 동기화 메소드(블록) 내에서만 사용할 수 있다.
사용하던 리소스들을 정리하고 run() 메소드를 빨리 종료해줘야 스레드를 실행 중간에 안전 종료할 수 있다.
while문으로 반복 실행 시 조건을 이용해run()메소드 종료를 유도
public class XXXThread extends Thread{
private boolean stop; //stop이 필드 선언
public void run(){
while(!stop){//stop이 true가 되면 while문을 빠져나감
//스레드가 반복 실행하는 코드;
}
//리소스 정리
//스레드가 사용한 리소스 정리
} // 스레드 종료
}
interrupt() 메소드 이용예외 처리를 통해 run() 메소드 정상 종료
interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다.
XThread thread = new XThread();
thread.start();
...
thread.interrupt();
public void run(){
try{
while(true){
...
Thread.sleep(1); //일시 정지
}catch(InterruptedException e){}
//스레드가 사용한 리소스 정리
}
스레드가 실행 대기/실행 상태일 때는 interrupt() 메소드가 호출돼도 InterruptedException이 발생하지 않는다. 일시 정지 상태일 때만 예외가 발생할 수 있다.
Thread의 interrupted()와 isInterrupted() 메소드는 interrupt() 메소드 호출 여부를 리턴한다.
중간에 예외처리를 통해 강제 종료되기 때문에 cleanup하게 종료되지 않아 권장되는 방식은 아니다.
주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드
주 스레드가 종료되면 데몬 스레드도 따라서 자동으로 종료된다.
주 스레드가 데몬이 될 스레드의 setDaemon(true)를 호출하면 된다.
public static void main(String[] args){
AutoSaveThread thread = new AutoSaveThread();
thread.setDaemon(true);
thread.start();
...
}
작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐에 들어오는 작업들을 스레드가 하나씩 맡아 처리하는 방식
병렬 작업 증가로 인한 스레드의 폭증을 막기 위해서 스레드풀을 사용한다. 작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리한다. 이렇게 하면 작업량이 증가해도 스레드의 개수가 늘어나지 않아 애플리케이션의 성능이 급격히 저하되지 않는다.

java.util.concurrent 패키지에서 ExecutorService 인터페이스와 Excecutors 클래스를 제공한다.
| 메소드명(매개변수) | 초기 수 | 코어 수 | 최대 수 |
|---|---|---|---|
newCachedThreadPool() | 0 | 0 | Integer.MAX_VALUE |
newFixedThreadPool(int n Threads) | 0 | 생성된 수 | nThreads |
ExecutorService executorService = Executors.newCachedThreadPool();
ExecutorService executorService = Executors.newFixedThreadPool(5);
ExecutorService threadPool = new ThreadPoolExecutor{
3, //코어 스레드 개수
100, //최대 스레드 개수
120L, //놀고 있는 시간
TimeUnit.SECONDS, //놀고 있는 시간 단위
new SynchronousQueue<Runnable>() //작업큐
);
데몬 스레드가 아니기 때문에 ExecutorService의 메소드를 사용해서 직접 종료해줘야 한다.
| 리턴 타입 | 메소드명(매개변수) | 설명 |
|---|---|---|
void | shutdown() | 현재 처리 중인 작업뿐만 아니라 작업 큐에 대기하고 있는 모든 작업을 처리한 뒤에 스레드풀을 종료시킨다. |
List<Runnable> | shutdownNow() | 현재 작업 처리 중인 스레드를 interrupt해서 작업을 중지시키고 스렏풀을 종료시킨다. 리턴값은 작업 큐에 있는 미처리된 작업(Runnable)의 목록이다. |
남아있는 작업을 마무리하고, 스레드풀을 종료할 때는 shutdown()을 호출하고, 남아있는 작업과는 상관없이 강제로 종료할 때는 shutdownNow()를 호출한다.
new Runnable(){
@Override
public void run(){
//스레드가 처리할 작업 내용
}
}
new Callable<T>(){
@Override
public T call() throws Exception{
//스레드가 처리할 작업 내용
return T;
}
}
Runnable의 run() 메소드는 리턴값이 없고, Callable의 call() 메소드는 리턴값이 있다. call()의 리턴 타입은 Callable<T>에서 지정한 T 타입 파라미터와 동일한 타입이어야 한다.
| 리턴타입 | 메소드명(매개변수) | 설명 |
|---|---|---|
void | execute(Runnable command) | Runnable을 작업 큐에 저장한다. 작업 처리 결과를 리턴하지 않는다. |
Future<T> | submit(Callable<T> task) | Callable을 작업 큐에 저장한다. 작업 처리 결과를 얻을 수 있도록 Future를 리턴한다. |