블로그 이름 뭐하지
[JAVA] 쓰레드 본문
프로세스
운영 체제(OS)로부터 자원을 할당 받는 작업의 단위로, 실행 중인 프로그램을 의미한다.
ex. JAVA 프로그램 실행시 해당 프로그램은 프로세스라는 이름으로 운영체제(Windows, Mac, Linux...) 위에서 실행
OS 위 에서 실행되는 모든 프로그램은 OS가 만든 프로세스에서 실행되는 것
(크롬 브라우저 창이 2개 띄워져 있으면 크롬 브라우저 프로세스도 2개가 실행되는 것)
OS가 프로세스를 할당할 때, 프로세스 안에 프로그램 Code, Deta, 메모리 영역(Stack, Heap)을 함께 할당한다.
Code : Java main 메서드 같은 코드
Data : 프로그램이 실행 중 저장할 수 있는 공간
(ex. 전역변수, static 변수, 배열...)
Memory(메모리 영역): Stack(지역변수, 매개변수, 리턴변수 저장), Heap(동적으로 필요한 변수 저장)
쓰레드
프로세스가 할당받은 자원을 이용하는 실행의 단위
프로세스 내에서 일하는 일꾼(코드 실행의 흐름)이라고 생각하면 쉽다.
프로세스가 작업 중인 프로그램에서 실행요청이 들어오면 쓰레드를 만들어 명령을 처리한다.
프로세스 안에는 여러 쓰레드가 있고, 쓰레드들은 실행을 위한 프로세스 내 주소 공간이나 메모리공간(Heap)을 공유받는다. 추가로 각 명령 처리를 위한 자신만의 메모리 공간도 할당받는다.
JAVA 쓰레드
일반 쓰레드와 동일하며 JVM 프로세스 안에서 실행되는 쓰레드이다.
Java 프로그램 쓰레드는 Java Main 쓰레드부터 실행되며 JVM에 의해 실행된다.
멀티쓰레드
java는 메인 쓰레드가 main() 메서드를 실행시키며 시작한다.
메인 쓰레드는 필요에 따라 작업 쓰레드를 생성하여 병렬로 코드를 실행한다
(= 자바는 멀티 쓰레드를 지원한다.)
싱글 쓰레드
프로세스 안에서 하나의 쓰레드만 실행되는 것
자바에서는 main() 메서드만 실행시킬 때 싱글 쓰레드라고 한다.
main() 메서드의 쓰레드를 메인 쓰레드라고 부르며,
JVM의 메인쓰레드가 종료되면 JVM도 함께 종료된다.
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
System.out.print("$");
// $ 100개를 순서대로 출력한다.
}
};
Thread thread1 = new Thread(task);
thread1.setName("thread1");
thread1.start();
}
}
멀티쓰레드
프로세스 안에서 여러 개의 쓰레드가 실행되는 것
Java 프로그램은 메인 쓰레드 외에 다른 작업 쓰레드를 생성하여 여러개의 실행 흐름을 만들 수 있다.
쓰레드들은 실행 순서나 걸리는 시간이 일정하지 않아 번갈아 가면서 수행된다.
public class Main {
public static void main(String[] args) {
//쓰레드1이 수행할 코드
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
System.out.print("$");
}
};
//쓰레드2가 수행할 코드
Runnable task2 = () -> {
for (int i = 0; i < 100; i++) {
System.out.print("*");
}
};
//$와 *가 번갈아가면서 출력됨
//$*$*$****$$$$***$*$**....
Thread thread1 = new Thread(task);
thread1.setName("thread1");
Thread thread2 = new Thread(task2);
thread2.setName("thread2");
thread1.start();
thread2.start();
}
}
장점
1) 여러 개의 작업을 동시에 가능하여 성능이 향상
2) 스택을 제외한 모든 영역의 메모리를 공유하므로 자원을 효율적으로 사용
3) 응답 쓰레드와 작업 쓰레드를 분리해 빠르게 응답이 가능(비동기)
단점
1) 동기화 문제 발생(프로세스의 자원을 서로 사용하려는 충돌이 발생할 수 있음)
2) 교착 상태(데드락)가 발생할 수 있음
(데드락: 둘 이상의 쓰레드가 서로의 자원을 원하는 상태가 되었을 때,
서로의 작업이 종료되기만을 기다려 작업이 진행되지 못하는 상태)
쓰레드의 구현과 실행
1) Thread로 구현 및 실행
Thread 클래스를 상속받아 쓰레드를 구현한다.
//Thread 클래스를 상속받아 쓰레드를 구현
class TestThread extends Thread {
@Override
public void run() {
//아래의 코드가 쓰레드가 수행할 작업임
//*
for (int i = 0; i <100; i++) {
System.out.print("*");
}
}
}
public class Main {
public static void main(String[] args) {
//쓰레드 생성
TestThread thread = new TestThread();
//쓰레드 실행
thread.start();
}
}
2) Runnable로 구현 및 실행
Runnable 인터페이스를 구현받아 쓰레드를 구현한다.
//Runnable 인터페이스를 구현받아 쓰레드를 구현
class TestRunnable implements Runnable {
@Override
public void run() {
//아래의 코드가 쓰레드가 수행할 작업임
for (int i = 0; i <100; i++) {
System.out.print("$");
}
}
}
public class Main {
public static void main(String[] args) {
Runnable run = new TestRunnable();
//쓰레드 생성
Thread thread = new Thread(run);
//쓰레드 실행
thread.start();
}
}
Thread를 상속받지 않고 Runnable로 구현하는 이유
클래스와 인터페이스의 차이 때문.
Java는 클래스의 다중 상속을 지원하지 않으므로
Thread를 상속할 경우 확장성이 떨어진다.
반대로 Runnable로 구현하면 다른 클래스를 상속받을 수 있다. (확장성 유리)
3) Runnable + 람다식을 이용해 쓰레드 구현 및 실행
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
//쓰레드가 실행할 작업 코드
int sum = 0;
for (int i = 0; i < 50; i++) {
sum += i;
System.out.println(sum);
}
//Thread.currentThread().getName(): 현재 작업중인 쓰레드의 이름을 반환함
System.out.println(Thread.currentThread().getName() + " 최종 합 : " + sum);
};
//쓰레드 생성
Thread thread1 = new Thread(task);
//setName() : 쓰레드에 이름을 부여함
thread1.setName("thread1");
Thread thread2 = new Thread(task);
thread2.setName("thread2");
//쓰레드 실행
thread1.start();
thread2.start();
}
}
데몬 쓰레드와 사용자 쓰레드
데몬 쓰레드
보이지 않는 곳(background)에서 실행되는 낮은 우선 순위를 가진 쓰레드
보조적 역할을 담당하며 대표적으로는 가비지 컬렉터(GC)가 있다.
우선순위가 낮아 다른 쓰레드가 모두 종료되면 강제 종료된다.
public class Main {
public static void main(String[] args) {
//데몬 쓰레드가 실행할 코드
Runnable demon = () -> {
for (int i = 0; i < 1000000; i++) {
System.out.println("demon");
}
};
Thread thread = new Thread(demon);
thread.setDemon(true); // true로 설정시 데몬스레드로 실행됨
thread.start();
//main 메서드가 실행할 코드
for (int i = 0; i < 100; i++) {
System.out.println("task");
}
}
}
//데몬 쓰레드가 100000번 수행하기 전에
//main 쓰레드가 100번 실행을 끝내고 종료함
//데몬 쓰레드도 강제 종료됨
사용자 쓰레드
보이는 곳(foreground)에서 실행되는 높은 우선 순위를 가진 쓰레드.
프로그램 기능을 담당하며 대표적인 사용자 쓰레드로는 메인 쓰레드가 있다.
JVM은 사용자 쓰레드의 작업이 끝나면 데몬 쓰레드도 자동으로 종료시킨다.
쓰레드 우선 순위
쓰레드 작업의 중요도에 따라 쓰레드의 우선 순위를 부여할 수 있다.
우선 순위를 높게 지정하면 더 많은 작업시간을 부여받아 빠르게 처리된다.
쓰레드는 생성시에 우선 순위가 정해진다.(직접 지정 or JVM에 의해 지정)
우선 순위는 최대, 최소, 보통 우선 순위로 나뉜다.
최대 우선 순위(MAX_PRIORITY) = 10
최소 우선 순위(MIN_PRIORITY) = 1
(기본 값) 보통 우선 순위(NROM_PRIORITY) = 5
더 자세하게는 1~10까지의 숫자로 지정할 수 있으며, OS가 아닌 JVM에서 설정한 우선순위이다.
쓰레드 우선 순위는 setPriorty() 메서드로 설정하며,
getPriorty() 메서드로 우선순위를 확인 할 수 있다.
//setPriority : 우선순위 부여
Thread thread1 = new Thread(task1);
thread1.setPriority(8);
//getPriority : 우선순위 확인
int threadPriority = thread1.getPriority();
System.out.println("threadPriority = " + threadPriority);
우선 순위가 높다고 해서 반드시 해당 쓰레드가 먼저 종료되지 않는다.
쓰레드마다 코드가 진행되는 시간이 다르기 때문이다.
쓰레드 그룹
서로 관련이 있는 쓰레드들은 그룹으로 묶어 다룰 수 있다.
JVM이 시작되면 system 그룹이 생성되고 쓰레드들은 기본적으로 system 그룹에 포함된다.
메인 쓰레드는 system 그룹 하위의 main 그룹에 포함된다.
모든 쓰레드들은 반드시 하나의 그룹에 포함되어 있어야 한다.
쓰레드 그룹을 지정받지 못한 쓰레드는 자신을 생성한 부모 쓰레드의 그룹과 우선순위를 상속받는다.
우리가 생성하는 쓰레드는 main 쓰레드 하위에 포함되므로,
그룹을 지정하지 않으면 자동으로 main 쓰레드 그룹에 포함된다.
쓰레드 그룹 생성
ThreadGroup 클래스로 객체를 만들어 Thread 객체 생성시 첫번째 매개변수로 넣는다.
쓰레드의 상태
쓰레드는 실행과 대기를 반복하며, 메서드를 수행한다.
해당 메서드가 종료되면 실행이 멈춘다.
쓰레드는 일시정지 상태를 만들 수 있다.
일시정지 상태에서는 쓰레드가 실행을 할 수 없는 상태가 된다.
다시 실행상태로 넘어가려면 일시정지 상태에서 실행 대기 상태로 넘어가야한다.
상태 | Enum | 설명 |
객체생성 | NEW | 쓰레드 객체 생성. 아직 start() 메서드 호출 전의 상태 |
실행대기 | RUNNABLE | 실행 상태로 언제든 갈 수 있는 상태 |
일시정지 | WAITING | 다른 쓰레드가 통지(notify)할 때까지 기다리는 상태 |
TIMED_WAITING | 주어진 시간동안 기다리는 상태 | |
BLOCKED | 사용하고자 하는 객체의 Lock이 풀릴 때까지 기다리는 상태 | |
종료 | TERMINATED | 쓰레드의 작업이 종료된 상태 |
쓰레드의 제어
sleep()
현재 쓰레드를 지정된 시간동안 멈추게 한다.
쓰레드 자기 자신에 대해서만 멈추게 할 수 있다.(특정 쓰레드 지목 안됨)
Thread.sleep(ms) : 밀리초 단위로 설정된다.
sleep 상태에 있는 동안 interrupt()를 만나면 다시 실행되므로
InterruptedException이 발생할 수 있어, 예외 처리를 해야한다.
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
try {
//1)예외처리 필수
//-interrupt()를 만나면 다시 실행되기 때문에
// interruptException이 발생할 수 있음
//2)특정 쓰레드 지목불가
Thread.sleep(2000);//TIMED_WAITING(주어진 시간동안 기다리는 상태)
//Thread.sleep() >> Thread는 클래스명이므로 바로 가져다 붙인 sleep()은 static 메서드
//sleep()은 어떤 객체를 지칭하여 멈추게 하는 것이 아니라,
//흐름(현재는 task 쓰레드)안에서 쓰레드 자체를 멈추게 하는 것
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task : " + Thread.currentThread().getName());
};
Thread thread = new Thread(task, "Thread"); //NEW
thread.start(); //NEW > RUNNABLE
try {
//1초가 지나고 나면 runnable 상태가 되어 다시 실행된다
thread.sleep(1000);
System.out.println("sleep(1000) : " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
interrupt()
일시정지 상태인 쓰레드를 실행 대기 상태로 만든다.
쓰레드가 start()된 후 동작하다 interrupt()를 만나면 interrupted 상태가 true가 된다.
isInterrupted() 메서드로 상태값을 확인 할 수 있다.
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
//while()문으로 isInterrupted 상태가 아닐경우에만
//try catch를 실행해서 오류가 발생하지 않도록 한다.
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
break;
}
}
System.out.println("task : " + Thread.currentThread().getName());
};
Thread thread = new Thread(task, "Thread");
thread.start();
thread.interrupt();
System.out.println("thread.isInterrupted() = " + thread.isInterrupted());
}
}
join()
정해진 시간동안 지정한 쓰레드가 작업하는 것을 기다린다.
시간이 지정되지 않았을 경우, 지정한 쓰레드의 작업이 끝날 때까지 기다린다.
Thread.join(ms) : 밀리초 단위로 설정된다.
join 상태에 있는 동안 interrupt()를 만나면 다시 실행되므로
InterruptedException이 발생할 수 있어, 예외 처리를 해야한다.
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
try {
Thread.sleep(5000); // 5초
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(task, "thread"); //NEW
thread.start(); //NEW > RUNNABLE
long start = System.currentTimeMillis();
try {
//시간을 지정하지 않았으므로 thread가 작업을 끝낼때까지 main 쓰레드는 기다리게 된다
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// thread 의 소요시간인 5000ms 동안 main 쓰레드가 기다렸기 때문에 5000이상이 출력됩니다.
System.out.println("소요시간 = " + (System.currentTimeMillis() - start));
}
}
yield()
남은 시간은 다음 쓰레드에게 양보하고 쓰레드 자신은 실행 대기 상태가 된다.
//thread1과 thread2가 같이 1초에 한번씩 출력되다가
//5초 뒤에 thread1에서 InterrutedException이 발생하면서 Thread.yield()가 실행
//thread1은 실행대기 상태로 변경되며 남은 시간은 thread2에게 양보됨
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
try {
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
}
} catch (InterruptedException e) {
Thread.yield();
}
};
Thread thread1 = new Thread(task, "thread1");
Thread thread2 = new Thread(task, "thread2");
thread1.start();
thread2.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread1.interrupt();
}
}
synchronized
멀티 쓰레드는 여러 쓰레드가 한 프로세스의 자원을 공유하므로 관련한 버그가 생길 수 있다.
한 쓰레드가 진행 중인 작업을 다른 쓰레드가 침범하지 못하도록 막는 것을 쓰레드 동기화(synchronization)라고 한다.
동기화를 하려면 다른 쓰레드의 침범을 막아야 하는 코드들을 '임계 영역'으로 설정한다.
임계 영역에는 Lock을 가진 하나의 쓰레드만 출입이 가능하다.(한 번에 한 쓰레드)
실행할 메서드 또는 실행할 코드 묶음 앞에 synchronized를 붙여 다른 쓰레드의 침범을 막을 수 있다.
public synchronized void asyncSum() {
...침범을 막아야하는 코드...
}
synchronized(해당 객체의 참조변수) {
...침범을 막아야하는 코드...
}
public class Main {
public static void main(String[] args) {
AppleStore appleStore = new AppleStore();
Runnable task = () -> {
while (appleStore.getStoredApple() > 0) {
appleStore.eatApple();
System.out.println("남은 사과의 수 = " + appleStore.getStoredApple());
}
};
//3개의 쓰레드를 한번에 만들어 start
//생성(NEW)과 동시에 start(NEW > RUNNABLE)
for (int i = 0; i < 3; i++) {
//하지만 동시에 수행하므로 남은 사과의 개수가 뒤죽박죽이거나
//없는 사과를 먹는 경우도 발생함
new Thread(task).start();
}
}
}
class AppleStore {
private int storedApple = 10;
public int getStoredApple() {
return storedApple;
}
public void eatApple() {
//synchronized()를 설정하면 한 쓰레드가 작업을 끝낼 때까지 다른 쓰레드가 들어가지 않음
synchronized (this) {
if (storedApple > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
storedApple -= 1;
}
}
}
}
wait()와 notify()
침범을 막은 코드를 수행하다가 작업을 수행할 상황이 아닌 경우 wait() 호출.
쓰레드가 Lock을 반납하고 대기함
대기하는 동안 다른 쓰레드는 Lock을 얻어 작업을 수행하고,
작업을 진행할 상황이 되면 notify()를 호출.
작업을 중단했던 쓰레드가 다시 Lock을 얻어 진행함
public class Main {
public static String[] itemList = {
"MacBook", "IPhone", "AirPods", "iMac", "Mac mini"
};
public static AppleStore appleStore = new AppleStore();
public static final int MAX_ITEM = 5;
public static void main(String[] args) {
// 가게 점원
Runnable StoreClerk = () -> {
while (true) {
//0부터 4 사이의 랜덤한 정수를 뽑아냄
int randomItem = (int) (Math.random() * MAX_ITEM);
// restock() 재고를 넣는 메서드
appleStore.restock(itemList[randomItem]);
try {
Thread.sleep(50);
} catch (InterruptedException ignored) {
}
}
};
// 고객
Runnable Customer = () -> {
while (true) {
try {
Thread.sleep(77);
} catch (InterruptedException ignored) {
}
int randomItem = (int) (Math.random() * MAX_ITEM);
//sale() 판매하는 메서드
appleStore.sale(itemList[randomItem]);
System.out.println(Thread.currentThread().getName() + " Purchase Item " + itemList[randomItem]);
}
};
new Thread(StoreClerk, "StoreClerk").start();
new Thread(Customer, "Customer1").start();
new Thread(Customer, "Customer2").start();
}
}
class AppleStore {
//inventory: 재고
private List<String> inventory = new ArrayList<>();
//재입고
public void restock(String item) {
synchronized (this) {
while (inventory.size() >= Main.MAX_ITEM) {
System.out.println(Thread.currentThread().getName() + " Waiting!");
try {
wait(); // 재고가 꽉 차있어서 재입고하지 않고 기다리는 중!
Thread.sleep(333);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 재입고
inventory.add(item);
notify(); // 재입고 되었음을 고객에게 알려주기
System.out.println("Inventory 현황: " + inventory.toString());
}
}
public synchronized void sale(String itemName) {
//재고가 없으면 기다림(wait())
while (inventory.size() == 0) {
System.out.println(Thread.currentThread().getName() + " Waiting!");
try {
wait(); // 재고가 없기 때문에 고객 대기중
Thread.sleep(333);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//재고 있을 때.
while (true) {
// 고객이 주문한 제품이 있는지 확인
for (int i = 0; i < inventory.size(); i++) {
if (itemName.equals(inventory.get(i))) {
inventory.remove(itemName);
notify(); // 제품 하나 팔렸으니 재입고 하라고 알려주기
return; // 메서드 종료
}
}
// 고객이 찾는 제품이 없을 경우 기다림(wait())
try {
System.out.println(Thread.currentThread().getName() + " Waiting!");
wait();
Thread.sleep(333);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Lock
synchronized 블럭으로 동기화하면 자동으로 Lock이 걸리고 풀리지만,
같은 메서드 내에서만 Lock을 걸 수 있다는 한계가 있다.
이 부분을 해결하기 위해 Lock 클래스를 활용해 동기화한다.
1) ReentrantLock
재진입 가능한 Lock. 가장 일반적인 배타 Lock.
특정조건에서 Lock을 풀고, 나중에 다시 Lock을 얻어 임계 영역으로 진입이 가능.
synchronized() 블럭대신 ReentrantLock()을 사용한다.
private ReentrantLock lock = new ReentrantLock();
두 개의 생성자를 가지며, boolean 값을 true로 설정할 경우, 가장 오래된 쓰레드가 Lock을 얻도록 처리한다.
ReentrantLock()
ReentrantLock(boolean)
사용가능한 메서드
메서드 | 리턴 값 | 설명 |
.lock() | void | Lock 설정 |
.unlock() | void | Lock 해제 |
.isLocked() | boolean | Lock이 설정되어 있는지 확인 |
.tryLock() | boolean | Lock polling(= spin Lock) : 다른 쓰레드가 Lock을 소유하고 있다면 그 Lock이 반환될 때까지 무한루프를 돌며 계속 기다리는 것 (busy waiting) |
.tryLock(long timeout, TimeUnit unit) throws InterruptedException |
boolean | 지정한 시간(timeout)동안 Lock을 얻지 못할 경우 다시 작업을 시도할 것인지 포기할 것인지 설정 가능 |
Lock을 생성하고, 소멸하고, 획득하고, 반환하는 모든 과정에 비용이 발생하는데,
락을 많이 사용할 수록 overhead(비용)가 많아지고, 실제 연산보다 락 자체를 사용하는데 쓰는 비용이 많아진다.
이때, Critical Setion(하나의 쓰레드만 들어와 작업할 수 있는 공간)의 수행시간이 짧을 경우,
tryLock을 사용하여 효율적인 Locking이 가능하다.
//기본적인 사용법
class TestClass{
private ReentrantLock lock = new ReentrantLock(); // Lock 생성
public testMethod(){
lock.lock();
try{
//Critical Section(: 보호구역, 하나의 쓰레드만 들어와 작업)
} finally {
lock.unlock();
}
}
}
2) ReentrantReadWriteLock
한 쌍의 Lock(읽기를 위한 Lock과 쓰기를 위한 Lock)을 제공한다.
읽기 잠금은 여러 쓰레드에서 동시에 보유가능하며,
쓰기 잠금은 단일 쓰레드에만 적용된다.
많은 수의 읽기 작업과 상대적으로 적은 수의 쓰기 작업을 수행할 때 사용한다.
읽기 Lock이 걸린 상태에서 쓰기 Lock을 걸 수는 없다.(데이터 변경 금지)
읽기 Lock : 쓰레드는 Lock 사용이 가능한 경우, Lock을 획득하는 lockRead()를 호출하고
그렇지 않을 경우에는 Lock이 해제 될 때까지 기다린다. 해제할 경우 unlockRead()를 호출한다.
쓰기 Lock : 쓰레드는 Lock 사용이 가능한 경우, Lock을 획득하는 lockWrite()를 호출하고
그렇지 않을 경우에는 Lock이 해제 될 때까지 기다린다. 해체할 경우 unlockWrite()를 호출한다.
3) StampedLock
ReentrantReadWriteLock에 낙관적인 Lock의 기능을 추가한 Lock이다.
낙관적인 Lock : 데이터를 변경하기 전에 Lock을 걸지 않는 것.
데이터 변경 시 충돌이 일어날 가능성이 적은 상황에서 사용한다.
읽기, 쓰기 작업이 모두 빠르게 처리된다.
읽기 Lock은 쓰기 Lock에 의해 바로 해제가 가능하다
무조건 읽기 Lock을 걸지 않고 쓰기와 읽기가 충돌할 때만 쓰기 후 읽기 Lock을 건다.
condition
waiting pool 내 쓰레드를 분리하여 특정조건이 만족될 때 깨운다.
ReentrantLock 클래스와 함께 사용된다.
(wait() & notify() 의 문제점인, waiting pool 내 쓰레드를 구분하지 못하는 것을 해결한 것)
public class Main {
public static final int MAX_TASK = 5;
private ReentrantLock lock = new ReentrantLock();
// lock으로 condition 생성
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private ArrayList<String> tasks = new ArrayList<>();
// 작업 메서드
public void addMethod(String task) {
lock.lock(); // 임계영역 시작
try {
while(tasks.size() >= MAX_TASK) {
String name = Thread.currentThread().getName();
System.out.println(name+" is waiting.");
try {
condition1.await(); // wait(); condition1 쓰레드를 기다리게 합니다.
Thread.sleep(500);
} catch(InterruptedException e) {}
}
tasks.add(task);
condition2.signal(); // notify(); 기다리고 있는 condition2를 깨워줍니다.
System.out.println("Tasks:" + tasks.toString());
} finally {
lock.unlock(); // 임계영역 끝
}
}
}
'JAVA' 카테고리의 다른 글
[JAVA] 모던 자바 : 람다식과 스트림 (0) | 2024.09.26 |
---|---|
[JAVA] 제네릭 (0) | 2024.09.24 |
[JAVA] 예외 처리 (0) | 2024.09.24 |
[JAVA] 추상 클래스와 인터페이스 (0) | 2024.09.23 |
[JAVA] 상속 (0) | 2024.09.23 |