Java 개념 정리IV
Java 개념 정리
쓰레드
1. 쓰레드, 프로세스, 멀티 쓰레드
1-1. 프로세스와 쓰레드(process & thread)
- 프로그램 (실행) -> 프로세스
- 프로그램: 실행 가능한 파일(HDD, SSD)
- 프로세스: 실행 중인 프로그램(메모리), 자원(resources)과 쓰레드로 구성
- 쓰레드: 프로세스 내에서 실제 작업을 수행. 모든 프로세스는 최소한 하나의 쓰레드를 가지고 있다.
- 싱글 쓰레드 프로세스 = 자원 + 쓰레드
- 멀티 쓰레드 프로세스 = 자원 + 쓰레드 + … + 쓰레드
1-2. 멀티프로세스 vs. 멀티쓰레드
- 멀티 태스킹(멀티 프로세싱): 동시에 여러 프로세스를 실행시키는 것
- 멀티 쓰레딩: 하나의 프로세스 내에 동시에 여러 쓰레드를 실행시키는 것
- 프로세스를 생성하는 것보다 쓰레드를 생성하는 비용이 적다.
- 같은 프로세스 내의 쓰레드들은 서로 자원을 공유한다.
1-3. 멀티쓰레드의 장점
- 대부분의 프로그램이 멀티쓰레드로 작성되어 있다. 그러나, 단점도 존재
- 장점:
- 시스템 자원을 보다 효율적으로 사용할 수 있다.
- 사용자에 대한 응답성이 향상된다.
- 작업이 분리되어 코드가 간결해진다.
- 단점:
- 동기화(synchronization)에 주의해야 한다.
- 교착상태(dead-lock)가 발생하지 않도록 주의해야 한다.
- 각 쓰레드가 효율적으로 고르게 실행될 수 있게 해야 한다.
1-4. 쓰레드의 구현과 실행
- Tread클래스를 상속
class MyThread extends Thread{ public void run(){ //Thread클래스의 run()을 오버라이딩 } } MyThread t1 = new MyThread(); // 쓰레드의 생성 t1.start(); // 쓰레드의 실행
- Runnable인터페이스를 구현
class MyThread2 implements Runnable{ public void run() { //Runnable인터페이스의 추상메서드 run()을 구현 } } Runnable r = new MyThread2(); Thread t2 = new Thread(r); //Thread(Runnable r) //Thread t2 = new Thread(new MyThread2()); t2.start();
1-5. start()와 run()
-
class ThreadTest{ public static void main(String args[]){ MyThread t1 = new MyThread(); t1.start(); } } class MyThread extends Thread{ public void run(){ //... } }
2. 싱글쓰레드와 멀티쓰레드
2-1. 싱글쓰레드 vs. 멀티스레드
- 싱글 쓰레드
class ThreadTest{ public static void main(String args[]){ for(int i=0; i<300; i++){ System.out.println("-"); } for(int i=0; i<300; i++){ System.out.println("|"); } } //main }
- 멀티 쓰레드
class ThreadTest{ public static void main(String args[]){ MyThread1 th1 = new MyThread1(); MyThread2 th2 = new MyThread2(); th1.start(); th2.start(); } } class MyThread1 extends Thread{ public void run(){ for(int i=0;i<300;i++){ System.out.println("-"); } }//run } class MyThread2 extends Thread{ public void run(){ for(int i=0;i<300;i++){ System.out.println("|"); } } //run }
2-2. 쓰레드의 우선순위(priority of thread)
- 작업의 중요도에 따라 쓰레드의 우선순위를 다르게 하여 특정 쓰레드가 더 많은 작업시간을 갖게 할 수 있다.
void setProiority(int newProiority) - 쓰레드의 우선순위를 지정한 값으로 변경한다. int getPriority() - 쓰레드의 우선순위를 반환한다 public static final int MAX_PRIORITY = 10 //최대우선순위 public static final int MIN_PRIORTY = 1 //최소우선순위 public static final int NORM_PRIORITY = 5 //보통우선순위
2-3. 쓰레드 그룹(ThreadGroup)
- 서로 관련된 쓰레드를 그룹으로 묶어서 다루기 위한 것 (보안상의 이유)
- 모든 쓰레드는 반드시 하나의 쓰레드 그룹에 포함되어 있어야 한다.
- 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 ‘main쓰레드 그룹’에 속한다.
- 자신을 생성한 쓰레드(부모 쓰레드)의 그룹과 우선순위를 상속받는다.
2-4. 데몬 쓰레드(daemon thread)
- 일반 쓰레드(non-daemon thread)의 작업을 돕는 보조적인 역할을 수행.
- 일반 쓰레드가 모두 종료되면 자동적으로 종료된다.
- 가비지 컬렉터, 자동저장, 화면자동갱신 등에 사용된다.
- 무한루프와 조건문을 이용해서 실행 후 대기하다가 특정조건이 만족되면 작업을 수행하고 다시 대기하도록 한다.
boolean isDaemon() - 쓰레드가 데몬쓰레드인제 확인한다. 데몬쓰레드이면 true를 반환 void setDaemon(boolean on) - 쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경. 매개변수 on을 true로 지정하면 데몬 쓰레드가 된다.
- setDaemon(boolean on)은 반드시 start()를 호출하기 전에 실행되어야 한다. 그렇지 않으면 IllegalThreadStateException이 발생한다.
3. 쓰레드의 상태와 실행제어
3-1. 쓰레드의 실행제어
- 쓰레드의 실행을 제어(스케줄링)할 수 있는 메서드가 제공된다. 이 들을 활용해서 보다 효율적인 프로그램을 작성할 수 있다.
static void sleep(long millis) - 지정된 시간동안 쓰레드를 일시정지시킨다. 지정한 시간이 지나고 나면, 자동적으로 다시 실행대기상태가 된다. void join() - 지정된 쓰레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다. void interrupt() - sleep()이나 join에 의해 일시정지상태인 쓰레드를 깨워서 실행대기상태로 만든다. 해당 쓰레드에서는 interruptedException이 발생함으로써 일시정지상태를 벗어나게 된다. void stop() - 쓰레드를 즉시 종료시킨다. void suspend() -쓰레드를 일시 정지시킨다. resume()을 호출하면 다시 실행대기상태가 된다. void resume() - suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기상태로 만든다. static void yield() - 실행중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 자신은 실행대기상태가 된다.
- resume(), stop(), suspend()는 쓰레드를 교착상태로 만들기 쉽기 때문에 deprecated되었다.
3-2. 쓰레드의 상태(state of thread)
- NEW : 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
- RUNNABLE : 실행 중 또는 실행 가능한 상태
- BLOCKED : 동기화블럭에 의해서 일시정지된 상태(lock이 풀릴 때 까지 기다리는 상태)
- WAITING, TIMED_WATING : 쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은(unrunnable) 일시정지 상태. TIMED_WAITING은 일시정지시간이 지정된 경우를 의미한다.
- TERMINATED : 쓰레드의 작업이 종료된 상태
3-3. 쓰레드의 실행제어 메서드
sleep()
- 현재 쓰레드를 지정된 시간동안 멈추게 한다.
static void sleep(long millis) //천분의 일초 단위 static void sleep(long millis, int nanos) //천분의 일초 + 나노초
- 예외처리를 해야 한다. (InterruptedException이 발생하면 깨어남)
try{ Thread.sleep(1, 500000); //쓰레드를 0.0015초 동안 멈추게 한다. } catch(InterruptedException e){}
- 특정 쓰레드를 지정해서 멈추게 하는 것은 불가능하다.
try{ //th1.sleelp(2000); 불가 Thread.sleep(2000); }catch(InterruptedException e){}
interrupt()
- 대기상태(WAITING)인 쓰레드를 실행대기 상태(RUNNABLE)로 만든다.
- void interrupt() - 쓰레드의 interrupted상태를 false에서 true로 변경
- boolean isInterrupted() - 쓰레드의 interrupted 상태를 반환
- static boolean interrupted() - 현재 쓰레드의 interrupted 상태를 알려주고, false로 초기화
class Thred{ boolean interrupted = false; boolean isInterrupted(){ return interrupted; } boolean interrupt(){ interrupted = true; } }
suspend(), resume(), stop()
- 쓰레드의 실행을 일시정지, 재개, 완전정지 시킨다. 교착상태에 빠지기 쉽다.
- void suspend() - 쓰레드를 일시정지 시킨다.
- void resume() - suspend()에 의해 일시정지된 쓰레드를 실행대기상태로 만든다.
- void stop() - 쓰레드를 즉시 종료시킨다.
- suspend(), resume(), stop()은 deprecated되었으므로, 직접 구현해야 한다.
class Thread implements Runnable{ boolean suspended = false; boolean stopped = false; public void run(){ while(!stopped){ if(!suspended){ /* 쓰레드가 수행할 코드를 작성 */ } } } public void suspended{ suspended = true; } public void reusme{ suspended = false; } public void stop(){ stopped = true; } }
yield()
- 남은 시간을 다음 쓰레드에게 양보하고, 자신(현재 쓰레드)은 실행대기한다.
- yield()와 interrupt()를 적절히 사용하면, 응답성과 효율성을 높일 수 있다.
class MyThread implements Runnable{ boolean suspended = false; boolean stopped = false; Thread th; MyThread(String name){ th = new Thread(this, name); } public void run(){ while(!stopped){ if(!suspended){ /* 작업수행 */ try{ Thread.sleep(1000); }catch(InterruptedException e){} }else{ Thread.yield(); } } } }
join()
- 지정된 시간동안 특정 쓰레드가 작업하는 것을 기다린다.
- void join() - 작업이 모두 끝날 때까지
- void join(long millis) - 천분의 일초 동안
- void join(long millis, int nanos) - 천분의 일초 + 나노초 동안
- 예외처리를 해야 한다. (InterruptedException이 발생하면 작업 재개)
public static void mina(String args[]){ Thread1 th1 = new Thread1(); Thread2 th2 = new Thread2(); th1.start(); th2.start(); startTime = System.currentTimeMillis(); try{ th1.join(); //main쓰레드가 th1의 작업이 끝날 때까지 기다린다. th2.join(); //main쓰레드가 th2의 작업이 끝날 때까지 기다린다. }catch(InterruptedException e){} }
4. 쓰레드의 동기화
4-1 쓰레드의 동기화 - synchronized
- 한 번에 하나의 쓰레드만 객체에 접근할 수 있도록 객체에 락(lock)을 걸어서 데이터의 일관성을 유지하는 것
//특정한 객체에 lock을 걸고자 할 때 synchronized(객체의 참조변수){ } public void withdraw(int money){ synchronized(this){ if(balance >= money){ try{ Thread.sleep(1000); }catch(Exception e){} balance -= money; } } } //메서드에 lock을 걸고자 할 때 public synchronized void calcSum(){ } public synchronized void withdraw(int money){ if(balance >= money){ try{ Thread.sleep(1000); }catch(Exception e){} balance -= money; } }
4-2 wait(), notify(), notifyAll()를 이용한 동기화
- 동기화의 효율을 높이기 위해 wait(), notify()를 사용
- Object클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.
- wait() - 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다.
- notify() - waiting pool에서 대기중인 쓰레드 중의 하나를 깨운다.
- notifyAll() - waiting pool에서 대기중인 모든 쓰레드를 깨운다.
class Account{ int balance = 1000; public synchronized void withdraw(int money){ while(balance < money){ try{ wait(); // 대기 - 락을 풀고 기다린다. 통지를 받으면 락을 재획득 }catch(InterruptedException e){} } balance -= money; } public synchronized void deposit(int money){ balance += money; notify(); // 통지 - 대기중인 쓰레드 중 하나에게 알림 } }
5. Lock & Condition
5-1. Lock과 Condition을 이용한 동기화
- java.util.concurrent.locks패키지를 이용한 동기화 (JDK 1.5)
- ReentrantLock - 재진입이 가능한 lock. 가장 일반적인 배타 lock
- ReentrantReadWriteLock - 읽기에는 공유적이고, 쓰기에는 배타적인 lock
- StampedLock - ReentrantReadWriteLock에 낙관적인 lock의 가능을 추가
- 낙관적인 잠금(Optimistic Lock): 일단 저지르고 나중에 확인
int getBalance(){ long stamp = lock.tryOptimisticRead(); //낙관적 읽기 lock을 건다. int curBalance = this.balance; //공유 데이터인 balance를 읽어온다. if(lock.validate(stamp)){ //쓰기 lock에 의해 낙관적 읽기 lock이 풀렸는지 확인 stamp = lock.readLock(); //lock이 풀렸으면, 읽기 lock을 얻으려고 기다린다. try{ curBalance = this.balance; //공유 데이터를 다시 읽어온다. }finally{ lock.unlockRead(stamp); //읽기 lock을 푼다. } } return curBalance; //낙관적 읽기 lock이 풀리지 않았으면 곧바로 읽어온 값을 반환 }
- ReentrantLock을 이용한 동기화
- ReentrantLock()
- ReentrantLock(boolean fair)
- synchronized 대신 lock()과 unlock()을 사용
- void lock() - lock을 잠근다.
- void unlock() - lock을 해지한다.
- boolean isLocked() - lock이 잠겼는지 확인한다.
5-2. volatile - cache와 메모리간의 불일치 해소
- 성능 향상을 위해 변수의 값을 core의 cache에 저장해 놓고 작업
- 여러 스레드가 공유하는 변수에는 volatile을 붙여야 항상 메모리에서 읽어옴
volatile boolean suspended = false; volatile boolean stopped = false; public synchronized void stop(){ stopped = true; }
6. join & fork 프레임웍
6-1. fork & join 프레임웍
- 작업을 여러 쓰레드가 나눠서 처리하는 것을 쉽게 해준다(JDK 1.7)
- RecursiveAction 또는 RecursiveTask를 상속받아서 구현
- RecursiveAction - 반환값이 없는 작업을 구현할 때 사용
- RecursiveTask - 반환값이 작업을 구현할 때 사용
6-2. compute()의 구현
- 수행할 작업과 작업을 어떻게 나눌 것인지를 정해줘야 한다.
- fork()로 나눈 작업을 큐에 넣고, compute()를 재귀호출 한다.
public Long compute(){ long size = to - from + 1; if(size<=5){ return sum(); } long half = (form+to)/2; SumTask leftSum = new SumTask(from, half); SumTask rightSum = new SumTask(half+1, to); leftSum.fork(); ///작업을 작업 큐에 넣는다. return rightSum.compute() + leftSum.join(); }
6-3. 작업 훔치기(work stealing)
- 작업을 나눠서 다른 쓰레드의 작업 큐에 넣는 것
6-4. fork()와 join()
- compute()는 작업을 나누고, fork()는 작업을 큐에 넣는다. (반복)
- join()으로 작업의 결과를 합친다. (반복)
- fork() - 해당 작업을 쓰레드 풀의 작업 큐에 넣는다. 비동기 메서드
- join() - 해당 작업의 수행이 끝날 때까지 기다렸다가, 수행이 끝나면 결과를 반환한다. 동기메서드