▩ 목 차 ▩
1. 쓰레드가 도대체 뭘까?
2. Runnable 인터페이스와 Thread 클래스
2-1. Thread 클래스의 생성자를 살펴보자.
2-2. 많이 사용되는 sleep() 메소드에 대해서 살펴보자.
2-3. Thread 클래스의 주요 메소드를 살펴보자
2-4. 쓰레드와 관련이 많은 Synchronized
2-4-1. synchronized 블록은 이렇게 사용한다
2-5. 쓰레드를 통제하는 메소드들
2-6. Object 클래스에 선언된 쓰레드와 관련있는 메소드들
2-7. ThreadGroup에서 제공하는 메소드들
■ 1. 쓰레드가 도대체 뭘까? ■
자바 프로그램을 실행하게 되면 JVM이 시작된다. 보통 이렇게 JVM이 시작되면 자바 프로세스가 시작한다. 이 프로세스라는 울타리 안에서 여러 개의 쓰레드라는 것이 아둥바둥 살게 된다.
==> 즉, 하나의 프로세스 내에 여러 쓰레드가 수행된다. 하지만, 거꾸로 생각해 여러 프로세스가 공유하는 하나의 쓰레드가 수행되는 일은 절대 없다. 어떤 프로세스든 간에 쓰레드가 하나 이상 수행된다.
정리를 해보자면, 우리가 이클립스를 통해 클래스를 실행시n키는 순간 자바 프로세스가 시작되고, main() 메소드가 수행하면서 하나의 쓰레드가 시작되는 것이다.
만약 많은 쓰레드가 필요하다면, 이 main() 메소드에서 쓰레드를 생성해 주면 된다.
[ 자바를 사용하여 웹을 제공할 때에는 Tomcat 같은 WAS를 사용한다. 이 WAS도 똑같이 main()메소드에서 생성한 쓰레드들이 수행되는 것이다. ]
그렇다면, 왜 쓰레드라는 것을 만들었을까?
이유1.
프로세스가 하나 시작하려면 많은 자원이 필요하다. 만약 하나의 작업을 동시에 수행하려고 할 때 여러개의 프로세스를 띄어서 실행하여 각각 메모리를 할당하여 주어야만 한다. JVM[프로세스]은 기본적으로 아무런 옵션 없이 실행하면 , 적어도 32MB~64MB의 물리 메모리를 점유한다. 그에 반해서, 쓰레드를 하나 추가하면 1MB 이내의 메모리를 점유한다. 그렇기에 많은 여러개의 프로세스를 띄우는것이 아닌 하나의 프로세스 내에 여러 쓰레드를 사용하는 것이다. [ 그래서 쓰레드를 "경량 프로세스"라고도 부른다. ]
이유2.
요즘 PC들은 대부분 멀티 코어 이상이다. 대부분의 작업은 단일 쓰레드로 실행하는 것보다는 다중 쓰레드로 실행하는 것이 더 빠른 시간에 결과를 제공해준다. 그렇기에 보다 빠른처리를 할 필요가 있을 때, 쓰레드를 사용하면 보다 빠른 계산을 처리할 수 있다.
■ 2. Runnable 인터페이스와 Thread 클래스 ■
쓰레드를 생성하는 것은 크게 두 가지 방법이 있다.
하나는 Runnable 인터페이스를 사용하는 것이고, 다른 하나는 Thread 클래스를 사용하는 것이다.
Thread 클래스는 Runnable 인터페이스를 구현한 클래스이므로, 어떤 것을 적용하느냐의 차이만 있다.
[ Thread 클래스를 사용하는 것이 더 편해서 Runnable 보다 Thread 클래스를 더 선호한다. ]
Runnable 인터페이스와 Thread 클래스는 모두 java.lang 패키지에 있다.
==> 따라서, 이 인터페이스나 클래스를 사용할 떄에는 별도로 import할 필요가 없다.
Runnable 인터페이스에 선언되어 있는 메소드는 단지 하나다.
바로 run()이라는 메소드이며 리턴값이 없다. 그에 반해 Thread클래스는 매우 많은 생성자와 메소드를 제공한다. 지금은 쓰레드를 생성하는 코드를 생성하는 방법을 알아보자.
[EX] - Runnalbe 인터페이스를 구현(implements)
package part25;
public class RunnableSample implements Runnable {
@Override
public void run() {
System.out.println("This is RunnableSample's run() method.");
}
}
[EX] - Thread 클래스를 확장(extends)
package part25;
public class ThreadSample extends Thread {
public void run() {
System.out.println("This is ThreadSample's run() method.");
}
}
위의 두 개의 쓰레드 클래스는 모두 쓰레드로 실행할 수 있다는 공통점이 있다. 하지만, 이 두개의 쓰레드 클래스를 실행하는 방식은 다르다.
[EX] - Runnalbe 인터페이스를 구현(implements)한 쓰레드 및 Thread 클래스를 확장(extends)한 쓰레드 실행
package part25;
public class RunThreads {
public static void main(String[] args) {
}
public void runBasic() {
RunnableSample runnable = new RunnableSample();
new Thread(runnable).start();
ThreadSample thread = new ThreadSample();
thread.start();
System.out.println("RunThreads.runBasic() method is ended.");
}
}
위의 코드를 보자.
무엇보다 중요한 사실은,
- 쓰레드가 수행되는 우리가 구현하는 메소드 run() 메소드다.
- 쓰레드를 시작하는 메소드는 start()이다.
즉, 다시 말해서, Runnable 인터페이스를 구현하거나 Thread 클래스를 확장할 때에는 run() 메소드를 시작점으로 작성해야만 한다.
그런데, 쓰레드를 시작하는 메소드는 run()이 아닌 start()라는 메소드다. 자바에서는 start() 메소드를 만들지 않아도, 알아서 자바에서 run() 메소드를 수행하도록 되어 있다.
먼저 RunnableSample을 시작한 코드를 보자.
new Thread(runnable).start();
Runnable 인터페이스를 구현한 RunnableSample 클래스를 쓰레드로 바로 시작할 수는 없다.
==> 따라서, Thread 클래스의 생성자에 해당 객체를 추가하여 시작해 주어야만 한다. 그러면 start()메소드를 호출하면 쓰레드가 시작된다.
ThreadSample을 시작한 코드를 보자.
thread.start();
Thread 클래스를 바로 확장한 클래스는 바로 start()메소드를 사용할 수 있다.
그런데 왜 이렇게 쓰레드를 생성하고 사용하는 방법을 2가지 방법으로 제공할까?
==> 자바에서 Thread 클래스를 확장 받아야만 쓰레드로 구현할 수 있는데, 다중 상속이 불가능하므로 해당 클래스를 쓰레드로 만들 수 없다. 하지만, 인터페이스는 여러 개의 인터페이스를 구현해도 전혀 문제가 발생하지 않는다. 따라서, 이러한 경우에는 Runnable 인터페이스를 구현해서 사용하면 된다.
정리하자면, 쓰레드 클래스가 다른 클래스를 확장할 필요가 있을 경우에는 Runnable 인터페이스를 구현하여 사용하면 되고, 그렇지 않은 경우에는 쓰레드 클래스를 사용하는 것이 편하다.
결과를 보게되면,
This is RunnableSample's run() method.
This is ThreadSample's run() method.
RunThreads.runBasic() method is ended.
This is RunnableSample's run() method.
RunThreads.runBasic() method is ended.
This is ThreadSample's run() method.
결과가 항상 같은것이 출력되는게 아니라 결과는 순서가 뒤죽박죽 출력이 된다.
==> 왜냐하면 쓰레드를 구현 할 때 start() 메소드를 호출하면, 쓰레드 클래스에 있는 run()메소드의 내용이 끝나든, 끝나지 않든 간에 쓰레드를 시작한 메소드에서는 그 다음 줄에 있는 코드를 실행한다.
[EX] - Runnalbe 인터페이스를 구현(implements)한 쓰레드 및 Thread 클래스를 확장(extends)한 쓰레드 실행2
package part25;
public class RunMultiThreads {
public static void main(String[] args) {
RunMultiThreads sample = new RunMultiThreads();
sample.runMultiThread();
}
public void runMultiThread() {
RunnableSample runnable[]=new RunnableSample[5];
ThreadSample thread[] = new ThreadSample[5];
for(int loop=0; loop<5; loop++) {
runnable[loop]=new RunnableSample();
thread[loop] = new ThreadSample();
new Thread(runnable[loop]).start();
thread[loop].start();
}
System.out.println("RunMultiThreads.runMultiThread() method is ended.");
}
}
This is RunnableSample's run() method.
This is ThreadSample's run() method.
This is RunnableSample's run() method.
RunMultiThreads.runMultiThread() method is ended.
This is ThreadSample's run() method.
This is RunnableSample's run() method.
This is ThreadSample's run() method.
This is RunnableSample's run() method.
This is ThreadSample's run() method.
This is RunnableSample's run() method.
This is ThreadSample's run() method.
위의 코드를 보자.
이 예제는 전에 보던 예제와 다르게 각각 5개의 RunnableSample과 ThreadSample의 객체를 생성하여 실행한다는 점이다.
이렇게 하면 총 10개의 쓰레드가 수행된다.
이 코드의 결과를 보게 되면 실행할 때마다 실행 결과가 달라지는 것을 알 수 있다.
==> 왜냐하면 총 10개의 쓰레드가 다른 쓰레드를 생각하지 않고 자신의 run()메소드의 내용만을 생각하기 때문이다. 즉, 순차적으로 실행되지 않는다는 것이다.
■ 2-1. Thread 클래스의 생성자를 살펴보자.
Thread 클래스는 다음과 같이 8개의 생성자가 있다.
생성자를 알아보기 전에 쓰레드의 이름을 먼저 알아보자. [ 모든 쓰레드는 이름이 있다. ]
만약 아무런 이름을 지정하지 않으면, 그 쓰레드의 이름은 "Thread-n"이다. 여기서 n은 쓰레드가 생성된 순서에 따라 증가한다.
[ 만약 쓰레드 이름이 겹친다고 해도 예외나 에러가 발생하지는 않는다. ]
어떤 쓰레드를 생성할 때 쓰레드를 묶어 놓을 수 있다. 그게 바로 ThreadGroup이다.
==> 이렇게 쓰레드의 그룹을 묶으면 ThreadGroup 클래스에서 제공하는 여러 메소드를 통해서 각종 정보를 얻을 수 있다.
그리고 생성자에 있는 stackSize라는 값은 스택의 크기를 말한다.
쓰레드에서 얼마나 많은 메소드를 호출하는지, 얼마나 많은 쓰레드가 동시에 처리되는지는 JVM이 실행되는 OS의 플랫폼에 따라서 매우 다르다. [ 참고로 메모리 영역의 Stack영역에 쓰레드가 생성될 때마다 별도의 Stack이 할당된다. ]
만약 Thread 이름을 지정하고 싶으면 어떻게 할까?
==> 생성자를 이용하면 된다. 아래 예제를 보자.
package part25;
public class NameThread extends Thread {
public NameThread() {
super("ThreadName");
}
public void run() {
}
}
위 코드를 보자.
생성자 부분에 super()메소드를 이용하여 내가 상속을 받은 클래스의 부모 클래스의 매개변수를 설정한 것이다
즉, Thread(String name)을 호출한 것과 동일한 효과를 보게 된 것이고 쓰레드 이름이 지정이 된 것이다.
그런데 이렇게 "ThreadName"이라고 지정해주며, 이 쓰레드 객체를 몇 십개 만들어도 "ThreadName" 이라는 동일한 이름을 가지게 된다.
==> 이러한 지정된 이름의 쓰레드를 생성하는 것 말고 다른 방법은 없을까?
생성자와 매개변수를 이용하여 쓰레드를 생성할 때 매개변수에 내가 원하는 이름을 지정하면 되는 것이다 아래와 같이 말이다.
public NameThread(String name) {
super(name);
}
그리고 쓰레드를 시작할 때에는 run()메소드가 진입점[start()가 시작되면 run()이 진입점이라 여기가 시작된다]이고, 쓰레드를 시작시킬 때에는 start() 메소드를 호출해야 한다. 이 메소드들에는 매개변수가 없다... ==> 그러면 쓰레드를 시작할 때 어떤 값을 전달하고 싶으면 어떻게 할 수 있을까?.. 아래와 같이 인스턴스 변수와 생성자를 이용하면 가능 할 것이다.
[EX] - 쓰레드를 시작할 때 값을 전달하기
package part25;
public class NameCalcThread extends Thread{
private int calcNumber;
public NameCalcThread(String name, int calcNumber) {
super(name);
this.calcNumber = calcNumber;
}
public void run() {
calcNumber++;
}
}
■ 2-2. 많이 사용되는 sleep() 메소드에 대해서 살펴보자.
Thread 클래스에는 deprecated[더 이상 사용하지 않는 것] 된 메소드도 많고, static 메소드[객체를 생성하지 않아도 사용할 수 있는 메소드]도 많이 있다. [static 메소드의 경우 대부분 해당 쓰레드를 위해 존재하는 것이 아닌, JVM에 있는 쓰레드를 관리하기 위한 용도로 사용됨]
static 타입 중 JVM 용도가 아닌 예외가 있다. 바로 아래에 있는 sleep()메소드다.
이 sleep() 메소드를 사용을 하면 해당하는 쓰레드에서 매개변수로 넘어온 시간 만큼 대기해주는 메소드다. 예를 보자.
[EX] - Thread 클래스에서 sleep() 메소드 선언
package part25;
public class EndlessThread extends Thread {
public void run() {
while(true) {
try {
System.out.println(System.currentTimeMillis());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
위의 코드를 보자.
run() 메소드 안을 보면 while(true)라고 되어 있다. 이때 break를 호출하거나, 예외를 발생시키지 않는 한 멈추지 않는다.
==> 무한 루프
while문 내의 문장을 보면 System.currentTimeMillis(); 구문을 통해 현재 시간을 밀리초 단위로 출력하고, Thread 클래스의 sleep() 메소드를 static하게 호출하여 1초간 멈춘다.
한 가지 고려해야 할 사항은 Thread.sleep() 메소드를 사용할 때에는 항상 try-catch로 묶어 주어야만 한다는 것이다. 또한 적어도 InterruptedException으로 catch 해 주어야만 한다. [ 적어도 라는 의미는 InterruptedException의 상위 예외를 사용해도 된다는 의미 ]
==> 왜냐하면 sleep()메소드는 InterruptedException을 던질 수도 있다고 선언되어 있기 때문이다.
[EX] - Thread 클래스에서 sleep() 메소드 선언한 Thread 사용
package part25;
public class RunEndlessThreads {
public static void main(String[] args) {
RunEndlessThreads sample = new RunEndlessThreads();
sample.endless();
}
public void endless() {
EndlessThread thread = new EndlessThread();
thread.run();
}
}
1663586857792
1663586858798
1663586859803
1663586860808
1663586861813
1663586862819
...
위의 예제를 한번 살펴보자.
main()메소드의 수행이 끝나더라도, main()메소드나, 다른 메소드에서 시작한 쓰레드가 종료하지 않으면 해당 자바 프로세스는 끝나지 않는다. [ 하지만, 데몬 쓰레드는 예외다. ]
■ 2-3. Thread 클래스의 주요 메소드를 살펴보자
Thread 클래스의 주요 메소드는 크게 보면 쓰레드의 속성을 확인하고, 지정하기 위한 메소드와 쓰레드의 상태를 통제하기 위한 메소드로 나눌 수 있다.
먼저 속성을 확인하고 지정하는 메소드를 살펴보자.
여기서, 쓰레드의 우선순위(Priority)라는 것이 처음 나왔다. 쓰레드 우선순위라는것은 말 그대로, 대기하고 있는 상황에서 더 먼저 수행할 수 있는 순위를 말한다. 대부분 이 값은 기본값으로 사용하는 것을 권장한다. [ 마음대로 우선순위를 정했다가는 잘못해서 장애로 연결될 수 있다. ]
위에 있는 모든 메소드를 사용할 일은 그리 많지 않은데 가끔 확인을 위해 필요하므로 예제를 통해 간단하게 살펴보자.
[EX] - 쓰레드 속성 확인 및 지정하기 위한 메소드
package part25;
public class RunDaemonThreads {
public static void main(String[] args) {
RunDaemonThreads sample = new RunDaemonThreads();
sample.checkThreadProperty();
}
public void checkThreadProperty() {
ThreadSample thread1 = new ThreadSample();
ThreadSample thread2 = new ThreadSample();
ThreadSample daemonThread = new ThreadSample();
System.out.println("thread1 id="+thread1.getId());
System.out.println("thread2 id="+thread2.getId());
System.out.println("thread1 name="+thread1.getName());
System.out.println("thread2 name="+thread2.getName());
System.out.println("thread1 priority="+thread1.getPriority());
daemonThread.setDaemon(true);
System.out.println("thread1 isDaemon="+thread1.isDaemon());
System.out.println("daemonThread isDaemon="+daemonThread.isDaemon());
}
}
thread1 id=14
thread2 id=15
thread1 name=Thread-0
thread2 name=Thread-1
thread1 priority=5
thread1 isDaemon=false
daemonThread isDaemon=true
위의 예제를 보자.
첫 두 줄을 보면 쓰레드의 아이디를 출력하였고, 그 다음 두 줄은 쓰레드의 이름이 Thread-0, Thread-1과 같이 자동으로 Thread 뒤에 일련 번호가 추가되는 것을 볼 수 있다.
그리고 우선순위가 있는데, 이 우선순위의 기본값이 5다.
[ 쓰레드에 대한 각종 상태를 확인 할 수 있다. ]
참고로, 쓰레드 API를 잘 살펴보면 다음과 같이 우선순위와 관계 있는 3개의 상수가 있다.
만약 우선 순위를 정할 일이 있다면, 숫자로 정하는 것보다는 위에 있는 상수들을 이용 할 것을 권장한다. [ 하지만, 우선순위는 되도록이면 지정하지 않는 것이 좋다 ]
그리고 예제 결과의 마지막을 보면 daemonThread라는 쓰레드 객체를 데몬 쓰레드를 지정하고 난 후 그 내용을 출력한 것을 볼 수 있다.
쓰레드가 수행하기 전에 데몬 여부를 지정해야만 그 쓰레드가 데몬 쓰레드로 인식된다.
그렇다면 여기서 데몬 쓰레드란 무엇일까?
==> 데몬 쓰레드가 아닌 사용자 쓰레드는 JVM이 해당 쓰레드가 끝날 때까지 기다린다고 했다.. 즉, 어떤 쓰레드를 데몬으로 지정하면, 그 쓰레드가 수행되고 있든, 수행되지 않고 있든 상관 없이 JVM이 끝날 수 있다.
단, 해당 쓰레드가 시작하기(start()메소드가 호출되기) 전에 데몬 쓰레드로 지정되어야만 한다. 쓰레드가 시작한 다음에는 데몬으로 지정할 수 없다.
[EX] - 데몬 쓰레드 클래스 생성
package part25;
public class DaemonThread extends Thread {
public void run() {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
}
}
위의 코드를 보자.
Thread 클래스의 sleep() 메소드를 사용하여 long의 최대값 만큼 대기하도록 해놓았다. 즉, 별 다른일이 없는 한 해당 쓰레드는 끝나지 않을 것이다. 이 쓰레드 클래스에 대한 객체를 만들어 사용해보자.
[EX] - 데몬 쓰레드 객체 실행 ( 데몬 쓰레드 지정 X )
public void runDaemonThread() {
DaemonThread thread = new DaemonThread();
thread.start();
}
위의 메소드를 실행을 하게 되면 아무런 메시지도 뿌리지 않고 해당 프로그램은 끝나지 않는다.
[EX] - 데몬 쓰레드 객체 실행 ( 데몬 쓰레드 지정 O )
public void runDaemonThread() {
DaemonThread thread = new DaemonThread();
thread.setDaemon(true);
thread.start();
}
위의 메소드를 실행을 하게 되면 프로그램이 대기하지 않고, 그냥 끝나버린다. 왜냐하면 데몬쓰레드로 설정을 해서 그렇다.
==> 데몬쓰레드로 설정되면 해당 쓰레드가 종료되지 않아도 다른 실행중인 일반 쓰레드가 없다면, 멈춰 버린다.
[ 예를들어 모니터링하는 쓰레드를 별도로 띄워 모니터링하다가, 주요 쓰레드가 종료되면 관련된 모니터링 쓰레드가 종료되어야 프로세스가 종료 될 수 있다. 그런데 모니터링 쓰레드를 데몬 쓰레드로 만들지 않으면 프로세스가 종료될 수 없게 된다. 이렇게 부가적인 작업을 수행하는 쓰레드를 선언 할 때 데몬 쓰레드를 만든다. ]
■ 2-4. 쓰레드와 관련이 많은 Synchronized
쓰레드 클래에스에서 제공하는 메소드를 살펴보기 전에 synchronized에 대해서 살펴보자.
[ *synchronized : 현재 데이터를 사용하고 있는 해당 스레드를 제외하고 나머지 스레드들은 데이터에 접근 할 수 없도록 막는 개념 ]
synchronized는 자바의 예약어 중 하나다. [ 변수명이나, 클래스명으로 사용할 수 없다. ]
그런데, Thread 클래스를 설명하다가 왜 갑자기 synchronized를 설명할까?
==> 왜냐하면 쓰레드와 synchronized는 뗄레야 뗄 수 없는 관계이기 때문이다.
공부를 하면서 "쓰레드에 안전하다" 라는 말이 많았는데, 어떤 클래스나 메소드가 쓰레드에 안전하려면, synchronized를 사용해야만 한다.
자바에서 여러 쓰레드가 한 객체에 선언된 메소드에 접근하여 데이터를 처리하려고 할 때 동시에 연산을 수행하면 값이 꼬이는 경우가 발생할 수 있다. [ 여기서 한 객체라는 것은 하나의 클래스에서 생성된 여러개의 객체가 아니라, 동일한 하나의 객체를 말한다. ]
단, 메소드에서 인스턴스 변수를 수정하려고 할 때에만 이러한 문제가 생긴다. 즉, 매개변수나 메소드에서만 사용하는 지역변수만 다르는 메소드는 전혀 synchronized로 선언할 필요가 없다.
그렇다면, 도대체 synchronized가 어떤 것이길래 쓰레드 안전과 연관이 있을까?
synchronized는 두 가지 방법으로 사용할 수 있다.
- 메소드 자체를 synchronized로 선언하는 방법(synchronized methods)
- 메소드 내의 특정 문장만 synchronized로 감싸는 방법(synchronized statements)
지금은 메소드를 synchronized로 선언하는 것에 대해 알아보자.
메소드를 synchronized로 선언하려면 메소드 선언문에 synchronized를 넣어주면 된다.
다음과 같은 plus()라는 메소드가 있다면,
public void plus(int value) {
amount+=value;
}
아래와 같이 바꾼다.
public synchronized void plus(int value) {
amount+=value;
}
synchronized로 선언된 메소드다.
synchronized로 있는 것과 없는 것의 차이는 크다. 만약 이 synchronized라는 단어가 메소드 선언부에 있으면, 동일한 객체의 이 메소드에 2개의 쓰레드가 접근하든, 100개의 쓰레드가 접근하든 간에 한 순간에는 하나의 쓰레드만 이 메소드를 수행하게 된다.
[EX] - 연산을 수행하는 클래스 생성
package part25;
public class CommonCalculate {
private int amount;
public CommonCalculate() {
amount=0;
}
public void plus(int value) {
amount += value;
}
public void minus(int value) {
amount -= value;
}
public int getAmount() {
return amount;
}
}
위의 코드를 보자.
이 클래스는 연산을 수행한다. amount라는 인스턴스 변수가 선언되어 있다. 그리고 plus()라는 메소드에서는 매개변수로 받은 값을 더하고, minus()라는 메소드에서는 매개변수로 받은 값을 뺀다. getAmount()라는 메소드는 현재의 amount 값을 출력한다.
[EX] - 연산을 수행하는 클래스의 객체를 매개 변수로 받아서 처리하는 쓰레드
package part25;
public class ModifyAmountThread extends Thread {
private CommonCalculate calc;
private boolean addFlag;
public ModifyAmountThread(CommonCalculate calc, boolean addFlag) {
this.calc = calc;
this.addFlag = addFlag;
}
public void run() {
for(int loop=0; loop<10000; loop++) {
if(addFlag) {
calc.plus(1);
}
else {
calc.minus(1);
}
}
}
}
위의 코드를 보자.
CommonCalculate 클래스의 객체를 받아서 addFlag가 true면 1을 더하고, addFlag가 false면 1을 빼는 연산을 수행한다.
덧셈이나 뺄셈 연산을 만번 수행하고 나서, 해당 쓰레드는 종료한다.
[EX] - 연산을 수행하는 클래스의 객체를 매개 변수로 받아서 처리하는 쓰레드의 객체를 만들어 실행
package part25;
public class RunSync {
public static void main(String[] args) {
RunSync runSync = new RunSync();
runSync.runCommonCalculate();
}
public void runCommonCalculate() {
CommonCalculate calc = new CommonCalculate(); //1
ModifyAmountThread thread1 = new ModifyAmountThread(calc, true); //2
ModifyAmountThread thread2 = new ModifyAmountThread(calc, true); //2
thread1.start(); //3
thread2.start(); //3
try {
thread1.join(); //4
thread2.join(); //4
System.out.println("Final value is "+calc.getAmount()); //5
} catch (Exception e) {
e.printStackTrace();
}
}
}
Final value is 14779
위의 코드에서 숫자로 처리된 주석을 보자.
- 앞서 만든 CommonCalculate라는 클래스의 객체를 calc라는 이름으로 생성했다.
- ModifyAmountThread라는 클래스의 객체를 생성할 때 calc를 매개변수로 넘겨주고, plus()메소드만 수행하도록 true를 두 번째 매개변수로 넘겼다.
- 각각의 쓰레드를 시작한다.
- try-catch 블록 안에서는 join()이라는 메소드를 각각의 쓰레드에 대해서 호출한다. 여기서 join() 메소드는 쓰레드가 종료될 때까지 기다리는 메소드다.
- join()이 끝나면 calc 객체의 getAmount() 메소드를 호출한다. getAmount() 메소드의 호출 결과는 join() 메소드 수행 이후이므로, 모든 쓰레드가 종료된 이후의 결과다. [ join() 메소드에 대한 내용은 조금 이따가 배울 것이다. ]
결론적으로 RunSync 클래스의 runCommonCalculate() 메소드가 수행된 후에는 두 개의 쓰레드에서 하나의 객체에 있는 amount라는 int 타입의 값에 1을 만 번 더한 결과를 출력한다. 즉, 정상적인 상황이라면, 결과는 20000이 출력되어야만 한다. 하지만 결과는 20000이 나오지 않았다. 처음 수행할 때만 그럴 수도 있으니, 5번 실행시켜보자.
for(int loop = 0; loop<5; loop++) {
runSync.runCommonCalculate();
}
Final value is 13763
Final value is 15095
Final value is 14710
Final value is 13751
Final value is 15233
만약 ModifyAmountThread라는 쓰레드에서 반복하는 횟수가 적으면 적을수로 결과는 우리가 예상한 값에 가깝거나, 예상한 대로 출력될 것이다. 하지만, 반복 횟수가 많아질수록, 그 결과는 정상적인 결과와 멀어진다.
왜 이러한 결과가 나왔을까?
==> 그 이유는 CommonCalculate 클래스의 plus()라는 메소드 때문이다. 이 메소드는 다른 쓰레드에서 작업하고 있다고 하더라도, 새로운 쓰레드에서 온 작업도 같이 처리한다. 따라서 데이터가 꼬일 수 있다.
plus() 메소드를 다시 살펴보자.
public void plus(int value) {
amount += value;
}
간단해보이지만, 내부적으로는 그리 간단하지 않다. 실제로 이 메소드의 내용을 풀어쓰면 다음과 같다.
amount = amount + value;
연산은 우측 항의 결과를 좌측 항에 있는 amount에 담는다. 예를 들어 우측 항에 있는 amount가 1이고, value가 1일 경우, 정상적인 경우라면 좌측 항의 결과에는 2가 된다. 그런데 좌측항에 2라는 값을 치환하기 전에 다른 쓰레드가 또 들어와서 이 연산을 수행하려고 한다.
아직 amount는 2가 안 된 상황에서 amount는 1이다. 따라서, 먼저 계산된 결과에서 2를 치환한다고 하더라도, 그 다음에 들어온 쓰레드도 1과 1을 더하기 때문에 다시 amout에 2를 치환한다. 아래의 표를 보며 이해하자.
이렇게 동시에 연산이 수행되기 때문에 우리가 원한 20000이라는 값이 출력되지 않은 것이다. [ 실제에서 예를 들면, 은행에서도 직원이 한 창구에서 한 고객의 요청만 처리한다. 만약 한 번에 여러 고객의 요청을 처리하면 해당 창구는 고객의 요청이 뒤죽박죽 되어서 한 것도 제대로 처리하기 어렵게 될 것이다. ]
==> 이러한 문제를 해결하기 위한 것이 바로 synchronized다. CommonCalculate 클래스의 plus() minus()메소드에 아까 배웠던 메소드에 synchronized 로 선언하는 방법을 씀으로써, 이 메소드는 동일한 객체를 참조하는 다른 쓰레드에서, 이 메소드를 변경하려고 하면 먼저 들어온 쓰레드가 종료될 때까지 기다린다.
public synchronized void plus(int value) {
amount += value;
}
public synchronized void minus(int value) {
amount -= value;
]
이렇게 변경한 후 결과는 다음과 같다.
Final value is 20000
Final value is 20000
Final value is 20000
Final value is 20000
Final value is 20000
언제 수행하든지, 이 예제가 수행한 결과는 우리가 원한 동일한 20000이라는 결과를 출력한다.
만약 ModifyAmountThread 클래스에서 객체를 만들때 생성자 2번째 매개변수 부분에 true 및 false를 넣어 각각의 객체를 생성하면 plus(), minus() 메소드로 인해 0이라는 결과가 출력될 것이다. 이제 plus(), minus() 메소드는 쓰레드에 안전하다고 할 수 있다.
그리고 ModifyAmountThread 클래스에서 객체 2개를 만들때 첫번째 매개변수 부분에 같은 객체(calc)를 생성자 매개변수로 넣어줬는데 이 매개변수로 넣어준 객체(calc)에 대해서 각각에 대한 값을 가지는게 아니라 서로 공유한다. 왜냐하면 메모리 관점에서 생각을 하면 heap영역에 calc라는 객체 영역이 할당이 되었는데, 이 부분을 heap영역의 thread1과 thread2이 서로 같은 calc라는 영역을 가르키기 때문이다. 그렇기에 thread1객체를 통해 calc의 값을 변경을 하더라도 같은 calc를 가르키는 thread2객체도 calc의 바뀐 값을 출력한다.
[ 이때 threa1과 thread2의 객체는 서로 다른 heap 영역에 할당되어있다. ]
즉, 그림으로 쉽게 말을 하자면
■ 2-4-1. synchronized 블록은 이렇게 사용한다
메소드에 간단히 synchronized를 추가해 주면 되는 것으로 보인다. 하지만 이렇게 하면, 성능상 문제점이 발생할 수도 있다.
[ 예를들어, 어떤 클래스에 30줄짜리 메소드가 있다고 가정하자. 그 클래스에도 amount라는 인스턴스 변수가 있고, 30줄짜리 메소드에서 amount라는 변수를 한 줄에서만 다룬다. 만약 해당 메소드 전체를 synchronized로 선언한다면, 나머지 29줄의 처리를 할 때 필요 없는 대기 시간이 발생하게 된다. 이러한 경우에는 메소드 전체를 감싸면 안되며, amount라는 변수를 처리하는 부분만 synchronized 처리를 해 주면 된다. ] 부분적으로 sychronized 처리를 하려면 아래 예제처럼 하면 된다.
[EX] - sychronized 부분적 처리
public synchronized void plus(int value) {
synchronized (this) {
amount += value;
}
}
public synchronized void minus(int value) {
synchronized (this) {
amount -= value;
}
}
위와 코드를 보자.
sychronized(this) 이후에 있는 중괄호 내에 있는 연산만 동시에 여러 쓰레드에서 처리하지 않겠다는 의미다. 소괄호 안에 this가 있는 부분에는 잠금 처리를 하기 위한 객체를 선언한다. 여기서는 그냥 this라고 지정했지만, 일반적으로는 다음과 같이 별도의 객체를 선언하여 사용한다.
[EX] - sychronized 부분적 처리에서 사용되는 별도의 객체 선언
private int amount;
Object lock = new Object();
public void plus(int value) {
synchronized (lock) {
amount += value;
}
}
public void minus(int value) {
synchronized (lock) {
amount -= value;
}
}
sychronized를 사용할 때에는 하나의 객체를 사용하여 블록 내의 문장을 하나의 쓰레드만 수행하도록 할 수 있다.
쉽게 생각하자면, 여기서 사용한 lock이라는 객체나, 앞서 사용한 this는 모두 문지기라고 할 수 있다. 그리고 그 문지기는 한 명의 쓰레드만 일을 할 수 있도록 허용해준다. 만약 블록에 들어간 쓰레드가 일을 다 처리하고 나오면, 문지기는 대기하고 있는 다른 쓰레드에게 기회를 준다.
이렇게, sychronized 블록을 사용할 때에는 lock이라는 별도의 객체를 사용할 수 있다. 그런데, 때에 따라서 이러한 객체는 하나의 클래스에서 두 개 이상 만들어 사용할 수도 있다.
만약 클래스에 amount라는 변수 외에 interest라는 변수가 있고, 그 interest라는 변수를 처리할 때에도 여러 쓰레드에서 접근하면 안되는 경우가 발생할 수 있다. 이럴때 만약 lock이라는 하나의 잠금용 객체만을 사용하면 amount라는 변수를 처리할 때, interest라는 변수를 처리하려는 부분도 처리를 못하게 된다. 따라서, 두 개의 별도의 lock 객체를 사용하면 보다 효율적인 프로그램이 된다. 예제를 보자.
[EX] - sychronized 부분적 처리에서 사용되는 2개의 별도의 객체 선언
private int amount;
private int interest;
public void addInterest(int value) {
interest+=value;
}
public void plus(int value) {
amount += value;
}
private int amount;
private int interest;
private Object interestLock = new Object();
private Object amountLock = new Object();
public void addInterest(int value) {
synchronized (interestLock) {
interest+=value;
}
}
public void plus(int value) {
synchronized (amountLock) {
amount += value;
}
}
여러 쓰레드에서 동시에 접근했을 때 안전하게 처리하기 위한 sychronized에 대해 살펴보았다.
자바 개발자로 살아가기 위해서는 절대로 이 부분은 모르고 지나치면 안된다. 꼭 머리 속에 sychronized에 대한 개념을 넣자.
sychronized를 사용할 때 잘하는 실수 한 가지가 있다. [ 앞의 RunSync 클래스의 일부 코드를 다시 보자. ]
CommonCalculate calc = new CommonCalculate(); //1
ModifyAmountThread thread1 = new ModifyAmountThread(calc, true); //2
ModifyAmountThread thread2 = new ModifyAmountThread(calc, true); //2
메소드를 sychronized 할 때에는 이처럼 같은 객체를 참조 할 때에만 유효하다. 이 코드를 잘 보자. calc라는 하나의 객체를 사용하여 thread1과 thread2를 생성했다.
CommonCalculate calc1 = new CommonCalculate(); //1
ModifyAmountThread thread1 = new ModifyAmountThread(calc1, true); //2
CommonCalculate calc2 = new CommonCalculate(); //1
ModifyAmountThread thread2 = new ModifyAmountThread(calc2, true); //2
만약에 위와 같이 두개의 쓰레드가 동일한 calc가 아닌 서로 다른 객체를 참조한다면 sychronized로 선언된 메소드는 같은 객체를 참조하는 것이 아니므로, sychronized를 안쓰는 것과 동일하다고 보면된다. 즉, sychronized를 사용안해도 별 문제가 없다는 말이다.
왜냐하면 메모리 관점에서 생각을 하면 heap영역에 calc라는 객체 영역이 할당이 되었는데, 이 부분을 heap영역의 thread1과 thread2이 서로 같은 calc라는 영역을 가르키기 때문이다. 그렇기에 thread1객체를 통해 calc의 값을 변경을 하더라도 같은 calc를 가르키는 thread2객체도 calc의 바뀐 값을 출력한다.
또 한가지 당부하고 싶은말은, sychronized는 여러 쓰레드에서 하나의 객체이 있는 인스턴스 변수를 동시에 처리할 때 발생할 수 있는 문제를 해결하기 위해서 필요한 것이라는 점이다. 즉, 인스턴스 변수가 선언되어 있다고 하더라도, 변수가 선언되어 있는 객체를 다른 쓰레드에서 공유할 일이 전혀 없다면 sychronized를 사용할 이유가 전혀 없다는 것이다. [ 혹시라도, sychronized를 배웠다고 모든 메소드에 이 예약어를 추가하는 사람이 있을 것 같아서.. ]
그리고 저번에 StringBuffer는 쓰레드에 안전하고, StringBuilder는 쓰레드에 안전하지 않다고 이야기 했다. 구체적으로 이야기하자면, StringBuffer는 sychronized 블록으로 주요 데이터 처리 부분을 감싸 두었고, StringBuilder는 sychronized라는 것이 사용되지 않았다.
==> 따라서 StringBuffer는 하나의 문자열 객체를 여러 쓰레드에서 공유해야 하는 경우에만 사용하고, StringBuilder는 여러 쓰레드에서 공유할 일이 없을 때 사용하면 된다.
결론적으로 필요에 따라 적절한 클래스를 선택하여 사용하는 것도 매우 중요하며, 그러기 위해서는 API 문서를 자주 참조하면서 개발해야만 한다.
■ 2-5. 쓰레드를 통제하는 메소드들
여러가지 이유로, 쓰레드를 통제해야 하는 경우가 있을 수 있다. 이번에는 쓰레드의 상태를 통제하는 메소드를 살펴보자.
먼저 getState()메소드에서 리턴하는 Thread.State에 대해서 알아보자.
자바의 Thread 클래스에는 State라는 enum 클래스가 있다. 이 클래스에 선언되어 있는 상수들의 목록은 다음과 같다.
이 클래스는 public static으로 선언되어 있다. 다시 말하면, Thread.State.NEW와 같이 사용할 수 있다는 의미다.
그리고, 어떤 쓰레드이건 간에"NEW -> 상태 -> TERMINEATED"의 라이프 사이클(생명주기)를 가진다.
여기서 "상태"에 해당하는 것은 NEW와 TERMINATED를 제외한 모든 다른 상태를 의미한다.
쓰레드 상태를 다이어그램으로 표시하면 다음과 같다.
이 그름을 보면 어떤 메소드가 호출되면 해당 상태로 전환되는지를 한 눈에 볼 수 있을 것이다.
다음 메소드로는 join()이라는 메소드가 있다. 이 메소드는 해당 쓰레드가 종료될 때까지 기다린다. 매개변수가 없는 join()메소드는 해당 쓰레드가 끝날 때까지 무한대로 대기한다.
만약, 특정 시간만큼만 기다리고 싶다면, join()메소드의 매개변수에 기다리고 싶은 시간을 지정하면 된다. 이 시간은 밀리초(1/1,000초)단위로 지정하면 되며, 만약 1분간 기다리고 싶다면 아래처럼 지정하면 된다. [ 만약 매개변수 값을 0으로 지정하면 join() 메소드를 사용하는 것과 동일하게 무한정 기다리게 된다. ] 그리고 더 세밀하게 지정하고 싶으면 매개변수가 2개인 join()메소드를 사용하여 첫번째 매개변수에 밀리초(1/1,000초), 두번째 매개변수 나노초(1/1,000,000,000초)단위이다. 이때 2번째 매개변수인 나노초에는 0~999,999까지만 지정해야한다.
thread.join(60000);
이번에는 interrupt() 메소드에 대해 살펴보자.
interrupt()메소드는 현재 수행중인 쓰레드를 중단시킨다. 그런데, 그냥 중지시키지는 않고, InterruptedException을 발생시키면서 중단시킨다. [ 이 예외는 sleep()과 join()메소드에서 발생한다고 했던 예외다.
즉, sleep()과 join()메소드와 같이 대기 상태를 만드는 메소드가 호출되었을 때에는 interrupt() 메소드를 호출 할 수 있다. 이 외에도, Object 클래스의 wait() 메소드가 호출된 상태에서도 이 메소드를 사용할 수 있다.
만약 쓰레드가 시작하기 전이나, 종료된 상태에 interrupt() 메소드를 호출하면 어떻게 될까?
==> 그러한 상황에서는 예외나 에러 없이 그냥 다음 문장으로 넘어간다.
[ 추가로, 자바의 Thread에는 stop()이라는 메소드가 있다. 이 stop() 메소드는 안전상의 이유로 deprecated(현재는 사용하지 않음) 되었으며, 이 메소드를 사용하면 안된다. 그러므로 interrupt() 메소드를 사용하여 쓰레드를 멈추어야 한다. ]
지금까지 배운 메소드들을 예제로 배워보자.
[EX] - sleep()
package part25;
public class SleepThread extends Thread {
long sleepTime;
public SleepThread(long sleepTime) {
this.sleepTime=sleepTime;
}
public void run() {
try {
System.out.println("Sleeping " +getName());
Thread.sleep(sleepTime);
System.out.println("Stopping "+getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
위의 코드를 보자.
생성자를 보면 밀리초 단위의 시간을 매개변수로 받아서 인스턴스 변수에 지정해 놓고, run()메소드에서 그 시간만큼 잠을 자는 것을 볼 수 있다. 그리고 잠자기 전과 후에 출력문을 놓아두어 상태가 변할 때를 알 수 있도록 해놓았다.
이제 RunSupportThreads 클래스를 만들자.
[EX]
package part25;
public class RunSupportThreads {
public static void main(String[] args) {
RunSupportThreads sample = new RunSupportThreads();
sample.checkThreadState1();
}
public void checkThreadState1() {
SleepThread thread = new SleepThread(2000); //1
try {
System.out.println("thread state="+thread.getState()); //2
thread.start();
System.out.println("thread state(after start)="+thread.getState());
Thread.sleep(1000); //3
System.out.println("thread state(after 1 sec)="+thread.getState());
thread.join(); //4
thread.interrupt(); //5
System.out.println("thread state(after join)="+thread.getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
thread state=NEW //1
thread state(after start)=RUNNABLE //2
Sleeping Thread-0
thread state(after 1 sec)=TIMED_WAITING //3
Stopping Thread-0
thread state(after join)=TERMINATED //4
위의 코드에서 숫자로 달린 주석을 보자.
- SleepThread의 생성자 매개변수에 2000이라는 값을 넘겨줌으로써, 해당 쓰레드가 2초동안 대기하도록 선언했다.
- 앞서 배운 상태 확인을 위한 getState()메소드를 사용하여 각 상황별 상태를 출력하도록 해 놓았다.
- sleep() 메소드를 사용하여 쓰레드가 시작하고 1초 동안 대기한 후 상태를 출력하도록 했다.
- join()메소드를 사용하여, 메소드가 끝날때까지 기다리도록 했다.
- 쓰레드가 종료된 이후에 interrupt() 메소드를 호출했다.
결과에서 숫자로 달린 주석을 보자.
- 아직 쓰레드가 시작한 상황이 아니다. 따라서 Thread.State 중 NEW 상태다.
- 쓰레드가 시작한 상황이며 아직 첫 출력문까지 도달하지 않았으므로, RUNNABLE 상태다.
- 2초간 잠자는 모드가 되어야 하므로, TIMED_WAITING 상태다.
- 쓰레드가 종료되기를 join() 메소드에서 기다린 후의 상태이므로, TERMINATED 상태다.
다음과 같이 checkJoin() 메소드를 만들어보자.
[EX]
public void checkJoin() {
SleepThread thread = new SleepThread(2000);
try {
thread.start();
thread.join(500);
thread.interrupt();
System.out.println("thread state(after join)="+thread.getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Sleeping Thread-0
thread state(after join)=TIMED_WAITING
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at part25.SleepThread.run(SleepThread.java:11)
위의 checkJoin()메소드 코드를 보자.
thread가 2초간 대기하기 때문에 checkJoin()메소드에서는 총 0.5초를 대기하게 된다. 따라서, thread가 종료하지 않은 상태다.
그러므로, interrupt() 메소드가 수행되고 해당 쓰레드가 중지된다. 그러므로, 위와 같이 InterruptedException이라는 것이 발생한 것을 볼 수 있다.
만약 join() 메소드의 매개변수에 "500"이 아닌 "2016"을 지정하게 되면 어떻게 될까?
==> 이 경우에는 대기를 거의 하지 않으므로 대기하지 않는다. 따라서, 쓰레드에 interrupt() 메소드를 호출해봤자 아무런 반응을 보이지 않으므로 다음과 같이 정상적인 결과를 출력한다.
Sleeping Thread-0
Stopping Thread-0
thread state(after join)=TERMINATED
Thread 클래스에 선언되어 있는 상태 확인을 위한 메소드는 다음과 같다.
interrupted()메소드는 잘 살펴보면 static 메소드다 따라서, 현재 쓰레드가 종료되었는지를 확인할 때 사용한다.
isInterrupted() 메소드는 다른 쓰레드에서 확인할 때 사용되고, interrupted()메소드는 본인의 쓰레드를 확인할 때 사용된다는 점이 다르다.
지금까지 살펴본 Thread 클래스에서 제공하는 메소드는 쓰레드를 생성하고 처리하는데 사용된다. JVM에서 사용되는 쓰레드의 상태들을 확인하기 위해서는 Thread 클래스의 static 메소드들을 알아야만 한다. 자바 기초를 배우는 나 같은 경우 이 메소드들을 자세히 살펴볼 일은 거의없을 것이다.
몇몇 메소드들은 알아두면 좋으므로 주요 static 메소드의 간단히 목록만 살펴보고 넘어가자.
지금까지는 Thread 클래스에 선언되어 있는 주요 메소드들을 알아보았다.
그런데 Thread 클래스에 선언되어 있는 메소드들 외에 Object 클래스에 있는 메소드들도 쓰레드를 통제하는데 사용된다.
■ 2-6. Object 클래스에 선언된 쓰레드와 관련있는 메소드들
Thread 클래스에 선언된 메소드 외에 쓰레드의 상태를 통제하는 메소드가 있다.
==> 바로 Object 클래스들에 선언되어 있는 메소드들이다. 아래를 보자.
간단하게 요약하자면 wait()메소드를 사용하면 쓰레드가 대기 상태가 되며, notify()나 notifyAll()메소드를 사용하면 쓰레드의 대기 상태가 해제된다. 예제를 보자.
[EX] - 쓰레드 클래스 생성
package part25;
public class StateThread extends Thread {
private Object monitor;
public StateThread(Object monitor) { //1
this.monitor=monitor;
}
@Override
public void run() {
try {
for(int loop=0;loop<10000; loop++) { //2
String a = "A";
}
synchronized (monitor) {
monitor.wait(); //3
}
System.out.println(getName()+" is notified.");
Thread.sleep(1000); //4
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
우의 코드에서 숫자 주석을 보자.
- monitor 라는 이름의 객체를 매개변수로 받아 인스턴스 변수로 선언해 두었다.
- 쓰레드를 실행중인 상태로 만들기 위해서 간단하게 루프를 돌면서 String 객체를 생성한다.
- synchronized 블록 안에서 monitor rorcpdml wait()메소드를 호출했다.
- wait() 상황이 끝나면 1초간 대기했다가 이 쓰레드는 종료한다.
[EX] - 쓰레드 클래스로부터 객체를 생성하여 사용 [ wait(), notify() ]
package part25;
public class RunObjectThreads {
public static void main(String[] args) {
RunObjectThreads sample = new RunObjectThreads(); //1
sample.checkThreadState2();
}public void checkThreadState2() {
Object monitor = new Object();
StateThread thread = new StateThread(monitor);
try {
System.out.println("thread state="+thread.getState());
thread.start(); //2
System.out.println("thread state(after start)="+thread.getState());
Thread.sleep(100);
System.out.println("thread state(after 0.1 sec)="+thread.getState());
synchronized (monitor) {
monitor.notify(); //3
}
Thread.sleep(100);
System.out.println("thread state(after notify)="+thread.getState());
thread.join(); //4
System.out.println("thread state(after join)="+thread.getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
thread state=NEW
thread state(after start)=RUNNABLE
thread state(after 0.1 sec)=WAITING
Thread-0 is notified.
thread state(after notify)=TIMED_WAITING
thread state(after join)=TERMINATED
위의 코드에서 숫자 주석을 봐보자.
- StateThread의 매개변수로 넘겨줄 monitor라는 Object 클래스 객체를 생성한다.
- 쓰레드 객체를 생성하고 시작한다.
- monitor 객체를 통하여 notify() 메소드를 호출한다.
- 쓰레드가 종료될 때까지 기다린 후 상태를 출력한다.
위에서 보는 것과 같이 wait()메소드가 호출되면, 쓰레드의 상태는 WAITING 상태가 된다. 이 쓰레드를 깨워주기 위해선 interrupt() 메소드를 호출하여 대기 상태에서 풀려날 수도 있겠지만, notify()메소드를 호출하여 대기 상태에서 풀려야 InterruptedException도 발생하지 않고, wait() 이후의 문장도 정상적으로 수행하게 된다.
즉, 다시 말해서 notify() 메소드는 wait()메소드를 정상적으로 깨우는 메소드라고 생각하면 된다.
[EX] - 쓰레드 클래스로부터 객체 2개를 생성하여 사용 [ wait(), notify() ]
public void checkThreadState3() {
Object monitor = new Object();
StateThread thread = new StateThread(monitor);
StateThread thread2 = new StateThread(monitor);
try {
System.out.println("thread state="+thread.getState());
thread.start();
thread2.start();
System.out.println("thread state(after start)="+thread.getState());
Thread.sleep(100);
System.out.println("thread state(after 0.1 sec)="+thread.getState());
synchronized (monitor) {
monitor.notify();
}
Thread.sleep(100);
System.out.println("thread state(after notify)="+thread.getState());
thread.join();
System.out.println("thread state(after join)="+thread.getState());
System.out.println("thread2 state(after join)="+thread2.getState());
System.out.println("");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
thread state=NEW
thread state(after start)=WAITING
thread state(after 0.1 sec)=WAITING
Thread-0 is notified.
thread state(after notify)=TIMED_WAITING
thread state(after join)=TERMINATED
출력 결과를 보면 이상이 없다고 생각할 수도 있다. 하지만 thread2는 notify(대기상태가 풀어지는것)되지 않았고, 끝나지도 않았다.
==> 왜냐하면 자바에서 notify()메소드를 호출하면 ,먼저 대기하고 있는 것부터 그 상태를 풀어주기 때문이다.
그래서 모든 쓰레드를 풀어주기 위해 notifyAll() 메소드를 사용하는 것이 좋다.
synchronized (monitor) {
// monitor.wait(); //3
monitor.notifyAll();
}
위처럼 코드를 변경하면 결과가 다음과 같이 바뀐다.
thread state=NEW
thread state(after start)=RUNNABLE
thread state(after 0.1 sec)=WAITING
Thread-0 is notified.
Thread-1 is notified.
thread state(after notify)=TIMED_WAITING
thread state(after join)=TERMINATED
thread2 state(after join)=TERMINATED
■ 2-7. ThreadGroup에서 제공하는 메소드들
앞에서 쓰레드 객체를 생성할 때 지정하는 ThreadGroup 클래스에 대해서 간단히 살펴보겠다.
ThreadGroup은 쓰레드의 관리를 용이하게 하기 위한 클래스다. 하나의 애플리케이션에는 여러 종류의 쓰레드가 있을 수 있으며, 만약 ThreadGroup 클래스가 없으면 용도가 다른 여러 쓰레드를 관리하기 어려울 것이다.
쓰레드 그룹은 기본적으로 운영체제의 폴더처럼 뻗어나가는 트리 구조를 가진다. 즉, 하나의 그룹에 속할 수도 있고, 그 아래에 또 다른 그룹에 포함할 수도 있다.
ThreadGroup 클래스에서 제공하는 메소드들
다른 메소드들은 이해가 쉽게 될 것이다.
여기서 enumerate() 메소드를 보면 해당 쓰레드 그룹에 포함된 쓰레드나 쓰레드 그룹의 목록을 매개 변수로 넘어온 배열에 담는다. 이 메소드의 리턴값은 배열에 저장된 쓰레드의 개수다.
==> 따라서, 쓰레드 그룹에 있는 모든 쓰레드의 객체를 제대로 담으려면 activeCount() 메소드를 통해서 현재 실행중인 쓰레드의 개수를 정확히 파악한 후,그 개수만큼 배열을 생성하면 된다.
[EX] - 쓰레드 그룹 생성 및 정보 확인
package part25;
public class RunGroupThreas {
public static void main(String[] args) {
RunGroupThreas sample = new RunGroupThreas();
sample.groupThread();
}
public void groupThread() {
try {
SleepThread sleep1 = new SleepThread(5000);
SleepThread sleep2 = new SleepThread(5000);
ThreadGroup group = new ThreadGroup("Group1");
Thread thread1 = new Thread(group,sleep1);
Thread thread2 = new Thread(group,sleep2);
thread1.start();
thread2.start();
Thread.sleep(1000);
System.out.println("Group name="+group.getName());
int activeCount=group.activeCount();
System.out.println("Active count="+activeCount);
group.list();
Thread[] tempThreadList = new Thread[activeCount];
int result = group.enumerate(tempThreadList);
System.out.println("Enumerate result="+result);
for(Thread thread:tempThreadList) {
System.out.println(thread);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Sleeping Thread-1
Sleeping Thread-0
Group name=Group1 //1
Active count=2 //2
java.lang.ThreadGroup[name=Group1,maxpri=10] //3
Thread[Thread-2,5,Group1] //3
Thread[Thread-3,5,Group1] //3
Enumerate result=2 //4
Thread[Thread-2,5,Group1]
Thread[Thread-3,5,Group1]
Stopping Thread-1
Stopping Thread-0
위의 출력 결과의 주석처리 부분을 보자.
1과 2에서 쓰레드 그룹 이름과 실행중인 쓰레드 개수를 확인할 수 있다. 그 다음 세 줄(3번)은 list() 메소드를 호출 했을 때의 결과이다.
4의 결과를 보면 enumerate() 메소드 수행 결과 2개의 데이터가 배열에 저장되었으며, 그 다음 두 줄에는 각 쓰레드에 대한 정보가 출력된 것을 볼 수 있다.
==> 위와 같이 쓰레드 그룹을 사용하면 쓰레드를 보다 체계적으로 관리 할 수 있다.
'JAVA > 자바의신 2' 카테고리의 다른 글
27장 Serializable과 NIO (1) | 2022.09.21 |
---|---|
24장 자바랭 다음으로 많이 쓰는 애들은 컬렉션 - Part3(Map) (1) | 2022.09.19 |
23장 자바랭 다음으로 많이 쓰는 애들은 컬렉션 - Part2(Set과 Queue) (0) | 2022.09.18 |
22장 자바랭 다음으로 많이 쓰는 애들은 컬렉션 - Part1(List) (0) | 2022.09.18 |
21장 실수를 방지하기 위한 제네릭 (0) | 2022.09.17 |