65강. 멀티 스레드
66강. 스레드 제어
ㅁ sec01.exam01 ~ sec01.exam05 까지의 내용이 빠져 있음. 대신 '이것이 자바다' 강의.
- [혼자 공부하는 자바] 65강. 12-1 멀티 스레드
- [혼자 공부하는 자바] 66강. 12-2 스레드 제어
- [자바 기초 강의] 136강. 14.1 멀티 스레드 개념
- [자바 기초 강의] 137강. 14.2 메인 스레드
- [자바 기초 강의] 138강. 14.3 작업 스레드 생성과 실행
- [자바 기초 강의] 139강. 14.4 스레드 이름
- [자바 기초 강의] 140강. 14.5 스레드 상태
- [자바 기초 강의] 141강. 14.6 스레드 동기화
- [자바 기초 강의] 142강. 14.7 스레드 안전 종료
- [자바 기초 강의] 143강. 14.8 데몬 스레드
- [자바 기초 강의] 144강. 14.9 스레드풀
ㅁ 키워드
- 프로세스 : 운영체제로부터 실행에 필요한 메모리를 할당받아 실행하는 애플리케이션
- 멀티 스레드 : 하나의 프로세스 내에 동시에 실행하는 스레드가 2개 이상인 경우
- 메인 스레드 : 자바의 모든 애플리케이션은 메인 스레드가 main() 메소드를 실행
- 동기화 메소드 : 한번에 하나의 스레드만 호출할 수 있도록 하는 메소드
- 작업 스레드 : 메인 작업 이외에 병렬 작업의 수만큼 생성하는 스레드
(Thread 클래스를 직접 객체화해서 생성할 수도 있고, Thread 클래스를 상속해서 하위 클래스를 만들어 생성할 수도 있음)
- 스레드 상태 : 스레드를 생성하고 시작하면 스레드는 다양한 상태를 가지게 된다.
- 일시정지: Thread.sleep() 메소드를 호출한 스레드는 주어진 시간 동안 일시정지 상태가 되고 다시 실행 대기 상태로 돌아감.
- 안전한 종료 : 스레드를 안전하게 종료하기 위해 stop 플래그나 interrupt() 메소드를 이용할 수 있다.
- 데몬 스레드 : 주 스레드가 종료되면 자동으로 종료되는 스레드
[자바 기초 강의] 136강. 14.1 멀티 스레드 개념
ㅁ 멀티 스레드 개념
- 운영체제는 실행 중인 프로그램을 프로세스로 관리한다.
멀티 태스킹은 두 가지 이상의 작업을 동시에 처리하는 것을 말한다.
이 때 운영체제는 멀티 프로세스를 생성해서 처리한다.
- 하지만 멀티 태스킹이 꼭 멀티 프로세스를 뜻하지는 않는다.
하나의 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진 프로그램도 있다.
ex) 메신저 - 채팅 작업을 하면서 동시에 파일 전송 작업 수행.
- 하나의 프로세스가 두 가지 이상의 작업을 처리할 수 있는 이유는 멀티 스레드(multi thread)가 있기 때문이다.
스레드(thread)는 코드의 실행 흐름을 말한다.
프로세스 내에 스레드가 두 개라면 두 개의 코드 실행 흐름이 생긴다는 의미이다.
- 멀티 프로세스가 프로그램 단위의 멀티 태스킹이라면, 멀티 스레드는 프로그램 내부에서의 멀티 태스킹이라고 볼 수 있다.
멀티 프로세스는 프로세스가 여러 개.
멀티 스레드는 한 프로세스 내에 스레드가 여러 개.
- 멀티 프로세스들은 서로 독립적이므로 하나의 프로세스에서 오류가 발생해도 다른 프로세스에게 영향을 미치지 않음.
그러나 멀티 스레드는 프로세스 내부에서 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스가 종료되므로 다른 스레드에 영향을 미친다.
ex) 워드와 엑셀을 동시에 사용하는 도중에 워드에 오류가 생기더라도 엑셀은 여전히 사용 가능하다.
반면 멀티 스레드로 동작하는 메신저는 파일을 전송하는 스레드에서 예외가 발생하면 전체 프로세스가(메신저 프로세스 자체)가 종료되기 때문에 채팅 스레드도 같이 종료된다.
- 그렇기에 멀티 스레드를 사용할 경우에는 예외 처리에 만전을 기해야 한다.
- 이전까지 한 예제는 전부 싱글 스레드였다.
ㅁ 멀티 스레드의 사용
- 동시에 해야할 작업이 있을 때 멀티 스레드를 사용한다.
데이터를 분할해서 병렬로 처리하거나, 게임 등 여러가지 작업을 동시에 하는 경우, 서버 프로그램을 개발할 때(서버 하나에 클라이언트가 다수인 경우), 안드로이드 앱에서 네트워크 통신 등
- 프로그램 개발에 있어 멀티 스레드는 꼭 필요한 기능이다.
웹 개발에 있어서는 멀티 스레드가 많이 사용되지는 않는다.
웹 서버를 개발하는 것이 웹 개발 과정이 아니기 때문.
웹 서버는 있고, 그 안에서 실행하는 웹 애플리케이션을 개발하는 것이 웹 개발 과정.
[자바 기초 강의] 137강. 14.2 메인 스레드
ㅁ 메인 스레드
- 이클립스에서 실행하거나 명령라인에서 자바 명령어로 메인 메소드가 있는 클래스를 실행시킬 때, 기본적으로 JVM은 메인스레드를 만든다.
- 그 메인 스레드가 메인 메소드를 찾아서 실행한다. 메인 메소드가 끝나면 메인 스레드도 종료된다. 메인 스레드가 끝나면 프로세스도 같이 종료된다(싱글 스레드의 경우).
- 모든 자바 프로그램은 메인 스레드가 main() 메소드를 실행하면서 시작된다.
메인 스레드는 main() 메소드의 첫 코드부터 순차적으로 실행하고, main() 메소드의 마지막 코드를 실행하거나 return 문을 만나면 실행을 종료한다.
- 메인 스레드는 필요에 따라 추가 작업 스레드들을 만들어서 실행시킬 수 있다.
메인 스레드(의 코드)가 작업 스레드1을 생성하고 실행시키고, 또 따로 작업 스레드2를 생성하고 실행시킬 수 있다.
- 싱글 스레드는 메인 스레드가 종료되면 프로세스도 종료된다.
하지만 멀티 스레드는 실행 중인 스레드가 하나라도 있다면 프로세스는 종료되지 않는다.
메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않는다.
ㅁ 자바에서는 스레드도 하나의 객체로 관리한다.
- 그래서 Thread라는 클래스가 있다.
클래스 Thread 타입으로 변수를 선언하고, Thread 객체를 대입해야 하는데,
현재 이 코드를 실행하는 Thread 객체의 참조를 구하는 법은 Thread.currentThread(); (정적) 메소드를 실행하여 현재 스레드의 객체를 리턴받는다.
- 스레드는 이름이 있다. 현재 스레드의 이름을 출력. currThread.getName());
[자바 기초 강의] 138강. 14.3 작업 스레드 생성과 실행
ㅁ 작업 스레드 생성과 실행
- 멀티 스레드로 실행하는 프로그램을 개발하려면 먼저 몇 개의 작업을 병렬로 실행할지 결정하고 (동시에 작업을 처리해야 하는 시점에 맞게) 각 작업별로 스레드를 생성해야 한다.
- 자바 프로그램은 메인 스레드가 반드시 존재하기 때문에 메인 작업 이외에 추가적인 작업 수만큼 스레드를 생성하면 된다. 자바는 작업 스레드도 객체로 관리하므로 클래스가 필요하다. Thread 클래스로 직접 객체를 생성해도 되지만, 하위 클래스를 만들어 생성할 수도 있다.
(1) Thread 클래스로 직접 생성
- java.lang 패키지에 있는 Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 다음과 같이 Runnable 구현 객체를 매개값으로 갖는 생성자를 호출하면 된다. (Thread 객체를 만들어서 Thread 타입 변수에 대입)
Thread thread = new Thread(Runnable target);
- Runnable은 스레드가 작업을 실행할 때 사용하는 인터페이스이다.
Runnable에는 run() 메소드가 정의되어 있는데, 구현 클래스는 run()을 재정의해서 스레드가 실행할 코드를 가지고 있어야 한다.
다음은 Runnable 구현 클래스를 작성하는 방법이다.
class Task implements Runnable {
@Override
public void run() {
// 스레드가 실행할 코드
}
}
- Runnable 구현 클래스는 작업 내용을 정의한 것이므로, 스레드에게 전달해야 한다.
Runnable 구현 객체를 생성한 후 Thread 생성자 매개값으로 Runnable 객체를 다음과 같이 전달하면 된다.
Runnable task = new Task(); // Task task = new Task(); 도 동일함.
Thread thread = new Thread(task);
ㅁ 예제
- Task.java
package sec01.exam28;
public class Task implements Runnable {
@Override
public void run() {
for(int i=0; i<5; i++) {
System.out.println("띵");
try {
Thread.sleep(1500);
}
catch (InterruptedException e) {}
}
}
}
- BeepPrintExample.java
package sec01.exam28;
import java.awt.Toolkit;
public class BeepPrintExample {
public static void main(String[] args) {
Runnable task = new Task();
Thread thread2 = new Thread(task);
thread2.start(); // 이게 위로 와야 동시 실행됨;
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i = 0; i<5; i++) {
toolkit.beep();
try { Thread.sleep(1500); } catch(Exception e) { }
}
}
}
- 명시적인 Runnable 구현 클래스를 작성하지 않고 Thread 생성자를 호출할 때 Runnable 익명 구현 객체를 매개값으로 사용할 수 있다. 오히려 이 방법이 더 많이 사용된다. (인터페이스를 구현한 코드가 여기서만 사용되는 경우)
Thread thread = new Thread( new Runnable() {
@Override
public void run() {
// 스레드가 실행할 코드
}
} );
- 작업 스레드 객체가 생성되었다고 해서 바로 작업 스레드가 실행되지는 않는다.
작업 스레드를 실행하려면 스레드 객체의 start() 메소드를 다음과 같이 호출해야 한다.
thread.start();
start() 메소드가 호출되면, 작업 스레드는 매개값으로 받은 Runnable의 run() 메소드를 실행하면서 작업을 처리한다.
- 작업 스레드가 생성되고 실행되기까지의 순서
i) 메인스레드가 실행되다 new Thread(task)로 스레드 객체 생성.
ii) start() 메소드 호출
iii) 메인 스레드 실행과 run() 메소드의 작업 스레드가 동시에 실행
ㅁ 예제
-
package sec01.exam29;
import java.awt.Toolkit;
public class BeepPrintExample {
public static void main(String[] args) {
//작업1
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(1500); } catch(Exception e) { }
}
}
});
thread.start();
//작업2
for(int i=0; i<5; i++) {
System.out.println("띵");
try {
Thread.sleep(1500);
}
catch (InterruptedException e) {}
}
}
}
- 1.5초마다 비프음과 "띵" 출력이 동시에 이루어짐.
(2) Thread 자식 클래스로 생성
- 작업 스레드 객체를 생성하는 또 다른 방법은 Thread의 자식 객체로 만드는 것이다. Thread 클래스를 상속한 다음 run() 메소드를 재정의해서 스레드가 실행할 코드를 작성하고 객체를 생성하면 된다.
public class WorkerThread extends Thread {
@Override
public void run() {
// 스레드가 실행할 코드
}
}
Thread thread = new WorkerThread(); // 스레드 객체 생성
thread.start();
- (1) 방식은 Runnable이라는 인터페이스의 추상메소드 run()을 오버라이딩한 것이고,
(2) 방식은 Thread가 직접 갖고 있는 run()을 오버라이딩하는 것.
- Thread 객체를 상속하는 자식 객체를 만들어서 부모 타입에 대입.
꼭 부모타입에 대입하지 않아도 됨. WorkerThread 타입으로 변수를 선언해도 됨.
( 자바에서 자료형을 Thread 대신 WorkerThread로 해도 됩니다. WorkerThread는 Thread 클래스를 상속받아 정의된 하위 클래스이므로, Thread 객체로 사용할 수 있는 모든 작업을 동일하게 수행할 수 있습니다.)
- 작업 스레드를 실행하는 방법은 동일하다. start() 메소드를 호출하면 작업 스레드는 재정의된 run()을 실행시킨다.
ㅁ 예제
- PrintThread.java
package sec01.exam30;
public class PrintThread extends Thread {
@Override
public void run() {
for(int i = 0; i<5; i++) {
System.out.println("띵");
try {
Thread.sleep(1500);
} catch (InterruptedException e) { }
}
}
}
- BeepPrintExample.java
package sec01.exam30;
import java.awt.Toolkit;
public class BeepPrintExample {
public static void main(String[] args) {
PrintThread pt = new PrintThread();
pt.start();
for(int i = 0; i<5; i++) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
toolkit.beep();
try { Thread.sleep(1500); } catch(Exception e) { }
}
}
}
- 명시적인 자식 클래스를 정의하지 않고, 다음과 같이 Thread 익명 자식 객체를 사용할 수도 있다.
오히려 이 방법이 더 많이 사용된다.
Thread thread = new Thread() {
@Override
public void run() {
// 스레드가 실행할 코드
}
};
thread.start();
ㅁ 예제
-
package sec01.exam30;
import java.awt.Toolkit;
public class BeepPrintExample {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i = 0; i<5; i++) {
toolkit.beep();
try { Thread.sleep(1500); }
catch(Exception e) { }
}
}
};
thread.start(); // 이게 위에 와야 동시 실행됨.
for(int i=0; i<5; i++) {
System.out.println("띵");
try {
Thread.sleep(1500);
}
catch (InterruptedException e) {}
}
}
}
[자바 기초 강의] 139강. 14.4 스레드 이름
ㅁ 스레드 이름
- 스레드는 이름을 갖고 있다. 스레드마다 이름을 갖고 있다. 기본 이름을 가지고 있고 필요에 따라 우리가 이름을 줄 수 있다.
- 스레드 이름은 언제 사용하냐면, 현재 코드가 어떤 스레드가 실행하는지 확인할 때 그때 스레드 이름을 한번 출력해보는것.
- 메인 스레드는 main이라는 이름을 가지고 있고, 작업스레드는 자동적으로 Thread-n이라는 이름을 가진다.
- 작업 스레드의 이름을 Thread-n 대신 다른 이름으로 설정하고 싶다면 Thread 클래스의 setName() 메소드를 사용하면 된다.
- getName()은 아무때나 호출할 수 있지만, setName("스레드 이름");은 스레드 실행 하기 전(start() 호출 전에)에 설정해야 함.
스레드 실행 중에 setName()으로 스레드 이름을 바꿀 수 없음.
- 셋네임과 겟네임은 스레드 참조가 필요함. 현재 스레드의 참조를 얻고 싶을 때는 currentThread() 사용.
Thread thread = new Thread(){
@Override
public void run() { // 쓰레드가 실행할 코드 }
}
thread.setName("스레드 이름");
- 스레드 이름은 디버깅(테스트)할 때 현재 코드를 어떤 스레드가 작업하는지 조사할 목적으로 주로 사용된다.
정적 메소드인 currentThread()로 스레드 객체의 참조를 얻은 다음 getName() 메소드로 이름을 출력해보면 된다.
Thread thread = Thread.currentThread(); // 이 코드를 실행하는 스레드 객체 참조 얻기(번지를 리턴함)(thread도객체다)
System.out.println(thread.getName() );
- ThreadNameExample.java
package ch14.sec04;
public class ThreadNameExample {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
System.out.println(mainThread.getName() + " 실행");
for(int i=0; i<3; i++) {
Thread threadA = new Thread() {
@Override
(1) public void run() { System.out.println(getName() + " 실행"); }
};
threadA.start();
}
Thread chatThread = new Thread() {
@Override
public void run() { System.out.println(getName() + " 실행"); }
};
chatThread.setName("chat-thread");
chatThread.start(); // 작업 스레드 이름 변경
}
}
[실행 결과]
main 실행
Thread-0 실행
Thread-1 실행
Thread-2 실행
chat-Thread 실행
- (1) 이건 getName앞에 참조변수가 안붙었음. 현재 객체의 메소드니까 앞에 참조변수가 안붙음.
- (1) getName() 메소드는 Thread의 인스턴스 메소드로 스레드의 이름을 리턴.
[자바 기초 강의] 140강. 14.5 스레드 상태
ㅁ 스레드는 상태를 가진다.
- 스레드 상태는 자동으로 변경될 수도 있고, 코드에 의해 변경될 수도 있다.
- 스레드 상태 제어
- sleep()이라는 메소드는 주어진 시간 동안 일시정지를 하고, 주어진 시간이 지나면 자동으로 실행 대기 상태로 가는데,
시간이 다 지나기 전에도 일시정지 상태에서 interrupt() 메소드가 호출되면 try 블록을 빠져나가서 catch 블록으로 가서, 스레드를 실행 대기 상태로 보낼 수 있다.
ㅁ 스레드 객체를 생성(new)하고, start() 메소드를 호출하면 곧바로 스레드가 실행되는 것이 아니라 실행 대기 상태(Runnable)가 된다. 실행 대기 상태란 실행을 기다리고 있는 상태를 말한다.
(실행을 대기하는 이유는 CPU 스케줄링에 따라서 스레드가 실행되기 때문)
ㅁ 실행 대기하는 스레드는 CPU 스케줄링에 따라 CPU를 점유하고 run() 메소드를 실행한다.
이때를 실행(Running) 상태라고 한다.
ㅁ 실행 스레드는 run() 메소드를 모두 실행하기 전에 스케줄링에 의해 다시 실행 대기 상태로 돌아갈 수 있다.
그리고 다른 스레드가 실행 상태가 된다.
이렇듯 스레드는 실행 대기 상태와 실행 상태를 번갈아 가면서 자신의 run() 메소드를 조금씩 실행한다.
(CPU가 몇초씩 여러 쓰레드를 실행시킴. 정확히는 CPU가 아니라 그 안의 코어가)
(연산을 하는 독립적인 단위가 코어. 코어가 쓰레드를 실행함. 한 CPU에 4개의 코어가 있으면 4개의 쓰레드를 실행시킬 수 있는거)
(논리 프로세서 : 코어 성능이 좋아서 운영체제가 한 코어에 한 쓰레드가 아니라, 한 코어를 논리적으로 두개로 쪼개서 두개의 코어처럼 사용. 코어는 6개 - 논리프로세서 12)
(실행 대기 상태 쓰레드가 한개가 아니고 여러개)
ㅁ 실행 상태에서 run() 메소드가 종료되면 더 이상 실행할 코드가 없기 때문에 쓰레드의 실행은 멈추게 된다.
이 상태를 종료 상태(Terminated)라고 한다.
ㅁ 실행 상태에서 (어떤 경우에는 실행 대기상태로 가지 않고) 일시 정지 상태로 가기도 하는데, 일시 정지 상태는 스레드가 실행할 수 없는 상태를 말한다.
스레드가 다시 실행 상태로 가기 위해서는 일시 정지 상태에서 실행 대기 상태로 가야만 한다.
ex) Thread.sleep(500); 하면 실행 상태에서 0.5초간 일시 정지. 실행 상태가 아닌 실행 대기상태로 감.
※ java.lang.Thread 가면 getState() 메소드가 있음.
반환형이 Thread.State임. 이게 뭐냐. 중첩 클래스나 중첩 인터페이스일 때 반환형이 이렇다.
Thread.state 중첩클래스는 반환형이 enum임.
열거 상수의 종류로 NEW, RUNNALBE, TERMINATED +
BLOCKED, TIMED_WAITING +
WAITING이 선언되어 있다.
BLOCKED와 TIMED_WAITING은 일시정지. 후자는 시간이 지나면 풀리고 전자는 조건이 만족되면 풀림.
ex) 스캐너 nextLine()하면 사용자가 엔터치기 전까지 멈춰있음.
WAITING은 wait 메소드 호출했을 때 일시정지되는 특별한 경우.
ㅁ 다음은 일시 정지로 가기 위한 메소드와 벗어나기 위한 메소드들을 보여준다.
ㅁ 주어진 시간 동안 일시 정지
- 실행 중인 스레드를 일정 시간 멈추게 하고 싶다면 Thread 클래스의 정적 메소드인 sleep()을 이용하면 된다.
매개값에는 얼마 동안 일시 정지 상태로 있을 것인지 밀리세컨드(1/1000) 단위로 시간을 주면 된다.
다음 코드는 1초 동안 일시 정지 상태를 만든다.
try {
Thread.sleep(1000);
}
catch (InterrupedException e) {
// interrupt() 메소드가 호출되면 실행
}
- 일시 정지 상태에서는 InterruptedException이 발생할 수 있기 때문에 sleep()은 예외 처리가 필요한 메소드이다.
InterruptedException에 대해서는 추후 설명한다.
import java.awt.Toolkit;
public class SleepExample {
public static void main(String[] args) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i=0; i<10; i++) {
toolkit.beep(); // 너무 빨리 실행하면 소리가 안들림. 소리 실행 시간이 있으니.
try {
Thread.sleep(3000); // main 스레드가 일시정지된다.
}
catch (InterrupedException e) { }
}
}
}
- Thread 클래스에 sleep() 메소드가 static으로 선언되어 있다.
public static void sleep(long millis) throws InterruptedException
저래서 호출한 쪽에서 예외처리코드(try ~ catch 블록)를 작성 해야함.
근데 sleep을 할 때(일시정지 상태일 때) 저 예외가 발생할 일은 거의 없어서 비워 놓음.
ㅁ 다른 스레드의 종료를 기다림
- 일시정지 상태에 있다가 다른 스레드가 종료되면 일시정지에서 풀려남.
다른 쓰레드가 종료될 때까지 기다렷다가 실행을 해야 하는 경우가 있다.
(계산 스레드의 작업이 종료된 후 결과값을 받아 처리하는 경우)
- ThreadA가 ThreadB의 join() 메소드를 호출하면 ThreadA는 ThreadB가 종료할 때까지 일시정지 상태가 된다.
ThreadB의 run() 메소드가 종료되고 나서야 비로소 ThreadA는 일시정지에서 풀려 다음 코드를 실행한다.
===ThreadA=====
threadB.start();
threadB.join(); // ThreadB가 아닌 ThreadA가 일시 정지
-
SumThread.java
package ch14.sec05.exam02;
public class SumThread extends Thread {
private long sum;
public long getSum() { return sum; }
public void setSum(long sum) { this.sum = sum; }
@Override
public void run() {
for(int i=1; i<=100; i++ ){ sum += i; }
}
}
JoinExample.java
package ch14.sec05.exam02;
public class JoinExample {
public static void main(String[] args) {
SumThread s = new SumThread();
s.start();
try {
s.join();
}
catch (InterruptedException e ) { }
System.out.println("1 ~ 100 합 : " + s.getSum() );
}
}
- join이 없으면 답이 0이 나오거나 5050이 이전의 값이 나올 수도 있다.
쓰레드는 start() 메소드가 호출된다고 바로 실행 상태로 가는 것이 아니고 실행 대기상태에 있기 때문.
ㅁ 다른 스레드에게 실행 양보
- 내가 실행 상태지만 실행 대기 상태로 빠지고 다른 쓰레드가 실행 기회를 얻을 수 있게 함.
- 스레드가 처리하는 작업은 반복적인 실행을 위해 for문이나 while문을 포함하는 경우가 많은데, 가끔 반복문이 무의미한 반복을 하는 경우가 있다.
public void run() {
while(true) {
if(work) {
System.out.println("ThreadA 작업 내용");
}
}
}
- work의 값이 false라면 while문은 어떠한 실행문도 실행하지 않고 무의미한 반복을 한다.
- 이때는 다른 스레드에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 프로그램 성능에 도움이 된다.
이런 기능을 위해 Thread는 yield() 메소드를 제공한다.
yield()를 호출한 스레드는 실행 대기 상태로 돌아가고, 다른 스레드가 실행 상태가 된다.
-
WorkThread.java
package ch14.sec05.exam03;
public class WorkThread extends Thread {
public boolean work = true;
public WorkThread(String name) { setName(name); } // 상속받은 Thread의 쓰레드 이름 설정하는 setName 메소드
@Override
public void run() {
while(true) {
if(work) { System.out.println(getName() + ": 작업처리");
} else { Thread.yield(); } // 실행 대기 상태가 되었다가 자신이 다시 실행 기회를 얻었을 때 다시 실행
}
}
}
YieldExample.java
package ch14.sec05.exam03;
public class YieldExample {
public static void main(String[] args) {
WorkThread threadA = new WorkThread("threadA");
WorkThread threadB = new WorkThread("threadB");
threadA.start(); // 실행이 아닌 실행 대기 상태가 됨.
threadB.start(); // 둘 중 뭐가 먼저 실행될지는 모른다!
try { Thread.sleep(5000); } // 메인스레드가 5초 쉰다.
catch (InterruptedException e) { }
threadA.work = false; // 메인스레드가 5초 쉬고 false로 바꾼다. 스레드A가 실행 대기상태가 된다.
try { Thread.sleep(10000); }
catch (InterruptedException e) { }
threadA.work = true; // 10초 뒤에 비로소 스레드A의 run이 실행된다.
}
}
[실행 결과]
threadA: 작업처리
...
threadB: 작업처리
...
threadB: 작업처리
threadB: 작업처리
threadB: 작업처리
...
threadA: 작업처리
...
threadB: 작업처리
...
- 여기에 동기화 처리를 한 경우, 쓰레드 A든 B든 먼저 시작된 쓰레드가 락을 건다.
동기화 처리를 한다면 첫번쨰 클래스의 getName이 있는 부분, 두번째 클레스의 threadA.work를 변경하는 두 문장이 대상이다.
[자바 기초 강의] 141강. 14.6 스레드 동기화
ㅁ 동기화 = 뭔가를 맞추는 것. 스레드를 맞춘다. 두 개 이상의 스레드의 실행 순서를 맞춘다.
ㅁ 멀티 스레드는 하나의 객체를 공유해서 작업할 수도 있다.
이 경우 다른 스레드에 의해 객체 내부 데이터가 쉽게 변경될 수 있기 때문에 의도했던 것과는 다른 결과가 나올 수 있다.
ㅁ 쓰레드가 사용 중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸면 된다.
이를 위해 자바는 동기화 메소드와 동기화 블록을 제공한다.
ㅁ 객체 내부에 동기화 메소드와 동기화 블록이 여러 개가 있다면 쓰레드가 이들 중 하나를 실행할 때,
다른 쓰레드는 해당 메소드는 물론이고 (이 객체의)다른 동기화 메소드 및 블록도 실행할 수 없다. (동기화 붙은것 전부 호출 불가)
- 하지만 일반 메소드는 실행 가능하다.
+ 쓰레드A가 그 객체의 다른 동기화 메소드와 동기화 블록, 일반메소드를 실행하는 것은 괜찮다. 자기가 잠궜으니까.
(다른 쓰레드의 접근을 막는거 = 다른 쓰레드가 일시정지됨.)
ㅁ 동기화 메소드 및 블록 선언
- 동기화 메소드를 선언하는 방법은 다음과 같이 synchronized 키워드를 붙이면 된다.
public synchronized void method() {
// 단 하나의 쓰레드만 실행하는 영역
}
쓰레드A가 동기화 메소드를 실행하는 즉시 이 객체는 잠금이 일어나고, 메소드 실행이 끝나면 잠금이 풀린다.
- 메소드 전체가 아닌 일부 영역을 실행할 때만 객체 잠금을 걸고 싶다면 다음과 같이 동기화 블록을 만들면 된다.
Public void method() {
// 여러 스레드가 실행할 수 있는 영역
synchronized(공유객체) { // 잠금을 할 객체를 넣어준다.
// 단 하나의 쓰레드만 실행하는 영역
}
// 여러 스레드가 실행할 수 있는 영역
}
동기화 블록 이외의 코드를 실행할 때는 잠금이 일어나지 않음.
동기화 블록 코드를 실행할 때 잠금이 일어남.
ㅁ 공유 객체의 변수의 값을 바꾸는 메소드는 여러 쓰레드가 동시에 실행하면 안 됨.
한 쓰레드만 이 메소드를 실행하고 다른 쓰레드가 못 쓰게 (이 객체의 동기화 처리된 부분을) 잠글 때 동기화 처리.
ㅁ wait()과 notify()를 이용한 스레드 제어
- wait()과 notify(), notifyAll()은 Object 클래스의 메소드이므로 모든 클래스가 가지고 있다.
- 경우에 따라서 두 개의 스레드를 교대로 번갈아 가며 실행할 때도 있다.
- 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고 자신은 일시정지 상태로 만들면 된다.
- 이 방법의 핵심은 공유 객체에 있다. 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 정해 놓는다.
- 한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 일시정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업을 하지 않도록 wait() 메소드를 호출하여 일시정지 상태로 만든다.
- 주의할 점은 이 두 메소드는 동기화 메소드, 동기화 블록 내에서만 사용할 수 있다는 것이다.
-
WorkObject.java
package ch14.sec06.exam02;
public class WorkObject {
public synchronized void methodA() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + ": methodA 작업 실행");
notify(); // 다른 쓰레드를 실행 대기 상태로 만듦.
try { wait(); // 자신의 쓰레드는 일시 정지 상태로 만듦
} catch (InterruptedException e) { }
}
public synchronized void methodB() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + ": methodB 작업 실행");
notify(); // 다른 쓰레드를 실행 대기 상태로 만듦.
try { wait(); // 자신의 쓰레드는 일시 정지 상태로 만듦
} catch (InterruptedException e) { }
}
}
ThreadA.java
package ch14.sec06.exam02;
public class ThreadA extends Thread {
private WorkObject workObject;
public ThreadA(WorkObject workObject) { // 공유 작업 객체를 받음
setName("ThreadA"); // 스레드 이름 변경
this.workObject = workObject;
}
@Override
public void run() {
for(int i=0; i<10; i++) {
workObject.methodA(); // 동기화 메소드 호출
}
}
}
ThreadB.java
package ch14.sec06.exam02;
public class ThreadB extends Thread {
private WorkObject workObject;
public ThreadB(WorkObject workObject) { // 공유 작업 객체를 받음
setName("ThreadB"); // 스레드 이름 변경
this.workObject = workObject;
}
@Override
public void run() {
for(int i=0; i<10; i++) {
workObject.methodB(); // 동기화 메소드 호출
}
}
}
WaitNotifyExample.java
package ch14.sec06.exam02;
public class WaitNotifyExample {
public static void main(String[] args) {
WorkObject workObject = new WorkObject(); // 공유 작업 객체 생성
ThreadA threadA = new ThreadA(workObject); // 4줄에 걸쳐 작업 스레드 생성 및 실행
ThreadB threadB = new ThreadB(workObject);
threadA.start();
threadB.start();
}
}
[실행 결과]
ThreadA: methodA 작업 실행
ThreadB: methodB 작업 실행
...
...
ThreadA: methodA 작업 실행
ThreadB: methodB 작업 실행
- 메소드 A와 메소드 B가 동시에 일어나서는 안 됨. 교대로 작업이 일어나야 함. WorkObject는 공유 객체.
- notify()는 예외처리를 안 해도 되지만, wait()은 throws InterruptedException이 있어서 예외처리를 해야 한다.
[자바 기초 강의] 142강. 14.7 스레드 안전 종료
ㅁ 스레드의 안전한 종료를 위한 방법
- 스레드를 종료하기 위해 stop() 메소드를 사용할 수 있지만 이 stop() 메소드는 불완전한 종료를 유발할 수 있기 때문에 가급적 사용하지 않는 것이 좋습니다. 불완전한 종료는 스레드가 데이터베이스같은 자원을 사용할 때 스레드를 갑자기 중지시키면 파일과 같은 자원들이 안전하게 닫히지 않을 수 있음.
그래서 stop() 메소드는 deprecated 되어서 더이상 사용하지 않도록 권고하고 있습니다.
- 스레드 안전한 종료를 위한 방법은 2가지가 있습니다.
(1) stop 플래그를 이용하는 방법
(2) interrupt() 메소드를 이용하는 방법
ㅁ 스레드는 실행 흐름이다. 파일을 저장하다가 갑자기 중단해버리면 파일이 깨질 수도 있는 것이다.
멀티스레드 환경에서 스레드 하나만 문제가 발생해도 프로세스가 종료되어버린다.
(다른 스레드는 문제가 없음에도 불구하고)
사용하던 자원들(파일, 데이터베이스 작업내용)에 손실이 발생할 수 있다.
ㅁ 스레드는 자신의 run() 메소드가 모두 실행되면 자동적으로 종료되지만(가장 안전한 종료다), 경우에 따라서는 실행 중인 스레드를 즉시 종료할 필요가 있다. 예를 들어 동영상을 사용자가 끝까지 보지 않고 멈춤을 요구하는 경우이다.
ㅁ 스레드를 강제 종료시키기 위해 Thread는 stop() 메소드를 제공하고 있으나 이 메소드는 deprecated(더 이상 사용하지 않음)되었다. 그 이유는 스레드를 갑자기 종료하면 사용 중이던 리소스들이 불완전한 상태로 남겨지기 때문이다.
여기서 리소스란 파일, 네트워크 연결 등을 말한다.
(지금도 stop() 메소드를 실행하면 멈추긴 함)
ㅁ 스레드를 안전하게 종료하는 방법은 사용하던 리소스들을 정리하고 run() 메소드를 빨리 종료하는 것이다.
주로 조건 이용 방법과 interrupt() 메소드 이용 방법을 사용한다.
ㅁ 조건 이용
- 스레드가 while 문으로 반복 실행할 경우, 조건을 이용해서 run() 메소드의 종료를 유도할 수 있다.
다음 코드는 stop 필드 조건에 따라서 run() 메소드의 종료를 유도한다.
public class XXXThread extends Thread {
private boolean stop;
public void run() {
while( !stop ) {
// 스레드가 반복 실행하는 코드;
}
// 스레드가 사용한 리소스 정리
}
}
- 다음 예제는 메인 스레드에서 3초 후에 stop 필드값을 true로 설정해서 PrintThread를 종료한다.
PrintThread.java
package ch14.sec07.exam01;
public class PrintThread extends Thread {
private boolean stop; // 플래그 필드. 플래그는 상태값(true, false)을 가지는 것을 말함. 기본값은 false.
public void setStop(boolean stop) { this.stop = stop; }
@Override
public void run() {
while( !stop ) { // stop 필드값에 따라 반복 여부 결정
System.out.println("실행 중");
}
System.out.println("리소스 정리"); // 리소스 정리
System.out.println("실행 종료");
}
}
SafeStopExample.java
package ch14.sec07.exam01;
public class SafeStopExample {
public static void main(String[] args) {
PrintThread pt = new PrintThread();
pt.start();
try { Thread.sleep(3000); }
catch ( InterruptedException e ) { }
pt.setStop(true); // PrintThread를 종료하기 위해 stop 필드값 변경
}
}
[실행 결과]
실행 중
실행 중
...
실행 중
리소스 정리
실행 종료
- 외부에서 이 스레드가 동작하고 있을 때 이 스레드의 setStop 메소드를 호출(stop 플래그를 설정)해서 외부에서 스레드를 멈출 수 있다.
ㅁ interrupt() 메소드를 이용하는 방법
- ThreadA에서 ThreadB를 만들고 threadB.start();를 호출한 후에, threadB.interrupt(); 호출.
- ThreadB는 run()에서 while(true) 무한 루프로 코드가 몇개 실행되고, 후에 Thread.sleep(1)로 간간히 일시정지상태가 됨.
- ThreadA가 ThreadB의 interrupt() 메소드 실행한 이후 ThreadB가 반복을 종료하는 방법은 2가지.
1) 일시정지가 있는 경우 : 일시정지 상태가 될 때 InterruptedException 발생
( interrupt() 호출 후 일시정지가 될 때 종료됨)
2) 일시정지가 없는 경우 : 다음 두 메소드의 return 값이 true가 됨.
interrupted()
isInterrupted()
ㅁ interrupt() 메소드 이용
- 실행흐름을 방해하는 역할. 실행을 방해하는 게 아니고, 일시정지 상태를 방해한다.
- interrupt() 메소드는 스레드가 일시정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다.
이를 이용하면 예외처리를 통해 run() 메소드를 정상 종료시킬 수 있다.
- 스레드를 실행 상태에서 일시정지 상태로 바꾸는 방법은 sleep(), join(), wait()이 있다.
sleep(), join(), wait() 메소드를 쓸 때는 InterruptedException에 대한 예외처리를 해야 하게끔 되어있다.
언제 이 예외가 발생되느냐. interrupt() 메소드가 호출되면 발생한다.
Thread.sleep(3000); 으로 3초간 일시정지 상태에 있는데, 그 전에 스레드를 종료시키기 위해 interrupt() 메소드를 호출할 수 있다.
(갑자기 종료되는게 아니라, run()의 마지막 부분 즉 리소스 정리하는 부분을 실행하고 종료)
-
PrintThread.java
package ch14.sec07.exam02;
public class PrintThread extends Thread {
@Override
public void run() {
try {
while(true) {
System.out.println("실행중...");
Thread.sleep(1);
}
} catch(Exception e) {
System.out.println( e.getMessage() ); // 현재 예외에 대한 메세지 출력
}
System.out.println("리소스 정리");
System.out.println("실행 종료");
}
}
InterruptExample.java
package ch14.sec07.exam02;
public class InterruptExample {
public static void main(String[] args) {
Thread thread = new PrintThread();
thread.start();
try {
Thread.sleep(3000); // 메인 스레드가 3초 기다리고 interrupt() 실행함.
}
catch (InterruptedException e) { }
thread.interrupt();
}
}
[실행 결과]
실행중...
실행중...
...
sleep interrupted
리소스 정리
실행 종료
- PrintThread thread = new PrintThread(); 대신 Thread thread = new PrintThread();를 사용할 수 있다.
대신 이렇게 부모 클래스로 선언하면 부모 클래스가 가진 변수와 메소드만 사용 가능.
- 0.1초라도 스레드가 일시정지 상태로 간다면 interrupt로 예외를 발생시켜 (예외처리를 하게 해서) try 블록을 빠져나가게 할 수 있음.
- 스레드의 run() 메소드 안에 그 스레드를 일시정지로 만드는 코드가 있어야 한다.
main 메소드의 Thread.sleep()은 저 스레드가 아닌 main 스레드를 3초간 일시정지 시키는 것.
ㅁ 스레드가 실행 대기 / 실행 상태일 때에는 interrupt() 메소드가 호출되어도 InterruptedException이 발생하지 않는다.
- 그러나 스레드가 어떤 이유로 일시 정지 상태(0.1초라도)가 되면 InterruptedException 예외가 발생한다.
ㅁ Thread.sleep(1); 이렇게 스레드를 일시정지로 만드는 코드를 안넣고도 같은 효과를 낼 수 없나? 있다.
- 일시정지 상태를 만들지 않고도 interrupt() 메소드 호출 여부를 알 수 있는 방법이 있다.
- Thread의 interrupted()와 isInterrupted() 메소드는 interrupt() 메소드 호출 여부를 리턴한다. (초기값 false)
- interrupted()는 정적 메소드이고, isInterrupted()는 인스턴스 메소드이다.
boolean status = Thread.interrupted(); // 이 코드 이전에 interrupt 메소드가 호출되었다면 true 리턴.
boolean status = threadA.isInterrupted(); // 이 코드 이전에 interrupt 메소드가 호출되었다면 true 리턴.
- 이전 예제에서, Thread.sleep(1);를 넣는 이유는 interrupt() 메소드를 호출하면 일시정지 상태가 있어야 예외가 발생. 그래서 예최를 처리하고 정리하고 끝낼 수가 있음.
근데 Thread.sleep(1); 이게 눈의 가시다. 성능에 도움이 안되고 안좋은 영향이 있다. (while문이 0.1초씩 쉬고 도니까)
일시정지가 되어야 예외가 발생하니까 어쩔 수 없이 넣은 거임.
그래서 Thread.sleep(1); 보다 좀 더 좋은 방법은, Thread.interrupted()라는 메소드를 호출해서 이 메소드의 반환값을 보는 것이다. 방해되어졌느냐를 물어본거. 방해되어졌느냐를 물어본다는 것은 interrupt() 메소드가 호출되었는지 본다는 것.
public class PrintThread extends Thread {
@Override
public void run() {
while(true) {
System.out.println("실행중...");
// Thread.sleep(1); 이거 대신에
if(Thread.interrupted()) {
break;
}
}
System.out.println("리소스 정리");
System.out.println("실행 종료");
}
}
- 예외처리가 필요한 코드를 사용하지 않았기 때문에 try~catch 블록도 필요가 없다.
- 위는 정적 메소드를 사용한 거.
public class PrintThread extends Thread {
@Override
public void run() {
while(true) {
System.out.println("실행중...");
// Thread.sleep(1); 이거 대신에
if(isInterrupted()) { // 현재 객체의 인터럽트 상태 확인
break;
}
}
System.out.println("리소스 정리");
System.out.println("실행 종료");
}
}
- run() 메소드는 Thread가 가지고 있는 멤버다.
Thread가 가지고 있던 것 중 물려받은 거에 isInterrupted() 메소드가 있다.
그래서 저렇게 사용 가능.
- this.isInterrupted()를 호출하는 것과 동일합니다.
- 왜 isInterrupted()를 직접 호출할 수 있는가?
isInterrupted()는 Thread 클래스의 인스턴스 메서드이므로,
Thread 클래스를 상속받은 PrintThread 클래스의 인스턴스에서는 isInterrupted() 메서드를 직접 호출할 수 있습니다.
※ Thread의 interrupted()와 isInterrupted() 메소드는 그저 interrupt() 메소드가 호출되었는지만 따짐.
- 스레드가 sleep() 등으로 일시정지 상태가 되어야 interrupt() 메소드로 try 블록을 종료시키는 건데,
sleep() 등 없이 그냥 쓴다. (코드를 간결하게 하고 성능을 위해)
[자바 기초 강의] 143강. 14.8 데몬 스레드
ㅁ 데몬(daemon) 스레드
- 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드
- 주 스레드가 종료되면 자동으로 종료되는 스레드
- 주 스레드에서 보조 스레드 객체의 setDemon(ture)를 호출해서 설정함
(이러면 주 스레드가 종료되면 보조 스레드도 자동 종료됨)
- 단, 보조 스레드가 start() 하기 전에 데몬 스레드로 설정해야 한다.
ㅁ 데몬 스레드(daemon)는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다.
- 주 스레드가 종료되면 데몬 스레드도 따라서 자동으로 종료된다.
- 어떤 스레드든 주 스레드가 될 수 있다. 그 스레드가 데몬 스레드를 만들 수 있다는 것.
ㅁ 데몬 스레드를 적용한 예
- 워드프로세서의 일정 간격 자동 저장. 워드프로세서가 종료되면 자동저장 기능도 종료된다.
- 미디어 플레이어의 동영상 및 음악 재생. 미디어 플레이어가 종료되면 동영상 및 음악이 계속 재생되지 않음.
- 가비지 컬렉터. JVM 마다 가비지 컬렉터가 있다.
JVM 자체 즉, 프로세스 자체가 종료되는데 JVM에 속한 가비지 컬렉터가 계속 실행되지 않음.
- 주 스레드(워드프로세서, 미디어 플레이어, JVM)가 종료되면 데몬 스레드도 같이 종료된다.
ㅁ 스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true)를 호출하면 된다.
public static void main(String[] args) {
AutoSaveThread threadA = new AutoSaveThread();
threadA.setDaemon(true);
threadA.start();
...
}
- threadA의 setDaemon() 메소드를 메인 스레드가 실행하고 있다.
- 메인 스레드는 주 스레드, AutoSaveThread는 데몬 스레드가 된다.
- 메인 스레드가 끝나면 threadA 스레드도 끝난다.
-
AutoSaveThread.java
package ch14.sec08;
public class AutoSaveThread extends Thread {
public void save() {
System.out.println("작업 내용을 저장함.");
}
@Override
public void run() {
while(true) { // 무한루프
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
save();
}
}
}
DaemonExample.java
package ch14.sec08;
public class DaemonExample {
public static void main(String[] agrs) {
AutoSaveThread autoSaveThread = new AutoSaveThread();
autoSaveThread.setDaemon(true);
autoSaveThread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) { }
System.out.println("메인 스레드 종료");
}
}
[실행 결과]
작업 내용을 저장함.
작업 내용을 저장함.
메인 스레드 종료
- 스레드의 실행 타이밍과 JVM의 스케줄링에 따라 ''작업 내용을 저장함.''이 두번만 출력될 수 있습니다.
(다시 해보니 세번 나옴)
- AutoSaveThread는 무한 루프이고 명시적으로 이 스레드를 종료하는 코드가 없음.
그럼에도 불구하고 주 스레드인 main 스레드가 종료되면 AutoSaveThread도 종료된다.
- autoSaveThread.setDaemon(true);가 없으면 메인 스레드는 끝나도 AutoSaveThread는 영원히 실행된다.
[자바 기초 강의] 144강. 14.9 스레드풀 ( 정말 중요 )
ㅁ 그때 그때 스레드를 만들어서 병렬적으로 처리할 수도 있지만, 미리 스레드를 10개 정도 만들어 놓음.
그리고 그 스레드 10개를 가지고 병렬적으로 처리해야 하는 작업을 처리함.
왜 이렇게 하냐, 스레드를 만드는 것도 시간도 필요하다. 생성시간을 줄여주자는 것.
for(int i=0; i<100; i++) {
Thread thread = new Thread() {
public void run() { ... }
}
}
run() 메소드의 실행 시간이 굉장히 짧다고 가정.
쓰레드 객체를 매번 새로 100개를 만드는 것은 낭비다.
10개를 만들어놓고 재사용할 수도 있다.
(1번째 스레드가 11번째 작업을, 2번째 스레드가 12번째 작업을)
- 네모가 작업 객체(작업 내용). 이 하나 하나가 Runnable 구현 객체다.
- 스레드풀에 제한된 스레드를 만들어 놓는다.
- 풀이란 뭐냐. 앞으로 풀이란 용어가 나오면 여러 객체를 관리한다고 생각하세요.
- 큐는 먼저 들어온 것을 먼저 처리하는 자료구조이다.
※ "풀(pool)"이라는 용어는 다양한 맥락에서 사용되지만,
컴퓨터 과학과 프로그래밍에서 "풀(pool)"은 일반적으로 "자원을 미리 할당해 놓고 필요할 때 재사용하는 집합"을 의미.
- 스레드풀에 10개의 스레드가 있고, 작업 큐에 10개의 작업이 왔다.
10개의 스레드가 작업 10개를 가져가서 처리한다. (큐니까 먼저 온 작업이 먼저 배정됨)
그 중 작업 처리가 빨리 끝난 스레드한테 11번째 작업을 배정한다.
이러면 스레드풀의 스레드는 10개밖에 안되는데 그 이상을 처리할 수 있다.
- 작업 양에 비해 스레드 개수가 적게 만들어지면, 예외가 발생하지는 않고 작업 처리가 늦어짐.
은행 직원이 10명 대신 2명만 있다면 일처리는 엄청 늦어진다.
- 애플리케이션의 성격에 따라서 작업 양이 많을 수도 있고 적을 수도 있다.
거기에 따라서 적절하게 스레드 개수를 정해줘야 한다.
처음에 스레드를 5개 만들었다가 5개를 또 만들 수 있다.
나중에 작업 양이 줄어들면 스레드 개수를 다시 줄일 수도 있다.
※ 애플리케이션: 특정 작업이나 문제를 해결하기 위해 설계된 컴퓨터 프로그램.
ㅁ 병렬 작업 처리가 많아지면 스레드의 개수가 폭증하여 CPU가 바빠지고 메모리 사용량이 늘어난다.
이에 따라 애플리케이션의 성능 또한 급격히 저하된다.
이렇게 병렬 작업 증가로 인한 스레드의 폭증을 막으려면 스레드풀 ThreadPool을 사용하는 것이 좋다.
ㅁ 스레드풀은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐(Queue)에 들어오는 작업들을 스레드가 하나씩 맡아 처리하는 방식이다.
- 스레드풀(Thread Pool)은 멀티스레드 환경에서 스레드 생성과 관리를 효율적으로 하기 위해 사용되는 기법입니다.
ㅁ 작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리한다.
이렇게 하면 작업량이 증가해도 스레드의 개수가 늘어나지 않아 애플리케이션의 성능이 급격히 저하되지 않는다.
ㅁ 스레드풀 생성
- 자바는 스레드풀을 생성하고 사용할 수 있도록 java.util.concurrent 패키지에서 ExecutorService 인터페이스와 Executors 클래스를 제공하고 있다.
- Executors 클래스는 ExecutorService를 생성하는 팩토리 메서드를 제공합니다.
- ExecutorService는 실제로 스레드 풀을 나타내는 인터페이스입니다.
※ 팩토리 메서드(Factory Method)는 객체 지향 프로그래밍에서 사용되는 디자인 패턴 중 하나입니다.
이 패턴은 객체 생성을 담당하는 메서드를 별도로 정의하여 객체 생성을 추상화하고, 클라이언트에게 구체적인 객체 생성 방법을 숨기는 데 목적이 있습니다.
- Executors의 다음 두 정적 메소드를 이용하면 간단하게 스레드풀인 ExecutorService 구현 객체를 만들 수 있다.
- 이 두 메소드를 실행하면 ExecutorService의 구현 객체를 반환한다. (그래서 ExecutorService 인터페이스로 받는다)
- 초기 수는 스레드풀이 생성될 때 기본적으로 생성되는 스레드 수를 말하고,
코어 수는 스레드가 증가된 후 사용되지 않는 스레드를 제거할 때 최소한 풀에서 유지하는 스레드 수를 말한다.
최대 수는 증가되는 스레드의 한도 수이다.
(1) ExecutorService executorService = Executors..newCachedThreadPool();
- 위와 같이 newCachedThreadPool() 메소드로 생성된 스레드풀의 초기 수와 코어 수는 0개이고,
작업 개수가 많아지면 새 스레드를 생성시켜 작업을 처리한다.
(처음 스레드 풀을 만들 때 스레드 수가 0이고, 작업이 들어오면 작업 수만큼 스레드를 생성시킨다)
- 60초 동안 스레드가 아무 작업을 하지 않으면 스레드를 풀에서 제거한다.
- 작업 양에 따라 스레드가 자동적으로 만들어지는 것은 장점이지만, 스레드 개수 제한을 하지 않아서 메모리 사용량을 예측하기가 힘들어서 이 방법은 잘 사용되지 않는다.
(2) ExecutorService executorService = Executors.newFixedThreadPool(5);
- 위와 같이 newFixedThreadPool(int nThreads)로 생성된 스레드 풀의 초기 스레드 수는 0개이고,
작업 개수가 많아지면 최대 5개까지 스레드를 생성시켜 작업을 처리한다.
이 스레드풀의 특징은 생성된 스레드를 제거하지 않는다는 것이다.
- 제한된 스레드 수를 쓰기 때문에 메모리를 효율적으로 쓸 수 있지만, 스레드 수에 비해 작업 양이 더 많은 경우엔 작업 대기 시간이 길어질 수 있다.
(번외) 위 두 메소드를 사용하지 않고 직접 ThreadPoolExecutor 클래스로 객체를 만들어서 스레드풀을 생성할 수도 있다.
- java.util.concurrent.ThreadPoolExecutor는 Executor, ExecutorService를 구현하고 있다.
- 아래 예시는 초기 수 0개, 코어 수 3개, 최대 수 100개인 스레드풀을 생성하는 코드이다.
그리고 추가된 스레드가 120초 동안 놀고 있을 경우 해당 스레드를 풀에서 제거한다.
ExecutorService threadPool = new ThreadPoolExecutor(
3, // 코어 스레드 개수
100, // 최대 스레드 개수
120L, // 놀고 있는 시간 (매개변수가 long형 변수로 받음)
TimeUnit.SECONDS, // 놀고 있는 시간 단위
new SynchronousQueue<Runnable> () // 작업 큐 (작업 큐에 작업을 넣을때 한번에 한 작업씩 큐에 올라감)
);
- 생성자가 상당히 복잡하게 되어 있다.
ㅁ 스레드풀 종료
- (멀티스레드를 사용하는)애플리케이션이 종료될 때 스레드풀도 같이 종료가 되어야 한다.
- 그러나 스레드풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아 있다.
- 스레드풀의 모든 스레드를 종료하려면 ExecutorService의 다음 두 메소드 중 하나를 실행해야 한다.
- Runnable(작업 객체 = 작업 내용)을 저장하는 List를 리턴한다.
- shutdownNow()는 데이터 손실이 있을 수 있다. 중간에 작업이 중지되어서.
-
ExecutorServiceExample.java
package ch14.sec09.exam01;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorServiceExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5); // 스레드풀 생성
// 작업 생성과 처리 요청
executorService.shutdownNow(); // 스레드풀 종료
}
}
- 이 예제는 작업 생성과 처리 요청 코드가 없기 때문에, 작업 생성과 처리 요청을 하는 코드를 실행하지 않아도 프로세스가 종료된다.
그 이유는 스레드풀에 생성된 스레드가 없기 때문이다.
ㅁ 작업 생성과 처리 요청
- Runnable과 Callable은 인터페이스다. 구현 클래스는 쓰레드가 처리해야 할 작업 내용을 가진다.
- 작업 생성은 Runnable 구현 객체를 생성하는 것을 말한다.
- 처리 요청은 Runnalbe 구현 객체를 생성하고 스레드풀의 작업 큐에 넣는 행위를 말한다.
- 스레드 풀의 작업 큐에 있는 작업 객체를 어떤 스레드가 처리하는지는 스레드풀 내에서 정할 문제이고,
우리는 작업 객체(Runnable 구현 객체)를 만들어서 스레드풀의 작업 큐에 넣어주면 된다.
- 하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현한다.
- Runnable과 Callable의 차이점은 작업 처리 완료 후 리턴값이 있느냐 없느냐이다.
- 스레드가 작업을 가져가서 처리하고 리턴값이 없다면 작업객체를 Runnable로 만들고,
스레드가 작업을 가져가서 처리하고 리턴값을 반환해야 한다면 작업객체를 Callable로 만들어라.
(대부분은 스레드풀을 사용하는 대부분의 작업은 리턴값이 없다.)
(1) java.lang.Runnable은 인터페이스이다.
abstract void run()이라는 추상 메소드만 있다.
구현 클래스는 이 run()을 재정의해서 작업 내용을 작성한다.
구현 클래스에서는 작업의 내용만 정의하고, 실제로 작업은 쓰레드가 처리한다.
(2) java.lang.Callable<V>은 제네릭 인터페이스이다.
V는 value(리턴값)의 약자다.
Callable을 구현할 때 반환하고자 하는 실제 데이터 타입을 명시한다.
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 여기에 원하는 연산이나 처리를 수행하고 Integer 값을 반환
return 42;
}
public static void main(String[] args) throws Exception {
MyCallable callable = new MyCallable();
Integer result = callable.call();
System.out.println("Result: " + result);
}
}
- call()이라는 추상 메서드에 throws Exception이 붙어있다.
[ V call() throws Exception ]
인터페이스에 있는 그대로 가져가서 구현 클래스에서 재정의해야 하기 때문이다.
- call()의 리턴 타입은 Callable<T>에서 지정한 T 타입 파라미터와 동일한 타입이어야 한다.
ㅁ 작업 처리 요청
- 작업 처리 요청이란 ExecutorService(스레드풀)의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위를 말한다.
- 작업 처리 요청을 위해 ExecutorService는 다음 두 가지 메소드를 제공한다.
- Runnable 또는 Callable 객체가 ExecutorService의 작업 큐에 들어가면 ExecutorService는 처리할 스레드가 있는지 보고, 없다면 스레드를 새로 생성시킨다.
- 스레드는 작업 큐에서 Runnable 또는 Callable 객체를 꺼내와 run() 또는 call() 메소드를 실행하면서 작업을 처리한다.
- 아래 예제는 이메일을 보내는 작업으로, 1000개의 Runnable을 생성한 다음 execute() 메소드로 작업 큐에 넣는다.
ExecutorService는 최대 5개의 스레드로 작업 큐에서 Runnable을 하나씩 꺼내어 run() 메소드를 실행하면서 작업을 처리한다.
RunnableExecuteExample.java
package ch14.sec09.exam02;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class RunnableExecuteExample {
public static void main(String[] args) {
String[][] mails = new String[1000][3];
for(int i=0; i<mails.length; i++) {
mails[i][0] = "admin@my.com";
mails[i][1] = "member" + i + "@my.com";
mails[i][2] = "신상품 입고";
}
ExecutorService ex = Executors.newFixedThreadPool(5);
for(int i=0; i<1000; i++) {
final int idx = i;
ex.execute( new Runnable() { // 작업 처리 요청
@Override
public void run() {
Thread thread = Thread.currentThread();
String from = mails[idx][0];
String to = mails[idx][1];
String content = mails[idx][2];
System.out.println("[" + thread.getName() + "]" +
from + " == > " + to + ": " + content);
}
});
}
ex.shutdown(); // ExecutorService 종료
}
}
[실행 결과]
[pool-1-thread-4]admin@my.com == > member3@my.com: 신상품 입고
[pool-1-thread-1]admin@my.com == > member0@my.com: 신상품 입고
[pool-1-thread-5]admin@my.com == > member4@my.com: 신상품 입고
[pool-1-thread-3]admin@my.com == > member2@my.com: 신상품 입고
[pool-1-thread-5]admin@my.com == > member6@my.com: 신상품 입고
[pool-1-thread-4]admin@my.com == > member5@my.com: 신상품 입고
...
- 자바에서 ExecutorService를 사용하여 스레드 풀에서 작업을 실행할 때는 작업이 완료되는 순서를 보장할 수 없습니다. 따라서 여러 스레드가 병렬로 실행될 때 출력 순서가 보장되지 않습니다. 이는 스레드 풀에서 각 작업이 어떤 스레드에 할당되고 실행되느냐에 따라 달라집니다.
- execute()로 작업처리 요청 보냈다고 바로 실행되는 것이 아님.
스레드풀의 작업큐에 보낸것 뿐이다.
언제 실행될지는 모름. 쓰레드가 배정되야 하고 쓰레드가 가져가서 처리해야 하는 것이기 때문에.
- fianl int idx = i; 를 쓰는 이유. // 자바 8부터는 final도 생략 가능.
왜 그냥 i를 안쓰고 idx를 따로 선언하고 거기에 i를 대입해서 쓰냐.
매개변수나 로컬변수를 로컬클래스 안에서 사용하면 final 특성을 갖기 때문에 반복문의 i의 값이 증가하지 않게된다.
- 자바에서는 익명 클래스 내부에서 외부의 지역 변수를 사용할 때, 그 변수는 반드시 final이거나 final처럼 동작해야 합니다. 이는 자바의 설계 결정이다.
- 익명 객체도 로컬 클래스를 정의해서 만드는 것과 동일하다.
- 아래 예제는 자연수를 덧셈하는 작업으로, 100개의 Callable을 생성하고 submit() 메소드로 작업 큐에 넣는다.
ExecutorService는 최대 5개 스레드로 작업 큐에서 Callable을 하나씩 꺼내어 call() 메소드를 실행하면서 작업을 처리한다.
CallableSubmitExample.java
package ch14.sec09.exam03;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableSubmitExample {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(5);
for(int i=1; i<=100; i++) {
final int idx = i;
Future<Integer> future = es.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i=1; i<=idx; i++) {
sum += i;
}
Thread thread = Thread.currentThread(); // 현재 작업을 하는 쓰레드 이름 구하려고
System.out.println("[" + thread.getName() + "] 1~" + idx + " 합 계산");
return sum; // 작업 처리 결과 리턴
}
});
try {
int result = future.get(); // Callable의 call() 메소드가 리턴한 값 얻기
System.out.println("\t리턴값: " + result);
} catch (Exception e) { e.printStackTrace(); }
}
es.shutdown();
}
}
[실행 결과]
[pool-1-thread-1] 1~1 합 계산
리턴값: 1
[pool-1-thread-2] 1~2 합 계산
리턴값: 3
...
[pool-1-thread-4] 1~99 합 계산
리턴값: 4950
[pool-1-thread-5] 1~100 합 계산
리턴값: 5050
- 쓰레드풀의 submit() 메소드를 이용하여 작업 객체를 Callable로 만든 다음에 작업 큐에 넣었다.
Callable<Integer>라는 것은 작업을 하고 난 뒤의 반환값의 자료형이 Integer라는 것.
- Future<Integer> future : Integer 값을 갖고 있는 객체라는 뜻.
- 30라인에서 Future의 get() 메소드는 작업이 끝날 때까지 기다렸다가 call() 메소드가 리턴한 값을 리턴한다.
submit()을 했다고 바로 작업 결과를 받을 수 없다.
쓰레드풀의 작업 큐에 넣기만 했지 언제 쓰레드가 실행할지는 모름.
그래서 일시정지 상태(Blocking 상태)가 된다.
- Callable이 보내는 리턴값을 future 객체에 저장했다가, future.get()으로 Callable의 call() 메소드가 리턴한 값 얻기.
- future.get() 메서드는 Future 객체가 작업을 완료할 때까지 블록킹(Blocking)되어 대기하며, 작업이 완료되면 작업의 결과를 반환합니다. 작업이 완료되지 않은 상태에서 future.get()을 호출하면, 호출한 스레드는 해당 작업이 완료될 때까지 대기 상태에 있게 됩니다.
'혼자 공부하는 자바' 카테고리의 다른 글
혼자 공부하는 자바 (60강 ~ 64강) (0) | 2024.06.17 |
---|---|
혼자 공부하는 자바 (56강 ~ 59강) (1) | 2024.06.16 |
혼자 공부하는 자바 (51강 ~ 55강) (0) | 2024.06.14 |
혼자 공부하는 자바 (46강 ~ 50강) (1) | 2024.06.09 |
혼자 공부하는 자바 (40강 ~ 45강) (0) | 2024.05.31 |