Dev/Java

Thread 를 알아보자 - mulit vs single, PrioirtyQueue, ThreadGroup, 실행제어( 2 / 3 )

린네의 2024. 4. 1. 14:22

 

 

예제 소스 링크

 

 

 

언제 싱글쓰레드가 멀티쓰레드보다 효율적일까?

이전 게시글에도 적었지만, 프로세스는 하나또는 그 이상의 쓰레드를 가질수 있으며 하나의 쓰레드는 하나의 코어에 의해 실행 된다.

 

어떤 작업을 하나의 쓰레드가 처리하는 것을 싱글 쓰레드, 여러개의 쓰레드가 처리하는 것을 멀티 쓰레드라고 한다. 하나의 일을 여러명이 나누어서 하면 빨리 끝나듯 쓰레드도 여러개면 작업이 빠르게 진행 될 것 같지만 그렇지는 않다. 그 이유는 작업 전환 시간(context switcing)에 시간이 걸리기 때문이다.  

 

  • 작업 전환 시간(context swtiching)

프로세스 또는 쓰레드 간의 작업 전환을 의미한다.  작업 전환을 할 때 현재 진행 중인 작업의 상태나 다음에 실행해야할 위치 같은 정보를 저장하고 읽어오는 시간을 의미한다. 일반적으로 프로세스간의 스위칭 시간이 쓰레드의 스위칭에 비해 더 오랜 시간이 걸린다.

 

 

그래서 싱글 코어에서 단순히 CPU만을 사용하는 계산작업이면 멀티쓰레드보다 싱글쓰레드로 프로그래밍 하는 것이 더 효율적이다.

 

비교를 위해 특정 문자를 300번 찍는 작업을 진행했다.

 

두 개의 쓰레드가 두 개의 작업을 하나씩 나누어서 수행한 경우(멀티쓰레드)하나의 쓰레드가 두 개의 작업을 혼자 수행한 경우(싱글쓰레드)를 비교하면 싱글쓰레드가 수행시간이 훨씬 적게 출력 되는 것을 확인할 수 있었다.

 

  • 멀티쓰레드 예제
package com.example.thread;

public class MultiThreadTest {

    public static void main(String[] args) {

        Thread_1 thread1 = new Thread_1();
        thread1.start();

        long startTIme = System.currentTimeMillis();
        for(int i=0; i < 300; i++) {
            System.out.printf("%s", new String("-"));
        }

        System.out.println(" ============ 결과2 ============");

        System.out.println("시작시간2 : " + (startTIme));
        System.out.println("종료시간2 : " + System.currentTimeMillis());
        System.out.println("소요시간2 : " + (System.currentTimeMillis() - startTIme));

    }

}

class Thread_1 extends Thread {
    @Override
    public void run() {
        long startTIme = System.currentTimeMillis();
        for(int i=0; i < 300; i++) {
            System.out.printf("%s", new String("#"));
        }

        System.out.println(" ============ 결과 ============");
        System.out.println("시작시간 : " + (startTIme));
        System.out.println("종료시간 : " + System.currentTimeMillis());
        System.out.println("소요시간 : " + (System.currentTimeMillis() - startTIme));
    }


}

 

 

  • 멀티쓰레드 - 실행결과
--------##---------------------------------------------------########################################-----------##################################################################################################################-------------------------------------------------------------------------------------------------------------------------------------------------------------#-------------------------------------------------------------------------############ ============ 결과2 ============
################################################################################################################################### ============ 결과 ============
시작시간 : 1711940401926
시작시간2 : 1711940401926
종료시간 : 1711940402001
소요시간 : 76
종료시간2 : 1711940402001
소요시간2 : 76

 

 

  • 싱글쓰레드 예제
package com.example.thread;

import net.minidev.json.JSONUtil;

public class SingleThreadTest {
    public static void main(String[] args) {

         long startTIme = System.currentTimeMillis();
         for(int i=0; i < 300; i++) {
             System.out.printf("%s", new String("#"));
         }

        System.out.println(" ============ 결과 ============");

        System.out.println("시작시간 : " + (startTIme));
        System.out.println("종료시간 : " + System.currentTimeMillis());
        System.out.println("소요시간 : " + (System.currentTimeMillis() - startTIme));

        for(int i=0; i < 300; i++) {
             System.out.printf("%s", new String("-"));
         }

        System.out.println(" ============ 결과2 ============");

        System.out.println("시작시간2 : " + (startTIme));
        System.out.println("종료시간2 : " + System.currentTimeMillis());
        System.out.println("소요시간2 : " + (System.currentTimeMillis() - startTIme));
    }
}

 

 

  • 싱글쓰레드 - 실행결과
