14.1 멀티 스레드 개념
운영체제는 실행 중인 프로그램을 프로세스로 관리한다. 멀티 태스킹은 두 가지 이상의 작업을 동시에 처리하는 것을 말하는데, 이때 운영체제는 멀티 프로세스를 생성해서 처리한다. 하지만 멀티 태스킹이 꼭 멀티 프로세스를 뜻하지는 않는다.
하나의 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진 프로그램들도 있다. 예를 들어 메신저는 채팅 작업을 하면서 동시에 파일 작업을 수행하기도 한다.
하나의 프로세스(프로그램 단위)가 두 가지 이상의 작업을 처리할 수 있는 이유는 멀티 스레드가 있기 때문이다.
스레드는 코드의 실행 흐름을 말하는데, 프로세스 내에 스레드가 두 개라면 두 개의 코드 실행 흐름이 생긴다는 의미이다.
스레드는 코드의 실행 흐름을 말함.
프로세스 내에 스레드가 두 개이상 -> 멀티 스레드
멀티 프로세스가 프로그램 단위의 멀티 태스킹이라면 멀티 스레드는 프로그램 내부에서의 멀티 태스킹이라고 볼 수 있다.
다음 그림은 멀티 프로세스와 멀티 스레드의 차이점을 보여준다.

멀티 프로세스들은 서로 독립적이므로 하나의 프로세스에서 오류가 발생해도 다른 프로세스에게 영향을 미치지 않는다. 하지만 멀티 스레드는 프로세스 내부에서 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스가 종료되므로 다른 스레드에게 영향을 미친다.
멀티 프로세스(프로세스는 프로그램 단위)는 서로 독립적이라 문제가 발생해도 다른 프로세스에게 영향을 미치지 않음.
멀티 스레드는 한쪽에서 예외가 발생하면 다른 스레드에게도 영향을 미친다.
예를 들어 워드와 엑셀을 동시에 사용하는 도중에 워드에 오류가 생겨 먹통이 되더라도 엑셀은 여전히 사용 가능하다.
(서로 다른 프로세스(프로그램 단위)이기 때문에 독립적임.)
그러나 멀티 스레드로 동작하는 메신저의 경우, 파일을 전송하는 스레드에서 예외가 발생하면 메신저 프로세스 자체가 종료되기 때문에 채팅 스레드도 같이 종료된다. 그렇기 때문에 멀티 스레드를 사용할 경우에는 에외 처리에 만전을 가해야함.
멀티 스레드는 데이터를 분할해서 병렬로 처리하는 곳에서 사용하기도 하고, 안드로이드 앱에서 네트워크 통신을 하기 위해 사용하기도 한다. 또한 다수의 클라이언트 요청을 처리하는 서버를 개발할 때에도 사용된다. 프로그램 개발에 있어서 멀티 스레드는 꼭 필요한 기능이기 때문에 반드시 이해하고 활용할 수 있도록 한다.
14.2 메인 스레드
모든 자바 프로그램은 메인 스레드가 main() 메소드를 실행하면서 시작된다. 메인 스레드는 main() 메소드의 첫 코드부터 순차적으로 실행하고, main() 메소드의 마지막 코드를 실행하거나 return 문을 만나면 실행을 종료한다.

메인 스레드는 필요에 따라 추가 작업 스레드들을 만들어서 실행시킬 수 있다. 다음 그림에서 오른쪽의 멀티 스레드를 보면 메인 스레드가 작업 스레드1을 생성하고 실행시킨 다음, 곧이어 작업 스레드2를 생성하고 실행시키는 것을 볼 수 있다.

싱글 스레드에서는 메인 스레드가 종료되면 프로세스도 종료된다. 하지만 멀티 스레드에서는 실행중인 스레드가 하나라도 있다면 프로세스는 종료되지 않는다. 메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않는다.
14.3 작업 스레드 생성과 실행
멀티 스레드로 실행하는 프로그램을 개발하려면 먼저 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해야 한다.

