반응형
1. 프로세스와 쓰레드
- 프로세스( process )란 간단히 말해서 '실행 중인 프로그램( program )'이다.
- 프로그램을 실행하면 O/S로부터 실행에 필요한 자원( 메모리 )을 할당받아 프로세스가 된다.
- 프로세스는 프로그램을 수행하는데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다.
- 즉 모든 프로세스에는 최소 하나 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 가진 프로세스를 '멀티 쓰레드 프로세스( multi-thread-process )'라고 한다.
- 하나의 프로세스가 가질 수 있는 쓰레드의 개수가 제한되어 있지 않으나 쓰레드가 작업을 수행하는데 개별적인 메모리 공간( Call Stack )을 필요로 하기 때문에 프로세스의 메모리 한계에 따라 생성할 수 있는 쓰레드의 수가 결정된다.
- 멀티태스킹과 멀티쓰레드
- 현재 우리가 사용하고 있는 OS( Windows )는 멀티태스킹( multi-tasking, 다중작업 )을 지원하기 때문에 여러 개의 프로세스가 동시에 실행될 수 있다.
- 이와 마찬가지로 멀티쓰레딩은 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것이다.
- 사실 동시에 실행된다라는 개념이 아닌 매우 짧은 시간 간격으로 두 작업을 왔다 갔다 하는 것이다.
- 멀티쓰레딩의 장단점
- CPU의 사용률을 향상시킨다.
- 자원을 보다 효율적으로 사용할 수 있다.
- 사용자에 대한 응답성이 향상된다.
- 작업이 분리되어 코드가 간결해진다.
- 메신저로 채팅하면서 파일을 다운로드하거나 음성 대화를 나눌 수 있는 이유가 바로 멀티쓰레드로 작성되어 있기 때문이다. 만일 싱글쓰레드로 작성되어 있가면 파일을 다운로드 받는 동안에는 다른 일을 전혀 할 수 없을 것이다.
- 여러 사용자에게 서비스를 해주는 서버 프로그램의 경우 멀티쓰레드로 작성하는 것이 필수적이어서 하나의 서버 프로세스가 여러 개의 쓰레드를 생성해서 쓰레드와 사용자의 요청이 일대일로 처리되도록 프로그래밍을 해야 한다.
- 만일 싱글쓰레드로 서버 프로그램을 작성한다면 사용자의 요청마다 새로운 프로세스를 생성해야 하는데 프로세스를 생성하는 것은 쓰레드를 생성하는 것에 비해 더 많은 시간과 메모리 공간이 필요하기 때문에 많은 수의 사용자 요청을 서비스하기 어렵다.
- 멀티쓰레딩에는 장점만 있는 것이 아니다.
- 멀티쓰레드 프로세스는 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업하기 때문에 발생할 수 있는 동기화( synchronization )와 교착상태( deadlock )와 같은 문제들을 고려해서 신중히 프로그래밍해야 한다.
2. 쓰레드의 구현과 실행
- 쓰레드를 구현하는 방법은 Thread 클래스를 상속받는 방법과 Runnable 인터페이스를 구현하는 방법 2가지가 있다.
- 두 방법 어느 것을 선택해도 상관없지만, 클래스를 상속받는 경우에는 다른 클래스를 상속받지 못한다는 제한이 있으며, 인터페이스를 구현하는 방법은 재사용성이 높고 코드의 일관성을 유지할 수 있기 때문에 보다 더 객체지향적인 방법이라고 할 수 있다.
1234567891011// 1. Thread 클래스 상속class MyThread extends Thread {public void run() { /* 작업 내용 */} // Thread 클래스의 run() 메소드를 오버라이딩}// 2. Runnable 인터페이스 상속class MyThread implements Runnable {public void run() { /* 작업 내용 */} // Runnable 인터페이스의 run()을 구현}cs - 쓰레드를 구현한다는 것은 위의 두 방법 중 어떤 것을 선택하든지 그저 쓰레드를 통해 작업하고자 하는 내용으로 run()의 몸통 {}을 채워주는 것이다.
- 쓰레드의 실행 - start()
- 쓰레드를 생성했다고 자동으로 실행되는 것이 아니다.
- start()를 호출해야만 쓰레드가 실행된다.
1234//쓰레드 참조 변수 t1, t2가 있다고 가정할 때t1.start(); // 쓰레드 t1를 실행t2.start(); // 쓰레드 t2를 실행cs - 사실 start()가 호출되었다고 해서 바로 실행되는 것이 아니라, 일단 실행 대기상태에 있다가 자신의 차례가 되어야 실행된다.
- 물론 실행 대기 중인 쓰레드가 하나도 없다면 바로 실행상태가 된다.
- 쓰레드의 실행 순서는 OS의 스케쥴러가 작성한 스케줄에 의해 결정된다.
- 한번 실행이 종료된 쓰레드는 다시 실행할 수 없다.
==> 즉, 하나의 쓰레드에 대해 start()가 단 한 번만 호출될 수 있다. - 그래서 만일 쓰레드의 작업을 한번 더 수행해야 한다면 새로운 객체를 만들어서 start()를 호출해야 한다.
3. start()와 run()
- 우리는 run() 메소드에 작업 내용을 작성했는데 실제로 실행되는 메소드는 start()라는 점에 대해 의문이 생길 것이다.
- main() 메소드에서 run() 메소드를 호출한다는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메소드를 호출하는 것이다.
- 반면에 start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출 스택( Call Stack )을 생성한 다음에 run()을 호출해서 생성된 호출 스택에 run()이 첫 번째로 올라가게 된다.
- 모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출 스택을 필요로 하기 때문에, 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출 스택이 생성되고 쓰레드가 종료되는 작업에 사용된 호출 스택은 소멸한다.
- 호출 스택을 배울 때 가장 위에 있는 메소드가 현재 실행 중인 메소드이고 나머지 메소드는 실행중인 메소드가 끝날 때까지 대기상태라는 걸 배웠다.
- 그러나, 쓰레드가 둘 이상일 때는 호출 스택의 최상위에 있는 메소드일지라고 대기 상태에 있을 수 있다.
- 스케줄러는 실행 대기 중인 쓰레드들의 우선순위를 고려해서 실행 순서와 실행시간을 결정하고, 각 쓰레드들은 작성된 스케쥴러에 따라 자신의 순서가 되면 지정된 시간 동안 작업을 수행한다.
- 이때 주어진 시간 동안 작업을 마치지 못한 쓰레드는 다시 차례가 돌아올 때까지 대기상태로 있게 되며, 작업을 마친 쓰레드, 즉 run() 수행이 종료된 쓰레드는 호출스택이 비워지면서 이 쓰레드가 사용하던 호출스택이 사라진다.
- main 쓰레드
- main() 메소드의 작업을 수행하는 것도 쓰레드이며 이를 main 쓰레드라고 한다.
- 우리는 지금까지 이미 쓰레드를 사용하고 있던 것이다. 앞서 쓰레드가 일꾼이라 하였는데, 프로그램이 실행되기 위해서는 작업을 수행하는 일꾼이 최소한 하나는 필요하다.
- 그래서 프로그램을 실행하면 기본적으로 하나의 쓰레드를 생성하고 그 쓰레드가 main 메소드를 호출해서 작업이 수행하도록 하는 것이다.
- 지금까지 main 메소드가 수행을 마치면 프로그램이 종료되었으나, main 메소드가 수행을 마쳤다 하더라도 다른 쓰레드가 아직 작업을 마치지 않은 상태라면 프로그램이 종료되지 않는다.
==> 바꿔 말해, 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램이 종료된다. - 쓰레드는 '사용자 쓰레드( user thread )'와 '데몬쓰레드( daemon thread )' 두 종류가 있다.
4. 싱글쓰레드와 멀티쓰레드
- 두 개의 작업을 하나의 쓰레드( th1 )로 처리하는 경우와 두 개의 쓰레드( th1, th2 )로 처리하는 경우를 가정해보자.
- 하나의 쓰레드로 두 작업을 처리하는 경우는 한 작업을 마친 후에 다른 작업을 시작하지만, 두 개의 쓰레드 작업을 하는 경우에는 짧은 시간 동안 2개의 쓰레드가 번갈아 가면서 작업을 동시에 처리하는 것과 같이 느끼게 된다.
- 하나의 쓰레드로 두 개의 작업을 수행한 시간과 두 개의 쓰레드로 두 개의 작업을 수행한 시간은 거의 같다.
- 오히려 두 개의 쓰레드로 작업한 시간이 싱글쓰레드로 작업한 시간보다 더 걸리게 되는데 그 이유는 쓰레드간의 작업전환( context switching )에 시간이 걸리기 때문이다.
- 작업전환을 할 때는 현재 진행 중인 작업의 상태, 예를 들면 다음에 실행해야 할 위치 등의 정보를 저장하고 읽어오는 시간이 소요된다.
- 우리가 실습하는 예제는 실행할 때마다 다른 결과를 얻을 수 있는데 그 이유는 실행 중인 예제가 OS의 프로세스 스케쥴러의 영향을 받기 때문이다.
- 자바가 OS에 독립적이라고는 하지만 실제로 OS 종속적인 부분이 몇 가지 있는데 쓰레드도 그중 하나이다.
- 두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우에는 싱글쓰레드 프로세스보다 멀티쓰레드 프로세스가 효율적이다.
- 예를 들면 사용자로부터 데이터를 입력받는 작업, 네트워크 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과 같이 외부기기와의 입력을 필요로 하는 경우가 이에 해당한다.
5. 쓰레드의 우선순위
- 쓰레드는 우선순위( priority )라는 속성( 멤버 변수 )을 가지고 있는데, 이 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다.
- 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다.
- 파일 전송 VS 채팅 ==> 우선순위가 무엇이 더 높아야 할까?
- 예를 들어 파일 전송 기능이 있는 메신저의 경우, 파일 다운로드를 처리하는 쓰레드보다 채팅 내용을 전송하는 쓰레드의 우선순위가 더 높아야 사용자가 채팅하는데 불편함이 없을 것이다.( 대신 파일 다운로드하는 시간이 좀 더 길어질 것이다. )
- 이처럼 시각적인 부분이나 사용자에게 빠르게 반응해야 하는 작업을 하는 쓰레드의 우선순위를 다른 작업을 수행하는 쓰레드에 비해 높아야 한다.
- 쓰레드의 우선순위 지정하기
void setPriority( int newPriority ) 쓰레드의 우선순위를 지정한 값으로 변경한다.( default : 5 ) int getPriority 쓰레드의 우선순위를 반환한다. public static int MAX_PRIORITY = 10; 최대 우선순위 public static int MIN_PRIORITY = 1; 최소 우선순위 public static int NORM_PRIORITY = 5; 보통 우선순위( default ) - 쓰레드가 가질 수 있는 우선순위 범위는 1 ~ 10이며 숫자가 높을수록 우선순위가 높다.
- 쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받는다.
==> main메소드를 수행하는 쓰레드의 우선순위가 5이므로, main메소드에서 생성한 쓰레드의 우선순위는 자동적으로 5가 된다.
6. 데몬쓰레드( daemon thread )
- 데몬쓰레드는 다른 일반쓰레드( 데몬쓰레드가 아닌 쓰레드 )의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다.
- 일반쓰레드가 모두 종료되면 데몬쓰레드는 강제적으로 자동종료 되는데 그 이유는 데몬쓰레드는 일반쓰레드의 보조역할을 수행하므로 일반쓰레드가 모두 종료되고 나면 데몬쓰레드의 존재의 의미가 없기 때문이다.
- 이 점을 제외하고는 데몬쓰레드와 일반쓰레드는 다르지 않으며 데몬쓰레드의 대표적인 예로는 GC( Garbage Collector ), 자동 저장 등이 있다.
- 데몬쓰레드는 무한루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.
- 데몬쓰레드는 일반쓰레드의 작성방법과 실행방법이 같으며 다만 쓰레드를 생성한 다음 실행하기 전에 setDaemon( true ) 메소드를 호출하기만 하면 된다.
boolean isDaemon() 쓰레드가 데몬쓰레드인지 확인한다. 데몬쓰레드이면 true를 반환 void setDaemon( boolean on ) 쓰레드를 데몬쓰레드로 또는 사용자 쓰레드로 변환한다.
매개변수 on의 값을 true로 지정하면 데몬쓰레드가 된다.
7. 쓰레드의 실행 제어
- 쓰레드 프로그래밍이 어려운 이유는 동기화( synchronization )와 스케줄링( scheduling ) 때문이다.
- 효율적인 멀티쓰레드 프로그램을 만들기 위해서는 보다 정교한 스케줄링을 통해 프로세스에게 주어진 자원과 시간을 여러 쓰레드가 낭비 없이 잘 사용하도록 프로그래밍을 해야 한다.
- 쓰레드의 스케줄링을 잘하기 위해서는 쓰레드의 상태와 관련된 메소드를 잘 알아야 한다.
- 쓰레드의 스케줄링과 관련된 메소드
static void sleep( long millis ) 지정된 시간( 천분의 1초 ) 동안 쓰레드를 일시정지시킨다. static void sleep( long millis, int nanos ) 지정한 시간이 지나고 나면, 자동적으로 다시 실행 대기 상태가 된다. void join() 쓰레드 자신이 하던 작업을 멈추고 다른 쓰레드가 지정된 시간 동안 작업을 수행하도록 하게 한다. void join( long millis ) void join( long millis, int nanos ) void interrupt() sleep()이나 join()에 의해 일시 정지상태인 쓰레드를 깨워서 실행 대기 상태로 만든다. 해당 쓰레드에서는 InterruptedException이 발생함으로써 일시정지상태를 벗어나게 된다. void stop() 쓰레드를 즉시 종료시킨다. void suspend() 쓰레드를 일시정지시킨다. resume()을 호출하면 다시 실행상태가 된다. void yield() 실행 중인 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 자신은 실행 대기 상태가 된다. - 쓰레드의 상태
NEW 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태 RUNNABLE 실행 중 또는 실행 가능한 상태 BLOCKED 동기화 블록에 의해서 일시 정지된 상태( lock이 풀릴 때까지 기다리는 상태 ) WAITING 쓰레드의 작업이 종료되지는 않았지만 실행 가능하지 않은 일시정지 상태
TIMED_WAITING은 일시 정시 시간이 지정된 경우를 의미한다.TIMED_WAITING TERMINATED 쓰레드의 작업이 종료된 상태
- sleep() - 일정 시간 동안 쓰레드를 멈추게 한다.
static void sleep( long millis )
static void sleep( long millis, int nanos )- 밀리세컨드와 나노세컨드의 시간 단위로 세밀하게 값을 지정할 수 있지만 어느 정도의 오차는 발생할 수 있다.
- sleep()에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 되거나 interrupt()가 호출되면 잠에서 깨어나 실행 대기 상태가 된다.( InterruptedException 발생 )
- 그래서 sleep()을 호출할 때는 항상 try-catch문으로 예외를 처리해줘야 한다.
- sleep()은 항상 현재 실행 중인 쓰레드에 대해 작동하기 때문에 th1.sleep( 2000 );과 같이 호출하였어도 실제로 영향을 받는 것은 main메소드를 실행하는 main쓰레드이다.
- sleep()은 static으로 선언되어 있으므로 참조 변수를 이용해서 호출하기보다는 Thread.sleep( 2000 );과 같이 해야 한다.
- interrupt()와 interrupted() - 쓰레드의 작업을 취소한다.
- 진행 중인 쓰레드의 작업이 끝나기 전에 취소시켜야 할 때가 있다.
- 예를 들어 큰 파일을 다운로드할 때 시간이 너무 오래 걸리면 중간에 다운로드를 포기하고 취소할 수 있어야 한다.
- interrupt()는 쓰레드에게 작업을 멈추라고 요청한다.
- 단지 멈추라는 요청만 하는 것일 뿐 쓰레드를 강제로 종료시키지는 못한다.
==> 그저 쓰레드의 interrupted 상태( 인스턴스 변수 )를 바꾸는 것일 뿐이다. - 그리고 interrupted()는 쓰레드에 대해 interrupt()가 호출되었는지를 알려준다.
- interrupt()가 호출되지 않았다면 false를, interrupt()가 호출되었다면 true를 반환한다.
- interrupt()가 호출되면 interrupted()의 결과가 false에서 true로 바뀌어 이를 이용하여 쓰레드를 멈출 수 있는 것이다.
void interrupt() 쓰레드의 interrupted() 상태를 false에서 true로 변경 boolean isInterrupted() 쓰레드의 interrupted() 상태를 반환( true or false ) static boolean interrupted() 현재 쓰레드의 interrupted() 상태를 반환 후 false로 변경 - 쓰레드가 sleep(), wait(), join()에 의해 일시정지상태( WAITING )에 있을 때 해당 쓰레드에 interrupt()를 호출하면 sleep(), wait(), join()에서 InterruptedException이 발생하고 쓰레드는 실행 대기상태( RUNNABLE )로 바뀐다.
==> 즉, 멈춰있던 쓰레드를 깨워서 실행 가능한 상태로 만드는 것이다.
- suspend(), resume(), stop()
- join()
8. 쓰레드의 동기화
- 싱글쓰레드 프로세스의 경우 프로세스 내에서 단 하나의 쓰레드만 작업하기 때문에 프로세스의 자원을 가지고 작업하는데 별 문제가 없지만, 멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다.
- 만일 쓰레드 A가 작업하던 도중에 다른 쓰레드 B에게 제어권이 넘어갔을 때, 쓰레드 A가 작업하던 공유데이터를 쓰레드 B가 임의로 변경하였다면 다시 쓰레드 A가 제어권을 받아서 작업을 마쳤을 때 원래 의도했던 것과는 다른 결과를 얻을 수 있다.
- 이러한 일이 발생하는 것을 방지하기 위해서 한 쓰레드가 특정 작업을 끝 마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요하다.
==> 그래서 도입된 개념이 바로 임계 영역( critical section )과 잠금( lock )이다. - 공유 데이터를 사용하는 코드 영역을 임계 영역으로 저장해놓고, 공유 데이터( 객체 )가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 된다.( lock을 획득한 쓰레드만 사용 가능하다. )
- 이처럼 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 하는 것을 '쓰레드의 동기화( synchronization )'라고 한다.
- synchronized를 이용한 동기화
- 먼저 가장 간단한 동기화 방법인 synchronized 키워드를 이용한 동기화에는 아래에 같은 2가지 방식이 있다.
1234567// 1. 메소드 전체를 임계 영역으로 지정public synchronized void calcSum() {}// 2. 특정한 영역을 임계 영역으로 지정synchronized( /*객체의 참조 변수*/ ) {}cs - 첫 번째 방법은 메소드 앞에 synchronized를 붙이는 것인데, synchronized를 붙이면 메소드 전체가 임계 영역으로 지정된다.
- 쓰레드는 synchronized 메소드가 호출된 시점부터 해당 메소드가 포함된 객체의 lock을 얻어 작업을 수행하다가 메소드가 종료되면 lock을 반환한다.
- 두 번째 방법은 메소드 내의 코드 일부를 블록으로 감싸고 블록 앞에 synchronized( 참조 변수 )를 붙이는 것인데, 이때 '참조 변수'는 락을 걸고자 하는 객체를 참조하는 것이어야 한다.
- 이 블록을 synchronized 블록이라고 부르며, 이 블록의 영역 안으로 들어가면서 쓰레드는 저장된 객체의 lock을 얻게 되고, 이 블록을 벗어나면 lock을 반납한다.
- 두 방법 모두 lock을 하나씩 가지고 있으며 해당 객체의 lock을 가지고 있는 쓰레드만 임계 영역의 코드를 수행할 수 있다.
- 그리고 다른 쓰레드들은 lock을 얻을 때까지 기다리게 된다.
- 임계 영역은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 메소드 전체에 락을 거는 것보단 synchronize블록으로 임계 영역을 최소화해서 보다 효율적인 프로그램이 되도록 노력해야 한다.
- 먼저 가장 간단한 동기화 방법인 synchronized 키워드를 이용한 동기화에는 아래에 같은 2가지 방식이 있다.
- wait()과 notify()
- synchronized로 동기화해서 공유 데이터를 보호하는 것까지는 좋은데, 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는 것도 중요하다.
- 만일 계좌에 출금할 돈이 부족해서 한 쓰레드가 락을 보유한 채로 돈이 입금될 때까지 오랜 시간을 보낸다면 다른 쓰레드들은 모두 해당 객체의 락을 기다리느라 다른 작업들도 원활히 진행되지 않을 수 있다.
- 이런 상황을 개선하기 위해서 고안된 것이 바로 wait()와 notify()이다.
- 동기화된 영역의 코드를 수행하다가 작업이 더 이상 진행할 상황이 아니라면 일단 wait()를 호출하여 쓰레드가 락을 반납하고 기다리게 한다.
- 그러면 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행할 수 있게 된다.
- 나중에 작업을 진행할 수 있는 상황이 되면 notify()를 호출해서 작업을 중단했던 쓰레드가 다시 락을 얻어 작업을 진행할 수 있게 한다.
- 이는 마치 빵을 사려고 빵집 앞에 줄을 서 있는 것과 유사한데, 자신의 차례가 되었는데도 자신이 원하는 빵이 나오지 않았다면 다음 사람에게 순서를 양보하고 기다리다가 자신이 원하는 빵이 나오면 통보를 받고 사가는 것이다.
- 차이가 있다면, 오래 기다린 쓰레드가 락을 얻는다는 보장이 없다.
- wait()이 호출되면 실행 중이던 쓰레드는 해당 객체의 대기실( waiting pool )에서 통지를 기다린다.
- notify()가 호출되면 해당 객체의 대기실에 있던 모든 쓰레드 중에서 임의의 쓰레드만 통지를 받는다.
- notifyAll()은 기다리고 있는 모든 쓰레드에게 통보를 하지만 그래도 lock을 얻을 수 있는 것은 하나의 쓰레드일 뿐이고 나머지 쓰레드는 통보를 받긴 했지만, lock을 얻지 못하며 다시 lock을 기다리는 신세가 된다.
12345void wait()void wait( long timeout )void wait( long timeout, int nanos )void notify()void notifyAll()cs - wait()은 notify() 또는 notifyAll()이 호출될 때까지 기다리지만, 매개변수가 있는 wait()은 지정된 시간 동안만 기다린다.
- 즉, 지정된 시간이 지난 후에 자동적으로 notify()가 호출되는 것과 같다.
- waiting pool은 객체마다 존재하는 것이므로 notifyAll()이 호출된다고 해서 모든 객체의 waiting pool에 있는 쓰레드가 깨워지는 것은 아니다.
- notifyAll()이 호출된 객체의 waiting pool에 대기 중인 쓰레드만 해당된다.
- wait(), notify(), notifyAll()
- Object 클래스에 정의되어 있다.
- 동기화 블록( synchronized 블록 ) 내에서만 사용할 수 있다.
- 보다 효율적인 동기화를 가능하게 한다.
반응형
'웹개발 > JAVA' 카테고리의 다른 글
[JAVA] 파일 입출력, MVC 모델 (0) | 2022.04.15 |
---|---|
[JAVA] 제네릭스( generics ) (0) | 2022.02.07 |
[JAVA] 컬렉션 프레임워크( Collection Framework ) - Arrays, Comparable & Comparator, HashSet, TreeSet, HashMap & HashTable (0) | 2022.02.04 |
[JAVA] 컬렉션 프레임워크( Collection Framework ) - ArrayList, LinkedList, Stack & Queue, Iterator (0) | 2022.02.03 |
[JAVA] Object, String, Math, Wrapper 클래스 (0) | 2022.01.29 |