프로세스(process) : 실행 중인 프로그램(program)
프로그램 -> 실행 -> OS가 실행에 필요한 메모리 할당 -> 프로세스
프로세스 자원(프로그램 수행에 필요한 데이터, 메모리) + 쓰레드
쓰레드 : 프로세스의 자원을 이용해서 실제로 작업을 수행하는 주체
(모든 프로세스에 1개 이상의 쓰레드 존재)
multi-thread process : 둘 이상의 쓰레드를 가진 프로세스 (자원+쓰레드+쓰레드...)
멀티태스킹 : 여러 프로세스 실행
멀티쓰레드 : 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업 수행
☑️ 프로세스의 성능은 쓰레드의 개수에 비례하지는 않는다
쓰레드를 구현한다 = 쓰레드로 작업할 내용을 run(){ ... }
의 몸통에 채운다
class MyThread extends Thread {
public void run() { /* 작업내용 */ } // Thread클래스의 run()을 오버라이딩
}
인스턴스 생성
ThreadEx1_1 t1 = new ThreadEx1_1(); // Thread의 자손 클래스의 인스턴스를 생성
메서드 호출 : 조상인 Thread 클래스의 메서드를 직접 호출
String getName()
: 쓰레드의 이름 반환
2. Runnable인터페이스를 구현 : 주로 사용
class MyThread implements Runnable (
public void run() { /* 작업내용 */ } // Runnable인터페이스의 run()을 구현
}
인스턴스 생성
Runnable r = new ThreadEx1_2(); // Runnable을 구현한 클래스의 인스턴스를 생성
Thread t2 = new Thread(r); // 생성자 Thread(Runnable target)
Thread t2 = new Thread (new ThreadExl_2 ()); // 위의 두 줄을 한 줄로 간단히
메서드 호출 : Thread 클래스의 static 메서드currentThread()
호출 -> 쓰레드에 대한 참조로 메서드 호출)
static Thread currentThread()
: 현재 실행중인 쓰레드의 참조를 반환
start()
: 쓰레드 실행
run()
: 단순히 클래스에 선언된 메서드 호출(생성된 쓰레드를 실행x)
start()
vs run()
start()
: 새로운 쓰레드가 작업을 실행하는데 필요한 호출 스택 생성 -> run()
이 첫번째로 올라가게 함
run()
실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다
메인 메서드가 수행을 마쳤더라도 다른 쓰레드가 작업을 마치지 않은 상태라면 프로그램이 종료되지 않는다
싱글코어일 때 싱글쓰레드 & 멀티쓰레드
싱글코어 & 멀티코어 멀티코어 & 멀티쓰레드 : 동시에 두 쓰레드 수행 가능
OS 종속적 : 프로세스/쓰레드 스케줄러가 프로세스/쓰레드 실행 순서/실행 시간을 결정
-> 프로세스/쓰레드에게 할당되는 실행시간이 일정하지 않음
쓰레드의 속성(멤버변수) 중 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라짐
void setPriority(int newPriority)
: 쓰레드의 우선순위를 지정한 값으로 변경.
int getPriority()
: 쓰레드의 우선순위를 반환
public static finalintMAX_PRIORITY = 10
: 최대우선순위
public static finalintMIN_PRIORITY = 1
: 최소우선순위
public static finalintN0RM_PRI0RITY = 5
: 보통우선순위
일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)의 작업을 돕는 보조적인 역할을 수행하는 쓰레드
ex) 가비지 컬렉터, 워드프로세서의 자동저장, 화면 자동 갱신 등
필요성 : 멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유
=>한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것 = 쓰레드의 동기화, 이때 필요한 개념 : 임계영역(critical section), 잠금(lock)
synchronized
를 이용한 동기화의 문제점 : 특정 쓰레드가 객체의 랙을 가진채로 기다릴 수 있음
(ex. 은행 - 계좌에 출금할 돈이 부족해서 한 쓰레드가 락을 보유한채로 돈이 입금될 때까지 기다리고 이로 인해 다른 쓰레드들도 원활히 진행되지 않는 상황)
-> wait()
, notify()
1.wait()
: 동기화된 임계영역의 코드를 수행하다가 작업을 더 이상 진행할 수 없을 때 호출하여 쓰레드가 락을 반납하고 기다림(다른 쓰레드가 락을 얻어 해당 객체에 대한 작업 수행)
2. notify()
: 작업을 중단했던 쓰레드가 다시 락을 얻어 작업하도록 함
notifyAll()
: 호출된 객체의 waiting pool 에 대기중인 모든 쓰레드를 깨움notifyAll()
로 해결하고자 하면 경쟁 상태(race condition) 발생ex) 식당 - 테이블의 음식이 줄어서 요리사 쓰레드에게 통지하기 위해 notify()
를 호출하더라도 손님 쓰레드가 통지를 받을 수 있다. 그 경우 lock을 얻어도 여전히 음식이 없어서 대기한다(waiting pool로 들어감)
-> notifyAll()
호출하면 손님 쓰레드가 대기하더라도 요리사 쓰레드도 결국 lock을 얻기에 기아현상을 방음. 그러나 불필요하게 손님 쓰레드까지 통지를 받아 요리사 쓰레드와 손님 쓰레드가 lock을 얻기 위해 경쟁하게 된다(=경쟁 상태, race condition)
synchronized 블럭의 '같은 메서드 내에서만 lock을 걸 수 있다'는 제약을 보완한 것
=lock 클래스
클래스명 | 설명 |
---|---|
ReentrantLock | 재진입이 가능한 lock. 가장 일반적인 배타 lock (==wait() & notify() ) |
ReentrantReadWriteLock | 읽기에는 공유적, 쓰기에는 배타적 |
StampedLock | ReentrantReadWriteLock + 낙관적인 lock |
ReentrantLock
특정 조건에서 lock을 풀고 나중에 lock 얻어 임계영역으로 들어와서 작업을 수행하는 방식
ReentrantReadWriteLock
StampedLock
lock을 걸거나 해지할 때 스탬프(long타입 정수값) 사용
낙관적 읽기 lock : 내용을 읽을 때 항상 lock을 거는 것이 아니라 읽기/쓰기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 거는 것
ex)
int getBalance () {
long stamp = lock.tryOptimisticRead (); // 낙관적 읽기 1ock을 건다.
int curBalance = this.balance; // 공유 데이터인 balance를 읽어온다.
if (!lock. validate (stamp)) { // 쓰기 lock에 의해 낙관적 읽기 1ock이 풀렸는지 확인
stamp = lock.readLock(); // lock이 풀렸으면, 읽기 lock을 얻기 위해 기다린다.
try {
curBalance = this. balance; // 공유 데이터를 다시 읽어온다.
} finally {
lock.unlockRead (stamp); // 읽기 lock을 푼다.
}
}
return curBalance; // 낙관적 읽기 lock이 풀리지 않았으면 곧바로 읽어온 값을 반환
}
그룹 A, B에 선별적으로 통지하기 위해 각 그룹의 쓰레드를 위한 Condition을 만들어서 각각의 waiting pool에서 따로 기다리도록 한다 -> wait()
& notify()
대신 await()
& signal()
사용
ex) 요리사 & 손님
private ReentrantLock lock = new ReentrantLock(); // lock을 생성
// lock으로 condition을 생성
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(); // wait(); COOK쓰레드를 기다리게 한다.
} catch (InterruptedException e){}
}
dishes.add(dish);
forCust.signal(); // notify(); 기다리고 있는 CUST를 깨우기 위함.
System.out.println("Dishes:" + dishes.toString ());
} finally {
lock.unlock ();
}
volatile
을 붙인다 / synchronized
블럭 사용synchronized
블럭 : 블럭을 들어가고 나올 때 캐시/메모리간에 동기화
JVM의 데이터 처리 단위 = 4byyte (int)
= 데이터를 읽고 쓸 때 하나의 명령어로 가능(다른 쓰레드가 끼어들 틈이 없음)
= long/double은 8 byte로 하나의 명령어로 불가능(다른 쓰레드가 끼어들 여지있음)
-> 변수 선언시에 volatile
을 붙임(변수의 원자화)
*volatile
: 변수의 읽기/쓰기를 원자화 한 것(동기화)
-> 동기화 필요시 synchronized 블럭 사용(synchronized
블럭 : 여러 문장을 원자화 한 것)
하나의 작업을 여러 단위로 쪼갬 -> 여러 쓰레드가 하나의 작업을 동시에 처리
`두 클래스 중에 하나를 상속받아 구현
RecursiveAction
: 반환값이 없는 작업을 구현할 때 사용 RecursiveTask
: 반환값이 있는 작업을 구현할 때 사용추상 메서드 compute()
구현 = 처리해야 할 작업
쓰레드풀(ForkJoinPool
) 생성 (프레임웍에서 제공)
기본적으로 코어의 개수와 동일한 개수의 쓰레드를 생성해 놓고 반복해서 재사용할 수 있게 함
수행할 작업 생성 -> invoke()
로 작업 시작
*작업 훔쳐오기(work stealing)
: 자신의 작업 큐가 비어있는 쓰레드가 다른 쓰레드의 작업 큐에서 작업을 가져와서 수행하는 것 -> 여러 쓰레드가 골고루 작업을 나누어 처리
compute()
, fork()
, join()
fork()
: 작업을 쓰레드의 작업 큐에 넣기
✚
compute()
: 작업 큐에 들어간 작업을 더 이상 나눌 수 없을 때까지 나누기
⬇️
각 쓰레드가 골고루 나눠서 처리
join()
: 작업의 결과를 얻음
☑️ fork()
와 join()
의 차이
fork() | join() | |
---|---|---|
역할 | 작업을 쓰레드 풀의 작업 큐에 넣는다 | 작업의 수행이 끝나기를 기다리고 끝나면 그 결과 반환 |
종류 | 비동기 메서드 | 동기메서드 |