1. 프로세스와 쓰레드
1) 프로세스와 쓰레드의 정의
- 프로세스(process) : 실행 중인 프로그램(program)
- 쓰레드(thread) : 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것
- 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 됨
- 프로세스에는 최소한 하나 이상의 쓰레드가 존재
- 멀티쓰레드 프로세스(multi-threaded process) : 둘 이상의 쓰레드를 가진 프로세스
- 프로세스의 메모리 한계에 따라 생성할 수 있는 쓰레드의 수가 결정됨
2) 멀티태스킹과 멀티쓰레딩
- 멀티태스킹(multi-tasking, 다중작업) : 여러 개의 프로세스를 동시에 실행할 수 있는 환경
- 멀티쓰레딩(multi-threading) : 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것
3) 멀티쓰레딩의 장단점
① 장점
- CPU의 사용률을 향상시킴
- 효율적인 자원 사용 가능
- 사용자에 대한 응답성 향상
- 작업이 분리되어 코드가 간결해짐
② 단점
- 여러 쓰레드가 프로새스 내 자원을 공유하기 때문에 동기화(synchronization), 교착상태(deadlock)과 같은 문제 발생
2. 쓰레드의 구현과 실행
1) 쓰레드의 구현
① Thread 클래스를 상속
② Runnable 인터페이스를 구현
① Thread 클래스를 상속
class MyThread extends Thread {
// Thread 클래스의 run()을 오버라이딩
public void run() { /* 작업내용 */ }
}
class ThreadEx {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
}
}
- Thread 클래스를 직접 상속받았기 때문에 Thread의 메서드를 바로 호출 가능
② Runnable 인터페이스를 구현
class MyThread implements Runnable {
// Runnable 인터페이스의 run()을 구현
public void run() { /* 작업내용 */ }
}
class ThreadEx {
public static void main(String[] args) {
Runnable r = new MyThread();
Thread t2 = new Thread(r);
t2.start();
// 한 줄로 쓸 수 있음
// Thread t2 = new Thread(new MyThread());
}
}
- Runnable인터페이스를 구현한 클래스의 인스턴스를 생성한 뒤, 이 인스턴스를 Thread클래스의 생성자의 매개변수로 제공함
- 이를 통해 Thread클래스를 직접 상속받지 않고 run() 메서드를 호출할 수 있게 함
-> Thread클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에, Runnable인터페이스를 구현하는 것이 일반적임
※ Thread클래스의 메서드 호출
static Thread currentThread() : 현재 실행 중인 쓰레드의 참조를 반환함
String getName() : 쓰레드의 이름을 반환함
- Thread를 상속받으면 getName() 바로 호출 가능
- Runnable을 구현하면 Thread.currentThread().getName()의 형태로 호출 가능
※ 쓰레드의 이름
Thread(Runnable target, String name)
Thread(String name)
void setName(String name)
- Thread클래스의 생성자나 메서드로 지정 또는 변경 가능
- 쓰레드의 이름을 정하지 않으면 'Thread-번호' 형식으로 지정됨
2) 쓰레드의 실행 - start()
- 쓰레드를 생성한 뒤 start()를 호출해야 쓰레드가 실행됨
- 실행대기 상태에 있다가 자신의 차례가 되면 실행됨 (실행대기 중인 쓰레드가 없으면 바로 실행)
- 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없음
- 즉, 하나의 쓰레드에 대해 start()가 한 번만 호출될 수 있음
3. start()와 run()
① main()에서 run() 호출
- 쓰레드 실행(X)
- 단순히 클래스에 선언된 메서드 호출(O)
② main()에서 start() 호출
- 새로운 쓰레드가 작업에 실행하는데 필요한 호출스택(call stack) 생성
- run() 호출
- 생성된 호출스택에 run()이 첫 번째로 올라가게 함
- 스케줄러가 정한 순서에 의해 번갈아가면서 실행
- run()의 수행이 종료된 쓰레드는 호출스택이 비워지면서 이 쓰레드가 사용하던 호출스택이 사라짐
※ main쓰레드
- main메서드의 작업을 수행하는 것도 쓰레드임
- main메서드가 수행을 마쳐도 다른 쓰레드가 아직 작업을 마치지 않았다면 프로그램이 종료되지 않음
- 즉, 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료됨
4. 싱글쓰레드와 멀티쓰레드
- 싱글 코어에서 단순히 CPU만을 사용하는 계산작업
싱글쓰레드 > 멀티쓰레드
(작업 전환(context switching)에 시간이 걸려 오히려 많은 시간 소요)
- 두 쓰레드가 서로 다른 자원을 사용하는 작업
싱글쓰레드 < 멀티쓰레드
(입력을 기다리는 구간 등 한 쓰레드가 아무 일도 하지 않을 때 다른 쓰레드를 실행시켜 시간 절약 가능)
5. 쓰레드의 우선순위
- 우선순위(priority) : 쓰레드의 속성(멤버변수)으로, 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라짐
- 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 하여 효율적으로 사용 가능
- 시각적인 부분이나 사용자에게 빠르게 반응해야하는 작업을 하는 쓰레드의 우선순위는 높아야 함
void setPriority(int newPriority) : 쓰레드의 우선순위를 지정한 값으로 변경
int getPriority() : 쓰레드의 우선순위 반환
public static final int MAX_PRIORITY = 10 // 최대우선순위
public static final int MIN_PRIORITY = 1 // 최소우선순위
public static final int NORM_PRIORITY = 5 // 보통우선순위
- 우선순위의 범위 : 1 ~ 10 (숫자가 높을수록 우선순위가 높음)
- 쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받음
- ex) main쓰레드의 우선순위는 5이므로 main메서드 내에서 생성하는 쓰레드의 우선순위는 기본적으로 5
- 쓰레드의 우선순위는 '쓰레드를 실행하기 전'에 설정 가능
6. 쓰레드 그룹(thread group)
- 쓰레드 그룹 : 서로 관련된 쓰레드를 그룹으로 다루기 위한 것
- 쓰레드 그룹에 다른 쓰레드 그룹을 포함시킬 수 있음
- 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹의 쓰레드 변경 (O)
- 다른 쓰레드 그룹의 쓰레드 변경 (X)
- 모든 쓰레드는 반드시 쓰레드 그룹에 포함
- 기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속함
쓰레드 그룹을 설정하는 생성자
Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
- JVM은 main과 system이라는 쓰레드 그룹을 만듦
- JVM운영에 필요한 쓰레드들을 생성해서 쓰레드 그룹에 포함
- 모든 쓰레드 그룹은 main쓰레드 그룹의 하위 쓰레드 그룹이 됨
쓰레드 그룹의 최대우선순위
- 쓰레드 그룹의 최대우선순위를 정할 수 있음
- 미설정시 기본적으로 자신이 속한 쓰레드 그룹의 최대우선선위를 따라감
- setMaxPriority(int pri) 는 쓰레드가 쓰레드 그룹에 추가되기 이전에 호출되어야 함
void setMaxPriority(int pri) : 쓰레드 그룹의 최대우선순위 설정
int getMaxPriority() : 쓰레드 그룹의 최대우선순위 반환
7. 데몬 쓰레드(daemon thread)
- 데몬 쓰레드 : 다른 일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)의 작업을 돕는 보조적인 역할을 수행하는 쓰레드
- 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료
- ex) 가비지 컬렉터, 워드프로세서의 자동저장, 화면자동갱신
- 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기(무한루프와 조건문을 이용)
- 데몬 쓰레드 생성 : start() 호출 전 setDaemon(true)를 호출함
- 데몬 쓰레드 실행 : 일반 쓰레드와 같음
- 데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 됨
boolean isDaemon() : 쓰레드가 데몬 쓰레드인지 확인 (데몬 쓰레드이면 true 반환)
void setDaemon(boolean on) : 쓰레드를 데몬 쓰레드 또는 사용자 쓰레드로 변경 (true를 주면 데몬 쓰레드가 됨)
※ 모든 쓰레드 출력 (종료된 쓰레드 제외)
getAllStackTraces() : 작업이 완료되지 않은 모든 쓰레드의 호출스택을 출력함
-> Map<Thread, StackTraceElement[]>의 형태
-> Thread의 getName(), getThreadGroup().getName(), isDaemon() 등 사용 가능
8. 쓰레드의 실행제어
- 효율적으로 멀티쓰레드 프로그램을 만들기 위해 정교한 스케줄링을 통해 프로세스에게 주어진 자원과 시간을 여러 쓰레드가 낭비없이 잘 사용하도록 프로그래밍 해야 함
1) 쓰레드의 스케줄링과 관련된 메서드
static void sleep(long millis) static void sleep(long millis, int nanos) |
지정된 시간(천분의 일초 단위)동안 쓰레드를 일시정지시킴 지정한 시간이 지나면 자동적으로 다시 실행대기상태가 됨 |
void join() void join(long millis) void join(long millis, int nanos) |
지정된 시간동안 쓰레드가 실행되도록 함 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속함 |
void interrupt() | sleep()이나 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기상태로 만듦 해당 쓰레드에서는 InterruptException이 발생함으로써 일시정지상태를 벗어남 |
void stop() | 쓰레드를 즉시 종료시킴 |
void suspend() | 쓰레드를 일시정지시킴 resume() 호출 시 다시 실행대기상태가 됨 |
void resume() | suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기상태로 만듦 |
static void yield() | 실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보(yield)하고 자신은 실행대기상태가 됨 |
- resume(), stop(), suspend()는 쓰레드를 교착상태(dead-lock)로 만들기 쉽기 때문에 deprecated됨
2) 쓰레드의 상태
NEW | 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태 |
RUNNABLE | 실행 중 또는 실행 가능한 상태 |
BLOCKED | 동기화블럭에 의해서 일시정지된 상태 (lock이 풀릴 때까지 기다리는 상태) |
WAITING, TIMED_WAITING |
쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은(unrunnable) 일시정지 상태. TIMED_WAITING은 일시정지시간이 지정된 경우 |
TERMINATED | 쓰레드의 작업이 종료된 상태 |
- 쓰레드의 상태는 Thread의 getState()메서드를 호출해서 확인 가능 (JDK1.5부터)
※ 쓰레드의 생성~소멸 과정
① 쓰레드 생성(NEW)하고 start()를 호출하면 실행대기열에 저장되어 자신의 차례가 될 때까지 기다림
(실행대기열 : 큐(queue)와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행됨)
② 실행대기상태(RUNNABLE)에 있다가 자신의 차례가 되면 실행상태가 됨
③ 주어진 실행시간이 다되거나 yield()를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 됨
④ 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태(WAITING, BLOCKED)가 될 수 있음
⑤ time-out, notify(), resume(), interrupt()가 호출되면 일시정지상태를 벗어나 다시 실행대기열에 저장되어 기다림(RUNNABLE)
⑥ 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸(TERMINATED)
3) sleep(long millis), sleep(long millis, int nanos)
- 지정된 시간동안 쓰레드를 멈추게 함
- 밀리세컨드(millis, 1000분의 일초), 나노세컨드(nanos, 10억분의 일초)로 시간 지정
- 지정된 시간이 다 되거나, interrupt()가 호출되면(InterruptedException 발생) 잠에서 깨어나 실행대기 상태가 됨
- 항상 try-catch문으로 예외처리 필요
- try-catch문을 포함하는 새로운 메서드를 만들어 사용하기도 함
void delay(long milis) {
try {
Thread.sleep(millis);
} catch(InterruptedException e) { }
}
- sleep()은 항상 현재 실행 중인 쓰레드에 대해 작동하기 때문에 static으로 선언되어 있음
- 참조변수를 이용해서 호출 (X)
- Thread.sleep(2000)과 같이 해야 함
class ThreadEx {
public static void main(String[] args) {
Thread1 t1 = new Thread1();
Thread2 t2 = new Thread2();
t1.start();
t2.start();
try {
t1.sleep(2000); // t1이 아닌 main에 적용됨
} catch(InterruptedException e) {}
}
class Thread1 extends Thread {
public void run() { ... }
}
class Thread2 extends Thread {
public void run() { ... }
}
4) interrupt(), interrupted()
- interrupt() : 쓰레드에게 작업을 멈추라고 요청 (강제로 종료시키지는 못함, 쓰레드의 interrupted상태(인스턴스 변수)를 바꿀 뿐임)
- interrupted() : 쓰레드에 대해 interrupt()가 호출되었는지 알려줌 (interrupt()가 호출되지 않았다면 false, 호출되면 true 반환)
Thread th = new Thread();
th.start();
...
th.interrupt(); // 쓰레드 th에 interrupt() 호출
class MyThread extends Thread {
public void run() {
while(!interrupted()) { // interrupted()의 결과가 false인 동안 반복
...
}
}
}
- 쓰레드가 sleep(), wait(), join()에 의해 '일시정지 상태(WAITING)'에 있을 때, 해당 쓰레드에 대해 interrupt() 호출 시 InterruptedException이 발생하여 쓰레드는 '실행대기 상태(RUNNABLE)'로 바뀜
5) suspend(), resume(), stop()
- suspend() : 쓰레드를 멈추게 함 (sleep()과 비슷)
- resume() : suspend()로 멈춘 쓰레드를 다시 실행대기 시킴
- stop() : 쓰레드를 즉시 종료시킴
- suspend()와 stop()은 교착상태(deadlock)을 일으키기 쉬워 'deprecated'됨
public class ThreadEx1 {
public static void main(String[] args) {
Thread1 th1 = new Thread1("*");
Thread1 th2 = new Thread1("**");
Thread1 th3 = new Thread1("***");
th1.start();
th2.start();
th3.start();
try {
Thread.sleep(2000);
th1.suspend();
Thread.sleep(2000);
th2.suspend();
Thread.sleep(3000);
th1.resume();
Thread.sleep(3000);
th1.stop();
th2.stop();
Thread.sleep(2000);
th3.stop();
} catch (InterruptedException e) {
}
}
}
class Thread1 implements Runnable {
volatile boolean suspended = false;
volatile boolean stopped = false;
Thread th;
Thread1(String name) {
th = new Thread(this, name);
}
public void run() {
while(!stopped) {
if(!suspended) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
System.out.println(Thread.currentThread().getName() + " - stopped");
}
public void suspend() { suspended = true; }
public void resume() { suspended = false; }
public void stop() { stopped = true; }
public void start() { th.start(); }
}
- deprecated된 suspend()와 stop() 대신 boolean형 변수 suspended와 stopped를 사용하여 구현
6) yield()
- yield() : 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보(yield)
- yield()와 interrupt()를 적절히 사용하면 프로그램의 응답성을 높이고 효율적인 실행을 가능하게 함
public class ThreadEx2 {
public static void main(String[] args) {
Thread2 th1 = new Thread2("*");
Thread2 th2 = new Thread2("**");
Thread2 th3 = new Thread2("***");
th1.start();
th2.start();
th3.start();
try {
Thread.sleep(2000);
th1.suspend();
Thread.sleep(2000);
th2.suspend();
Thread.sleep(3000);
th1.resume();
Thread.sleep(3000);
th1.stop();
th2.stop();
Thread.sleep(2000);
th3.stop();
} catch (InterruptedException e) {
}
}
}
class Thread2 implements Runnable {
volatile boolean suspended = false;
volatile boolean stopped = false;
Thread th;
Thread2(String name) {
th = new Thread(this, name);
}
public void run() {
String name = th.getName();
while (!stopped) {
if (!suspended) {
System.out.println(name);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(name + " - interrupted");
}
} else {
Thread.yield();
}
}
System.out.println(name + " - stopped");
}
public void suspend() {
suspended = true;
th.interrupt();
System.out.println(th.getName() + " - interrupted by suspend()");
}
public void stop() {
stopped = true;
th.interrupt();
System.out.println(th.getName() + " - interrupted by stop()");
}
public void resume() {
suspended = false;
}
public void start() {
th.start();
}
}
- 5)의 예제에서 수정한 것
① else문을 추가하여 yield()를 호출 -> 불필요하게 while문이 돌지 않도록 함
② suspend()와 stop()에 interrupt()를 호출 -> main쓰레드에서 Thread.sleep()으로 인한 지연시간이 발생할 수 있기 때문에 InterruptedException을 발생시켜 일시정지상태에서 벗어나게 함
7) join(), join(long millis), join(long millis, int nanos)
- join() : 다른 쓰레드의 작업을 기다림
- 시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다림
- 작업 중에 다른 쓰레드의 작업이 먼저 수행되어야 할 때 사용
- sleep()처럼 interrupt()에 의해 대기상태에서 벗어날 수 있음
- try-catch문으로 감싸야 함
- sleep()과 유사하지만 join()은 현재 쓰레드가 아닌 특정 쓰레드에 대해 동작하여 static 메서드가 아니라는 차이점이 있음
public class ThreadEx20 {
public static void main(String[] args) {
ThreadEx20_1 gc = new ThreadEx20_1();
gc.setDaemon(true);
gc.start();
int requiredMemory = 0;
for(int i = 0; i < 20; i++) {
requiredMemory = (int) (Math.random() * 10) * 20;
// 필요한 메모리가 사용할 수 있는 양보다 클 경우 or
// 전체 메모리의 60% 이상을 사용했을 경우
// gc를 깨움
if(gc.freeMemory() < requiredMemory ||
gc.freeMemory() < gc.totalMemory() * 0.4) {
gc.interrupt();
try {
// join()을 이용해서 gc가 작업할 시간을 주고 main쓰레드는 기다리도록 함
gc.join(1000);
} catch (InterruptedException e) {
}
}
gc.usedMemory += requiredMemory;
System.out.println("usedMemory: " + gc.usedMemory);
}
}
}
class ThreadEx20_1 extends Thread {
final static int MAX_MEMORY = 1000;
int usedMemory = 0;
public void run() {
while(true) {
try {
Thread.sleep(10 * 1000);
} catch(InterruptedException e) {
System.out.println("Awaken by interrupt().");
}
gc();
System.out.println("Garbage Collected. Free Memory : " + freeMemory());
}
}
public void gc() {
usedMemory -= 300;
if(usedMemory < 0) usedMemory = 0;
}
public int totalMemory() { return MAX_MEMORY; }
public int freeMemory() { return MAX_MEMORY - usedMemory; }
}
- 쓰레드를 이용하여 JVM의 가비지 컬렉터(garbage collector)를 흉내낸 예제
9. 쓰레드의 동기화
- 멀티쓰레드 환경에서는 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 해야 함
- 임계 영역(critical section) : 공유 데이터를 사용하는 코드 영역
- 잠금(락, lock) : 객체마다 하나씩 가지고 있는 것으로, 쓰레드는 공유 데이터(객체)의 lock을 획득하여 임계 영역 내의 코드를 수행한 뒤 임계 영역을 벗어날 때 lock을 반납함
- 쓰레드의 동기화(synchronization) : 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것
1) synchronized를 이용한 동기화
- 임계 영역을 설정하는데 사용
① 메서드 전체를 임계 영역으로 지정
public synchronized void calc() { ... }
② 특정한 영역을 임계 영역으로 지정 (synchronized 블럭)
synchronized(객체의 참조변수) { ... }
- lock의 획득과 반납은 자동적으로 이루어짐
- 임계 영역만 설정해주면 됨
- 임계 영역을 최소화하여 효율적인 프로그램이 되도록 해야 함
public class ThreadEx21 {
public static void main(String[] args) {
RunnableEx21 r = new RunnableEx21();
new Thread(r).start();
new Thread(r).start();
}
}
class Account {
// balance 변수는 꼭 private 여야 함! (외부에서 접근할 수 없게)
private int balance = 1000;
public int getBalance() {
return balance;
}
//메서드 전체를 동기화
public synchronized void withdraw(int money) {
if(balance >= money) {
try { Thread.sleep(1000); }
catch(InterruptedException e) { }
balance -= money;
}
}
}
class RunnableEx21 implements Runnable {
Account acc = new Account();
public void run() {
while(acc.getBalance() > 0) {
// 100,200,300 중 한 값 임의로 선택하여 출금(withdraw)
int money = (int)(Math.random() * 3 + 1) * 100;
acc.withdraw(money);
System.out.println("balance: " + acc.getBalance());
}
}
}
- 은행계좌에서 출금하는 쓰레드
- 여러 쓰레드가 동시에 공유 객체(balance)에 접근하여 잘못된 결과를 초래할 수 있음
- 출금하는 메서드(withdraw())를 동기화(synchronized)하여 한 쓰레드만 접근할 수 있게 보장
2) wait()과 notify()
- synchronized로 동기화하여 공유 데이터를 보호할 수 있지만, 특정 쓰레드가 락을 가진 상태로 오래 보내면 다른 쓰레드가 객체를 사용할 수 없음
- wait()과 notify()로 해결
- wait() : 동기화된 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면 wait()을 호출하여 쓰레드가 락을 반납하고 기다림
- notify() : 작업을 중단했던 쓰레드가 다시 락을 얻어 작업을 진행할 수 있게 함
- 재진입(reentrance) : wait()에 의해 lock을 반납했다가, 다시 lock을 얻어서 임계영역에 들어오는 것
※ notify()와 notifyAll()의 차이
- notify() : 해당 객체의 대기실에 있던 모든 쓰레드 중 임의의 쓰레드에게만 통보
- notifyAll() : 기다리고 있는 모든 쓰레드에게 통보, 하지만 lock을 얻을 수 있는 것은 하나의 쓰레드뿐임
※ wait(), notify(), notifyAll()
- Object에 정의되어 있음
- 동기화 블록(synchronized블록)내에서만 사용 가능
- 보다 효율적인 동기화 가능
void wait()
void wait(long timeout)
void wait(long timeout, int nanos)
void notify()
void notifyAll()
-> wait()은 notify(), notifyAll()이 호출될 때까지 기다림
-> 매개변수가 있는 wait()은 지정된 시간이 지난 후 자동적으로 notify() 호출
import java.util.*;
public class ThreadWaitEx1 {
public static void main(String[] args) {
Table table = new Table(); // 여러 쓰레드가 공유하는 객체
new Thread(new Cook(table), "COOK1").start();
new Thread(new Customer(table, "donut"), "CUST1").start();
new Thread(new Customer(table, "burger"), "CUST2").start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.exit(0);
}
}
class Customer implements Runnable {
private Table table;
private String food;
Customer(Table table, String food) {
this.table = table;
this.food = food;
}
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
String name = Thread.currentThread().getName();
table.remove(food);
System.out.println(name + " ate a " + food);
}
}
}
class Cook implements Runnable {
private Table table;
Cook(Table table) {
this.table = table;
}
public void run() {
while (true) {
int idx = (int) (Math.random() * table.dishNum());
table.add(table.dishNames[idx]);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
}
}
}
class Table {
String[] dishNames = { "donut", "donut", "burger" };
final int MAX_FOOD = 6;
private ArrayList<String> dishes = new ArrayList<>();
public synchronized void add(String dish) {
// 테이블에 음식이 가득 찼으면, 음식을 추가하지 않음
while (dishes.size() >= MAX_FOOD) {
String name = Thread.currentThread().getName();
System.out.println(name + " is waiting. ");
try {
wait(); // COOK 쓰레드를 기다리게 하기
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
dishes.add(dish);
notify(); // 기다리고 있는 CUST 깨우기
System.out.println("Dishes: " + dishes.toString());
}
public void remove(String dishName) {
synchronized (this) {
String name = Thread.currentThread().getName();
while (dishes.size() == 0) {
System.out.println(name + " is waiting.");
try {
wait();
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
while (true) {
for (int i = 0; i < dishes.size(); i++) {
if (dishName.equals(dishes.get(i))) {
dishes.remove(i);
notify();
return;
}
}
try {
System.out.println(name + " is waiting. ");
wait(); // 원하는 음식이 없는 CUST 쓰레드를 기다리게 함
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
}
}
public int dishNum() {
return dishNames.length;
}
}
- 손님, 요리사, 음식이 올려진 테이블을 wait()과 notify()로 효율적으로 구현한 예제
- 그러나 notify()가 호출되었을 때, 요리사 쓰레드와 손님 쓰레드 중 누가 통지받을지 알 수 없음 (기아현상 발생 가능)
- 기아(starvation) 현상 : 쓰레드가 통지를 받지 못하고 오랫동안 기다리게 되는 현상
- 기아 현상을 막기 위해 notifyAll() 사용 가능 (모든 쓰레드에 통지)
- 그러나 notifyAll()가 호출되었을 때, 여러 쓰레드가 lock을 얻기 위해 경쟁함 (경쟁 상태)
- 경쟁 상태(race condition) : 여러 쓰레드가 lock을 얻기 위해 서로 경쟁하는 것
-> Lock, Condition으로 해결
3) Lock, Condition을 이용한 동기화
- java.util.concurrent.locks 패키지의 lock클래스들을 이용하여 동기화할 수 있음
- JDK1.5부터 추가된 클래스
ReentrantLock : 재진입이 가능한 lock, 가장 일반적인 배타 lock
ReentrantReadWriteLock : 읽기에는 공유적이고, 쓰기에는 배타적인 lock
StampedLock : ReentrantReadWriteLock에 낙관적인 lock의 기능 추가
① ReentrantLock
- 가장 일반적인 lock
- reentrant(재진입할 수 있는) : 특정 조건에서 lock을 풀고 나중에 다시 lock을 얻고 임계영역으로 들어와서 이후의 작업을 수행할 수 있음
② ReentrantReadWriteLock
- 읽기를 위한 lock과 쓰기를 위한 lock을 제공
- 읽기 lock은 중복 O
- 쓰기 lock은 중복 X
- 읽기 lock이 걸린 상태에서 쓰기 lock 걸기 X
- 쓰기 lock이 걸린 상태에서 읽기 lock 걸기 X
③ StampedLock
- 읽기를 위한 lock과 쓰기를 위한 lock과 낙관적 읽기 lock을 제공
- lock을 걸거나 해지할 때 '스탬프(long타입의 정수값)' 사용
- 낙관적 읽기 lock(optimistic reading lock)이 걸려있을 때 쓰기 lock이 들어오면 풀림
- 즉, 무조건 읽기 lock을 걸지 않고 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 걸음
int getBalance() {
long stamp = lock.tryOptimisticRead();
int curBalance = this.balance;
if(!lock.validate(stamp)) {
stamp = lock.readLock();
try {
curBalance = this.balance;
} finally {
lock.unlockRead(stamp);
}
}
return balance;
}
- ReentrantLock의 생성자
ReentrantLock()
ReentrantLock(boolean fair) // true로 주면, lock이 풀렸을 때 가장 오래 기다린 쓰레드가 획득할 수 있게 공정(fair)하게 처리함, 성능은 떨어짐
- ReentrantLock의 메서드
void lock() lock을 잠금
void unlock() lock을 해지
boolean isLocked() lock이 잠겼는지 확인
boolean tryLock() lock이 걸려 있으면 lock을 얻으려고 기다리지 않음
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException lock이 걸려 있으면 지정된 시간만큼만 기다림
-> synchronized와 다르게 lock클래스들은 수동으로 lock을 잠그고 해제해야 함
-> unlock()은 try-finally문으로 감싸는게 일반적
- Condition : 쓰레드를 구분하기 위해 사용
- 이미 생성된 lock으로부터 new Condition()을 호출해서 생성함
private ReentrantLock lock = new ReentrantLock();
// lock으로 condition을 생성
private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondition();
- wait()¬ify() 대신 Condition의 await()&signal() 사용
Object | Condition |
void wait() | void await() void awaitUninterruptibly() |
void wait(long timeout) | boolean await(long time, TimeUnit unit) long awaitNanos(long nanosTimeout) boolean awaitUntil(Date deadline) |
void notify() | void signal() |
void notifyAll() | void signalAll() |
import java.util.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.StampedLock;
public class ThreadWaitEx2 {
public static void main(String[] args) {
Table2 table = new Table2(); // 여러 쓰레드가 공유하는 객체
new Thread(new Cook2(table), "COOK1").start();
new Thread(new Customer2(table, "donut"), "CUST1").start();
new Thread(new Customer2(table, "burger"), "CUST2").start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.exit(0);
}
}
class Customer2 implements Runnable {
private Table2 table;
private String food;
Customer2(Table2 table, String food) {
this.table = table;
this.food = food;
}
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
String name = Thread.currentThread().getName();
table.remove(food);
System.out.println(name + " ate a " + food);
}
}
}
class Cook2 implements Runnable {
private Table2 table;
Cook2(Table2 table) {
this.table = table;
}
public void run() {
while (true) {
int idx = (int) (Math.random() * table.dishNum());
table.add(table.dishNames[idx]);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
}
}
}
class Table2 {
String[] dishNames = { "donut", "donut", "burger" };
final int MAX_FOOD = 6;
private ArrayList<String> dishes = new ArrayList<>();
private ReentrantLock lock = new ReentrantLock();
private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondition();
public void add(String dish) {
lock.lock(); // 락 걸기
try {
// 테이블에 음식이 가득 찼으면, 음식을 추가하지 않음
while (dishes.size() >= MAX_FOOD) {
String name = Thread.currentThread().getName();
System.out.println(name + " is waiting. ");
try {
forCook.await(); // COOK쓰레드를 기다리게 함
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
dishes.add(dish);
forCust.signal(); // 기다리고 있는 CUST 깨우기
System.out.println("Dishes: " + dishes.toString());
} finally {
lock.unlock(); // unlock을 해줘야 함
}
}
public void remove(String dishName) {
lock.lock();
String name = Thread.currentThread().getName();
try {
while (dishes.size() == 0) {
System.out.println(name + " is waiting.");
try {
forCust.await();
Thread.sleep(500); // CUST쓰레드를 기다리게 함
} catch (InterruptedException e) {
}
}
while (true) {
for (int i = 0; i < dishes.size(); i++) {
if (dishName.equals(dishes.get(i))) {
dishes.remove(i);
forCook.signal(); // 기다리고 있는 COOK 깨우기
return;
}
}
try {
System.out.println(name + " is waiting. ");
forCust.await(); // 원하는 음식이 없는 CUST 쓰레드를 기다리게 함
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
} finally {
lock.unlock();
}
}
public int dishNum() {
return dishNames.length;
}
}
- 손님, 요리사, 음식이 올려진 테이블을 Lock과 Condition으로 효율적으로 구현한 예제
- 이전 예제와 다르게 wait()¬ify() 대신 손님Condition과 요리사Condtion을 생성한 후 각 상황에 맞게 await()&signal()을 사용해줌으로써 기아 현상이나 경쟁 상태를 개선함
4) volatile
- 멀티 코어 프로세서에서는 코어마다 별도의 캐시를 가지고 있음
- 메모리에 저장된 값이 변경되어도 캐시에 저장된 값이 갱신되지 않아 값이 다르게 나오는 경우 발생
- volatile을 붙이면 코어가 변수의 값을 읽어올 때 캐시가 아닌 메모리에서 읽어옴
- synchronized블럭을 사용하는 것과 같은 효과 (쓰레드가 블럭으로 들어갈때 나올때 메모리와 캐시를 동기화하기 때문)
※ volatile로 long과 double 원자화
- JVM은 데이터를 4 byte (=32bit) 단위로 처리하는데 8 byte인 long 타입과 double 타입의 변수는 변수의 값을 읽는 도중 다른 쓰레드가 개입될 여지가 있음
- volatile을 붙여 원자화시킬 수 있음
volatile long sharedVal;
volatile double sharedVal2;
5) fork & join 프레임웍
- JDK 1.7부터 추가
- 하나의 작업을 작은 단위로 나눠서 여러 쓰레드가 동시에 처리하는 것을 쉽게 만들어줌
- 다음의 두 클래스 중 하나를 상속받아 구현해야 함
RecursiveAction 반환값이 없는 작업을 구현할 때 사용
RecursiveTask 반환값이 있는 작업을 구현할 때 사용
- 두 클래스에 공통으로 가지고 있는 추상 메서드 compute()에 작업할 내용을 구현함
- 쓰레드풀과 수행할 작업을 생성하여 invoke()로 작업을 시작
(쓰레드 시작 때 run()이 아니라 start()로 시작하듯이, 작업 시작도 coumpute()가 아니라 invoke()로 시작)
※ 쓰레드풀 (ThreadPool)
- 쓰레드가 수행해야하는 작업이 담긴 큐 제공
- 각 쓰레드는 자신의 작업 큐에 담긴 작업을 순서대로 처리
- 쓰레드를 반복해서 생성하지 않아도 됨
- 너무 많은 쓰레드가 생성되어 성능이 저하되는 것을 막아줌
- fork() : 해당 작업을 쓰레드 풀의 작업 큐에 넣음 (비동기 메서드)
- join() : 해당 작업의 수행이 끝날 때까지 기다렸다가, 수행이 끝나면 그 결과를 반환 (동기 메서드)
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class ForkJoinEx2 {
static final ForkJoinPool pool = new ForkJoinPool(); // 쓰레드 풀 생성
public static void main(String[] args) {
long from = 1L, to = 100_000_000L;
SumTask task = new SumTask(from, to);
long start = System.currentTimeMillis();
long result = pool.invoke(task);
System.out.println("Elapsed time(4 core): " + (System.currentTimeMillis() - start));
System.out.printf("sum of %d~%d=%d\n", from, to, result);
System.out.println();
result = 0L;
start = System.currentTimeMillis();
for (long i = from; i <= to; i++)
result += i;
System.out.println("Elapsed time(1 core): " + (System.currentTimeMillis() - start));
System.out.printf("sum of %d~%d=%d\n", from, to, result);
System.out.println();
}
}
class SumTask extends RecursiveTask<Long> {
long from, to;
SumTask(long from, long to) {
this.from = from;
this.to = to;
}
public Long compute() {
long size = to - from + 1;
// 총 개수가 5 이하일 때는 sum()으로 계산
if (size <= 5)
return sum();
// 전체 범위를 반으로 나누어서 계산
long half = (to + from) / 2;
SumTask leftSum = new SumTask(from, half);
SumTask rightSum = new SumTask(half + 1, to);
leftSum.fork();
return rightSum.compute() + leftSum.join();
}
public Long sum() {
long tmp = 0L;
for (long i = from; i <= to; i++)
tmp += i;
return tmp;
}
}
- SumTask() 작업을 나눠서 수행
① 전체 사이즈가 5 이하면 for문으로 계산
② 전체 범위를 반으로 나누어서 계산
③ 반복적인 재귀호출을 통해 나누어질 때까지 나누어서 계산
- fork&join 프레임웍으로 계산한 결과보다 for문으로 계산한 결과가 시간이 덜 걸림
-> 항상 멀티쓰레드가 빠르고 효율적인 것은 아님!
'공부 > Java' 카테고리의 다른 글
[Java] Logging, SLF4J, Logback (0) | 2021.10.06 |
---|---|
[Java-14] 람다식(Lambda expression) (0) | 2021.07.06 |
[Java-12] 애너테이션(Annotation) (0) | 2021.07.02 |
[Java-12] 열거형(Enums) (0) | 2021.07.02 |
[Java-12] 지네릭스(Generics) (0) | 2021.07.02 |