############################################################################################################################################################################################################################################################################################################ ============ 결과 ============
시작시간 : 1711940767792
종료시간 : 1711940767841
소요시간 : 51
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ============ 결과2 ============
시작시간2 : 1711940767792
종료시간2 : 1711940767859
소요시간2 : 67

 

 

두 개의 쓰레드로 작업하는데도 더 많은 시간이 걸린 이유는 쓰레드간의 작업시전환에 소요되는 시간과 한 쓰레드가 화면에 출력하는 시간동안 다른 쓰레드는 대기를하면서 발생되는 출력대기시간 때문이다.

 

멀티 쓰레드 개념을 정리하면서 헷갈리는 단어 두개를 정리했다. 병행과 병렬은 비슷해서 혼란스러울 수 있으니 차이점을 명확히 알고있도록 하자.

  • 병행(concurrent) : 여러 쓰레드가 여러 작업을 동시에 진행하는 것
  • 병렬(parallel) : 하나의 작업을 여러 쓰레드가 나눠서 처리하는 것 

그림으로 표현하면 다음과 같다.

 

[출처]&nbsp;https://github.com/Febase/FeBase/blob/master/Javascript/Asynchronous.md

 

 

그렇다면 멀티쓰레드는 언제 쓰는가?

두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우 싱글쓰레드프로세스보다 멀티쓰레드 프로세스가 더 효율적이다. 예를 들면 사용자로부터 데이터를 입력 받거나, 네트워크로 파일을 주고 받거나, 프린터로 파일을 출력하는 작업이 이에 해당한다.

 

사용자로부터 데이터를 입력받는 부분과 화면에 출력하는 부분을 두 개의 쓰레드로 나누어 처리하면 한 쓰레드가 화면에 출력할동안 다른 쓰레드가 대기할 필요가 없다. 

 

 

[출처] 자바의 정석

 

 

 

 

쓰레드의 우선순위는 OS의 스케쥴링 정책과 JVM구현에 따라 다르다

쓰레드의 우선순위와 관련한 메서드 상수는 다음과 같다.

    /**
     * The minimum priority that a thread can have.
     */
    public static final int MIN_PRIORITY = 1;

    /**
     * The default priority that is assigned to a thread.
     */
    public static final int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public static final int MAX_PRIORITY = 10;


  // setPriority
  public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }
    
  
  // getPriority
  public final int getPriority() {
        return priority;
    }

 

 

쓰레드의 우선순위 범위는 1~10이며 숫자가 높을수록 우선순위가 높다. 숫자가 높을수록 우선순위가 높고, 쓰레드를 생성한 쓰레드로부터 우선순위를 상속 받는다.

 

다음 예제에서 우선순위를 별도로 지정하지 않은 threadP1는 main 메서드 실행시 수행되는 쓰레드의 우선순위가 기본으로 5로 설정되어 있으므로 해당 우선순위를 상속받아 5가 된다. 

 

package com.example.thread;

public class ThreadPriorityTest {


    public static void main(String[] args) {

        Thread_P1 threadP1 = new Thread_P1();
        Thread_P2 threadP2 = new Thread_P2();
        threadP2.setPriority(7);

        System.out.println("priority of th1 - " + threadP1.getPriority());
        System.out.println("priority of th2 | " + threadP2.getPriority());

        threadP1.start();;
        threadP2.start();
    }


}

class Thread_P1 extends Thread{

    @Override
    public void run() {

        for(int i=0; i<300; i++) {
            System.out.print("-");
            for(int x=0; x< 10000000; x++);
        }

    }
}

class Thread_P2 extends Thread{
    @Override
    public void run() {

        for(int i=0; i<300; i++) {
            System.out.print("|");
            for(int x=0; x< 10000000; x++);
        }

    }

}

 

