본문 바로가기

Java/기본

[Java] 스레드(Thread) 제어하기 - 우선순위 설정, 동기화, 메서드 사용하기

스레드 우선순위

스레드는 우선순위를 할당할 수 있다. 스레드가 여러개인 경우 우선순위가 높은 스레드가 제어권을 가질 기회가 많아진다. 우선순위는 1~10까지 int 값으로 할당된다. 기본 우선순위는 5이다.

 

우선순위가 높은 스레드는 실행기회를 많이 갖는다. 우선순위가 높다고 해서 자원을 모두 가져가거나 항상 먼저 실행된다는 의미가 아니다. 프로세스가 스레드를 번갈아 수행하는데 코드를 좀더 자주 실행한다는 의미이다.

public static final int MAX_PRIORITY 가장 높은 순위. 상수 10
public static final int NORM_PRIORITY 일반적인 순위. 상수 5
public static final int MIN_PRIORITY 가장 낮은 순위. 상수 1

프로세서는 한번에 스레드 1개밖에 실행시킬 수 없다.  만약 동일한 기준이라면 무엇이 먼저 실행되는지 알 수 있을까? 정답은 알 수 없다. 동일한 기준인 경우 운영체제 스케줄러의 기준에 의해 순서가 정해지는데, 스케줄러에 의해 자원이 할당되고 순서가 정해지는 기준은 시스템마다 다르기 때문이다.

 

 

코드 예제

스레드 우선순위 할당하고 우선순위 확인하기

public class PrioritySetting extends Thread{
    PrioritySetting(String name){
        super(name);
    }
    
    @Override
    public void run() {
        String name  = Thread.currentThread().getName();
        int priority = Thread.currentThread().getPriority();

        System.out.println(name+ " | "+ priority);
    }
}

 

public class Execute {
    public static void main(String[] args) {
        Thread thread01 = new Thread(new PrioritySetting("MIN"));
        Thread thread02 = new Thread(new PrioritySetting("MAX"));
        Thread thread03 = new Thread(new PrioritySetting("NORM"));

        thread01.setPriority(Thread.MIN_PRIORITY);
        thread02.setPriority(Thread.MAX_PRIORITY);
        thread03.setPriority(Thread.NORM_PRIORITY);

        thread01.start();
        thread02.start();
        thread03.start();
    }
}

 

실행결과

Thread-1 | 10
Thread-2 | 5
Thread-0 | 1

 


스레드의 상태

상태
구분 내용
초기 상태 NEW 스레드 객체를 생성한 상태
실행 가능  RUNNABLE start()메서드로 객체를 호출하여 실행할 수 있는 상태
실행 중 RUNNING 실행가능한 스레드 중에서 스케줄러가 선택하여 스레드가 실행 중인 상태. CPU에 의해 코드가 한줄씩 실행된다. CPU는 n개의 스레드 중 1개밖에 처리할 수 없으며 n-1개의 스레드는 대기상태가 된다
대기상태 WATING 실행 중지. 다른 스레드의 통보(notify)를 기다리는 상태
TIMED_WATING 실행 중지. 주어진 시간동안 대기상태
BLOCKED 다른 스레드에서 락(lock)을 획득한 이유로 락이 걸린 상태
종료 TERMINATED 스레드가 종료된 상태. 한번 종료되면 다시 시작될 수 없다.

스레드 상태는 getState() 스태틱 메서드를 이용해 확인할 수 있다.

 


스레드 동기화(synchronized)

멀티스레드 프로그램은 스레드끼리 객체를 공유해 작업하는 경우가 있다. synchronized 키워드를 사용하면 특정 스레드가 사용중인 객체에 다른 스레드가 접근할 수 없게 되어 동기화가 보장된다. synchronized 키워드는 인스턴스, 정적 메서드, 특정 코드 영역 등에 붙일 수 있다.

  • 임계영역(critical section)이란 스레드 1개만 실행하거나 접근이 가능한 자원 및 코드의 범위를 말한다.
  • 락(lock)이란 공유객체에 여러 스레드가 접근하지 못하게 하는 과정이다. 락은 모든 객체가 메모리 힙 영역에 생성될 때 자동으로 할당된다.

 