자바 프로그램은 메인 스레드가 반드시 존재하기 때문에 메인 작업 이외에 추가적인 작업 수만큼 스레드를 생성하면 된다. 자바는 작업 스레드도 객체로 관리하므로 클래스가 필요하다. Thread 클래스로 직접 객체를 생성해도 되지만, 하위 클래스를 만들어 생성할 수도 있다.
Thread 클래스로 직접 생성
java.lang 패키지에 있는 Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 다음과 같이 Runnable 구현 객체를 매개값으로 갖는 생성자를 호출하면 된다.

Runnable은 스레드가 작업을 실행할 때 사용하는 인터페이스다. Runnable에는 run() 메소드가 정의되어 있는데, 구현 클래스는 run()을 재정의해서 스레드가 실행할 코드를 가지고 있어야 한다. 다음은 Runnable 구현 클래스를 작성하는 방법이다.
-> Runnable 인터페이스를 구현하고 Thread 매개변수로 넣어줘야 Thread 객체 사용 가능.

Runnable 구현 클래스는 작업 내용을 정의한 것이므로, 스레드에게 전달해야 한다. Runnable 구현 객체를 생성한 후 Thread 생성자 매개값으로 Runnable 객체를 다음과 같이 전달하면 된다.

명시적인 Runnable 구현 클래스를 작성하지 않고
Thread 생성자를 호출할 때 Runnable 익명 구현 객체를 매개값으로 사용할 수 있다.
오히려 이 방법이 더 많이 사용된다.

작업 스레드 객체가 생성되었다고 해서 바로 작업 스레드가 실행되지는 않는다. 작업 스레드를 실행 하려면 스레드 객체의 start() 메소드를 다음과 같이 호출해야 한다.

start() 메소드가 호출되면, 작업 스레드는 매개값으로 받은 Runnable의 run() 메소드를 실행하면서 작업을 처리한다.
다음은 작업 스레드가 생성되고 실행되기까지의 순서를 보여준다.

다음은 메인 스레드가 동시에 두 가지 작업을 처리할 수 없음을 보여주는 예제이다. 원래 목적은 0.5초 주기로 비프음을 발생시키면서 동시에 프린팅까지 하는 작업이었지만, 메인 스레드는 비프음을 모두 발생한 다음에야 프린팅을 시작한다.
package ch14.sec03.exam01;
import java.awt.*;
public class BeepPrintExample {
public static void main(String[] args) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
try {
Thread.sleep(500); // 0.5초 일시정지
} catch (Exception e) {
// TODO: handle exception
}
}
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500); // 0.5초간 일시정지
} catch (Exception e) {
// TODO: handle exception
}
}
}
}

원래 목적대로 0.5초 주기로 비프음을 발생시키면서 동시에 프린팅을 하고 싶다면 두 작업 중 하나를 작업 스레드에서 처리하도록 해야 한다. 이제 프린팅은 메인 스레드가 담당하고 비프음을 들려주는 것은 작업 스레드가 담당하도록 수정하자.
package ch14.sec03.exam02;
import java.awt.*;
public class BeepPrintExample {
public static void main(String[] args) { // 메인 스레드 실행
Thread thread = new Thread(new Runnable() { // 작업 스레드 생성
@Override
public void run() {
// TODO Auto-generated method stub
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
try {
Thread.sleep(500);
} catch (Exception e) {
// TODO: handle exception
}
}
}
});
thread.start(); // 작업 스레드 실행
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500); // 0.5초간 일시정지
} catch (Exception e) {
// TODO: handle exception
}
}
}
}

