이번에는 스레드 상태에 이어서 스레드 상태 제어에 대하여 알아 보겠습니다.

 

스레드 상태에 대한 부분은 다음의 링크를 통해 제 글을 보고 와주세요!

(제 설명이 부족했을 수도 있으니 검색을 통해서 본인이 알아보고 오셔도 좋습니다!)

 

Java - 스레드 상태(Thread State)

 

Java - 스레드 상태(Thread State)

이번에는 스레드 상태에 대해서 알아보도록 하겠습니다. 스레드 프로그래밍을 진행하다보면 동기화/비동기화 뿐만 아니라 스레드의 행동을 직접 제어해야 할 경우가 생깁니다. 1000장의 문서를 복사해야한다고 가..

widevery.tistory.com

스레드 상태 제어는 총 3개의 part로 나누어서 진행할 예정입니다.

 

스래드의 상태 중 실행, 실행 대기, 일시 정지 3가지를 조금 더 구체적으로 살펴보면 다음과 같습니다.

 

[그림 1 - 스레드의 실행, 실행 대기, 일시 정지의 구체적인 상태]

 

그림 1 에서 잘 보이지 않지만 취소선이 있는 suspend(), resume(), stop() 이 3가지는 스레드의 안전성을 해친다고 하여 더 이상 사용하지 않도록 권장된 Deprecated 메소드라고 합니다.

 

 

스레드 상태 제어와 관련된 메소드들을 다음의 표를 통하여 알아보도록 하겠습니다.

 

메소드 설명
interrupt() 일시 정지 상태의 스레드에서 InterruptedException 예외를 발생시켜, 예외 처리 코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 한다.

notify()

notifyAll()

동기화 블록 내에서 wait() 메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만든다.
resume()

suspend(0 메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만든다.

 - Deprecated (대신 notify(), notifyAll() 사용)

sleep(long millis)

sleep(long millis, int nanos)

주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다.

join()

join(long millis)

join(long millis, int nanos)

join() 메소드를 호출한 스레드는 일시 정지 상태가 된다. 실행 대기 상태로 가려면, join() 메소드를 멤버로 가지는 스레드가 종료되거나, 매개값으로 주어진 시간이 지나야 한다.

wait()

wait(long millis)

join(long millis, int nanos)

동기화(synchronized) 블록 내에서 스레드를 일시 정지 상태로 만든다. 매개값으로 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. 시간이 주어지지 않으면 notify(), notifyAll() 메소드에 의해 실행 대기 상태로 갈 수 있다.
suspend() 스레드를 일시 정지 상태로 만든다. resume() 메소드를 호출하면 다시 실행 대기 상태가 된다. - Deprecated (대신 wait() 사용)
yield() 실행 중에 우선순위가 동일한 다른 스레드에게 실행을 양보하고 실행 대기 상태가 된다.
stop() 슬드를 즉시 종료시킨다. - Deprecated

 

위 표에서 wait(), notify(), notifyAll() 은 Object 클래스의 메소드이고, 그 이외의 메소드는 Thread 클래스의 메소드입니다.

 

이번 글에서는 sleep(), yield(), join() 메소드에 대하여 알아 보도록 하겠습니다.

 

 

 

 

 

1. 주어진 시간동안 일시 정지(sleep())

 

   · Thread.sleep() 메소드를 호출한 스레드는 주어진 시간 동안 일시 정지 상태가 되고, 다시 실행 대기 상태로 돌아갑니다.

   · 매개값에는 얼마 동안 일시 정지 상태로 있을 것인지, 밀리세컨드(1/1000) 단위로 시간을 주면 됩니다. ex) Thread.sleep(3000); ->3초

   · 일시 정지 상태에서 주어진 시간이 되기 전에 interrupt() 메소드가 호출되면 InterruptedException이 발생하기 때문에

     예외 처리가 필요합니다.

 

 

 

다음은 sleep() 예제 입니다.

 

 

 