결과는 어떻게 될까? 우선순위를 높였으니 theadP2 가 먼저 수행되어 |||||| 가 떠야 할 것 같지만 내 테스트 환경에서는 그렇지 않았다.

 

  • 우선순위 예제 - 실행결과
-|-|-----------------------------------------------------------------------------------------------|----||||||||||||||||||------||||||||-------||||||||||||||||||||||||--------------------------||-|-|-|-|--|-|-|--||-------------------------------------------------------------------------------------------------------------------||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||----||||||||||||||||||||||||||||||||||||||||||||||||||||||||-------------------------------||||||||||||||||||||||||||||||||||||

 

 

자바의 장점 중 하나는 OS에 독립적인 부분이다. 하지만 실제로는 OS종속적인 부분이 몇가지가 있는데 그 중 하나가 쓰레드다. 우선순위에 차등을 두어 쓰레드를 실행하려면 OS별 스케줄링 정책과 JVM구현을 직접 확인해봐야한다.

 

따라서 작업에 우선순위를 두고 싶다면 PriorityQueue 를 쓰는게 나을 수 있다.

package com.example.thread;

import java.util.PriorityQueue;

public class ThreadPriorityTest {


    public static void main(String[] args) {

        Thread_P1 threadP1 = new Thread_P1();
        Thread_P2 threadP2 = new Thread_P2();
        threadP2.setPriority(7);

        System.out.println("priority of th1 - " + threadP1.getPriority());
        System.out.println("priority of th2 | " + threadP2.getPriority());

        //threadP1.start();;
        //threadP2.start();

        PriorityQueue<Thread> threadQueue = new PriorityQueue<>((t1, t2) -> {
            // 쓰레드의 우선순위 비교
            return Integer.compare(t2.getPriority(), t1.getPriority());
        });

      threadQueue.add(threadP1);
      threadQueue.add(threadP2);

        // 큐에서 쓰레드를 꺼내 실행
        while (!threadQueue.isEmpty()) {
            threadQueue.poll().start();
        }

    }


}

 

PrioirtyQueue 에서 Comparable 을 넘겨 쓰레드의 Prioirty 에 따라 정렬될 수 있도록 작성했다.

 

  • PriorityQueue 생성자
public PriorityQueue(Comparator<? super E> comparator) {
        this(DEFAULT_INITIAL_CAPACITY, comparator);
    }

 

 

쓰레드 그룹

쓰레드 그룹은 서로 관련된 쓰레드를 그룹으로 다루기 위한 것으로 쓰레드 그룹을 생성해서 쓰레드를 그룹으로 묶어서 관리할 수 있다.

보안상의 이유로 도입되었는데, 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경할 수 있지만 다른 쓰레드 그룹의 쓰레드는 변경할수 없다.

 

Thread 의 생성자를 이용하여 생성할 수 있다.

Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGrouop group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)

 

자바 어플리케이션이 실행되면 JVM은 main과 system이라는 쓰레드 그룹을 만들고 JVM운영에 필요한 쓰레드들을 생성해서 이 쓰레드 그룹에 포함시킨다. 생성하는 모든 쓰레드 그룹은 main쓰레드 그룹의 하위 쓰레드 그룹에 해당하며 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 자동으로 main쓰레드 그룹에 속하게 된다. 

 

  • 쓰레드그룹은 main쓰레드 그룹의 하위 쓰레드 그룹으로 포함되어 있다
  • setMaxPriority()는 쓰레드가 쓰레드 그룹에 추가되기 이전에 호출되어야 한다
  • 참조변수 없이 쓰레드를 바로 실행하더라도 가비지컬렉터의 제거 대상에 해당하지 않는다. 쓰레드의 참조가 ThreadGroup에 내장되어 있기 때문이다.

 

 

데몬 쓰레드 

데몬 쓰레드는 다른 일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)의 작업을 돕는 보조적인 역할을 하는 쓰레드다. 일반 쓰레드가 종료되면 데몬 쓰레드도 자동으로 종료 된다. 

 

데몬 쓰레드의 예시로는 가비지컬렉터, 워드프로세서의 자동저장, 화면갱신 등이 있다.

 

boolean isDaemon()

void setDaemon(boolean on)

 