Thread 자식 클래스로 생성
작업 스레드 객체를 생성하는 또 다른 방법은 Thread의 자식 객체로 만드는 것이다.
Thread 클래스를 상속한 다음 run() 메소드를 재정의해서 스레드가 실행할 코드를 작성하고 객체를 생성하면 된다.
여기서는 Thread가 가지고 있는 run 메서드를 재정의 하는 것이다. 아까 위에서는 Runnable 이라는 인터페이스의 추상메서드의 run을 재정의 하는 것임. -> Thread도 run을 가지고 있다!

작업 스레드를 실행하는 방법은 동일하다. start() 메소드를 호출하면 작업 스레드는 재정의된 run()을 실행시킨다.


명시적인 자식 클래스를 정의하지 않고, 다음과 같이 Thread 익명 자식 객체를 사용할 수도 있다.
오히려 이 방법이 더 많이 사용된다.

다음은 Thread의 익명 자식 객체로 작업 스레드를 정의하고 비프음을 실행하도록 이전 예제를 수정한 것이다.
package ch14.sec03.exam03;
import java.awt.*;
public class BeepPrintExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
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) {
// TODO: handle exception
}
}
};
};
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500); // 0.5초간 일시정지
} catch (Exception e) {
// TODO: handle exception
}
}
}
}

14.4 스레드 이름
스레드는 자신의 이름을 가지고 있다. 메인 스레드는 'main' 이라는 이름을 가지고 있고, 작업 스레드는 자동적으로 'Thread-n' 이라는 이름을 가진다. 작업 스레드의 이름을 Thread-n 대신 다른 이름으로 설정하고 싶다면 Thread 클래스의 setName() 메소드를 사용하면 된다.

스레드 이름은 디버깅할 때 어떤 스레드가 작업을 하는지 조사할 목적으로 주로 사용된다. 현재 코드를 어떤 스레드가 실행하고 있는지 확인하려면 정적 메소드인 currentThread()로 스레드 객체의 참조를 얻은 다음 getName() 메소드로 이름을 출력해보면 된다.

다음은 현재 실행 중인 스레드의 참조를 얻어 이름을 콘솔에 출력하고, 작업 스레드의 이름을 setName() 메소드로 수정하는 방법을 보여준다.
package ch14.sec04;
public class ThreadNameExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
Thread mainThread = Thread.currentThread(); // 지금 실행중인 스레드가 뭔지 알아보기.
System.out.println(mainThread.getName() + "실행"); // getName으로 실행중인 스레드의 이름을 확인.
for (int i = 0; i < 3; i++) {
Thread thread = new Thread() { // 익명 자식 객체로 구현.
@Override
public void run() {
System.out.println(getName() + "실행");
}
};
thread.start(); // start 메서드로 run 메서드 실행시키기.
}
Thread chatThread = new Thread() {
@Override
public void run() {
System.out.println(getName() + "실행");
}
};
chatThread.setName("chat-thread"); // cahtThread의 이름을 chat-thread로 변경.
chatThread.start();
}
}

14.5 스레드 상태
스레드 객체를 생성(NEW)하고, start() 메소드를 호출하면 곧바로 스레드가 실행되는 것이 아니라 실행 대기 상태(RUNNABLE)가 된다. 실행 대기 상태란 실행을 기다리고 있는 상태를 말한다.
실행 대기하는 스레드는 CPU 스케쥴링에 따라 CPU를 점유하고 run() 메소드를 실행한다. 이때를 실행(RUNNING) 상태라고 한다. 실행 스레드는 run() 메소드를 모두 실행하기 전에 스케줄링에 의해 다시 실행 대기 상태로 돌아갈 수 있다. 그리고 다른 실행 상태가 된다. (CPU 안에는 코어라는 게 존재하고 코어가 스레드들을 실행킨다.)
1. Start() 호출 -> 2. 해당 스레드는 대기열에 들어감
-> 3. 대기열에 들어간 스레드들은 번갈아가면서 조금씩 자신의 run()를 호출.
( run() 하고 대기열로 이동 run() 하고 대기열로 이동 이런식으로 )
- > 4. 모든 스레드들의 run() 메서드가 끝나면 종료(TERMINATED)가 된다.
이렇게 스레드는 실행 대기 상태와 실행 상태를 번갈아 가면서 자신의 run() 메소드를 조금씩 실행한다. 실행 상태에서 run() 메소드가 종료되면 더 이상 실행할 코드가 없기 때문에 스레드의 실행은 멈추게 된다. 이 상태를 종료 상태(TERMINATED) 라고 한다.