스레드 동기화 과정

특정 스레드 A가 실행중(Running)synchronized 선언된 블록을 만나게 되면 object's Lock Pool 로 이동하고 (lock)을 획득한다. 다른 스레드는 Running Blocked 되어 실행가능한(Runnable) 상태가 된다.
특정 스레드 Asynchronized 블록 실행이 종료된다면 A락을 해제한다.

A가 락을 해제하면 다른 스레드 B가 실행가능한(Runnable)상태에서 실행(Running)을 하게 된다. 만약 스레드 Bsynchronized 선언된 블록을 만나면 Bobject's Lock Pool 로 이동하고 락(lock)을 획득한다. 이후 과정은 앞과 동일하다.

 


임계영역 설정하기

임계영역을 설정하는 방법은 2가지다.

1) 동기화 메서드

  • 접근제한자와 리턴형태 사이에 synchronized 키워드를 명시한다
public synchronized void methodA(){
}

 

2) 동기화 블록

  • 임계영역으로 설정할 부분을 synchronized(공유객체){  } 코드로 감싼다
  • 메서드 전체를 동기화하기에 코드가 많거나 특정 부분에만 동기화가 필요한 경우에 사용하는 방법이다
public void methodA(){
	synchronized(공유객체){
		//공유객체가 자기자신이라면 () 안에 키워드 this를 넣는다
	}
}



코드 예제

계좌에서 돈 출금시 스레드 1개만 접근하도록 하는 예제이다. 특정 스레드가 접근하면 다른 스레드는 계좌에 접근할 수 없다. 락이 해제되고 다른 스레드가 접근하기 전 잔액은 동기화된다.

public class TestSynchro {
    public static void main(String[] args) {
        ATM atm = new ATM();
        Thread a = new Thread(atm, "철수");
        Thread b = new Thread(atm, "영희");

        a.start();
        b.start();
    }
}

//ATM 에서 돈찾기
class ATM implements Runnable{
    private int deposit = 10000;    //계좌 잔액
    
    @Override
    public void run() {
        synchronized (this){            //synchronize 선언하여 임계영역 설정
            for(int i=0; i<5; i++) {    //1000원씩 5번 출금시도
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ie) {
                    ie.getMessage();
                }
                if (getMoney() <= 0) {
                    break;
                }
                withdraw(1000);
            }
        }
    }

    //출금 후 잔액 변경하고 메시지 출력하는 메서드
    public void withdraw(int money){
        if(getMoney() > 0){
            deposit -= money;
            System.out.println(Thread.currentThread().getName()+ "가 출금합니다. 잔액 : "+ getMoney());
        } else {
            System.out.println(Thread.currentThread().getName()+ "잔액부족");
        }
    }

    public int getMoney(){
        return deposit;
    }
}

 

실행 결과

영희가 출금합니다. 잔액 : 9000
영희가 출금합니다. 잔액 : 8000
영희가 출금합니다. 잔액 : 7000
영희가 출금합니다. 잔액 : 6000
영희가 출금합니다. 잔액 : 5000
철수가 출금합니다. 잔액 : 4000
철수가 출금합니다. 잔액 : 3000
철수가 출금합니다. 잔액 : 2000
철수가 출금합니다. 잔액 : 1000
철수가 출금합니다. 잔액 : 0

 

 

 


스레드 제어

동기화까지 알아봤으니 메서드를 사용하여 스레드를 제어하는 것에 대해 알아보자. 스레드 제어는 스레드의 상태를 변화시켜 여러 스레드를 활용하는 것을 말한다.

 

스레드 주요 메서드