setDaemon 을 사용하면 쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경할 수 있다. 매개변수 on의 값을 true로 지정하면 데몬 쓰레드가 된다.

 

데몬 쓰레드는 무한루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.

 

프로그램을 실행하면 JVM은 가비지 컬렉션, 이벤트처리, 그래픽처리와 같이 프로그램이 실행되는 데 필요한 보조작업을 수행하는 데몬 쓰레드들을 자동적으로 생성해서 실행 시킨다. 이 데몬 쓰레드들은 'system쓰레드 그룹' 또는 'main쓰레드 그룹'에 속한다.

 

 

쓰레드의 실행제어

  • 스케줄링 관련 메서드 
메서드 설명
static void sleep(long millis)
static void sleep(long millis, int nanos)
지정된 시간(밀리세컨드단위)동안 쓰레드를 일시정지시킨다.
지정한 시간이 지나고 나면, 자동적으로 다시 실행대기상태가 된다.
void join()
void join(long millis)
void join(long millis, int nanos)
지정된 시간동안 쓰레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다.
void interrupt() sleep()이나 join()에 의해 일시정지상태인 쓰레드를 꺠워서 실행대기상태로 만든다. 해당 쓰레드에서는 interrpuedException이 발생함으로써 일시정지상태를 벗어나게 된다.
void stop() 쓰레드를 즉시 종료 시킨다.
void suspend() 쓰레드를 일시정지시킨다.resume()을 호출하면 다시 실행대기 상태가 된다.
void resume() suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기상태로 만든다.
vstatic void yield() 실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 자신은 실행대기상태가 된다.

 

 

resume(), stop(), suspned() 는 쓰레드를 교착상태(dead-lock)로 만들기 쉽기 때문에 deprecated 되었다.

 

자세한 이유는 아래 공식홈페이지에 잘 나와 있다.

 

https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html

 

 

Java Thread Primitive Deprecation

Java Thread Primitive Deprecation Why is Thread.stop deprecated? Because it is inherently unsafe. Stopping a thread causes it to unlock all the monitors that it has locked. (The monitors are unlocked as the ThreadDeath exception propagates up the stack.) I

docs.oracle.com

 

 

  • 쓰레드의 상태
상태 설명
NEW 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태 
RUNNABLE 실행 중 또는 실행 가능한 상태
BLOCKED 동기화블럭에 의해서 일시정지된 상태 ( lock 이 풀릴 때 까지 기다리는 상태)
WAITING
TIMED_WAITING
쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은(UNRUNNABLE) 일시정지상태. TIMED_WAITING은 일시정지시간이 지정된 경우를 의미한다
TERMINATED 쓰레드의 작업이종료된 상태

 

 

스케줄링 메서드와 쓰레드 상태간의 관계는 다음그림으로 설명이 가능하다.

 

[출처] 자바의 정석



실행대기열은 큐(queue)와 같은구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행되는 구조로 되어있으며, 주어진 실행시간이 다되거나 yield()를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 된다.

 

 

  • sleep()

sleep()은 지정된 시간동안 쓰레드를 멈추게 한다.  밀리세컨드(1000분의 일초)와 나노세컨드(10억분의 일초)의 시간단위로 세밀하게 값을 지정할 수 있지만 어느정도의 오차는 발생한다.

 

sleep()에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 되거나 interrupt()가 호출되면(InterruptedException발생), 잠에서 깨어나 실행대기 상태가 된다. 그래서 sleep() 호출시에는 try-catch 구문이 반드시 필요하다.

 

sleep()이 항상 현재 실행 중인 쓰레드에 대해 작동하기 때문에 참조변수를 이용해서 호출하기 보다는 Thread.sleep(2000)과 같이 해야한다. ( 참조 변수로 호출해도 실행중인 쓰레드, 즉 main쓰레드가 영향을 받는다.)

 

예시 코드를 보자

 

  • main쓰레드에 sleep 걸기
package com.example.thread;

public class ThreadSleepTest {
    public static void main(String[] args) {

        Thread_S1 threadS1 = new Thread_S1();
        Thread_S2 threadS2 = new Thread_S2();

        threadS1.start();
        threadS2.start();

        try {
            
            Thread.sleep(2000);
            
        } catch (InterruptedException e) {
            System.out.println(" call error!!!!");
            throw new RuntimeException(e);
        }

		System.out.println("\nmain종료");
    }

    
}

