: 운영체제에서는 실행 중인 하나의 애플리케이션을 프로세스라고 함
: 사용자가 애플리케이션을 실행하면
운영체제로부터 실행에 필요한 메모리를 할당받아
애플리케이션의 코드를 실행하는 것
: 하나의 애플리케이션은 멀티 프로세스(multi process)를
만들기도 함
ex)
메모장 애플리케이션을 2개 실행했다면
2개의 메모장 프로세스가 생성된 것
: 사전적 의미로 한 가닥의 실이라는 뜻
: 한 가지 작업을 실행하기 위해 순차적으로
실행할 코드를 실처럼 이어놓았다고 해서 유래된 이름
: 하나의 스레드는 하나의 코드 실행 흐름이기 때문에
한 프로세스 내에 스레드가 2개라면 2개의 코드 실행 흐름이
생긴다는 의미
운영체제는 두 가지 이상의 작업을 동시에 처리하는
멀티 태스킹(multi tasking)을 할 수 있도록 CPU 및 메모리 자원을
프로세스마다 적절히 할당해주고, 병렬로 실행시킴
ex) 원서로 문서 작업을 하면서 동시에
윈도우 미디어 플레이어로 음악을 들을 수 있다.
멀티 태스킹은 꼭 멀티 프로세스를 뜻하는건 아님
한 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진
애플리케이션도 있음
ex) 미디어 플레이어와 메신저
미디어 플레이어는 동영상 재생과 음악 재생이라는
두 가지 작업을 동시에 처리하고,
메신저는 채팅 기능을 제공하면서 동시에 파일 전송 기능을
수행하기도 함
→ 하나의 프로세스가 두 가지 이상의 작업을 처리하는 것
'멀티 스레드'
- 멀티 프로세스 vs 멀티 스레드
• 멀티 프로세스
: 자신의 메모리를 가지고 실행하므로 서로 독립적
따라서 하나의 프로세스에서 오류가 발생해도
다른 프로세스에 영향을 미치지 않음
• 멀티 스레드
: 하나의 프로세스 내부에 생성되기 때문에
하나의 스레드가 예외를 발생시키면
프로세스 자체가 종료될 수 있어 다른 스레드에 영향을 미침
ex) 멀티 프로세스인 워드와 엑셀을 동시에 사용하던 도중,
워드에 오류가 생겨 먹통이 되더라도 엑셀은 여전히 사용 가능함
그러나 멀티 스레드로 동작하는 메신저의 경우
파일을 전송하는 스레드에서 예외가 발생하면
메신저 프로세스 자체가 종료되므로 채팅 스레드도 같이 종료됨
그렇기 때문에 멀티 스레드에서는 예외 처리에 만전을 기해야 함
- 멀티 스레드는 다양한 곳에서 사용됨
대용량 데이터의 처리 시간을 줄이기 위해
데이터 분할해서 병렬로 처리하기도 하고,
UI를 가지고 있는 애플리케이션에서 네트워크 통신을
하기 위해 사용되기도 함.
또한 다수 클라이언트의 요청을 처리하는 서버를 개발할 때에도 사용됨
: 자바의 모든 애플리케이션은
메인 스레드가 main()메소드를 실행하면서 시작함
: 메인 스레드는 main() 메소드의 첫 코드부터 아래로 순차적으로 실행하고,
main() 메소드의 마지막 코드를 실행하거나 return문을 만나면 실행이 종료됨
public static void main(String[] args) {
// 코드의 실행 흐름 → 스레드
String data = null;
if(...) {
}
while(...) {
}
System.out.println("...");
}
→ 메인 스레드는 필요에 따라 작업 스레드들을 만들어서
병렬로 코드를 실행할 수 있음
즉, 멀티 스레드를 생성해서 멀티 태스킹을 수행함
: 오른쪽 멀티 스레드 애플리케이션을 보면
메인 스레드가 작업 스레드1을 생성하고 실행한 다음,
곧이어 작업 스레드2를 생성하고 실행함
: 싱글 스레드 애플리케이션에서는 메인 스레드가 종료하면
프로세스도 종료됨
: 멀티 스레드 애플리케이션에서는 실행 중인 스레드가 하나라도 있다면,
프로세스는 종료되지 않음
: 메인 스레드가 작업 스레드보다 먼저 종료되더라도
작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않음
: 멀티 스레드로 실행하는 애플리케이션을 개발하려면
먼저 몇 개의 작업을 병렬로 실행할지 결정하고
각 작업별로 스레드를 생성해야함
: 어떤 자바 애플리케이션이건 메인 스레드는 반드시 존재하기 때문에
메인 작업 이외에 추가적인 병렬 작업의 수만큼 스레드를 생성하면 됨
: 자바에서는 작업 스레드도 객체로 생성되기 때문에
클래스가 필요함
: java.lang.Thread 클래스를 직업 객체화해서 생성해도 되지만,
Thread 클래스를 상속해서 하위 클래스를 만들어 생성할 수도 있음
Thread thread = new Thread(Runnable target);
: java.lang.Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면
Runnable을 매개값으로 갖는 생성자를 호출해야함
- Runnable
: 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체
: 인터페이스 타입이기 때문에 구현 객체를 만들어 대입해야함
: run() 메소드 하나가 정의되어 있는데,
구현 클래스는 run()을 재정의해서 작업 스레드가 실행할 코드를 작성해야함
- Runnable 구현 클래스
class Task implements Runnable {
public void run() {
스레드가 실행할 코드;
}
}
: Runnable은 작업 내용을 가지고 있는 객체이지 실제 스레드는 아님
: Runnable 구현 객체를 생성한후,
이것을 매개값으로 해서 Thread 생성자를 호출해야 비로소 작업 스레드가 생성됨
Runnable task = new Task();
Thread thread = new Thread(task);
: 코드를 좀 더 절약하기 위해
Thread 생성자를 호출할 때 Runnable 익명 객체를
매개값으로 사용할 수 있음
→ 이 방법이 더 많이 사용됨
Thread thread = new Thread( new Runnable() {
//익명 구현 객체
public void run() {
스레드가 실행할 코드;
}
}
: 작업 스레드는 생성되는 즉시 실행되는 것이 아니라,
start() 메소드를 호출해야만 실행됨
thread.start();
: start() 메소드가 호출되면,
작업 스레드는 매개값으로 받은 Runnable의 run() 메소드를
실행하면서 자신의 작업을 처리함
👩💻 메인 스레드만 이용한 경우
import java.awt.Toolkit;
/*
* 0.5초 주기로 비프(beep)음을 발생시키면서
* 동시에 출력하는 작업이 있다고 가정
*
* 비프음 발생과 출력은 서로 다른 작업이므로
* 메인 스레드가 동시에 두 가지 작업을 처리할 수 없음
*
* 아래 코드와 같이 작성시
* 메인 스레드는 비프음을 모두 발생한 다음, 출력을 시작함
*/
public class BeepPrintExample1 {
public static void main(String[] args) {
//Toolkit 객체 얻기
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i=0; i<5; i++) {
//비프음 발생
toolkit.beep();
try { Thread.sleep(500); /*0.5초간 일시 정지*/
} catch(Exception e) {}
}
for(int i=0; i<5; i++) {
System.out.println("띵");
try {Thread.sleep(500); /*0.5초간 일시 정지*/
} catch(Exception e) {}
}
}
}
💻 결과
띵
띵
띵
띵
띵
→ 비프음을 발생시키면서
동시에 출력을 하려면 두 작업 중 하나를
메인 스레드가 아닌 다른 스레드에서 실행해야 함
출력은 메인 스레드가 담당하고
비프음을 들려주는 것은 작업 스레드가 담당하도록 수정
👩💻 비프음을 들려주는 작업 정의
import java.awt.Toolkit;
public class BeepTask implements Runnable {
public void run() {
//스레드 실행 내용
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i=0; i<5; i++) {
toolkit.beep();
try { Thread.sleep(500); }
catch(Exception e) {}
}
}
}
👩💻 메인 스레드와 작업 스레드가 동시에 실행
public class BeepPrintExample2 {
public static void main(String[] args) {
//BeepTask 객체를 생성
Runnable beepTask = new BeepTask();
//작업 스레드 생성
Thread thread = new Thread(beepTask);
//작업 스레드의 start()메소드를 호출
//작업 스레드에 의해
//BeepTask 객체의 run() 메소드가 실행되어
//비프음이 발생
thread.start();
//그와 동시에 메인 스레드는
//for문을 실행시켜 0.5초 간격으로 "띵"을 출력
for(int i=0; i<5; i++) {
System.out.println("띵");
try {Thread.sleep(500);}
catch(Exception e) {}
}
}
}
👩💻 Runnable 익명 구현 객체로 대체하여
작업 스레드를 만들 수 있는 방법
import java.awt.Toolkit;
public class BeepPrintExample3 {
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) {}
}
}
}
: 작업 스레드가 실행할 작업을 Runnable로 만들지 않고,
Thread의 하위 클래스로 작업 스레드를 정의하면서 작업 내용을
포함시킬 수도 있음
- 작업 스레드 클래스 정의하는 방법
:Thread 클래스를 상속한 후 run() 메소드를
재정의(overriding)해서 스레드가 실행할 코드를 작성하면됨
: 작업 스레드 클래스로부터 작업 스레드 객체를 생성하는 방법은
일반적인 객체를 생성하는 방법과 동일함
public class WorkerThread extends Thread {
//run() 메소드 재정의
@Override
public void run() {
스레드가 실행할 코드;
}
}
Thread thread = new WorkerThread();
→ 코드를 좀 더 절약하기 위해
Thread 익명 객체로 작업 스레드 객체를 생성할 수도 있음
Thread thread = new Thread() {
// 익명 자식 객체
public void run() {
스레드가 실행할 코드;
}
};
: 이렇게 생성된 작업 스레드 객체에서
start() 메소드를 호출하면
작업 스레드는 자신의 run() 메소드를 실행하게 됨
thread.start();
👩💻 비프음 들려주는 스레드
/*
* Runnable을 생성하지 않고
* Thread의 하위 클래스로 작업 스레드를 정의한 것
*/
public class BeepThread extends Thread {
@Override
public void run() {
//스레드 실행내용
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i=0; i<5; i++) {
toolkit.beep();
try { Thread.sleep(500); }
catch(Exception e) {}
}
}
}
👩💻 메인 스레드와 작업 스레드가 동시에 실행
/*
* BeepThread 클래스를 이용해서
* 작업 스레드 객체를 생성하고 실행함
*/
public class BeepPrintExample4 {
public static void main(String[] args) {
//BeepThread 객체를 생성
Thread thread = new BeepThread();
//start() 메소드 호출
thread.start();
/*
그와 동시에 메인 스레드는
for문을 실행시켜 0.5초 간격으로
"띵"을 출력함
*/
for(int i=0; i<5; i++) {
System.out.println("띵");
try { Thread.sleep(500); }
catch(Exception e) {}
}
}
}
👩💻 Thread 익명 자식 객체를 이용해서
작업 스레드를 만들 수 있는 또 다른 방법
import java.awt.Toolkit;
public class BeepPrintExample5 {
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(500); }
catch(Exception e) {}
}
}
};
thread.start();
for(int i=0; i<5; i++) {
System.out.println("띵");
try { Thread.sleep(500); }
catch(Exception e) {}
}
}
}
: 스레드는 자신의 이름을 가지고 있음
: 디버깅할때 어떤 스레드가 어떤 작업을 하는지
조사할 목적으로 가끔 사용됨
: 메인 스레드는 'main'이라는 이름을 가지고 있고,
직접 생성한 스레드는 자동적으로 'Thread-n'이라는 이름으로 설정됨
→ n은 스레드의 번호를 말함
→ Thread-n 대신 다른 이름으로 설정하고 싶다면
Thread 클래스의 setName() 메소드로 변경하면 됨
thread.setName("스레드 이름");
→ 스레드 이름을 알고 싶을 경우에는 getName() 메소드로 변경하면됨
thread.getName();
: setName()과 getName()은 Thread 클래스의 인스턴스 메소드이므로
스레드 객체의 참조가 필요함
: 만약 스레드 객체의 참조를 가지고 있지 않다면,
Thread 클래스의 정적 메소드인 currentThread()을 이용해서
현재 스레드의 참조를 얻을 수 있음
Thread thread = Thread.currentThread();
👩💻 ThreadA 클래스
public class ThreadA extends Thread {
public ThreadA() {
//스레드 이름 설정
setName("ThreadA");
}
public void run() {
//ThreadA 실행내용
for(int i=0; i<2; i++) {
//getName() - 스레드 이름 얻기
System.out.println(getName() + "가 출력한 내용");
}
}
}
👩💻 ThreadB 클래스
public class ThreadB extends Thread{
public void run() {
//ThreadB 실행내용
for(int i=0; i<2; i++) {
//getName() - 스레드 이름 얻기
System.out.println(getName() + "가 출력한 내용");
}
}
}
👩💻 메인 스레드 이름 출력 및 UserThread 생성 및 시작
/*
* 메인 스레드의 참조를 얻어
* 스레드 이름을 콘솔에 출력하고
* 새로 생성한 스레드의 이름을
* setName() 메소드로 설정한 후,
* getName() 메소드로 읽어오기
*/
public class ThreadNameExample {
public static void main(String[] args) {
//이 코드를 실행하는 스레드 객체 얻기
Thread mainThread = Thread.currentThread();
System.out.println("프로그램 시작 스레드 이름: " +
mainThread.getName());
//ThreadA 생성
ThreadA threadA = new ThreadA();
System.out.println("작업 스레드 이름: " +
threadA.getName());
//ThreadA시작
threadA.start();
//ThreadB생성
ThreadB threadB = new ThreadB();
System.out.println("작업 스레드 이름: " +
threadB.getName());
threadB.start();
}
}
💻 결과
프로그램 시작 스레드 이름: main
작업 스레드 이름: ThreadA
ThreadA가 출력한 내용
ThreadA가 출력한 내용
작업 스레드 이름: Thread-1
Thread-1가 출력한 내용
Thread-1가 출력한 내용
: 싱글 스레드 프로그램에서는 1개의 스레드가
객체를 독차지해서 사용하면 되지만,
멀티 스레드 프로그램에서는 스레드들이 객체를
공유해서 작업해야하는 경우가 있음
이 경우에 주의해야할 점이 있음
- 공유 객체를 사용할 때의 주의할 점
: 멀티 스레드 프로그램에서
스레드들이 객체를 공유해서 작업해야 하는 경우,
스레드 A가 사용하던 객체를
스레드 B가 상태를 변경할 수 있기 때문에
스레드 A가 의도했던 거소가는 다른 결과를 산출할 수도 있음
ex)
여러 사람이 계산기를 함께 나눠 쓰는 상황이라면
사람 A가 계산기로 작업을 하다가
계산 결과를 메모리에 저장한 뒤 잠시 자리를 비웠는데,
이때 사람 B가 계산기를 만져서 사람 A가 메모리에
저장한 값을 다른 값으로 변경하는 것과 동일함
그런 다음 사람 A가 돌아와 계산기에 저장된 값을
이용해서 이후 작업을 진행한다면 결국 사람 A는
엉터리 값을 이용하게 됨
👩💻 공유객체
public class Calculator {
private int memory;
public int getMemory() {
return memory;
}
//계산기 메모리에 값을 저장하는 메소드
public void setMemory(int memory) {
//매개값을 memory 필드에 저장
this.memory = memory;
//스레드를 2초간 일시 정지시킴
try {
Thread.sleep(2000);
} catch(InterruptedException e) {}
//Thread.currentThread().getName() - 스레드 이름 얻기
//this.memory - 메모리값
System.out.println(Thread.currentThread().getName() + ": " +
this.memory);
}
}
👩💻 User1 스레드
public class User1 extends Thread {
private Calculator calculator;
public void setCalculator(Calculator calculator) {
//스레드 이름을 user1로 설정
this.setName("User1");
//공유 객체인 calculator를 필드에 저장
this.calculator = calculator;
}
public void run() {
//공유 객체인 calculator의 메모리에 100을 저장
calculator.setMemory(100);
}
}
👩💻 User2 스레드
public class User2 extends Thread {
private Calculator calculator;
public void setCalculator(Calculator calculator) {
//스레드 이름을 user1로 설정
this.setName("User2");
//공유 객체인 calculator를 필드에 저장
this.calculator = calculator;
}
public void run() {
//공유 객체인 calculator의 메모리에 100을 저장
calculator.setMemory(50);
}
}
👩💻 메인 스레드가 실행하는 코드
public class MainThreadExample {
public static void main(String[] args) {
Calculator calculator = new Calculator();
// User1 스레드 생성
User1 user1 = new User1();
// 공유 객체 설정
user1.setCalculator(calculator);
// User1 스레드 시작
user1.start();
// User2 스레드 생성
User2 user2 = new User2();
// 공유 객체 설정
user2.setCalculator(calculator);
// user2 스레드 시작
user2.start();
}
}
💻 결과
User2: 50
User1: 50
: User1 스레드가 Calculator 객체의 memory 필드에
100을 먼저 저장하고 2초간 일시 정지 상태가 됨
그동안에 User2 스레드가 memory 필드값을 50으로 변경
2초가 지나 User1 스레드가 다시 실행 상태가 되어
memory 필드값을 출력하면 User2 스레드가 저장한 50이 출력됨
- 동기화 메소드
: 스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없게 하려면
스레드 작업이 끝날 때까지 객체에 잠금을 걸어서
다른 스레드가 사용할 수 없도록 해야함
: 자바는 임계 영역을 지정하기 위해 동기화 메소드를 제공함
+) 임계 영역
: 멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역
: 스레드가 내부의 동기화 메소드를 실행하면
즉시 객체에 잠금을 걸어 다른 스레드가 동기화 메소드를
실행하지 못하도록 해야함
public synchronized void method() {
//단 하나의 스레드만 실행
임계 영역;
}
: 동기화 메소드를 만들려면
메소드 선언에 synchronized 키워드를 붙이면 됨
, 인스턴스와 정적 메소드 어디든 붙일 수 있음
: 동기화 메소드는 메소드 전체 내용이 임계 영역이므로
스레드가 동기화 메소드를 실행하는 즉시
객체에는 잠금이 일어나고,
스레드가 동기화 메소드를 실행 종료하면 잠금이 풀림
: 만약 동기화 메소드가 여러 개 있을 경우,
스레드가 이들 중 하나를 실행할 때 다른 스레드는
해당 메소드는 물론이고 다른 동기화 메소드도 실행할 수 없음
하지만 이때 다른 스레드에서 일반 메소드는 실행이 가능함
👩💻 동기화 메소드로 수정된 공유 객체
public class Calculator {
private int memory;
public int getMemory() {
return memory;
}
//계산기 메모리에 값을 저장하는 메소드
public synchronized void setMemory(int memory) {
//매개값을 memory 필드에 저장
this.memory = memory;
//스레드를 2초간 일시 정지시킴
try {
Thread.sleep(2000);
} catch(InterruptedException e) {}
//Thread.currentThread().getName() - 스레드 이름 얻기
//this.memory - 메모리값
System.out.println(Thread.currentThread().getName() + ": " +
this.memory);
}
}
→ 위 코드 수정하여
실행해보면 User1은 100,
User2는 50 이라는 출력값을 얻을 수 있음
→ User1 스레드는 Calculator 객체의 동기화 메소드인
setMemory()를 실행하는 순간 Calculator 객체를 잠금 처리함
메인 스레드가 User2 스레드를 실행하지만,
동기화 메소드인 setMemory()를 실행하지는 못하고
User1이 setMemory()를 모두 실행할 동안 대기해야함
→ User1 스레드가 setMemory() 메소드를 모두 실행하고 나면
User2 스레드가 setMemory() 메소드를 실행함
결국 User1 스레드가 Calculator 객체를 사용할 동안
User2 스레드는 Calculator 객체를 사용하지 못하므로
User1 스레드는 방해받지 않고 안전하게
Calculator 객체를 사용할 수 있게 되는 것