실행 상태에서 일시 정지 상태( ex. Thread.sleep() / wait() / join() )로 가기도 하는데, 일시 정지 상태는 스레드가 실행할 수 없는 상태를 말한다. 스레드가 다시 실행 상태로 가기 위해서는 일시 정지 상태에서 실행 대기 상태로 가야만 한다. 다음은 일시 정지로 가기 위한 메소드와 벗어나기 위한 메소드들을 보여준다.


위 표에서 wait()과 notify(), notifyAll()은 Object 클래스의 메소드이고 그 외는 Thread 클래스의 메소드이다.
wait(), notify(), notifyAll() 메소드의 사용 방법은 스레드 동기화에서 알아보기로 하고, 여기서는 Thread 클래스의 메소드만 살펴보자.
어떤 스레드가 무의미한 행동을 하는 경우가 있다. 의미 없는 동작을 반복만 하는 그런 경우가 존재할 수도 있는데 그럴 때 다른 스레드가 실행되게끔 실행 대기로 빠져주는 게 낫다. 그럴 때 사용하는 게 yield()이다.
주어진 시간 동안 일시 정지
실행 중인 스레드를 일정 시간 멈추게 하고 싶다면 Thread 클래스의 정적 메소드인 sleep() 을 이용하면 된다. 매개값에는 얼마 동안 일시 정지 상태로 있을 것인지 밀리세컨드(1/1000) 단위로 시간을 주면 된다. 다음 코드는 1초 동안 일시 정지 상태를 만든다.

일시 정지 상태에서는 InterruptedException이 발생할 수 있기 때문에 sleep() 은 예외 처리가 필요한 메소드이다.
InterruptedException에 대해서는 14.7에서 자세히 설명한다. 다음 예제는 3초 주기로 비프음을 10번 발생시킨다.
package ch14.sec05.exam01;
import java.awt.*;
public class SleepExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 10; i++) {
toolkit.beep();
try {
Thread.sleep(3000); // 3초 일시 정지
} catch (Exception e) {
// TODO: handle exception
}
}
}
}
다음 스레드의 종료를 기다림
스레드는 다른 스레드와 독립적으로 실행하지만 다른 스레드가 종료될 때까지 기다렸다가 실행을 해야 하는 경우도 있다.
예를 들어 계산 스레드의 작업이 종료된 후 그 결과값을 받아 처리하는 경우이다.
이를 위해 스레드는 join() 메소드를 제공한다. 다음 그림에서 ThreadA가 ThreadB의 join() 메소드를 호출하면 ThreadA는 ThreadB가 종료할 때까지 일시 정지 상태가 된다. ThreadB의 run() 메소드가 종료되고 나서야 비로소 ThreadA는 일시 정지에서 풀려 다음 코드를 실행한다.

다음 SumThread가 계산 작업을 모두 마칠 때까지 메인 스레드가 일시 정지 상태에 있다가 SumThread가 최종 계산된 결과값을 산출하고 종료하면 메인 스레드가 결과값을 받아 출력하는 예제이다.
package ch14.sec05.exam02;
public class SumThread extends Thread { // 스레드를 상속 받아서 SumThread 클래스를 만들었음.
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;
}
}
}
package ch14.sec05.exam02;
public class JoinExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
SumThread sumThread = new SumThread(); // 스레드 생성
sumThread.start(); // 스레드 시작 -> 1~100까지 더함.
try {
sumThread.join(); // 스레드가 끝날때 까지 기다림.
} catch (Exception e) {
// TODO: handle exception
}
System.out.println("1~100의 합 " + sumThread.getSum());
}
}
다른 스레드에게 실행 양보
스레드가 처리하는 작업은 반복적인 실행을 위해 for 문이나 while 문을 포함하는 경우가 많은데, 가끔 반복문이 무의미한 반복을 하는 경우가 있다. 다음 코드를 보자. work의 값이 false라면 while 문은 어떠한 실행문도 실행하지 않고 무의미한 반복을 한다.

