개발/Java Study
10주차 : 멀티쓰레드 프로그래밍
박비버
2021. 2. 20. 21:21
1. Thread 클래스와 Runnable 인터페이스
- Thread 클래스 상속받아서 구현하면 다른 클래스를 상속받을 수 없어서 Runnable을 사용하는 것이 일반적
- Runnable 을 구현한 클래스의 인스턴스를 생성한 후 생성자 Thread(Runnable target)를 통해서 Runnable 인터페이스를 구현한 인스턴스를 참조하도록 되어있음
- start() 호출 해야마 쓰레드가 실행됨
class MyThread extends Thread {
public void run() {}
}
class MyThread implements Runnable {
public void run() {}
}
class ThreadEx1_1 extends Thread {
public void run() {
for(int i=0; i<5; i++){
System.out.println(getName()); // Thread에 getName 들어있음
}
}
}
class ThreadEx1_2 implements Runnable {
public void run() {
for(int i=0; i<5; i++){
System.out.println(Thread.currentThread().getName());
// Thread.currentThread() 현재 실행중인 Thread의 참조를 반환
}
}
}
class ThreadEx1{
public static void main(String args[]) {
ThreadEx1_1 t1 = new ThreadEx1_1();
Runnable r = new ThreadEx1_2();
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
}
2. 쓰레드의 상태
상태 | 설명 |
NEW | 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태 |
RUNNABLE | 실행 중 또는 실행 가능한 상태 |
BLOCKED | 동기화 블럭에 의해서 일시정지된 상태 (lock 이 풀릴 때까지 기다리는 상태) |
WAITING, TIMED_WAITING |
쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은 일시정지 상태 TIMED_WAITING 은 일시정지 시간이 지정된 경우 |
TERMINATED | 쓰레드의 작업이 종료된 상태 |
메서드 | 설명 |
static void sleep(long millis) static void sleep(long millis, int nanos) |
지정된 시간 동안 쓰레드를 일시 정지 |
void join() void join(long millis) void join(long millis, int nanos) |
지정된 시간동안 실행함 |
void interrupt() | sleep, join에 의해 일시 정지인 쓰레드를 실행 대기로 전환 |
void stop() | 쓰레드 즉시 종료 |
void suspend() | 쓰레드 일시정지 |
void resume() | suspend에 의해 일시정지 상태의 쓰레드를 실행대기로 전환 |
static void yield() | 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 실행 대기로 전환 |
3. 쓰레드의 우선순위
- 우선순위(priority)라는 속성(멤버변수)를 가지고 있음
- 숫자가 높을수록 우선순위가 높음
- main 메서드의 우선순위는 5. main메서드에서 생성된 쓰레드의 우선순위는 자동적으로 5
- 쓰레드의 우선순위와 관련된 구현이 JVM마다 다를 수 있음
void setPriority(int newPriority)
int getPriority()
4. Main 쓰레드
- 프로그램 시작시 가장 먼저 실행되는 쓰레드
- 멀티 스레드는 메인 쓰레드가 종료 되더라도 실행중인 다른 쓰레드가 있다면 프로세스가 종료 되지 않음
public static void main(String args[]) {
}
5. 동기화 (Synchronization)
- 멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업함
- 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는것
synchronized를 이용한 동기화
- synchronized 가 호출된 시점 부터 해당 메서드가 포함된 객체의 lock을 얻어서 작업을 수행하다가 메서드가 종료되면 lock을 반환
- lock을 걸고자 하는 객체를 참조변수로
- critical section은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 임계영역을 최소화 해서 효율적으로 돌아가도록 해야함
1. 메서드 전체를 임계 영역(critical section)으로 지정
public synchronized void calcSum() {
//...
}
2. 특정한 영역을 임계 영역으로 지정
synchronized (객체의 참조변수) {
//...
}
class ThreadEx22 {
public static void main(String args[]) {
Runnable r = new RunnableEx22();
new Thread(r).start();
new Thread(r).start();
}
}
class Account {
private int balance = 1000; // private으로 해야 동기화가 의미가 있다.
public 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 RunnableEx22 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());
}
} // run()
}
- 아래와 같이 설정도 가능함
public void withdraw(int money){
synchronized(this){
if(balance >= money) {
try { Thread.sleep(1000);} catch(InterruptedException e) {}
balance -= money;
}
}
} // withdraw
wait()과 notify()
- critial section의 코드를 더이상 진행할 수 없으면, wait() 호출하여 쓰레드가 락을 반납하고 기다리게 함
- 락이 반납되면 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 진행 할 수 있음
- notify() 를 호출해서 작업을 중단했던 쓰레드가 다시 lock을 얻어서 진행 (재진입, reentrance)
- 오래 기다린 쓰레드가 락을 얻는다는 보장이 없음
- 운이 나쁘면.. 오래기다려야할 수 있음, 기아현상(starvation) -> notifyAll()을 사용해서 해결 가능
- race condition : 여러 쓰래드가 lock을 얻기 위해 서로 경쟁하는 것
Lock과 Condition을 이용한 동기화
- ReentrantLock : 특정 조건에서 lock을 풀고 나중에 다시 lock을 얻고 critial section으로 들어와서 이후의 작업을 수행
- ReentrantReadWriteLock : 읽기 lock이 걸려 있으면 다른 쓰레드가 읽기 lock을 중복해서 걸고 읽기를 수행, 읽기 lock이 걸린 상태에서 쓰기lock은 허용되지 않음
- StampedLock : lock을 걸거나 해지할 때 stamp(long 타입 정수값) 사용, optimistic reading lock이 추가됨
- optimistic reading lock : 읽기 lock이 걸려 있으면, 쓰기 lock을 얻기 위해서는 읽기 lock이 풀릴 때까지 기다려야하는 데에 비해 optimistic reading lock은 쓰기 lock에 의해 바로 풀림
ReentrantLock | 재진입이 가능한 Lock 가장 일반적인 배타 lock |
ReentrantReadWriteLock | 읽기에는 공유적이고, 쓰기에는 배타적인 lock |
StampedLock | ReentrantReadWriteLock에 낙관적인 lock의 기능을 추가 |
volatile
- 멀티 코어 프로세서에는 코어마다 별도의 캐시를 가지고 있음
- 메모리에 저장된 변수의 값이 변경되었는데도 캐시에 저장된 값이 갱신되지 않아서 메모리 값과 다른 경우 발생 => 쓰레드가 계속 실행됨
- 변수 앞에 volatile을 붙이면, 코어가 변수의 값을 읽어올 때 캐시가 아닌 메모리에서 읽어옴, 불일치 해결됨
volatile long balance;
synchronized int getBalance() {
return balance;
}
synchronized void withdraw (int money) {
if(balance >= money) {
balance -= money;
}
}
6. 데드락
- 두 쓰레드가 자원을 점유한 상태에서 서로 상대편이 점유한 자원을 사용하려고 기다리느라 진행이 멈춰있는 상태
출처 :
자바의 정석, 남궁 성 지음