SleepExample.java - 3초 주기로 10번 비프음 발생

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package thread_state_control;
 
 
public class SleepExample {
 
    public static void main(String[] args) {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        for(int i=0; i<10; i++) {
            toolkit.beep();
            try {
                Thread.sleep(3000);    // 3초 동안 메인 스레드를 일시 정지 상태를 만듭니다.
            } catch(InterruptedException e) {
                
            }
        }
    }
}
 

 

실행해보면 정상적으로 소리가 3초 간격으로 10번 동안 나는 것을 확인 하실 수 있습니다.

 

 

 

 

 

2. 다른 스레드에게 실행 양보(yield())

 

   · yield()는 실행 되고 있는 스레드 주체가 혼자만 작동하지 않고, 다른 스레드가 실행 기회를 가질 수 있도록 해줍니다.

   · yield()는 sleep()과 함께 글의 마지막 부분에 제 나름대로의 설명을 추가하도록 하겠습니다.

 

 

다음은 yield()를 사용 했을 때의 스레드들의 상태 변화를 나타낸 것입니다.

 

[그림 2 - yield()로 인한 스레드의 상태 변화]

 

다음은 yield() 예제 입니다.

 

 

 

YieldExample.java - 스레드 실행 양보 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package thread_state_control;
 
public class YieldExample {
 
    public static void main(String[] args) {
        ThreadA threadA = new ThreadA();
        ThreadB threadB = new ThreadB();
        // ThreadA, ThreadB 모두 실행
        threadA.start();
        threadB.start();
        
        try { Thread.sleep(3000); } catch (InterruptedException e) {}
        threadA.work = false;    // ThreadB만 실행
        
        try { Thread.sleep(3000); } catch (InterruptedException e) {}
        threadA.work = true;        // ThreadA, ThreadB 모두 실
        
        try { Thread.sleep(3000); } catch (InterruptedException e) {}
        // ThreadA, ThreadB 모두 종료
        threadA.stop = true;
        threadB.stop = true;
    }
}
 

 

ThreadA.java - 스레드 실행 양보 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package thread_state_control;
 
public class ThreadA extends Thread {
 
    public boolean stop = false;
    public boolean work = true;
    
    @Override
    public void run() {
        while(!stop) {    // stop이 true가 되면 while문 종료
            if(work) {
                System.out.println("ThreadA 작업 내용");
            } else {
                Thread.yield();    // work가 false가 되면 다른 스레드에게 실행 양보
            }
        }
        System.out.println("ThreadA 종료");
    }
}
 

 

ThreadB.java - 스레드 실행 양보 예제 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package thread_state_control;
 
public class ThreadB extends Thread {
 
    public boolean stop = false;
    public boolean work = true;
    
    @Override
    public void run() {
        while(!stop) {    // stop이 true가 되면 while문 종료
            if(work) {
                System.out.println("ThreadB 작업 내용");
            } else {
                Thread.yield();    // work가 false가 되면 다른 스레드에게 실행 양보
            }
        }
        System.out.println("ThreadB 종료");
    }
}
 

 

실행을 하게 되면 ThreadA와 ThreadB가 번갈아가며 실행이 되다가 yield()로 인하여 ThreadB가 더 많은 실행 기회를 가지게 되어서

ThreadB만 실행이 되는 상황이 나옵니다.

그러다 다시 ThreadA와 ThreadB가 번갈아가며 실행이 되면서 결국 마지막에 모든 스레드가 종료가 됩니다.

 

 

 

 

 

 

3. 다른 스레드의 종료를 기다림(join())

 

   · 스레드는 다른 스레드와 독립적으로 실행하는 것이 기본입니다.

   · 하지만 다른 스레드가 종료될 때까지 기다렸다가 실행해야 하는 경우가 발생할 수도 있습니다. 

   · join() 메소드는 sleep(), yield() 와 함께 마지막 부분에서 제 나름대로의 방식으로 설명을 추가하겠습니다.

 

 