class Thread_S1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 300; i++) {
            System.out.print("-");
        }

        System.out.println("S1 종료");
    }
}

static class Thread_S2 extends Thread {
    @Override
    public void run() {
        for(int i=0; i<300; i++) {
            System.out.print("#");
        }

        System.out.println("S2 종료");
    }
}
    }

 

 

( 사실 내가 참고해서 작성하고 있는 교재에는 참조변수로 실행하는 예제가 먼저 나오는데,  이상하게 참조변수에 sleep 을 걸려니까 함수가 없다고 나왔다. 버전이 올라가면서 바뀌었나? 검색해봤는데 대두되는 내용이 없어서 혹시 아시는 분 있으면 댓글로 공유 부탁드린다. )

 

실행결과는 다음과 같다. 

 

  • 결과1
--------------------------------###################--################---###--#------------#-##-------###-################--------------####-------------------------------------------------------------------###########################################################-----#--------------------------------------------------------#####################################-----------------------------------------------------####--######--#-------------------------------####----------######
S1 종료
#####################################################################################################################
S2 종료
main종료

 

  • 결과2
##########----------------------------------------####################################--###--#########--#########-----------------------------###################################################--------------------######################################################################################################################################################################################--
S2 종료
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
S1 종료
main종료

 

 

 

비교를 위해 threadS1 과 threadS2 내부에 Thread.sleep() 을 걸어 봤다.

 

  • threadS1, threadS2 에 sleep() 걸기
package com.example.thread;

public class ThreadSleepTest {
    public static void main(String[] args) {

        Thread_S1 threadS1 = new Thread_S1();
        Thread_S2 threadS2 = new Thread_S2();


        long starttime = System.currentTimeMillis();
        System.out.println("system start -> " + starttime);

        threadS1.start();
        threadS2.start();

        System.out.println("before sleep -> " + (starttime - System.currentTimeMillis()));

        try {

            Thread.sleep(2000);

        } catch (InterruptedException e) {
            System.out.println(" call error!!!!");
            throw new RuntimeException(e);
        }

        System.out.println("\nmain종료");
        long endtime = System.currentTimeMillis();
        System.out.println("system end -> " + endtime);
        System.out.println("발생차이" + (endtime - starttime));

    }


}

class Thread_S1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 300; i++) {
            System.out.print("-");
            try {

                Thread.sleep(2000);

            } catch (InterruptedException e) {
                System.out.println(" call error!!!!");
                throw new RuntimeException(e);
            }
        }

        System.out.println("\nS1 종료");
    }
}

class Thread_S2 extends Thread {
    @Override
    public void run() {
        for(int i=0; i<300; i++) {
            try {

                Thread.sleep(2000);

            } catch (InterruptedException e) {
                System.out.println(" call error!!!!");
                throw new RuntimeException(e);
            }

            System.out.print("#");
        }
        System.out.println("\nS2 종료");
    }
}

 

 

  • threadS1, threadS2 에 sleep() 걸기 - 결과
system start -> 1711956919365
-before sleep -> -30
-
main종료
#system end -> 1711956921401
발생차이2036
-##-#-#-#-#-#-#-#--#-#-##-

 

 

ThreadS1 에만 sleep() 을 걸어보자.

  • threadS1 에 sleep() 걸기
package com.example.thread;

public class ThreadSleepTest {
    public static void main(String[] args) {

        Thread_S1 threadS1 = new Thread_S1();
        Thread_S2 threadS2 = new Thread_S2();


        long starttime = System.currentTimeMillis();
        System.out.println("system start -> " + starttime);

        threadS1.start();
        threadS2.start();

        System.out.println("before sleep -> " + (starttime - System.currentTimeMillis()));

        try {

            Thread.sleep(2000);

        } catch (InterruptedException e) {
            System.out.println(" call error!!!!");
            throw new RuntimeException(e);
        }

        System.out.println("\nmain종료");
        long endtime = System.currentTimeMillis();
        System.out.println("system end -> " + endtime);
        System.out.println("발생차이" + (endtime - starttime));

    }


}

