-
728x90
1. 프로세스와 쓰레드
프로세스 - 실행 중인 프로그램. 프로그램을 실행하면 OS로부터 자원(메모리)를 할당받아 프로세스가 된다.
쓰레드 - 자원을 이용해서 실제로 작업을 수행하는 것프로세스는 데이터, 메모리 등의 자원, 쓰레드로 구성되어 있다.
모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재하며, 둘 이상 쓰레드를 가진 프로세스를 '멀티쓰레드 프로세스' 라고 한다.
프로세스의 메모리 한계에 따라서 생성할 수 있는 쓰레드의 수가 결정된다.
멀티 태스킹, 멀티 쓰레딩
멀티쓰레딩 - 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업하는 것
실제로 동시에 처리되는 작업의 개수는 코어의 개수(cpu 개수) 와 같지만, '쓰레드 수 > 코어의 수' 일때, 코어가 번갈아서 쓰레드를 처리하기에 동시에 처리하는 것처럼 보인다.
멀티쓰레딩 장점
1. cpu 사용률 향상
2. 효율적인 자원 사용
3. 사용자에 대한 응답성 향상
4. 작업이 분리되어 코드가 간결해진다우리가 메신저로 채팅하면서 파일을 다운로드 받거나 음성대화를 나눌 수 있는것은 멀티쓰레딩 덕분이다.
멀티쓰레딩 단점
1. 자원을 공유하면서 발생 가능한 동기화
2. 교착상태 - 2개의 쓰레드가 자원을 점유한 상태로 서로 상대편이 점유한 자원을 사용하려고 기다리느라 진행이 멈춰있는 상태2. 쓰레드의 구현과 실행
1. Thread 클래스 상속받기
- 다른 클래스를 상속받을 수 없다는 문제가 있다.
class MyThread extends Thread { public void run() { ... } }
자손 클래스에서 조상인 Thread클래스의 메서드를 직접 호출 가능하다
class MyThread extends Thread { public void run() { for(int i = 0; i < 5; i++) { // 조상인 Thread의 getName()을 호출 System.out.println(getName()); } } }
2. Runnable 인터페이스를 구현하는 방법
- 일반적으로 많이 사용한다.
class MyThread implements Runnable { public void run() { ... } }
Thread클래스의 static메서드인 currentThread()를 호출하여 쓰레드에 대한 참조를 얻어 와야만 호출이 가능하다
class MyThread implements Runnable { public void run() { for(int i = 0; i < 5; i++) { // Thread.currentThread() - 현재 실행중인 Thread를 반환한다 System.out.println(Thread.currentThread().getName()); } } }
쓰레드를 구현한다는 것은 추상메서드 run()의 몸통{ } 을 구현한다는 것이다.
쓰레드의 이름을 지정하지 않으면 'Thread-번호'의 형식으로 이름이 정해진다.
start()
start( ) - 호출해야만 쓰레드가 실행된다
종료된 쓰레드는 다시 실행할 수 없다 -> 하나의 쓰레드에 대해서 start( )를 한번만 호출할 수 있다.
두번 이상 호출하면 에러 발생 ( IllegalThreadStateException )
MyThread t1 = new MyThread(); t1.start();
3. start(), run()
main메서드에서 run( )을 호출하는 것은 생성된 쓰레드를 실행 시키는 것이 아니라 단순히 클래스에 선언된 메서드를 호출하는 것이다.
start( )는 호출스택을 생성한 다음에 run( )을 호출해서, 생성된 호출스택에 run( )이 첫 번째로 올라가게 된다.
새로운 쓰레드를 생성하고 실행할때 마다 새로운 호출스택이 생성되고 쓰레드가 종료되면 작업에 사용된 호출스택은 소멸됨
주의, 호출스택의 최상위에 있는 메서드여도 대기상태에 있을 수 있다.
main 쓰레드
main메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main 쓰레드라 부르고, 실행 중인 쓰레드가 하나도 없을때 프로그램 종료
4. 싱글 , 멀티 쓰레드
싱글 vs 멀티쓰레드 ( 싱글 코어 ) 단순히 cpu만 사용하는 계산 작업이면 싱글쓰레드가 도 효율적이다.
이유는, 멀티 쓰레드 사용시 나타나는 "작업전환 시간" 때문이다.
멀티쓰레드 사용시 나타나는 기타 시간
- 작업전환 시간 ( 하나의 쓰레드에서 다른 쓰레드로 넘어가는 시간 )
- 대기시간 ( 한 쓰레드가 화면에 출력하는 동안 나머지 한개는 기다려야 함 )자바가 OS에 있어서 독립적이라고 하지만, 실제로는 종속적인 부분이 몇가지 있는데 쓰레드가 그 중 하나이다.
두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우에는 싱글쓰레드 프로세스 보다 멀티쓰레드 프로세스가 효율적이다.
5. 쓰레드의 우선순위
우선순위 값에 따라서 쓰레드가 얻는 실행시간이 달라진다 ( 시각적, 사용자에게 빠른 반응하는 경우가 우선순위가 높다. )
쓰레드 우선순위 지정
void setPriority(int newPriority) // 쓰레드의 우선순위를 지정한 값을 변경한다 int getPriority() // 쓰레드의 우선순위 반환 // 우선순위는 1 ~ 10, 클수록 높다 public static final int MAX_PRIORITY = 10 // 최대 우선순위 public static final int MIN_PRIORITY = 1 // 최소 우선순위 public static final int NORM_PRIORITY = 5 // 보통 우선순위 ex) main 쓰레드
쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받는다.
6. 쓰레드 그룹
서로 관련된 쓰레드를 그룹으로 다루기 위함이다. ( 쓰레드 그룹에 다른 쓰레드 그룹을 포함 시킬 수 있다. )
쓰레드 그룹은 보안상의 이유로 도입되었다.
따라서 다른 쓰레드 그룹의 쓰레드를 변경할 수 없다.
쓰레드를 쓰레드 그룹에 포함시키려면 Thread의 생성자를 이용해야 한다.
Thread(ThreadGroup group, String name) Thread(ThreadGroup group, Runnable target) Thread(ThreadGroup group, Runnable target, String name) Thread(ThreadGroup group, Runnable target, String name, long stackSize)
모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 하기에, 그룹을 지정하는 생성자를 사용하지 않은 쓰레드는 기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 소속하게 된다.
ThreadGroup getThreadGroup( ) // 쓰레드 자신이 속한 쓰레드 그룹을 반환한다 void uncaughtException(Thread t, Throwable e ) // 쓰레드 그룹의 쓰레드가 처리되지 않은 예외에 의해 실행이 종료되었을 때, JVM에 의해 이 메서드가 자동적으로 호출됨
7. 데몬 쓰레드
데몬 쓰레드 - 다른 일반 쓰레드의 작업을 돕는 보조적인 역할을 하는 쓰레드, 일반 쓰레드가 모두 종료되면 강제적으로 자동 종료됨
ex) 가비지 컬렉터, 워드프로세서의 자동저장, 화면자동갱신
데몬 쓰레드들은 'system 쓰레드 그룹' 또는 'main 쓰레드 그룹' 에 속한다
데몬 쓰레드의 작성방법, 실행방법은 일반 쓰레드와 같지만, 일반 쓰레드 실행전 setDaemon(true)를 호출하기만 하면 된다
데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다
Thread t = new Thread(new class이름()); t.setDaemon(true); // 이 부분이 없으면 종료되지 않는다. t.start(); // start 전에 꼭 실행해야 하고 start 후에 실행시, IllegalThreadStateException 발생
boolean isDaemon() // 쓰레드가 데몬 쓰레드인지 확인. 데몬 쓰레드면 true 반환 void setDaemon(boolean on) // 쓰레드가 데몬쓰레드로 또는 사용자 쓰레드로 변경한다. // 매개변수 on의 값을 true로 지정하면 데몬 쓰레드가 된다
getAllStackTraces( )
실행 중 또는 대기상태, 즉 작업이 완료되지 않은 모든 쓰레드의 호출스택을 출력할 수 있다.
8. 쓰레드의 실행 제어
쓰레드의 상태
상태 설명 NEW 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태 RUNNABLE 실행 중 또는 실행 가능한 상태 BLOCKED 동기화 블럭에 의해서 일시정지된 상태 ( lock이 풀릴때까지 기다리는 상태) WAITING
TIMED_WAITING쓰레드의 작업이 종료되지 않았지만, 실행가능하지 않은 일시정리 상태.
TIMED_WAITING은 일시정지시간이 지정된 경우TERMINATED 쓰레드의 작업이 종료된 상태 쓰레드의 생성부터 소멸
1. 쓰레드가 생성하고 start( )호출하면 실행대기열(큐와 같이 선입선출) 에 저장되어 자신의 차리를 기다림
2. 자신의 상태되면 실행
3. 실행시간이 다 되거나 yield( )를 만나면 다시 실행대기 상태
4. suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다
- I/O block은 입출력작업에서 발생하는 지연상태
5. 일시 정지시간이 다되거나(time-out), notify(), resume(), interrupt()가 호출되면 다시 실행대기열에 저장되어 자신의 차례를 기다림
6. stop() 호출시 쓰레드 소멸sleep(long millis) - 일정시간동안 쓰레드를 멈추게 한다
static void sleep(long millis) static void sleep(long millis, int nanos)
지정된 시간이 다 되거나 interrupt( )가 호출되면 (InterruptedException 발생) 실행대기 상태가 된다.
항상 try-catch 문으로 예외 처리해주기 ( 항상 해주기 힘들면 새로운 함수 만들어 놓기 )
void delay(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { } }
sleep은 staitc으로 선언되어 있으며 참조변수를 통해 호출보다는 Thread.sleep() 이런식으로 해야함
-> sleep()은 항상 현재 실행중인 쓰레드에 대해 작동하므로 실제로 영향 받는것은 main쓰레드 이다.
// t1.sleep(); Thread.sleep(); 해주기
interrupt(), interrupted() - 쓰레드의 작업을 취소한다.
진행 중인 쓰레드의 작업이 끝나기 전에 취소해야 할때 사용
interrupt는 강제로 종료 못시키고, interrupted상태 ( 인스턴스 변수)를 바꾸는 것 뿐이다
Thread t = new Thread(); t.start(); ... t.interrupt(); // t에 interrupt 호출
interrupted( )는 interrupt( )가 호출되었는지 알려준다. ( 호출 O -> false, 호출 X -> true )
void interrupt( ) // 쓰레드의 interrupted 상태를 f -> t boolean isInterrupted() // 쓰레드의 interrupted상태 반환 static boolean interrupted() // 현재 쓰레드의 interrupted상태 반환 후, false로 변경
일시정지 상태 (WAITING) -> interrupt()발생 -> InterruptedException -> 실행대기 상태(RUNNABLE)
yield( ) - 다른 쓰레드에게 양보한다
쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보한다.
yield( ), interrupt( )를 적절히 사용하면 프로그램의 응답성을 높이고 보다 효율적이게 된다.
while(!stopped) { if(!suspended) { ... try { Thread.sleep(1000); }catch (InterrruptedExceptin e) {} } } -- 변경 -- ( yield 추가 ) while(!stopped) { if(!suspended) { ... try { Thread.sleep(1000); }catch (InterrruptedExceptin e) {} } else { Thread.yield(); } }
'바쁜 대기상태' - 잠시 실행을 멈추게 한 상태면, 쓰레드는 주어진 실행시간을 그저 while문을 의미 없이 돌면서 안비하게 된다. ( 이때 yield가 효율적이다.
join( ) - 다른 쓰레드의 작업을 기다린다
쓰레드 자신이 하던 작업을 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 한다.
시간을 지정하지 않으면, 해당 쓰레드가 작업을 마칠 때까지 기다린다// join은 static 메서드가 아니다 void join() void join(long millis) void join(long millis, int nanos)
try-catch문으로 감싸줘야한다 ( interrupt()에 의해 대기상태에서 벗어날 수 있다. )
try { t.join(); } catch (InterruptedException e) {}
8. 쓰레드의 동기화
임계구역 - 다른 쓰레드에 의해 방해받지 않도록 하는 것
쓰레드의 동기화 - 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것동기화 방법
1. synchronized 키워드 ( 임계 구역 설정 )
synchronized는 임계영역을 설정하는데, synchronized메서드가 호출된 시정부터 해당 메서드가 포함된 객체의 lock을 얻어 작업을 수행하다가 메서드가 종료되면, lock을 반환한다
// 1. 메서드 전체 public synchronized void clacSum() {} public synchronized void withdraw(int money) { if(balance >= money) { try { Thread.sleep(1000); } catch(Exception e) { } balance -= money; } } // 2. 특정한 영역 ( 메서드 내 ) -> synchronized 블럭 public void withdraw(int money) { synchronized(this) { if(balance >= money) { try { Thread.sleep(1000); } catch(Exception e) { } balance -= money; } } } // synchronized블럭으로 임계영역을 최소화해서 보다 효율적인 프로그램되도록 하기 // 여기서 balance는 클래스에서 private라는 접근제어자를 갖는 변수가 되어야 동기화가 된다
synchronized블럭 영역 안으로 들어가면서부터 쓰레드는 지정된 객체의 lock을 얻고, 블럭을 벗어나면 lock을 반납한다.
wait(), notify()
특정 쓰레드가 객체의 락을 가진 상태로 오랜시간을 보내지 않도록 하는 것도 중요하다.
동기화된 임계영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, wait( )를 호출하여 쓰레드가 락을 반납
나중에 작업을 진행할 수 있는 상황이 되면 notify()를 통해 락을 얻어 작업을 진핼할 수 있게 한다
wait( )은 notify() or notifyAll( )이 호출되는 것을 기다리지만, 매개변수가 있는 wait()는 지정된 시간동안만 기다린다.
시간이 지난 후는 자동적으로 notify( )가 호출된다.
wait(), notify(), notifyAll( ) ( notifyAll은 호출된 객체의 waiting pool에 대기 중인 쓰레드만 해당 )
- Object에 정의 되어 있음
- 동기화 블록(synchronized 블럭)내에서만 사용가능
- 보다 효율적인 동기화를 가능하게 한다기아 현상 - 쓰레드가 오랫동안 기다리는 현상
경쟁 상태 - 여러 쓰레드가 lock을 얻기위해 서로 경쟁하는 것이 현상을 막기 위해서는 notify() 말고 notifyAll() 을 사용해야 한다. 하지만 lock을 얻기위한 경쟁 상태가 된다
Lock과 Condition을 이용한 동기화
동기화 하는 방법에는 java.util.concurrent.locks 패키지가 제공하는 lock 클래스를 이용하는 방법도 있다.
lock 클래스 종류
ReentrantLock - 재진입이 가능한 lock. 가장 일반적인 베타 lock
ReentrantReadWriteLock - 읽기에는 공유적이고, 쓰기에는 베타적인 lock
StampedLock - 낙관적인 lock 기능 + ReentrantReadWriteLock수동으로 lock을 잠그고 풀어야 한다
// lock 클래스들은 lock을 수동으로 잠그고 해제해야한다 void lock() // lock 잠금 void unlock() // lock 해지 boolean isLocked() // lock이 잠겼는지 확인
synchronized 와 비교
synchronized(lock) { //임계영역 } lock.lock(); try { // 임계 영역 } finally { lock.unlock(); } // unlock을 잊지 않기 위해 try-finally 문으로 쓰기
tryLock ()
boolean tryLock( ) boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
지정된 시간동안 lock을 얻지 못하면 다시 작업을 시도할 것인지 포기할 것인지 사용자가 결정
1. ReentrantLock
- 특정 조건에서 lock을 풀고 나중에 다시 lock을 얻고 임계영역으로 들어와 작업 수행가능
- 우리가 아는 lock
ReentrantLock() ReentrantLock(boolean fair)
Condition
- 쓰래드 구분을 위해 사용
- 이미 생성된 lock으로 부터 newCondition()을 호출해서 생성한다
private ReentrantLock lock = new ReentrantLock(); // lock 생성 // lock으로 condition 생성 private Condition forCook = lock.newCondition();
wati( ) & notify( ) 대신에 awiat( ) & signal( ) 사용
Object Condition void wait() void await()
void awaitUninterruptibly()void wait(long timeout) boolean await(long time, TimeUnit unit)
long awaitNanos(long nanosTimeout)
boolean awaitUntil(Date deadline)void notify() void signal() void notifyAll() void signalAll() 2. ReentrantReadWriteLock
- 읽기 lock이 걸려 있으면 다른 쓰레드가 읽기 lock을 중복해서 걸고 읽을 수 있다. ( 읽기 & 쓰기는 불가능 )
3. StampedLock
- lock을 걸거나 해지할 때 스탬프(long타입의 정수값)을 사용
- 낙관적 읽기 lock - 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 거는 것이다.
volatile
멀티 코어 프로세서는 코어마다 캐시를 갖는데, 캐시에 저장된 값이 갱신되지 않아서 메모리에 저장된 값과 다른 경우에 발생
변수앞에 volatile을 붙이면 캐시와 메모리간의 값 불일치 해결
변수에 volatile을 붙이는 대신에 synchronized블럭을 사용해도 같은 효과가 있다 ( 원자화 O, 하지만 동기화는 synchronized만 )
JVM은 4byte까지가 최소의 작업 단위 인데, 8byte인 long, double 타입 변수는 하나의 명령어로 값을 읽거나 쓸 수 없어서, 변수의 값을 읽는 과정에 다른 쓰레드가 끼어들 여지가 있다. ( 그래서 변수 앞에 써줘야 한다. )
변수의 원자화
volatile long val; ( long 타입의 변수를 원자화 )
fork & join 프레임 웍
이 프레임웍은 하나의 작업을 작은 단위로 나눠서 여러 쓰레드가 동시에 처리하는 것을 쉽게 만들어준다
RecursiveAction 반환값이 없는 작업을 구현할 때 사용
RecursiveTask 반환값이 있는 작업을 구현할 때 사용두 클래스 모두 compute() 라는 추상메서드를 상속을 통해 구현하면 된다
fork( ) 해당 작업을 쓰레드 풀의 작업 큐에 넣는다. 비동기 메서드 ( 결과를 기다리지 않고 return 문으로 넘긴다 )
join( ) 해당 작업의 수행이 끝날 때까지 기다렸다가 수행이 끝나면 그 결과를 반환한다. 동기메서드return문에서 compute()가 재귀호출될때, join( )은 호출되지 않는다.
'자바' 카테고리의 다른 글
[자바] 스트림 (0) 2022.07.25 [자바] 람다 (0) 2022.07.25 [자바] 애너테이션 (0) 2022.07.23 [자바] 열거형 ( enum ) (0) 2022.07.23 [자바] 제네릭스 (0) 2022.07.22