자바 안에서 말하는 쓰레드란?
자바 파일이 컴파일러에 의해 JVM에 적재되고 나면, 하나의 프로세스가 실행된다. 이 프로세스 안에서 실행하는 일련의 흐름 단위를 쓰레드라고 한다.
쓰레드를 생성하는 방법
1. Runnable 인터페이스 사용
Runnable
인터페이스는 java.lang
패키지에 있다. 그리고 이 인터페이스는 run()
이라는 메소드만 선언되어있다.
In most cases, the Runnable interface should be used if you are only planning to override the run() method and no other Thread methods. This is important because classes should not be subclassed unless the programmer intends on modifying or enhancing the fundamental behavior of the class.
보통,Thread
클래스에서 다른 메소드를 사용하지 않고 ,run()
메소드만 사용하고 싶을 경우Runnable
인터페이스를 통해 쓰레드를 생성한다고 한다.
Runnable 인터페이스를 이용하여 스레드를 생성하는 코드예제
package e.thread;
public class RunnableSample implements Runnable{
@Override
public void run() {
System.out.println("This is Runnable Sample's run() method");
}
}
package e.thread;
public class RunThreads {
public static void main(String[] args) {
RunThreads threads = new RunThreads();
threads.runBasic();
}
private void runBasic() {
RunnableSample runnableSample = new RunnableSample();
new Thread(runnableSample).start();
System.out.println("RunThreads.runBasic() method is ended");
}
}
Runnable
구현 클래스 자체는 스레드가 아니다. 스레드가 실행하는 코드만 포함하고 있는 클래스이다. 따라서, Runnable
구현 클래스로 생성한 객체를 Thread
클래스 생성자의 인수로 넘겨 호출해야만 작업 스레드를 실행할 수 있다.
2. Thread 클래스 사용
Thread 클래스는 Runnable 인터페이스를 구현한 클래스이다. 상속받아서 사용해야 하므로 다른 클래스를 상속받을 수 없다.
Thread 클래스를 상속받아 스레드를 생성하는 코드예제
package e.thread;
public class ThreadSample extends Thread{
public void run(){
System.out.println("This is ThreadSample's run() method.");
}
}
package e.thread;
public class RunThreads {
public static void main(String[] args) {
RunThreads threads = new RunThreads();
threads.runBasic();
}
private void runBasic() {
ThreadSample threadSample = new ThreadSample();
threadSample.start();
System.out.println("RunThreads.runBasic() method is ended");
}
}
여기서 우리가 염두해야 할 것은,
- 쓰레드가 수행될 때는
run()
이 실행된다. - 쓰레드를 시작하는 메소드는
start()
이다.- 해당 메소드 실행 시, 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택을 생성한 다음,
run()
을 호출해 그 안에run()
이 저장된다. 즉, 쓰레드를 사용하기 위해start()
를 실행시키는 순간, 쓰레드만의 독립적인 작업 공간인 호출스택이 만들어지는 것이다.
- 해당 메소드 실행 시, 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택을 생성한 다음,
- 한 번 사용한(
start()
가 이미 호출된) 쓰레드는 재사용 할 수 없다.
그래서, Runnable 인터페이스를 구현하거나, Thread 클래스를 확장할 때는 run()
메소드를 시작점으로 작성해야 한다.
그렇다면 왜 이 두가지 방법을 제공하는가?
- 쓰레드 클래스가 다른 클래스를 확장할 필요가 있을 경우에는
Runnable
인터페이스를 구현한다. - 그렇지 않을 경우(다른 클래스 확장 x)에는 쓰레드 클래스를 사용하는 것이 편하다.
Thread 클래스의 주요 메소드
run()
: 쓰레드 실행시 구현되어야 하는 부분getId()
: 쓰레드의 고유 ID 리턴getName()
,setName(String name)
: 쓰레드의 이름을 리턴, 설정getPriority()
,setPriority(int newPriority)
: 쓰레드의 우선 순위 확인, 지정MAX_PRIORITY
: 가장 높은 우선순위, 값은 10이다.NORM_PRIORITY
: 일반 쓰레드의 우선순위, 값은 5이다.MIN_PRIORITY
: 가장 낮은 우선순위, 값은 1이다.
isDaemon()
,setDaemon(boolean on)
: 쓰레드 데몬여부 확인, 데몬으로 설정getStackTrace()
: 쓰레드의 스택정보 확인getState()
: 쓰레드의 상태 확인getThreadGroup()
: 쓰레드의 그룹 확인
package e.thread;
public class RunDaemonThreads {
public static void main(String[] args) {
RunDaemonThreads sample = new RunDaemonThreads();
sample.checkThreadProperty();
}
private 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 =13
thread2 id =14
thread1 name =Thread-0
thread2 name =Thread-1
thread1 priority =5
thread1 isDaemon=false
daemonThread isDaemon=true
쓰레드의 종류
- 사용자 쓰레드(비데몬 쓰레드) : 사용자가 생성하여 제어하는 쓰레드, JVM이 해당 쓰레드가 끝날 때까지 기다린다.
- 데몬 쓰레드 : 수행 완료여부에 상관없이 JVM이 마칠 수 있다. 일반적으로
start()
전에 데몬 쓰레드로 셋팅하면 사용자 쓰레드를 데몬 쓰레드로 변경 할 수 있다.- 사용자가 직접 생성하지 않아도 기본 적으로 데몬 쓰레드를 여러개 생성한다.
- 가비지 컬렉터, 모니터링 자동저장, 화면 갱신등에 주로 데몬 쓰레드를 이용한다.
package e.thread;
public class DaemonThread extends Thread{
public void run(){
try{
Thread.sleep(Long.MAX_VALUE);
} catch (Exception e){
e.printStackTrace();
}
}
}
package e.thread;
public class RunDaemonThreads {
public static void main(String[] args) {
DaemonThread thread = new DaemonThread();
//thread.setDaemon(true);
//thread.start();
}
주석 해제를 하지 않고 그대로 실행하면, 함수가 종료되지 않고 계속 수행되는 것을 볼 수 있다.
여기서 주석 해제를 하고 (=> 쓰레드를 데몬 쓰레드로 셋팅하고) 실행하면, 프로세스가 바로 종료된다.
synchronized
앞서 여러 곳에서 thread-safe
라는 단어를 여러 차례 썼다. 이 말은 즉슨, 여러 쓰레드에서 어떤 한 객체를 접근하더라도 도이에 연산을 수행하여 값이 꼬이는 경우가 없다는 말이다. 이렇게 데이터 충돌 현상 없이 수행하기 위해서는 synchronized
를 사용하는데, 아래 두 가지 방법으로 사용할 수 있다고 한다.
- 메소드 자체를
synchronized
로 선언하는 방법(synchronized methods) - 메소드 내 특정 문장만
synchronzied
로 감싸는 방법(synchronized statements)
메소드를 synchronized
로 선언하는 방법
백문이 불여일견, 코딩을 해보며 확인해본다.
package e.thread.sync;
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;
}
}
package e.thread.sync;
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 i = 0; i < 10000; i++){
if(addFlag) calc.plus(1);
else calc.minus(1);
}
}
}
package e.thread.sync;
public class RunSync {
public static void main(String[] args) {
RunSync runSync = new RunSync();
runSync.runCommonCalculate();
}
private void runCommonCalculate() {
CommonCalculate calc = new CommonCalculate();
ModifyAmountThread thread1 = new ModifyAmountThread(calc,true);
ModifyAmountThread thread2 = new ModifyAmountThread(calc, true);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println("Final value is " + calc.getAmount());
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
각각 계산을 연산을 수행하는 클래스, 더하기/빼기를 멀티쓰레드로 수행하는 클래스, 상위 2개의 객체를 실질적으로 실행시키는 메인메소드로 구분지을 수 있다.
여기서 코드를 실행해보면, 20000
이 아닌 훨씬 더 작은 숫자가 나온다. CommonCalculate
객체 내 amount
인스턴스 변수 때문이다.
public void plus(int value){
amount += value;
}
위 코드 실행 구간에서, 기존 amount에서 value 값을 더해 amount 값에 대입하기 직전, 다른 쓰레드가 들어와서 다시 더하기 연산을 수행하게 된다. 결국 좌측 항에 치환이 2번 되어야 하는데 1번만 되는 이런 현상이 발생하여 결과적으로는 20000
보다 더 작은 숫자가 출력되는 것이다.
그래서 우리는 해당 코드를 아래와 같이 수정한다.
public synchronized void plus(int value){
amount += value;
}
이렇게 예약어 synchronized
를 추가함으로써 동일한 객체를 참조하는 다른 쓰레드에서 이 메소드를 변경하려고 하면, 먼저 들어온 쓰레드가 종료될 때 까지 기다렸다가 수행을 하게 된다. (다시 실행해보면 20000
이 출력된다!)
하지만 synchronized
는 성능상 문제점이 있는데 ?
public synchronized void plus(int value){
amount += value;
methodA();
methodB();
methodC();
methodD();
methodE();
}
위와 같은 상황이 발생하였을 때, amount 인스턴스 변수 충돌 문제를 위해서 후속단의 메소드A~E를 처리 할 때 필요 없는 대기시간이 발생한다.(메소드 A~E에서는 동기화가 필요 없을 때를 가정한다.
이러한 경우, 메소드 전체를 감싸기 보다는 동기화가 필요한 변수만 synchronized 블록처리를 해주면 좋다.
Object lock = new Object();
public synchronized void plus(int value){
synchronized(lock){
amount += value;
}
methodA();
methodB();
methodC();
methodD();
methodE();
}
이렇게 변경하면, amount
변수를 처리하는 부분만 여러 쓰레드에서 처리를 하지 않도록 설정할 수 있다.
추가적으로, 클래스 내 여러 인스턴스 변수가 선언되어 각 변수별로 synchroznied
처리를 해주어야 할 경우, 여러 lock
객체를 생성하여 각각 Thread-safe하게 처리 할 수 있다.
Object amountLock = new Object();
Object hitLock = new Object();
public synchronized void plus(int value){
synchronized(amountLock){
amount += value;
}
synchronized(hitLock){
hit += 1;
}
methodA();
methodB();
methodC();
methodD();
methodE();
}
쓰레드의 상태와 제어
쓰레드의 상태는 java.lang.Thread
클래스 내부에 State
라는 이름을 가진 열거형 클래스로 관리되고 있다.
- NEW : 쓰레드가 생성되었지만 아직 실행되지 않은 상태
- RUNNABLE: 현재 CPU를 점유하고 작업을 수행 중인 상태. 운영체제의 자원분배(컨텍스트 스위칭 등)으로 인해 WAITING, BLOCKED 등이 될 수 있다.
- BLOCKED :
Monitor
를 획득하기 위해 다른 스레드가 락을 해제하기를 기다리는 상태 - WAITING :
wait()
,join()
,park()
메소드 등을 이용해 대기하고 있는 상태 - TIME_WAITING :
sleep()
,wait()
,join()
등을 이용해 대기하고 있는 상태 (WAITING
상태와의 차이점은 메서드의 인수로 최대 대기 시간을 명시할 수 있어, 외부적 변화 뿐 아니라 시간에 의해서도 대기 상태가 해제 될 수 있다는 점) - TERMINATED : 쓰레드가 종료된 상태, 사실상 소멸된 것으로 이 상태로 존재하지는 않는다.
쓰레드 객체의 상태를 제어하는 Thread
클래스의 메소드
sleep()
: 주어진 시간동안 쓰레드를WAITING
상태로 만든다.- 대기 시간동안
interrupt()
메소드가 호출되면InterruptedException
이 발생하기 때문에 예외처리가 필요하다.
- 대기 시간동안
interrupt()
: 일시정지 상태의 쓰레드에서InterruptedException
예외를 발생시켜, 예외 처리 코드(catch)에서 실행 대기 상태로 가거나, 종료 상태로 갈 수 있도록 한다.- 쓰레드 시작 전, 혹은 종료된 상태에서 호출 시 예외나 에러 없이 다음 문장으로 넘어간다.
join()
: 다른 쓰레드가 실행을 종료할 때 까지 대기한다.- 대기 시간동안
interrupt()
메소드가 호출되면InterruptedException
이 발생하기 때문에 예외처리가 필요하다.
- 대기 시간동안
isAlive()
: 쓰레드가 살아있는지 확인한다.yield()
: 현재 실행중인 쓰레드는 양보를 하고 다음 우선순위를 가진 쓰레드가 실행할 수 있도록 하는 메소드이다.
쓰레드 객체의 상태를 제어하는 Object
클래스의 메소드
wait()
,wait(long timeout)
,wait(long timeout, int nanos)
: 동기화 블록 내에서 쓰레드를 일시정지 상태로 만든다. 매개변수로 시간을 입력하지 않을 경우,notify()
,notifyAll()
등 메소드에 의해 실행 대기상태로 갈 수 있다.notify()
,notifyAll()
: 동기화 블록 내에서wait()
메소드에 의해 일시 정지 상태에 있는 쓰레드를 실행 대기 상태로 만든다.
쓰레드 그룹
쓰레드 그룹은 관련된 쓰레드를 묶어 관리할 목적으로 이용된다. JVM이 실행될 때, system 쓰레드 그룹을 만들고, JVM은 운영에 필요한 쓰레드들을 생성한다. 여기서 생성한 쓰레드들을 system 쓰레드 그룹에 포함시킨다.
참고 자료 :