C14

0

이것이 자바다

목록 보기
12/18
post-thumbnail

멀티 스레드

운영체제는 실행 중인 프로그램을 프로세스로 관리한다. 멀티 태스킹은 두 가지 이상의 작업을 동시에 처리하는 것을 말하는데, 이때 운영체제는 멀티 프로세스를 생성해서 처리한다.하지만 멀티 태스킹이 꼭 멀티 프로세스를 뜻하지 않는다.

하나의 프로세스 내에서 여러 작업을 처리할 수 있는 이유는 멀티 스레드가 있기 때문이다. 스레드는 코드의 실행 흐름을 말한다. 스레드가 두개라면 두개의 코드 실행 흐림이 생긴다는 의미이다.

멀티 프로세스들은 서로 독립적이므로 하나의 프로세스에서 오류가 발생해도 다른 프로세서에는 영향을 미치지 않는다. 그러나 멀티 스레드로 동작하는 메신저의 경우, 파일을 전송하는 스레드에서 예외가 발생하면 메신저 프로세스 자체가 종료되기 때문에 채팅 스레드도 종료된다.

메인 스레드

모든 자바프로그램은 메인 스레드가 메인 메소드를 실행하면서 시작된다. 메인 스레드는 메인 메소드의 첫 코드부터 순차적 실행후 마지막 코드를 실행하거나 return문을 만나면 실행 종료된다.

작업 스레드 생성과 실행

자바 프로그램은 메인 스레드가 반드시 존재하기 때문에 멘인 작업 이외에 추가적인 작업 수만큼 스레드를 생성하면 된다. 자바는 작업 스레드로 객체로 관리하므로 클래스가 필요. 스레드 클래스로 직접 객체를 생성해도 되지만, 하위 클래스를 만들어 생성할 수도 있다.

Thread 클래스 직접 생성

	Thread thread = new Thread(Runnable target);