다음 그림은 join()을 사용 했을 때의 스레드 상태를 나타낸 것입니다.

 

[그림 3 - join() 사용시 스레드의 상태]

 

 

다음은 yield()를 사용한 예제 입니다.

 

 

 

SumThread.java - 1부터 100까지 합을 계산하는 스레드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package thread_state_control;
 
public class SumThread extends Thread {
 
    private long sum;
    
    public long getSum() {
        return sum;
    }
    
    public void setSum(long sum) {
        this.sum = sum;
    }
    
    @Override
    public void run() {
        for(int i=1; i<=100; i++) {
            sum+=i;
        }
    }
}
 

 

JoinExample.java - 다른 스레드가 종료될 때까지 일시 정지 상태 유지

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package thread_state_control;
 
public class JoinExample {
 
    public static void main(String[] args) {
        SumThread sumThread = new SumThread();
        sumThread.start();
        
        try {
            sumThread.join();    // sumThread가 종료할 때까지 메인 스레드를 일시 정지시킴
        } catch(InterruptedException e) {
        }
        
        System.out.println("1~100 합: " + sumThread.getSum());
    }
}
 

 

join() 예제를 실행해 보시면 정상적으로 5050이 출력 되는 것을 확인 하실 수 있습니다.

하지만 JoinExample의 9 ~ 12 행인 try ~ catch 구문을 주석처리 하고 실행하면 0이 나오게 됩니다.

(교재에서는 컴퓨터 성능에 따라서 다른 값이 출력 될 수도 있다고 합니다. 저는 0이 나왔습니다.)

이러한 이유는 SumThread가 계산 작업을 완료하지 않은 상태에서 합을 먼저 출력하기 때문입니다.

 

이렇듯 sleep(), yield(), join()에 대하여 알아 보았습니다.

 

개인적으로 sleep() yield() join()이 다 비슷비슷 하게 느껴져서 헷갈리지만 제 나름대로의 생각은 다음과 같습니다.

 

1. sleep()의 예시

   - 혼자서 프린터를 사용하여 문서 1000장을 복사하는데 너무 지치니까 100장 마다 5분 씩 쉬고 다시 복사를 합니다.

2. yield()의 예시

   - 프린터를 사용하여 문서 1000장을 복사하는데 복사하는 도중, 다른 사람이 사용하려 하면 사용하도록 하고 자신은 잠시 쉽니다.

3. join()의 예시

   - 하나의 내용에 대한 문서가 2000장 입니다. 이 때, 1000장 씩 나눠서 역할을 분담 받았습니다.

     단, 2000장의 문서가 모두  모여야 내용이 완전해지고, 그렇지 않을 경우 의미 없고, 알 수 없는 내용의 문서가 되어 버립니다.

   - 결국 다른 사람이 담당하는 문서의 내용을 합하여 하나의 내용으로 만들어야 하기 때문에

     다른 사람이 담당하는 역할이 끝날 때까지 기다립니다.

 

대략 이런 식으로 sleep(), yield(), join()을 이해를 해 봤습니다. (정확한지는 모르겠습니다.)

피드백을 주시면 감사히 받겠습니다.

 

 

이번에는 스레드 상태에 대해서 알아보도록 하겠습니다.

 

스레드 프로그래밍을 진행하다보면 동기화/비동기화 뿐만 아니라 스레드의 행동을 직접 제어해야 할 경우가 생깁니다.

 

1000장의 문서를 복사해야한다고 가정합니다.

공유된 프린터는 1개 밖에 없는데, 이 때 1000장을 전부 복사하는 동안 혼자서 쓸 수 없을 겁니다.

다른 사람들도 프린터를 사용해야 하는 경우가 분명 생길거에요.

그럴 때, 100장씩 복사를 하도록 하고, 그 때마다 다른 사람들이 사용하도록 통제할 수도 있을거에요.

 

이처럼 스레드도 상태를 제어해야 할 필요성이 있는데, 제어를 하기 전에 해당 스레드의 상태를 알아야 합니다.