메서드 내용
static int activeCount() 현재 활동 중인 스레드 개수
static Thread  currentThread() 현재 실행 중인 스레드 리턴
long  getId() 스레드 id 리턴
String  getName() 스레드 이름 리턴
int  getPriority() 스레드 우선순위 리턴
Thread.State  getState() 스레드 상태 리턴
void  interrupt() 스레드 중단시키기
boolean  isAlive() 스레드 활동여부 결과 리턴
void join() 다른 스레드가 join() 메서드를 호출, 즉 끼어들게 되면
그 스레드가 종료될 때까지 대기
void  run() 스레드 기능 수행하기
void  setDaemon(boolean on) 스레드 daemon으로 설정
void setName(String name) 스레드 이름 설정
void  setPriority(int newPriority) 스레드 우선순위 설정
static void  sleep(long millis) 지정된 밀리초 만큼 스레드 멈추기 (1초는 1000밀리초)
static void  sleep(long millis, int nanos) 지정된 밀리초+나노초 만큼 스레드 멈추기 (1밀리초는 1,000,000나노초)
void  start() 스레드 시작하기
static void  yield() 수행 중인 스레드 중 우선순위가 동일한 다른 스레드에 제어권을 넘기기

 

스레드 상태 제어에 사용되는 메서드

  • sleep() : 스레드 일시정지
  • yield() : 다른 스레드에 양보
  • join() : 다른 스레드 종료 기다리기
  • wait(), notify() : 스레드간 교대 작업
  • interrupt() : 스레드 종료

 


 

sleep()

스레드를 잠시 일시정지할 목적으로 사용한다. 인자값으로 일시정지할 시간만큼 밀리초를 할당한다. 실행중인 상태에서 아래 .sleep() 메서드를 만나면 실행대기 상태가 되고 시간이 끝나면 실행가능 상태로 돌아간다.

InterruptedException 예외가 발생하기 때문에 예외처리 코드가 필수이다. 참고로 1초는 1,000밀리초이다.

 try {
	Thread.sleep(1000);
} catch (InterruptedException ie) {
	ie.getMessage();
}

 

 

yield()

다른 스레드에 자원사용을 양보할 때 사용한다. 우선순위가 낮은 코드의 경우 다른 스레드에 자원 할당을 위해 사용한다.

아래 코드를 보자

 

ThreadA.java

public class ThreadA extends Thread{

    boolean stop = false;
    boolean work = true;

    @Override
    public void run() {
        while (!stop){
            if(work){
                System.out.println("ThreadA - AAA");
                try {
                    sleep(500);
                } catch (InterruptedException ie){}
            } else{
                //다른 스레드에 실행 양보
                Thread.yield();
            }
        }
        System.out.println("ThreadA 종료");
    }
}

 

ThreadB.java

public class ThreadB extends Thread {

    boolean stop = false;
    boolean work = true;

    @Override
    public void run() {
        while (!stop){
            if(work){
                System.out.println("ThreadB -       BBB");
                try {
                    sleep(500);
                } catch (InterruptedException ie){}
            } else{
                //다른 스레드에 실행 양보
                Thread.yield();
            }
        }
        System.out.println("ThreadB 종료");
    }
}

 

 

아래 main을 실행하면 work 플래그 값에 따라 다른 스레드에 실행을 양보하며 번갈아 실행된다.

public class Execute {
    public static void main(String[] args) {
        ThreadA a = new ThreadA();
        ThreadB b = new ThreadB();

        a.start();
        b.start();

        try{
            Thread.sleep(2000);
        } catch (InterruptedException ie){ ie.getMessage(); }

        //a 일시정지
        a.work = false;

        try{
            Thread.sleep(1000);
        } catch (InterruptedException ie){ ie.getMessage(); }

        //a 실행 재개
        a.work = true;

        try{
            Thread.sleep(2000);
        } catch (InterruptedException ie){ ie.getMessage(); }

        //b 일시정지
        b.work = false;

        //a, b 실행종료
        a.stop = true;
        b.stop = true;
    }
}

 

 

join()