이때는 다른 스레드에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 프로그램 성능에 도움이 된다.
이런 기능을 위해 Thread는 yield() 메소드를 제공한다. yield()를 호출한 스레드는 실행 대기 상태로 돌아가고, 다른 스레드가 실행 상태가 된다.

다음은 무의미한 반복을 하지 않고 다른 스레드에게 실행을 양보하도록 이전 코드를 수정한 것이다.

다음 예제에서는 처음 5초 동안은 ThreadA와 ThreadB가 번갈아 가며 실행하닥 5초 뒤에 메인 스레드가 ThreadA의 work 필드를 false로 변경함으로써 ThreadA가 yield() 메소드를 호출한다. 따라서 ThreadB가 더 많은 실행 기회를 얻게 된다.
그리고 10초 뒤에 ThreadA의 work 필드를 true로 변경해 ThreadA와 ThreadB가 다시 번갈아 가며 실행되로록 하자.
package ch14.sec05.exam03;
public class WorkThread extends Thread {
// 스레드를 상속받고 WorkThread 스레드를 생성.
public boolean work = true;
// 생성자
public WorkThread(String name) {
setName(name); // 스레드의 이름을 변경.
}
// 메소드
@Override
public void run() {
while (true) {
if (work) {
System.out.println(getName() + "작업처리");
} else {
Thread.yield();
// 이 스레드를 실행시키지 않고 다른 스레드에게 넘긴다.
}
}
}
}
package ch14.sec05.exam03;
public class YieldExample {
public static void main(String[] args) {
WorkThread workThreadA = new WorkThread("workThreadA");
WorkThread workThreadB = new WorkThread("workThreadB");
workThreadA.start();
workThreadB.start();
try {
Thread.sleep(5000);
// 5초뒤에는 workThreadA가 실행을 하지 않고 B에게 넘김.
} catch (InterruptedException e) {
}
workThreadA.work = false;
try {
Thread.sleep(10000);
// 10초뒤에는 workThreadA가 다시 실행을 할 수 있도록 바꿈.
} catch (InterruptedException e) {
}
workThreadA.work = true;
}
}

14.6 스레드 동기화
멀티 스레드는 하나의 객체를 공유해서 작업할 수도 있다. 이 경우, 다른 스레드에 의해 객체 내부 데이터가 쉽게 변경될 수 있기 때문에 의도했던 것과는 다른 결과가 나올 수 있다. 다음 그림을 보자.

User1Thread는 Calculator 객체의 memory 필드에 100을 먼저 저장하고 2초간 일시 정지 상태가 된다.
그동안 User2Thread가 memory 필드값을 50으로 변경한다. 2초가 지나 User1Thread가 다시 실행 상태가 되어 memory 필드의 값을 출력하면 User2Thread가 저장한 50이 나온다.
그런데 이렇게 하면 User1Thread에 저장된 데이터가 날아가버린다. 스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸면 된다. 이를 위해 자바는 동기화 메소드와 블록을 제공한다.

객체 내부에 동기화 메소드와 동기화 블록이 여러 개가 있다면 스레드가 이들 중 하나를 실행할 때 다른 스레드는 해당 메소드는 물론이고 다른 동기화 메소드 및 블록도 실행할 수 없다. 하지만 일반 메소드는 실행이 가능하다.
동기화 메소드 및 블록 선언
동기화 메소드를 선언하는 방법은 다음과 같이 synchronized 키워드를 붙이면 된다. synchronized 키워드는 인스턴스와 정적 메소드 어디든 붙일 수 있다.