Runnable은 스레드가 작업을 실행할 때 사용하는 인터페이스이다. Runnable에는 run() 메소드가 정의되어 있는데 구현 클래스는 run()을 재정의해서 사용한다.

	class Task implements Runnable {
    @Override
    public void run() {
    // 스레드가 실행할 코드
    }

Runnable 구현 클래스는 작업 내용을 정의한 것이므로 스레드에게 전달해야 한다. Runnable 구현 객체를 생성한 후 Thread 생성자 매개값으로 Runnable 객체를 다음과 같이 전달하면 된다.

	Runnable task = new Task();
    Thread thread = new Thread(task);

명시적인 Runnable 구현 클래스를 작성하지 않고 Thread 생성자를 호출할 때 Runnable 익명 구현 객체를 매개값으로 사용할 수 있다.

	Thread thread = new Thread(new Runnable() {
    @Override
    public void run() { 
    //스레드가 실행할 내용
    }
   });
작업 스레드를 실행하려면 start() 메소드를 호출 해야 한다. 메인 스레드는 동시에 두 가지 작업을 처리할 수 없다.

Thread 자식 클래스로 생성

작업 스레드 객체를 생성하는 또다른 방법은 Thread의 자식 객체로 만드는 것이다. 상속한 다음 run() 메소드를 재정의해서 스레드가 실행할 코드를 작성하고 객체를 생성하면 된다.

public class WorkerThread extends Thread {
	@Override
    public void run() {
    //스레드가 실행할 코드
    }
 }
 
 //스레드 객체 생성
 Thread thread = new Thread();
작업 스레드를 실행하는 방법은 동일하며 start() 메소드를 호출하면 작업 스레드는 재정의된 run()을 실행시킨다.

위와 다른 방법으로 명시적인 자식 클래스를 정의하지 않고, 아래와 같이 Thread 익명 자식 객체를 사용할 수도 있다.(이 방법이 더 많이 사용)

Thread thread = new Thread() {
	@Override
    public void run() {
    //스레드가 실행할 코드
    }
 };
 thread.start();

스레드 이름

르세드는 자신의 이름을 가지고 있는데 메인 스레드는 'main'이라는 이름으 가지고 있고 작업스레드는 자동적으로 'Thread-n'이라는 이름을 가진다. 작업 스레드의 이름을 Thread-n 대신 다른 이름으로 설정하고 싶다면 Thread 클래스의 setName() 메소드를 사용하면 된다.

	Thread.setName("스레드 이름");

스레드 이름은 디버깅할 때 어떤 스레드가 작업을 하는지 조사할 목적으로 주로 사용된다. 현재 코드를 어떤 스레드가 실행하고 있는지 확인하려면 정적 메소드인 currentThread()로 스레드 객체의 참조를 얻은 다음 getName()메소드로 이름을 출력해보면된다.

	Thread thread = Thread.currentThread();
    System.out.println(Thread.getName());
```java
public class ThreadNameExample {
    public static void main(String[] args) {
        Thread mainThread = Thread.currentThread();
        System.out.println(mainThread.getName() + "실행");

        for(int i=0; i<3; i++) {
            Thread threadA = new Thread() {
                @Override
                public void run() {
                    System.out.println(getName() + "실행");
                }
            };
            threadA.start();
        }

        Thread chatTread = new Thread() {
            @Override
            public void run() {
                System.out.println(getName() + "실행");
            }
        };
        chatTread.setName("chat-thread");
        chatTread.start();
    }
}

스레드 상태

스레드 객체를 생성(NEW) 하고 start()메소드를 호출하면 곧바로 스레드가 실행되는 것이 아니라 실행대기(Runnable)가 된다.

실행 대기하는 스레드는 CPU 스케쥴링에 따라 CPU를 점유하고 run()메소드를 실행한다. 이때를 실행(RUNNING) 상태라고 한다. 실행 스레드는 run()메소드를 모두 실행하기 전에 스케쥴링에 의해 다시 실행 대기 상태로 돌아갈 수 있다. 그리고 다른 스레드가 실행상태가 된다.

이렇게 스레드는 실행 대기 상태와 실행 상태를 번갈아 가면서 자신의 run()메소드를 조금씩 실행한다. 실행 상태에서 run()메소드가 종료되면 더 이상 실행할 코드가 없어 종료상태(TERMINATED)가 된다

실행 상태에서 일시 정지 상태로 가기도 하는데, 일시 정지 상태는 스레드가 실행할 수 없는 상태를 말한다. 스레드가 다시 실행 상태로 가기 위해서는 일시 정지 상태에서 실행 대기 상태로 가야만 한다.

주어진 시간 동안 일시 정지

	try { 
    	Thread.sleep(1000); //1/1000 초
        } catch(InterruptExeception e) { 
        // interrupt() 메소드가 호출되면 실행
        }

다른 스레드의 종료를 기다림

스레드는 다른 스레드와 독립적으로 실행하지만 다른 스레드가 종료될 때까지 기다렸다가 실행을 해야 하는 경우도 있다. 예를 들어 계산 스레드의 작업이 종료된 후 그 결과값을 받아 처리하는 경우이다.

이를 위해 스레드는 join() 메소드를 제공한다.

아래 예제는 SumThread가 계산 작업을 모두 마칠 때까지 메인 스레드가 일시 정지 상태에 있다가 SumThread가 최종 계산된 결과값을 산출하고 종료하면 메인 스레드가 결과값을 받아 출력하는 예제이다.

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;
        }
    }
}
public class JoinExample {
    public static void main(String[] args) {
        SumThread sumThread = new SumThread();
        sumThread.start();
        try {
            sumThread.join();
        } catch (InterruptedException e) {
        }

        System.out.println("1~100의 합 : " + sumThread.getSum());
    }

}
			1~ 100 합 : 5050

다른 스레드에게 실행 양보

무의미한 반복을 하는 경우가 있는데 이때는 다른 스레드에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 프로그램 성능에 도움이 된다. 이런 기능을 위해 Thread는 yield() 메소드를 제공한다. yield()를 호출한 스레드는 실행 대기 상태로 돌아가고 다른 스레드가 실행 상태가 된다.

public class WorkThread extends Thread{
    //필드
    public boolean work = true;

    //생성자
    public WorkThread(String name) {
        setName(name);
    }

    //메소드
    @Override
    public void run() {
        while(true) {
            if(work) {
                System.out.println(getName() + " : 작업처리");
            }else {
                Thread.yield();
            }
        }
    }
}
public class YieldExample {
    public static void main(String[] args) {
        WorkThread workThreadA = new WorkThread("WorkThreadA");
        WorkThread workThreadB = new WorkThread("WorkthreadB");
        workThreadA.start();
        workThreadB.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
        }
        workThreadA.work = false;

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
        }
        workThreadB.work = true;
    }

}

스레드 동기화

멀티 스레드는 하나의 객체를 공유해서 작업할 수도 있다. 이 경우, 다른 스레드에 의해 객체 내부 데이터가 쉽게 변경될 수 있기 때문에 의도했던 것과 다른 결과가 나올 수 있다.

스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸면 된다. 이를 위해 자바는 동기화(synchronized) 메소드와 블록을 제공한다.

객체 내부에 동기화 메소드와 블록이 여러 개가 있다면 스레드가 이들 중 하나를 실행할 때 다른 스레드는 해당 메소드는 물론이고 다른 동기화 메소드 및 블록도 실행할 수 없다. 하지만 일반 메소드는 실행이 가능하다.

동기화 메소드 및 블록 선언

동기화 메소드 선언은 synchronized 키워드를 붙이면 된다. 인스턴스, 정적 메소드 어디든 붙일 수 있다.

	public synchronized void method() {
    //단 하나의 스레드만 실행하는 영역
    }
    
    public void method() { 
    //여러 스레드가 실행할 수 있는 영역
    
    synchronized(공유객체) {
    //단 하나의 스레드만 실행하는 영역
    }
    
    //여러 스레드가 실행할 수 있는 영역
    }

wait()과 notify()를 이용한 스레드 제어

경우에 따라서는 두 개의 스레드를 교차로 실행할 때도 있다. 정확한 교대 작업이 필요할 경우 , 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고 자신은 일시 정지 상태로 만들면된다.

이 방법의 핵심은 공유 객체에 있다. 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 정해 놓는다. 한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업을 하지 않도록 wait() 메소드를 호출하여 일시 정지 상태로 만든다.

notify() 는 wait() 에 의해 일시 정지된 스레드 중 한개를 실행 대기 상태로 만들고, notifyAll()은 wait()에 의해 일시 정지된 모든 스레드를 실행 대기 상태로 만든다. 주의할 점으로는 이 두메소드는 동기화 메소드 또는 동기화 블록내부에서만 사용가능하다.

스레드 안전 종료

스레드를 강제 종료시키기 위해 Thread는 stop() 메소드를 제공하고 있으나 이 메소드는 deprecated( 더 이상 사용하지 않음) 되었다. 이유는 갑자기 종료하면 리소스가 불안정하게 남겨지기 때문.

리소스들을 정리하고 run()메소드를 빨리 종료하는 interrupt()메소드를 사용.

조건 이용

스레드가 while 문으로 반복 실행할 경우, 조건을 이용해서 run() 메소드의 종료를 유도할 수 있다.

interrupt() 메소드 이용

interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptException 예외를 발생시키는 역할을 한다. 이 방법을 이용하면 예외 처리를 통해 run()메소드를 정상 종료 시킬 수 있다.

데몬 스레드

데몬 스레드는 주 스레드를 보조하는 역할한다. 주 스레드가 종료되면 데몬 스레드도 종료된다.

스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemeon(true)를 호출하면 된다.

	public static void main(String[] args) {
    	AutoSaveThread thread = new AutoSaveThread();
        thread.setDaemon(true);
        thread.start();
        ...
	}

스레드풀

병렬 작업 처리가 많아지면 스레드의 개수가 폭증하여 CPU가 바빠지고 메모리 사용량이 늘어난다. 이에 따라 성능 저하가 되며 스레드의 폭증을 막으려면 스레드풀을 사용하는 것이 좋다.

한마디로 스레드풀은 작업 처리에 사용되는 스레드의 개수를 제한하는 것이다. 작업처리 방식은 큐에 들어오는 작업들을 스레드가 하나씩 맡아 처리 후 작업 처리가 끝나면 다시 작업 큐에서 새로운 작업을 가져와 처리한다.

스레드풀 생성

Executors의 아래 두 정적 메소드를 이용하면 간단하게 스레드풀인 ExecutorService 구현 객체를 만들 수 있다.

메소드명(매개변수)초기수코어 수최대 수
newCachedThreadPool()00Integer.MAX_VALUE
newFixedThreadPool(int nThreads)00nThreads

초기 수는 스레드풀이 생성될 때 기본적으로 생성되는 스레드 수를 말하고, 코어 수는 스레드가 증가된 후 사용되지 않는 스레드를 제거할 때 최소한 풀에서 유지하는 스레드 수를 말한다. 그리고 최대 수는 증가되는 스레드의 한도 수이다.

	ExecutorService threadPool = new ThreadPoolExecutor(
    3, 					//코어 스레드 개수
    100,				//최대 스레드 개수
    120L,				//놀고 있는 시간
    TimeUnit.SECONDS,	//놀고 있는 시간 단위
    new synchronousQueue<Runnable>() //작업 큐
    );

스레드풀 종료

스레드풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아 있다. 스레드풀의 모든 스레드를 종료하려면 ExecutorService의 다음 두 메소드 중 하나를 실행해야 한다.

리턴타입메소드명(매개변수)설명
voidshutdown()현재 처리 중인 작업뿐만 아니라 작업 큐에 대기하고 있는 모든 작업을 처리한 뒤에 스레드풀을 종료시킨다.
List<Runnable>shutdownNow()현재 처리 중인 스레드를 interrupt해서 작업을 중지시키고 스레드풀을 종료 시킨다. 리턴값은 작업 큐에 있는 미처리된 작업(Runnable)의 목록이다.

작업 생성과 처리 요청

하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현된다. 차이점은 작업 처리 완료 후 리턴값이 있느냐 없느냐이다.

Runnable의 run()메소드는 리턴값이 없고, Callable의 call() 메소드는 리턴값이 있다. call()의 리턴 타입은 Callable<T>에 지정한 T 타입 파라미터와 동일한 타입이어야한다.

확인문제

1. 4번

2.

	1. new musicRunnable()
    2. extends Thread
    3. implements Runnable

3. 2번

4. 4번

5. 2번

6.

	if(this.isInterrupted()) {
    break;
    }

7. 3번

8. thread.setDaemon(true);

9. 4번

10. 4번

0개의 댓글

관련 채용 정보