다른 스레드의 종료가 끝날 때까지 대기하고 나서 실행된다. 주로 특정스레드의 실행값을 가져오기 위한 목적으로 사용한다. 예를 들어 특정 스레드에서 계산 작업을 맡고 있다면, 다른 스레드에서는 특정 스레드가 종료된 후에 접근하는 것이 안전하다.

아래 코드를 보자

 

1부터 주어진 정수까지 더한 값을 구하는 코드이다. main 스레드에서 sum 값을 가져오기 전에 join메서드를 호출해 계산이 종료되기까지 기다린다.

public class MakeSum extends Thread{
    private int sum = 0;
    private int index = 0;

    MakeSum(){
        this(10);
    }

    MakeSum(int index){
        this.index = index;
    }

    @Override
    public void run() {
        for(int i=1; i<=index; i++){
            sum += i;
        }
    }

    public int getSum(){
        return sum;
    }
}
public class Execute {
    public static void main(String[] args) {
        MakeSum makeSum = new MakeSum(10);
        makeSum.start();

        try{
            //makeSum 실행이 종료될 때까지 기다리기
            makeSum.join();
        } catch (InterruptedException ie){
            ie.getMessage();
        }
        System.out.println(makeSum.getSum());
    }
}

 

실행 결과

55

 

 

wait(), notify(), notifyAll()

스레드 2개를 번갈아가며 실행하는 경우 사용하는 코드이다. 스래드A와 스레드 B가 있다고 하자. 스레드A가 자신의 작업이 끝나면 스레드 B를 일시정지에서 해제한다. 동시에 자신은 일시정지로 만든다. 다시 스레드 B의 작업이 끝나면 다시 스레드 A를 일시정지에서 해제한다. 종료될 때까지 반복한다.

 

참고로 이 메서드들은 java.lang.Object 클래스에 정의된 메서드이다. 때문에 IDE를 활용해 코딩하는 경우 모든 클래스 및 메서드에서 자동완성으로 뜨는 것을 확인할 수 있다. 하지만 위 메서드들은 동기화(Synchronized) 처리된 코드에서 사용할 수 있는 것을 명심하자.

 

- wait()에 시간 인자값을 넣으면 주어진 시간 이후에 자동으로 실행대기 상태로 전환된다

- notify() 메서드는 wait()로 일시정지 중인 스레드 1개를 실행대기 상태로 전환시킨다

- notifyAll() 메서드는 wait()로 일시정지 중인 다른 모든 스레드를 실행대기 상태로 전환시킨다

  물론, 그중에서 실행되는 것은 1개이다.

 

 

코딩 방법

  • 공유객체에 두 스레드가 작업할 코드를 각각 동기화 메서드로 구분해두기
  • 작업완료되면 다른 스레드를 호출(notify)하고 자신은 실행중지(wait)하는 코드 작성하기

 

아래 코드를 보자

 

 

철수가 영희에게 고백하는 코드이다. 고백하지도 않았는데 거절할 수는 없다. 거절하기도 전에 중복 고백할 수도 없다.

즉 고백과 거절은 서로 교대로 이루어진다.

//공유 객체
public class Propose {
    private String propose;

    //동기화 메서드 - 프로포즈 하기
    public synchronized void setPropose(String propose){
        //이미 프로포즈를 했다면 대기
        if(this.propose != null){
            try {
                wait();     //자신은 일시정지 설정 - 철수가 영희의 대답을 기다린다
            } catch (InterruptedException ie){
                ie.getMessage();
            }
        }
        this.propose = propose;
        System.out.println("철수의 고백 : "+ propose);
        notify();           //영희 일시정지 해제
    }

    //동기화 메서드 - 프로포즈 받기
    public synchronized String getPropose(){
        //아직 프로포즈를 받지 않았다면 대기
        if(this.propose == null){
            try {
                wait();     //자신은 일시정지 설정 - 영희가 다른 남자가 고백해주길 기다린다
            } catch (InterruptedException ie){
                ie.getMessage();
            }
        }
        String answer = propose;
        System.out.println("영희의 거절 : "+ answer+ "\n");
        propose = null;     //프로포즈 null 로 만들어서 다시 고백받기
        notify();           //자신은 일시정지 해제 - 영희의 여지 남기기
        return answer;
    }
}

 