스레드가 동기화 메소드를 실행하는 즉시 객체는 잠금이 일어나고, 메소드 실행이 끝나면 잠금이 풀린다.
메소드 전체가 아닌 일부 영역을 실행할 때만 객체 잠금을 걸고 싶다면 다음과 같이 동기화 블록을 만들면 된다.


다음 예제는 공유 객체로 사용할 Calculator이다. setMemory1()을 동기화 메소드로, setMemory2()를 동기화 블록을 포함하는 메소드로 선언했다. 따라서 setMemory1과 setMemory2는 하나의 스레드만 실행 가능한 메소드가 된다.
package ch14.sec06.exam01;
public class Calculator { // 공유 객체 생성
private int memory;
public int getMemory() { // 게터 메서드
return memory;
}
public synchronized void setMemory1(int memory) { // 동기화 메소드 이므로 어떤 스레드에서 호출시 다른 스레드는 사용 못함.
this.memory = memory;
try {
Thread.sleep(2000);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
public void setMemory2(int memory) {
synchronized (this) { // 동기화 블록 이므로 어떤 스레드에서 호출시 다른 스레드는 사용 못함.
this.memory = memory;
try {
Thread.sleep(2000);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
}
}
setMemory1() 과 setMemory2()는 동일하게 매개값을 메모리에 저장하고, 2초간 일시 정지 후에 메모리값을 출력한다.
다음 예제는 Calculator를 공유해서 사용하는 User1Thread와 User2Thread를 보여준다. run() 메소드에서 User1Thread는 매개값 100으로 setMemory1() 메소드를 호출하고, User2Thread는 매개값 50으로 setMemory2() 메소드를 호출한다.
package ch14.sec06.exam01;
public class User1Thread extends Thread { // 스레드를 상속받고 User1Thread 스레드 생성
private Calculator calculator;
public User1Thread() {
setName("User1Thread"); // 스레드의 이름 변경
}
public void setCalculator(Calculator calculator) { // 세터 메서드로 Calculator 타입의 객체를 받음.
this.calculator = calculator;
}
@Override
public void run() { // 공유 객체의 setMemory1 메서드 사용. 동기화 메서드 이므로 다른 스레드에서 이 객체를 동시에 사용 못할거다.
calculator.setMemory1(100);
}
}
package ch14.sec06.exam01;
public class User2Thread extends Thread { // 스레드를 상속받고 User2Thread 스레드 생성.
private Calculator calculator;
public User2Thread() {
setName("User2Thread"); // 스레드의 이름을 바꿈.
}
public void setCalculator(Calculator calculator) {
this.calculator = calculator; // 세터 메서드로 Calculator 타입의 객체를 입력받음.
}
@Override
public void run() {
calculator.setMemory2(50); // 공유 객체의 setMemory2 메서드를 실행. 동기화 메서드 이므로 다른 스레드에서 이 객체를 동시에 실행 불가.
}
}
package ch14.sec06.exam01;
public class SynchronizedExample {
public static void main(String[] args) {
Calculator calculator = new Calculator(); // 공유 객체 calculator 생성
User1Thread user1Thread = new User1Thread(); // user1Thread 생성
user1Thread.setCalculator(calculator); // 세터 메서드로 calculator 넣음.
user1Thread.start(); // user1Thread 스레드의 run 메서드 실행.
User2Thread user2Thread = new User2Thread(); // user2Thread 스레드를 생성.
user2Thread.setCalculator(calculator); // user2Thread에 calculator 객체 넣음.
user2Thread.start(); // user2Thread 스레드의 run 메서드 실행
}
}

정확히 User1Thread가 저장한 값 100이 출력되었고, User2Thread가 저장한 값 50이 출력되었다. 다음 그림을 보면 왜 이런 값이 나왔는지 이해할 수 있다.

User1Thread는 Calculator의 동기화 메소드인 setMemory1()을 실행하는 순간 Calculator 객체를 잠근다.
따라서 User2Thread는 객체가 잠금 해제될 때까지 Calculator의 동기화 블록을 실행하지 못한다.
2초 일시 정지 후에 잠금이 해제되면 비로소 User2Thread가 동기화 블록을 실행한다.
Calculator에서 setMemory1()을 일반 메소드로 변경하고, setMemory2()의 동기화 블록을 제거한 후 SynchronizedExample을 다시 실행하면 출력 결과가 달라진다. 그 이유는 이 절 앞에서 보여준 동기화되지 않았을 때의 그림을 보면 알 수 있을 것이다.
wait() [정지] notify() [깨워줌]를 이용한 스레드 제어
경우에 따라서는 두 개의 스레드를 교대로 번갈아 가며 실행할 때도 있다. 정확한 교대 작업이 필요할 경우, 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고 자신은 일시 정지 상태로 만들면 된다.
이 방법의 핵심은 공유 객체에 있다. 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 정해 놓는다. 한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업을 하지 않도록 wait() 메소드를 호출하여 일시 정지 상태로 만든다.

notify()는 wait()에 의해 일시 정지된 스레드 중 한 개를 실행 대기 상태로 만들고, notifyAll()은 wait()에 의해 일시 정지된 모든 스레드를 실행 대기 상태로 만든다. 주의할 점은 이 두 메소드는 동기화 메소드 또는 동기화 블록 내에서만 사용할 수 있다느 것이다.
다음 예제는 WorkObject에 두 스레드가 해야 할 작업을 동기화 메소드인 methodA()와 methodB()로 각각 정의해 두고, ThreadA와 ThreadB가 교대로 methodA() 와 methodB()를 호출하도록 한 것이다.
package ch14.sec06.exam02;
public class WorkObject { // 공유객체
public synchronized void methodA() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " methodA 작업 실행");
notify(); // 다른 스레드를 실행 대기 상태로 만듦.
try {
wait(); // 자신의 스레드를 일시정지로 만듦.
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO: handle exception
}
}
public synchronized void methodB() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " methodB 작업 실행");
notify(); // 다른 스레드를 실행 대기 상태로 만듦.
try {
wait(); // 자신의 스레드를 일시정지로 만듦.
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO: handle exception
}
}
}
package ch14.sec06.exam02;
public class ThreadA extends Thread { // ThreadA 스레드를 생성.
private WorkObject workObject;
public ThreadA(WorkObject workObject) { // workObject 객체를 입력 받음.
setName("ThreadA"); // 스레드의 이름을 바꿈.
this.workObject = workObject;
}
@Override
public void run() { // 스레드의 실행 내용
for (int i = 0; i < 10; i++) {
workObject.methodA();
}
}
}
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();
}
}
}
package ch14.sec06.exam02;
public class WaitNotifyExample {
public static void main(String[] args) {
// TODO Auto-generated method stub
WorkObject workObject = new WorkObject();// 공유 객체 생성
ThreadA threadA = new ThreadA(workObject); // 공유 객체 입력
ThreadB threadB = new ThreadB(workObject); // 공유 객체 입력
try {
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
}
threadA.start(); // 스레드 실행
try {
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
}
threadB.start(); // 스레드 실행
}
}


14.7 스레드 안전 종료
'자바 > 이것이 자바다' 카테고리의 다른 글
| 16. 람다식 (1) | 2023.11.20 |
|---|---|
| 15. 컬렉션 자료구조 (0) | 2023.11.18 |
| 13. 제네릭 (1) | 2023.11.17 |
| 12. java.base 모듈 (1) | 2023.11.17 |
| 11. 예외 처리 (0) | 2023.11.15 |