이 글은 유튜브 '자바의 정석 - 기초편'을 보고 정리한 글입니다.
📂content
1. suspend(), resume(), stop()
- 스레드의 실행을 일시정지, 재개, 완전정지 시킨다.
void suspend()
스레드를 일시정지 시킨다.
void resume()
suspend()에 의해 일시정지된 스레드를 실행대기상태로 만든다.
void stop()
스레드를 즉시 종료시킨다.
스레드를 생성하고 start()를 하면 실행대기 상태가 된다.
자신의 차례가 오면 실행했다가 stop()을 호출하거나 자신의 일을 다 하면 소멸된다.
그런데 실행 중에 suspend()를 호출하면 일시정지 상태가 되었다가,
resume()을 호출하면 다시 줄을 서서 실행대기 상태가 된다.
그런데 위 메서드들은 사용권장하지 않는다.
위 메서드들은 데드락(교착상태)을 일으킬 가능성이 있기 때문이다.
- suspend(), resume(), stop()은 교착상태에 빠지기 쉬워서 deprecated되었다. (사용권장X)
그래서 직접 구현하는 것이 낫다.
class ThreadEx17_1 implements Runnable {
boolean suspended = false; //일시 정지되었는가?
boolean stopped = false; //정지되었는가?
public void run() {
while(!stopped) {
if(!suspended) {
/* 스레드가 수행할 코드를 작성 */
}
}
}
public void suspend() { suspended = true;}
public void resume() {suspended = false;}
public void stop() {stopped = true;}
실제로는 더 복잡하게 코드가 구성이 된다.
⍟실습
Ex13_10
class Ex13_10 {
public static void main(String args[]) {
MyThread th1 = new MyThread("*");
MyThread th2 = new MyThread("**");
MyThread th3 = new MyThread("***");
th1.start();
th2.start();
th3.start();
try {
Thread.sleep(2000);
th1.suspend(); // 쓰레드 th1을 잠시 중단시킨다.
Thread.sleep(2000);
th2.suspend();
Thread.sleep(3000);
th1.resume(); // 쓰레드 th1이 다시 동작하도록 한다.
Thread.sleep(3000);
th1.stop(); // 쓰레드 th1을 강제종료시킨다.
th2.stop();
Thread.sleep(2000);
th3.stop();
} catch (InterruptedException e) {}
} // main
}
class MyThread implements Runnable {
volatile boolean suspended = false; //쉽게 바뀌는 변수
volatile boolean stopped = false;
Thread th;
MyThread(String name) {//생성자
//Runnable을 구현한 것은 자기 자신으므로 this로 한다.
th = new Thread(this, name); //Thread(Runnable r, String name)
}
void start() {
th.start();
}
void stop() {
stoppped = true;
}
void suspend() {
suspended = true;
}
void resume() {
suspended = false;
}
public void run() {
while(!stopped) {
if(!suspended){
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
}
}
} // run()
}
volatile을 넣지 않으면 java가 멈추지 않고 돌아간다.
volatile에 자세히 알려면 cpu와 캐시, 메모리 등에 대해서 알아야 한다. 간단히 설명하자면 아래와 같다.
cpu에는 코어들이 있다. 이 코어 내부에 캐시라는 메모리가 있다. 그리고 RAM이라는 메모리가 있다.
이 메모리에 suspended, stopped라는 변수가 올라간다. 이 값이 현재 false라고 하자. 그런데 이 값을 코어의 캐시가 빨리 작업을 하기 위해 복사해서 가지고 있다. 즉, RAM에 있는 것이 원본이고 코어의 캐시에 있는 것이 복사본이 되는 것이다. 그런데 RAM에 있는 값이 false에서 true로 바뀐다고 할 때, 캐시에서 바뀐 값을 못 가지고 있을 수 있다.
효율적으로 하고자 한 건데 그렇게 되지 않는 것이다.
그런데 이때 volatile을 사용하면 복사본을 사용하지 않는다. 원본을 직접 가서 읽어오는 것이다.
즉, 간단히 말하면 volatile을 붙이면서 이 값은 자주 바뀌니까 복사본을 쓰지 말고 원본을 쓰라고 말해주는 것이다.
2. join()
- 지정된 시간동안 특정 스레드가 작업하는 것을 기다린다.
스레드 A, 스레드 B가 있다고 하자. A,B가 실행이 되고 있는데, A입장에서 B가 작업을 끝내야 A가 작업을 진행할 수 있다고 하자. 이 때(A가 B가 끝날 때까지 기다려야 할 때) join()이 사용되는 것이다.
void join()
작업이 모두 끝날 때까지
void join(long millis)
천분의 일초 동안
void join(long millis, int nanos)
천분의 일초 + 나노초 동안
- 예외처리를 해야 한다. (InterruptedException이 발생하면 작업 재개)
public static void main(String args[]) {
ThreadEx19_1 th1 = new ThreadEx19_1();
ThreadEx19_2 th2 = new ThreadEx19_2();
th1.start();
th2.start();
startTime = System.currentTimeMillis();
try {
th1.join(); //main쓰레드가 th1의 작업이 끝날 때까지 기다린다.
th2.join(); //main쓰레드가 th2의 작업이 끝날 때까지 기다린다.
} catch(InterruptedException e) {}
System.out.print("소요시간:" + (System.currentTimeMillis() - ThreadEx19.startTime));
} //main
⍟실습
Ex13_11
class Ex13_11 {
static long startTime = 0;
public static void main(String args[]) {
ThreadEx11_1 th1 = new ThreadEx11_1();
ThreadEx11_2 th2 = new ThreadEx11_2();
th1.start();
th2.start();
startTime = System.currentTimeMillis();
try {
th1.join(); // main쓰레드가 th1의 작업이 끝날 때까지 기다린다.
th2.join(); // main쓰레드가 th2의 작업이 끝날 때까지 기다린다.
} catch(InterruptedException e) {}
System.out.print("소요시간:" + (System.currentTimeMillis() - Ex13_11.startTime));
} // main
}
class ThreadEx11_1 extends Thread {
public void run() {
for(int i=0; i < 300; i++) {
System.out.print(new String("-"));
}
} // run()
}
class ThreadEx11_2 extends Thread {
public void run() {
for(int i=0; i < 300; i++) {
System.out.print(new String("|"));
}
} // run()
}
- join()을 사용하지 않으면 th1과 th2을 기다리지 않는다 .그래서 소요시간이 거의 바로 출력된다. join()을 사용하면 th1과 th2가 끝날때까지 기다렸다가 소요시간을 출력한다.
추가예시
아래 코드는 join()을 사용하여 GC(가비지콜렉터)를 흉내낸 코드이다. 데몬스레드(보조스레드)이라서 무한루프로 돌아간다. 데몬스레드는 참고로 일반스레드가 하나도 없으면 자동종료된다. 10초마다 GC(사용하지 않는 객체를 제거. 사용할 수 있는 메모리를 올리기 위해)를 수행한다.
public void run() {
while(true) {
try {
Thread.sleep(10 * 1000); //10초를 기다린다.
} catch(InterruptedException e) {
System.out.println("Awaken by interrupt().");
}
gc(); //garbage collection을 수행한다.
System.out.println("Garbage Collected. Free Memory :" + freeMemory());
}
}
랜덤으로 사용할 메모리값을 결정하게 함.
for(int i = 0; i < 20; i++){
requiredMemory = (int)(Math.random() * 10) * 20;
//필요한 메모리가 사용할 수 있는 양보다 적거나 전체 메모리의 60%이상 사용했을 경우 gc를 깨운다.
if(gc.freeMemory() < requiredMemory ||
gc.freeMemory() < gc.totalMemory() * 0.4){//0. 메모리가 부족한 경우
gc.interrupt(); //1-1. 잠자고 있는 스레드 gc를 깨운다.
try {//1-2. join() 사용하여 gc가 메모리를 정리할 시간을 준다.
//이 부분 없으면 오류
gc.join(100);
} catch(InterruptedException e) {}
}
gc.usedMemory += requiredMemory; //2. 메모리 사용
System.out.println("useMemory:" + gc.usedMemory);
}
메모리가 부족하면 프로그램이 죽는다.
3. yield()
- 남은 시간을 다음 스레드에게 양보하고, 자신(현재 스레드)은 실행대기한다.
static 메서드라서 남에게 못 시키고 자신에게만 명령 가능
- yield()와 interrupt()를 적절히 사용하면, 응답성과 효율을 높일 수 있다.
class MyThreadEx18 implements Runnable {
boolean suspended = false;
boolean stopped = false;
Thread th;
MyThreadEx18(String name) {
th = new Thread(this, name);
}
public void run() {
while(!stopped) {
if(!suspended) {
/*
작업 수행
*/
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
} else {
Thread.yield();
}
} //while
}
public void start() {
th.start();
}
public void resume() {
suspended = false;
}
public void suspend() {
suspended = true;
th.interrupt(); //자고 있을 수 있기 때문에 깨운다.
}
public void stop() {
stopped = true;
th.interrupt(); //자고 있을 수 있기 때문에 깨운다.
}
만약 stopped = false, suspended = true라면 스레드가 일시정지상태이다.
그렇다면 위 코드에서 while문은 !false = true라서 돌아가지만, if문은 !true = false라서 수행되지 않는다.
그렇다면 시간은 주어졌지만 할 일이 없이 계속 돌아간다. 이것을 busy-waiting이라고 한다.
그래서 이럴 때는 else에 yield()를 넣는 것이다. 그래서 자기 자신에게 주어진 시간을 다른 스레드에게 준다.
그런데 yield()는 os스케줄러에게 알려주는 용이다. 그래서 반드시 yield()가 동작한다는 보장은 없다. 그래서 yield()를 쓰기 전과 후가 큰 차이는 없다. 그래도 도움이 되기는 하다.
4. 스레드의 동기화(synchronization)
- 멀티 스레드 프로세스에서는 다른 스레드의 작업에 영향을 미칠 수 있다.
- 진행중인 작업이 다른 스레드에게 간섭받지 않게 하려면 '동기화'가 필요
스레드의 동기화 - 한 스레드가 진행중인 작업을 다른 스레드가 간섭하지 못하게 막는 것
- 동기화하려면 간섭받지 않아야 하는 문장들을 '임계 영역'으로 설정
- 임계영역은 락(lock)을 얻은 단 하나의 스레드만 출입가능(객체 1개에 락 1개)
5. synchronized를 이용한 동기화
- synchronized로 임계영역(lock이 걸리는 영역)을 설정하는 방법 2가지
임계영역 : 다른 스레드들이 간섭하지 못하는 문장들을 묶은 영역. 그리고 이 영역은 synchronized를 사용하면 영역이 지정됨. 그런데 이 영역은 한 번에 한 스레드만 사용할 수 있기 때문에 영역을 최소화해야한다. 임계영역이 많을 수록 성능이 떨어진다. 멀티스레드의 장점은 동시에 여러 스레드가 돌아가는 것인데, 임계영역은 1번에 1개의 스레드만 들어갈 수 있기 때문이다. 그래서 영역과 개수를 최소화해야한다. 그래서 가능하면 (2번처럼) 메소드 전체를 임계영역에 넣지 않는 것이 좋다.
위 코드를 예시로 들어보자면, withdraw라는 메소드가 있다. 어떤 스레드가 출금 작업을 완전히 마치기 전까지 다른 스레드가 출금메소드에서 임계영역의 문장을 실행할 수 없다.
2번째 이미지의 this는 객체를 가리키는 참조변수이다.
임계영역에는 한 번에 한 개의 스레드만 들어갈 수 있다.
⍟실습
Ex13_13
class Ex13_13 {
public static void main(String args[]) {
Runnable r = new RunnableEx13();
new Thread(r).start();
new Thread(r).start();
}
}
class Account2 {
private int balance = 1000; // private으로 해야 동기화가 의미가 있다.
public synchronized int getBalance() {//읽는 과정에서도 동기화가 되어있어야한다.
return balance;
}
public synchronized void withdraw(int money){ // synchronized로 메서드를 동기화
if(balance >= money) {
try { Thread.sleep(1000);} catch(InterruptedException e) {}
balance -= money;
}
} // withdraw
}
class RunnableEx13 implements Runnable {
Account2 acc = new Account2();
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());
}
} // run()
}
- balance를 private로 하지 않으면 외부에서 값을 불러와 마음대로 지정할 수 있기 때문에 private로 해야함.
- sleep()은 결과를 보기 쉽게 하는 용으로 사용(신경X). 없다면 데이터가 잘못된 경우 찾기가 어렵다.
- 만약에 synchronized라는 키워드가 없다면 balance가 음수가 나온다. 그런데 항상 문제가 있는 것은 아니다. 문제가 있을 때도 있고 없을 때도 있다.
스레드가 A,B가 있다고 하자. 이 둘이 작업을 할 텐데, 이때 balance가 200이고 A가 money = 200을 뺀다고 하자. 위 코드에서 withdraw메소드의 if문에서 200 >= 200이므로 참이 된다. 그래서 if문을 통과는 했는데 시간이 다 되어서 B에게 넘어갔다고 하자. 스레드 B는 money=100을 빼간다고 할 때, 아직 A에서 돈을 빼간 것이 아니라서 여전히 balance=200이다. 그래서 if문에서 200 >= 100이므로 참이 되어서 withdraw에서 100원을 빼가면 balance=100이다. 그리고 A 차례가 되어서 if문에서 이미 참이되었으므로 잔고검사를 안 하고 200원을 빼간다. 그래서 balance - money = 100 - 200 = -100이 되는 것이다. 그래서 이런 일을 막기 위해 synchronized를 사용해야 한다.
- 읽고 쓸때 동기화를 해주어야 한다.
6. wait()과 notify()
- 동기화의 효율을 높이기 위해 wait(), notify()를 사용
동기화를 하면 데이터가 보호는 되는데 한 번에 한 스레드만 임계영역에 들어갈 수 있다는 것은 동기화의 단점이다. 그래서 동기화를 하면 프로그램의 효율이 떨어진다.
- Object클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.
* wait() - 객체의 lock을 풀고 스레드를 해당 객체의 waiting pool에 넣는다.
* notify() - waiting pool에서 대기중인 스레드 중의 하나를 깨운다.
* notifyAll() - waiting pool에서 대기중인 모든 스레드를 깨운다.
notifyAll()을 사용하는 것이 좋다. notify()로 하면 운이 없다면 특정 스레드는 계속 잠을 잘 수 있기 때문이다. 그렇기 때문에 무조건 다 깨우는 것이 좋다. 그래도 한 스레드만 lock을 얻는다.
class Account {
int balance = 1000;
public synchronized void withdraw(int money){
while(balance < money) {
try {
wait(); //대기 - 락을 풀고 기다린다. 통지를 받으면 락을 재획득(ReEntrance)
} catch(InterruptedException e) {}
}
balance -= money;
} // withdraw
public synchronized void deposit(int money){
balance += money;
notify(); //통지 - 대기중인 스레드 중 하나에게 알림
}
}
스레드 A,B가 있다고 하자. A가 lock을 가지고 withdraw를 실행하고 있다고 하자. 그렇다면 다른 스레드가 Account객체에 들어오지 못 한다. 왜냐하면 객체 1개에 lock은 1개이다.
그렇기 때문에 B가 deposit을 하고 싶어도 lock을 이미 A가 갖고 있어서 수행하지 못 한다.
그래서 wait()을 호출한다. A가 락을 풀고 B가 락을 가지고 온다. 그렇다면 deposit을 수행하고 notify()를 통해 대기중인 스레드를 깨운다. 그렇다면 A가 다시 락을 얻고 출금을 진행한다.
6-1.wait()과 notify() 예제
- 요리사는 Table에 음식을 추가. 손님은 Table의 음식을 소비(Table에서 dish를 제거)
- 요리사와 손님이 같은 객체(Table)을 공유하므로 동기화가 필요
- 동기화를 하지 않는다면 에러가 난다.(단, 항상 나는 것은 아님)
[예외1] 요리사가 Table에 요리를 추가하는 과정에 손님이 요리를 먹음
[예외2] 하나 남은 요리를 손님2가 먹으려하는데, 손님1이 먹음
[그림1의 문제점] Table을 여러 스레드가 공유하기 때문에 작업 중에 끼어들기 발생
[해결책] Table의 add()와 remove()를 synchronized로 동기화
[그림2의 문제] 예외는 발생하지 않지만, 음식이 없을 때, 손님(CUST2)이 Table에 lock건 상태를 지속
요리사가 Table의 lock을 얻을 수 없어서 음식을 추가하지 못함
[해결책] 음식이 없을 때, wait()으로 손님이 lock을 풀고 기다리게 하자.
요리사가 음식을 추가하면, notify()로 손님에게 알리자(손님이 lock을 재획득)
⍟실습
Ex13_15 -> 동기화O
import java.util.ArrayList;
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);
} // while
}
}
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) {}
} // while
}
}
class Table2 {
String[] dishNames = { "donut","donut","burger" }; // donut의 확률을 높인다.
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(); // CUST쓰레드를 기다리게 한다.
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(); // 잠자고 있는 COOK을 깨우기 위함
return;
}
} // for문의 끝
try {
System.out.println(name+" is waiting.");
wait(); // 원하는 음식이 없는 CUST쓰레드를 기다리게 한다.
Thread.sleep(500);
} catch(InterruptedException e) {}
} // while(true)
} // synchronized
}
public int dishNum() { return dishNames.length; }
}
class Ex13_15 {
public static void main(String[] args) throws Exception {
Table2 table = new Table2();
new Thread(new Cook2(table), "COOK").start();
new Thread(new Customer2(table, "donut"), "CUST1").start();
new Thread(new Customer2(table, "burger"), "CUST2").start();
Thread.sleep(2000);
System.exit(0);
}
}
- 요리사는 테이블이 가득 차면 대기(wait())하고, 음식을 추가하고나면 손님에게 통보(notify())한다.
- 손님은 음식이 없으면 대기(wait())하고, 음식을 먹고나면 요리사에게 통보(notify())한다.
- 이런 식으로 코드를 짜면 전과 달리 한 스레드가 lock을 오래 쥐는 일이 없어짐. 효율적이 됨
대기실에는 손님, 요리사가 모두 대기하고 있다. 그래서 wait(), notify()는 손님과 요리사를 구분하지 않는다. 즉, 호출되는 대상이 불분명하다는 단점이 있다. 그래서 이것을 구분하기 위해 lock&condition이 나온 것이다.
출처
'🎥Back > 자바의 정석' 카테고리의 다른 글
[JAVA의 정석] 스트림 (0) | 2024.03.08 |
---|---|
[JAVA의 정석]람다식과 함수형 인터페이스 (2) | 2024.03.06 |
[JAVA의 정석]데몬 스레드, 스레드의 상태 (0) | 2024.02.29 |
[JAVA의 정석]싱글 스레드와 멀티스레드, 스레드의 I/O 블락킹 (0) | 2024.02.29 |
[JAVA의 정석]스레드 (0) | 2024.02.29 |