//철수
public class ThreadGiver extends Thread {
    private Propose propose;
    private int count;

    public ThreadGiver(Propose propose, int count){
        this.propose = propose;
        this.count = count;
    }

    @Override
    public void run() {
        for(int i=1; i<=count; i++){
            String data = propose.getPropose();
        }
    }
}

 

//영희
public class ThreadReceiver extends Thread{
    private Propose propose;
    private int count;

    public ThreadReceiver(Propose propose, int count){
        this.propose = propose;
        this.count = count;
    }

    @Override
    public void run() {
        for(int i=0; i<count; i++){
            String data = (i+1)+ "번째";
            propose.setPropose(data);
        }
    }
}

 

public class Execute {
    public static void main(String[] args) {
        Propose propose = new Propose();
        int count = 5;

        ThreadReceiver rec = new ThreadReceiver(propose, count);
        ThreadGiver giv = new ThreadGiver(propose, count);

        rec.start();
        giv.start();
    }
}

 

실행 결과

철수의 고백 : 1번째
영희의 거절 : 1번째

철수의 고백 : 2번째
영희의 거절 : 2번째

철수의 고백 : 3번째
영희의 거절 : 3번째

철수의 고백 : 4번째
영희의 거절 : 4번째

철수의 고백 : 5번째
영희의 거절 : 5번째

 

 

interrupt()

스레드를 강제로 종료하는데 사용하는 메서드이다. 스레드는 run() 메서드를 모두 실행하면 자동으로 종료되지만 그전에 의도적으로 스레드를 종료시킬 목적으로 사용한다. 스레드가 갑자기 중지되는 경우 사용중인 자원을 적절히 해제할 필요가 있다. 이 때문에 안전하게 스레드를 종료하는 코드가 필요하다.

 

스레드를 (안전하게) 종료시키는 방법 2가지

 

1) 플래그 변수 사용하기

플래그 변수에 true/false 값을 넣어 while 문 바깥에 자원해제 코드를 삽입한다.

public class ThreadEx extends Thread{
    private boolean stop;
    
    public void setStop(boolean stop){
        this.stop = stop;
    }

    @Override
    public void run() {
        while(!stop){
            System.out.println("실행 ing");
        }
        System.out.println("스레드 자원해제");
    }
}

 

public class Execute {
    public static void main(String[] args) {
        ThreadEx threadEx = new ThreadEx();
        threadEx.start();
        
        try{
            Thread.sleep(1000);
            threadEx.setStop(true);
        } catch (InterruptedException ie){
            ie.getMessage();
        }
    }
}

 

실행 결과

...
실행 ing
실행 ing
실행 ing
스레드 자원해제

 

2) interrupt() 메서드 사용하기

interrupt() 메서드는 스레드 일시정지 상태에서만 효과가 나타난다. 실행대기 상태이거나 실행중인 상태에서는 메서드를 호출해도 종료되지 않는다. 즉 interrupt()를 사용해 스레드를 종료시키기 위해서는 일부러 일시정지시키는 코드가 필요하다.

public class ThreadEx extends Thread{
    @Override
    public void run() {
        try{
            while(true){
                System.out.println("실행 ing");
                Thread.sleep(1);        //interrupt() 메서드 사용을 위해 일부러 짧은시간 일시정지시킴
            }
        } catch (InterruptedException ie){
            ie.getMessage();
        }
        System.out.println("스레드 자원해제");
    }
}

 

public class Execute {
    public static void main(String[] args) {
        ThreadEx threadEx = new ThreadEx();
        threadEx.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException ie) {
            ie.getMessage();
        }
        threadEx.interrupt();
    }
}

 

실행 결과

...
실행 ing
실행 ing
실행 ing
스레드 자원해제