그러기 위해서는 getState() 메서드를 사용하여 상태를 확인합니다.

 

□ 스레드 상태

    · 스레드는 start() 하게 되면 일반적으로 다음과 같은 상태로 진행 됩니다.

[그림 1 - 스레드의 일반적인 실행 상태]

 

    · 경우에 따라서 스레드는 실행 상태에서 실행 대기 상태로 가지 않을 수도 있습니다.

    · 실행 상태에서 일시 정지 상태로 가기도 하는데, 일시 정지 상태는 스레드가 실행할 수 없는 상태입니다.

    · 스레드가 다시 실행 상태로 가기 위해서는 일시 정지 상태에서 실행 대기 상태로 가야합니다.

 

[그림 2 - 스레드의 실행과 일시 정지 상태]

 

이처럼 프로그래밍 하면서 스레드의 상태를 알 수 있도록 해주는 메소드는 getState() 입니다.

getState()의 스레드 상태에 따른 Thread.State 열거 상수가 있는데 열거 상수는 다음의 표와 같습니다.

 

상태 열거 상수 설명
객체 생성 NEW 스레드 객체가 생성, 아직 start() 메소드가 호출되지 않은 상태
실행 대기 RUNNABLE 실행 상태로 언제든지 갈 수 있는 상태
일시 정지 WAITING 다른 스레드가 통지할 때까지 기다리는 상태
TIMED_WAITING 주어진 시간 동안 기다리는 상태
BLOCKED 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태
종료 TERMINATED 실행을 마친 상태

 

이제 프로그래밍을 통해 스레드의 상태를 직접 확인해 봅시다.

 

이 예제는 스레드의 상태를 출력해주며, 생성자 매개값으로 받은 타겟 스레드의 상태를 0.5초 주기로 출력 해주는 클래스 입니다.

 

 

 

StatePrintThread.java - 타겟 스레드의 상태를 출력하는 스레드

package thread_state;

public class StatePrintThread extends Thread {

	private Thread targetThread;
	
	public StatePrintThread(Thread targetThread) {
		this.targetThread = targetThread;
	}
	
	@Override
	public void run() {
		while(true) {
			Thread.State state = targetThread.getState();	// 스레드 상태 얻기
			System.out.println("타겟 스레드 상태: " + state);
			
			// 객체 생성 상태일 경우 실행 대기 상태로 만듬
			if(state == Thread.State.NEW) {
				targetThread.start();
			}
			
			// 종료 상태일 경우 while문을 종료함
			if(state == Thread.State.TERMINATED) {
				break;
			}
			try {
				Thread.sleep(500);
			} catch(Exception e) {}
		}
	}
}

 

 

다음은 타겟 스레드 클래스입니다.

10억 번 루핑을 돌게 해서 RUNNABLE 상태를 유지하고, sleep() 메소드를 호출해서 1.5초간 TIMED_WAITING 상태를 유지합니다.

또한 다시 10억 번 루핑을 돌게 해서 RUNNABLE 상태를 유지합니다.

 

 

TargetThread.java - 타겟 스레드

package thread_state;

public class TargetThread extends Thread {

	@Override
	public void run() {
		for(long i=0; i<1000000000; i++) {}
		
		try {
			Thread.sleep(1500);
		} catch(Exception e) {}
		
		for(long i=0; i<1000000000; i++) {}
	}
}

 

TargetThread가 객체로 생성되면 NEW 상태를 가지고, run() 메소드가 종료되면 TERMINATED 상태가 되므로 결국 다음과 같은 상태로 변합니다.

 

NEW -> RUNNABLE -> TIMED_WAITING -> RUNNABLE -> TERMINATED

 

다음은 StatePrintThread를 생성해서 매개값으로 전달받은 TargetThread의 상태를 출력하는 클래스입니다.

 

 

 

ThreadStateExample.java - 실행 클래스

package thread_state;

public class ThreadStateExample {