class Thread_S1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 300; i++) {
            System.out.print("-");
            try {

                Thread.sleep(2000);

            } catch (InterruptedException e) {
                System.out.println(" call error!!!!");
                throw new RuntimeException(e);
            }
        }

        System.out.println("\nS1 종료");
    }
}

class Thread_S2 extends Thread {
    @Override
    public void run() {
        for(int i=0; i<300; i++) {

            System.out.print("#");
        }
        System.out.println("\nS2 종료");
    }
}

 

ThreadS1 에 해당하는 프로세스만 나중에 출력되는 것을 볼 수 있다. 실제로 돌려보면 지정한 초단위로 출력된다.

 

  • threadS1 에 sleep() 걸기 - 결과
system start -> 1711957170152
-##########################################################################################################before sleep -> -23
##################################################################################################################################################################################################
S2 종료

main종료
-system end -> 1711957172182
발생차이2030
-----------

 

결과적으로 Thread.sleep()을 사용하면 Thread.sleep()이 호출된  thread 에 대해 (위 예제에서는 main

, threadS1, threadS2 쓰레드에 해당한다 ) 걸리는 것을 확인할 수 있었다.

 

  • interrupt

진행중인 쓰레드의 작업이 끝나기 전에 취소 시켜야할 때 사용한다. 단지 멈추라고 요청하는 것일 뿐, 쓰레드를 강제로 종료시키지는 않는다. interrupt를 사용하면 쓰레드의 interrupted상태(인스턴스 변수)만 변경 된다.

 

void interrupt() // 쓰레드의 interrupted상태를 false에서 true로 변경
boolean isInterrupted() // 쓰레드를 interrupted상태를 반환
static boolean interrupted // 현재 쓰레드의 interrupted 상태를 반환 후, false로 변경

 

interrupted()는 쓰레드에 대해 interrput()가 호출되었는지 알려준다. interrupt()가 호출되지 않았다면 false를, interrupt()가 호출되었다면 true를 반환한다. 반환후에는 false 로 변경한다. 

 

interrupted() 를 사용해서 interrupt()가 호출되었는지 아닌지 감시하는 코드를 작성할 수 있다.

 

package com.example.thread;

public class ThreadInterruptTest {

    public static void main(String[] args) {
        ThreadInter1 th = new ThreadInter1();
        th.start();
        
    }
    
    
    
}

class ThreadInter1 extends Thread {
    @Override
    public void run() {
        while(!interrupted()) {
          // interrupt 상태가 아니라면 반복   
            
        }
    }
}

 

 

isInterrupted() 와 interrupted() 의 차이는 아래와 같다.

    public static boolean interrupted() {
        Thread t = currentThread();
        boolean interrupted = t.interrupted;
        // We may have been interrupted the moment after we read the field,
        // so only clear the field if we saw that it was set and will return
        // true; otherwise we could lose an interrupt.
        if (interrupted) {
            t.interrupted = false;
            clearInterruptEvent();
        }
        return interrupted;
    }

    /**
     * Tests whether this thread has been interrupted.  The <i>interrupted
     * status</i> of the thread is unaffected by this method.
     *
     * @return  {@code true} if this thread has been interrupted;
     *          {@code false} otherwise.
     * @see     #interrupted()
     * @revised 6.0, 14
     */
    public boolean isInterrupted() {
        return interrupted;
    }

 

 

쓰레드가 sleep(), wati(), join()에 의해 '일시정지 상태(WAITING)에 있을 때 해당 쓰레드에 대해 interrupt()를 호출하면 sleep(), wait(), join()에서 Interrupted Exception이 발생하고 쓰레드는 실행대기 상태(RUNNABLE)로 바뀐다.

 

 

 

 

간단하게 말하면 멈춰있던 쓰레드를 깨워서 실행가능한 상태로 만드는 것이다.

 

아래예제를 보자.

package com.example.thread;

import javax.swing.*;

public class ThreadInterruptTest {

    public static void main(String[] args) {
        ThreadInter1 th = new ThreadInter1();
        th.start();

        String input = JOptionPane.showInputDialog("아무값이나 입력하세요.");
        System.out.println("입력하신 값은" + input + "입니다.");

        th.interrupt();
        System.out.println("isInterrupted() -> " + th.isInterrupted());
        System.out.println(th.getState());
    }
}