	public static void main(String[] args) {
		StatePrintThread statePrintThread = 
				new StatePrintThread(new TargetThread());
		statePrintThread.start();
	}
}

 

실행을 하면

 

타겟 스레드 상태: NEW

타겟 스레드 상태: RUNNABLE

타겟 스레드 상태: RUNNABLE

타겟 스레드 상태: TIMED_WAITING

타겟 스레드 상태: TIMED_WAITING

타겟 스레드 상태: TIMED_WAITING

타겟 스레드 상태: RUNNABLE

타겟 스레드 상태: RUNNABLE

타겟 스레드 상태: RUNNABLE

타겟 스레드 상태: TERMINATED

 

이와 같은 실행 결과를 확인할 수 있다.. 고 이것이 자바다에 나와 있습니다.

 

저는 다음과 같은 상태변화만 확인할 수 있었습니다.

 

타겟 스레드 상태: NEW

타겟 스레드 상태: TIMED_WAITING

타겟 스레드 상태: TIMED_WAITING

타겟 스레드 상태: TIMED_WAITING

타겟 스레드 상태: RUNNABLE

타겟 스레드 상태: TERMINATED

 

제가 자세하게 안다면 이와 같은 현상에 대해서 자세하게 알 수 있을텐데 아직 많이 부족하여 잘 모르겠네요..

첫 번째 10억 번 루핑도는 부분에서 상태가 잡히지 않는 것 같습니다.

출력을 찍어보면 정상적으로 되는 것 같은데 해당 상태가 RUNNABLE이 맞다고 생각하는데 첫 번째 루핑 넘어간 다음 sleep()으로 바로 넘어가서 TIMED_WAITING이 출력되네요.

 

시스템 환경을 다양하게해서 확인해 봐야 할 것 같습니다.

 

 

안녕하세요 활짝웃자입니다.

Java / HTML5 / CSS / JSP / Spring / DataBase : Postgresql / Apache Tomcat을 모두 사용하게 되어서

기본 문법만 하고 프로그래밍 했었는데 필요하게 된 부분들이 많아져서 글을 작성하게 되었습니다.

 

이것이 자바다 교재를 사용하여 진행하였습니다.

총 2권으로 나누어져 있는데, 1권은 기본 문법 + Java에서 자주 사용되는 API등을 다루는 내용입니다.

2권은 멀티스레드, 제네릭, 람다식, 컬렉션 프레임워크, 스트림과 병렬 처리, 네티워킹 등 기본 문법을 숙지 후, 개발 업무 시 자주 사용되는 부분을 설명하는 내용입니다.

 

Java 게시글은 다음과 같은 방식으로 진행 됩니다.

1. 이것이 자바다 교재가 아니어도 Java 기본 문법을 알고 계신다고 생각하며 진행합니다.

2. 모든 내용을 다루지는 않을 것이며 참고한 부분은 분명하게 출처를 밝히면서 진행합니다.

 

 

 

멀티태스킹두 가지 이상의 작업을 동시에 처리하는 것입니다.

이때 사용되는 방법 중 하나인 멀티 스레드가 있습니다.

멀티스레드에서 동기화, 비동기화의 개념이 존재 하는데 해당 개념은 다음의 출처를 통해 확인해 주시기 바랍니다.

 

Synchronous(동기) vs Asynchronous(비동기)

 

Synchronous(동기) Vs Asynchronous(비동기)

 

nesoy.github.io

동기화를 실험해 보기 위해 비동기를 먼저 확인해 보도록 합시다.

 

 

Calculator.java - 공유객체

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package thread_synchronized;
 
public class Calculator {
 
    private int memory;
    
    public int getMemory() {
        return memory;
    }
    
    public  void setMemory(int memory) {
        this.memory = memory;
        try {
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + ": " + this.memory);
        } catch(InterruptedException e) {
            /* 이것이 자바다 교재에서는 이 부분(catch)으로 실행이 되는 것 같지만
             * 제가 실행할 때는 정상적으로 출력이 되지 않아서
             * try 부분에 확인을 위한 코드를 삽입 하였습니다.
             */
            System.out.println(Thread.currentThread().getName() + ": " + this.memory);
        }
    }
}
 

 

 

User1.java - User1 스레드

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package thread_synchronized;
 
public class User1 extends Thread {
 
    private Calculator calculator;
    
    public void setCalculator(Calculator calculator) {
        this.setName("User1");
        this.calculator = calculator;
    }
    
    public void run() {
        calculator.setMemory(100);
    }
}
 

 

 

User2.java - User2 스레드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package thread_synchronized;
 
public class User2 extends Thread {
 
    private Calculator calculator;
    
    public void setCalculator(Calculator calculator) {
        this.setName("User2");
        this.calculator = calculator;
    }
    
    public void run() {
        calculator.setMemory(50);
    }
}
 

 

 

MainThreadExample.java - 메인 스레드가 실행하는 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package thread_synchronized;
 
public class MainThreadExample {
 
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        
        User1 user1 = new User1();
        user1.setCalculator(calculator);
        user1.start();
        
        User2 user2 = new User2();
        user2.setCalculator(calculator);
        user2.start();
    }
}

 

MainThreadExample.java를 실행하면 다음과 같은 결과를 확인 하실 수 있습니다.

 

User1: 50

User2: 50

 

위와 같이 동시에 실행하는 것만 생각하고, 공유가 되면 안 되는 부분을 해결하기 위해 synchronized를 사용합니다.

 

MainThreadExample, User1, User2는 동일하므로 Calculator만 수정합니다.

 

 

수정된 Calculator.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package thread_synchronized;
 
public class Calculator {
 
    private int memory;
    
    public int getMemory() {
        return memory;
    }
    
    public synchronized void setMemory(int memory) {
        this.memory = memory;
        try {
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + ": " + this.memory);
        } catch(InterruptedException e) {
            /* 이것이 자바다 교재에서는 이 부분(catch)으로 실행이 되는 것 같지만
             * 제가 실행할 때는 정상적으로 출력이 되지 않아서
             * try 부분에 확인을 위한 코드를 삽입 하였습니다.
             */
            System.out.println(Thread.currentThread().getName() + ": " + this.memory);
        }
    }
}
 

 

Calculator.java를 위와 같이 수정해 주시고, 실행하시면 다음과 같이

 

User1: 100

User2: 50

 

동시에 실행을 하지만 공유가 되지 않은 결과가 출력되는 것을 확인하실 수 있습니다.

 

 

소개해드린 예제는 이것이 자바다에 있습니다!

동시에 실행은 해야 하지만 공유가 되면 안 되는 동기화 프로그래밍을 해야 되는 경우가 의외로 많습니다.

 

제가 진행했던 개인 프로젝트 중 Java를 사용한 간단한 Hash비교 기능과 랜섬웨어에 의하여 암호화된 파일들을 복호화 해주는 백신 아닌 백신을 제작하였었는데, 당시 빠른검사, 정밀검사 기능을 제작하던 중, GUI에서 Progress Bar를 사용하여 검사 중인 파일의 경로 + 파일명, 진행상태 표시를 위한 Progress Bar 를 실행하니 실시간으로 진행되는 파일명 출력과 게이지가 차지를 않고, 검사 종료 후, 맨 마지막에 검사한 파일의 파일명만 출력되고, 게이지 또한 공백 상태에서 꽉찬 게이지 형식으로 변경이 되었었습니다.

 

당시 pannel thread와 검사 thread 간의 임계영역 간섭 문제였던걸로 생각이 들어서 검사 thread에 synchronized를 적용하니 정상적으로 일반 백신처럼 진행되는 것을 확인 할 수 있었습니다.

 

모든 상황이 저와 같지는 않으시겠지만 굉장히 다양한 경우가 많으니 검색등을 통하여 알아보시기 바랍니다!

 

 

+ Recent posts