class ThreadInter1 extends Thread {
    @Override
    public void run() {

        int i = 50;

        while(i!=0 && !isInterrupted()) {
            System.out.println(i--);
            for(long x=0; x<250000000L;x++); // 시간 지연
        }
        System.out.println("카운트가 종료되었습니다.");

    }
}

 

입력폼에 값을 입력하면 interrupt()가 수행되며 쓰레드가 종료 된다. 또한 카운트는 끝까지 실행되지 않는다. 상태는 RUNNABLE 인 것을 알 수 있다.

50
... 생략
24
23
22
21
20
19
18
입력하신 값은test입니다.
isInterrupted() -> true
카운트가 종료되었습니다.
RUNNABLE

 

 

이번에는 run 함수에 sleep() 을 추가 했다. 

class ThreadInter1 extends Thread {
    @Override
    public void run() {

        int i = 50;

        while(i!=0 && !isInterrupted()) {
            System.out.println(i--);

            try {
                Thread.sleep(1000); //1초 지연
            } catch (InterruptedException e) {
                //throw new RuntimeException(e);
            }
        }

        System.out.println("카운트가 종료되었습니다.");

    }
}

 

카운트를 종료시키는 조건인 isInterrupted()가 false 임에 따라 카운트는 즉시 종료되지 않고 계속 실행 된다.  

sleep()에 의해 상태값은 TIMED_WAITING 으로 바뀌었다.

50 
... 생략
48
입력하신 값은 ㅑㅑ입니다.
47
isInterrupted() -> true
TIMED_WAITING
46
45
44
43
... 생략
카운트가 종료되었습니다.

 

 

try - catch 문 안에 interrupt() 를 추가하면 isInterrupted() 값을 true로 바꿔 카운터를 종료 시킬 수 있다.  

 

여기서 주의해야하는 부분은  main에서  아래 코드 부분이다.

th.interrupt();
System.out.println("isInterrupted() -> " + th.isInterrupted());

 

위 코드에서 sleep() 의 유무에 따라 결과가 달라질 수 있다.

  1. sleep()이 있는 경우 :  isinterrupted()의 값이 false 또는 true 가 될 수 있음.   ( InterruptedException 이 발생해서 바뀌어야할 상수 값이 바뀌지 않아 false가 반환 됨.  따라서 이 때 catch 문에 interrupt()를 한번더 실행하면 완벽하게 interrupted상수값을  true로 바꿀 수 있디.  catch 에 interrput()를 한번 더 추가하지 않아도 true 가 찍히는 경우가 있는데, InterrputedException 이 발생되기 전에 th.isInterrupted()를 출력하면 true, 그게 아니면 false 가 찍힌다.  main에서 th.isInterrupted() 가 interrputed상수에 접근하는 시점이 InterruptedException 발생 전인지 후인지 차이로 값이 달라지는 것이다. )                                                                                                                                                                                                                                                                                                                                               
  2. sleep()이 없는 경우 : isInterrupted()의 값이 true ( interrupt() 가 발생하면 interrputed 상수의 값은 true 로 바뀜 ) 

 

  • yield

쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보한다. 예를들어 스케줄러에 의해 1초의 실행시간을 할당받은 쓰레드가 0.5초의 시간동안 작업한 상태에서 yield()가 호출되면, 나머지 0.5초는 포기하고 다시 실행대기상태가 된다.

 

yield()는 성능 이슈가 있는 것 같아서 사용을 지양하고 쓰레드 개수를 조절하는 방향으로 성능 개선을 하는게 좋을 것 같다.

 

  • join

쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 사용한다.

 

void join()
void join(long millis)
void join(long millis, int nanos)

 

시간을 지정하지 않으면 해당 쓰레드가 작업을 모두 마칠 때 까지 기다리게 된다. 작업중에 다른 쓰레드의 작업이 먼저 수행되어야할 필요가 있을 때 사용한다.

 

join()도 interrupt()에 의해 대기상태에서 벗어날 수 있고 try-catch 가 필요하다. 이런점에서 sleep() 과 유사한데, 차이점은 join()은 현재 쓰레드가 아닌 특정 쓰레드에 대해 동작하므로 static 메서드가 아니라는 점이다.