티스토리 뷰

동작하고 있는 프로그램을 프로세스(Process)라고 합니다. 보통 한 개의 프로세스는 한가지의 일을 하지만, 쓰레드를 이용하면 한 프로세스 내에서 여러가지 일을 동시에 진행할 수 있습니다.

 

Thread 클래스와 Runnable 인터페이스

Thread를 구현하는 방법은 두가지가 있습니다.

 

(1) Thread 클래스를 상속

 

public class Test extends Thread {
    private int id;
    
    public Test(int id) {
        this.id = id;
    }
    
    @Override
    public void run() {
        System.out.println(this.id+" thread start.");
        
        try {
            Thread.sleep(1000);
        }catch(Exception e) {

        }
        
        System.out.println(this.id+" thread end.");
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Test test = new Test(i);
            test.start();
        }
        System.out.println("main end.");
    }
}

 

Thread 클래스를 상속하는 Test 클래스를 생성하였습니다. Test 클래스에서 run() 메소드를 오버라이딩한 뒤, start() 메소드를 호출하여 구현된 run() 메소드를 실행할 수 있습니다.

 

결과는 아래와 유사합니다.

 

0 thread start.
3 thread start.
1 thread start.
main end.
2 thread start.
4 thread start.
0 thread end.
2 thread end.
1 thread end.
3 thread end.
4 thread end.

 

Thread는 동시에 여러 일을 수행하기 때문에 순서대로 실행되고 종료되지 않습니다. 또한, "main end" 보다 더 늦게 실행되고 종료될 수 있습니다.

 

모든 쓰레드를 수행한 뒤 main을 종료하고 싶다면 join() 메소드를 사용하면 됩니다. join() 메소드는 쓰레드의 수행이 종료될 때까지 기다리는 메소드입니다.

 

(2) Runnable 인터페이스를 구현

 

public class Test implements Runnable {
    private int id;
    
    public Test(int id) {
        this.id = id;
    }
    
    @Override
    public void run() {
        System.out.println(this.id+" thread start.");
        
        try {
            Thread.sleep(1000);
        }catch(Exception e) {

        }
        
        System.out.println(this.id+" thread end.");
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Test test = new Thread(new Test(i));
            test.start();
        }
        System.out.println("main end.");
    }
}

 

Runnable 인터페이스를 구현하면 run() 메소드의 구현을 강제할 수 있습니다. Runnable 인터페이스를 구현한 클래스를 생성할 때는 Thread() 생성자에 구현 클래스 생성자를 넘기는 방식을 사용합니다.

 

Runnable 인터페이스는 구현 메소드가 run() 하나 뿐인 함수형 인터페이스이기 때문에 Java8 이상부터는 람다식으로 간단하게 작성할 수 있습니다.

 

Thread 상태

Thread를 다룰 때 그 상태를 알아야합니다. 상태를 알기 위해서는 getState() 메소드를 사용합니다. 일반적으로 쓰레드를 start() 하면 아래와 같은 상태로 진행이 됩니다.

 

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

 

실행중인 쓰레드의 상태를 변경하는 것을 쓰레드 상태 제어라고 합니다. 쓰레드 상태 제어의 방법은 다음과 같습니다.

 

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

실행 중인 Thread를 일정 시간 멈추게 하고 싶다면 Thread.sleep()을 사용합니다. sleep()의 매개변수로 밀리세컨드(1/1000) 단위로 정지할 시간을 지정합니다. 즉, Thread.sleep(3000)이라면 3초동안 일시정지 상태가 됩니다.

 

import java.awt.Toolkit;
 
public class SleepThread {
    public static void main(String []args){
        Toolkit toolkit =  Toolkit.getDefaultToolkit();
        for(int i=0; i<10; i++){
            toolkit.beep();
            System.out.println("3초 기다리세요.");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

(2) 다른 쓰레드에게 실행 양보 - yield()

Thread가 처리하는 작업은 반복적인 실행을 위해 반복문이 자주 사용되는데 이때 경우에 따라 아무 일도 하지 않고 실행만 되는 쓰레드가 생길 수 있습니다. 이때 이 쓰레드를 그냥 실행하기보다 다른 동작을 하는 쓰레드를 실행시키고 동작하지 않는 쓰레드는 실행 대기 상태로 보내는 것이 성능상 유리합니다. Thread.yield() 메소드를 호출하면 호출한 쓰레드는 실행 대기 상태로 이동하고 동일한 우선순위 또는 높은 우선순위를 가진 쓰레드가 실행됩니다.

 

package study.thread;

public class YieldThread extends Thread {
    private String name;
    private boolean stop;
    private boolean isWorking;

    public YieldThread(String name) {
        this.name = name;
        stop = false;
        isWorking = true;
    }

    public void setStop(boolean stop) {
        this.stop = stop;
    }

    public void setWorking(boolean working) {
        isWorking = working;
    }

    public void run() {
        while (!stop) {
            if (isWorking) {
                System.out.println(name + " is working...");
            } else {
                // isWorking이 아니라면 실행 대기 상태로 이동
                Thread.yield();
            }
        }
    }
}

 

쓰레드를 생성하고 실행하는 코드는 다음과 같습니다.

 

package study.thread;

class ThreadA extends YieldThread {
    public ThreadA() {
        super("ThreadA");
    }
}

class ThreadB extends YieldThread {
    public ThreadB() {
        super("ThreadB");
    }
}

public class MainThread {
    public static void main(String[] args) {
        ThreadA threadA = new ThreadA();
        ThreadB threadB = new ThreadB();

        threadA.start();
        threadB.start();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // ThreadB만 실행
        threadA.setWorking(false);

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // ThreadA만 실행
        threadA.setWorking(true);
        threadB.setWorking(false);

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // ThreadA, ThreadB 종료
        threadA.setStop(true);
        threadB.setStop(true);
    }
}

 

(3) 다른 쓰레드의 종료를 기다림 - join()

다른 쓰레드(ThreadA)가 종료될 때 기다렸다가 실행해야 하는 경우 ThreadA.join() 메소드를 통해 쓰레드가 종료될 때까지 기다린 뒤 다른 작업을 수행할 수 있습니다. 아래 예제의 경우 join()이 없다면 원하는 결과가 출력되지 않습니다.

 

package study.thread;
 
class SumThread extends Thread{
    private long sum;
    
    public long getSum(){
        return sum;
    }
 
    public void run(){
        for(int i = 1; i <= 1000; i++){
            sum += i;
        }
    }
}
 
public class JoinExample {
    public static void main(String []args){
        SumThread sumThread = new SumThread();
        sumThread.start();
        
        try {
            sumThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("sum: " + sumThread.getSum());
    }
}

 

Thread 우선순위

멀티 쓰레드는 동시성(Concurrency) 또는 병렬성(Parallelism)으로 실행됩니다. 동시성이란 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행되는 성질을 말하고 병렬성은 멀티 작업을 위해 여러 개의 코어가 개별 스레드를 동시에 수행하는 것을 말합니다.

 

 

싱글 코어 CPU를 이용한 멀티 쓰레드 작업은 병렬로 실행되는 것처럼 보이지만 사실은 번갈아가며 실행되는 동시성 작업입니다. 

 

멀티 쓰레드의 순서를 정하는 것을 쓰레드 스케줄링(Thread Scheduling)이라고 합니다. 쓰레드 스케줄링 방식에는 우선순위(Priority) 방식과 순환 할당(Round-Robin) 방식이 있습니다.

 

(1) 우선순위 방식

우선 순위가 높은 쓰레드가 실행 상태를 더 많이 가지도록 스케줄링하는 것을 말하며, setPriority() 메소드를 사용하여 우선순위를 설정합니다. 기본 우선 순위는 5(Thread.NORM_PRIORITY)이며 1(Thread.MIN_PRIORITY)이 가장 낮은 우선순위이고 10(Thread.MAX_PRIORITY)이 가장 높은 우선순위입니다.

 

(2) 순환 할당 방식

시간 할당량(Time Slice)을 정해서 하나의 스레드를 정해진 시간만큼 실행하고 다시 다른 스레드를 실행하는 방식을 말합니다. 순환 할당 방식은 JVM에 의해 정해지기 때문에 개발자가 임의로 수정할 수 없습니다.

 

Main Thread

모든 자바 애플리케이션은 Main Thread가 main() 메소드를 실행하면서 시작됩니다. Main Thread는 JVM이 프로그램을 실행할 때 기본적으로 실행하는 쓰레드입니다.

 

Main Thread의 작업을 돕는 쓰레드를 Demon Thread라고 하며 이 쓰레드는 메인 쓰레드가 종료되면 강제로 종료되게 됩니다. 어떠한 쓰레드를 데몬 쓰레드로 지정하고자 할 때는 쓰레드명.setDemon(true)로 한 뒤, 해당 쓰레드를 start하면 됩니다.

 

동기화

멀티 스레드 환경에서의 문제는 스레드들이 객체를 공유하여 작업하는 경우 공유하는 객체가 서로의 작업에 영향을 미칠 수 있다는 것입니다. 이를 방지하기 위해 동기화 방식을 사용할 수 있습니다.

 

// 공유 객체
public class ShareThread {
    private int value = 0;
    
    // 동기화 메소드
    public synchronized void setValue(int value) {
        this.value = value;
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
        }
        System.out.println(Thread.currentThread().getName() + "의 Value 값은 " + this.value +" 입니다.");
        
    }
    
    // 또는
    // 동기화 블록
    public void setValue(int value) {
        synchronized (this) {
            this.value = value;
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
            }
            System.out.println(Thread.currentThread().getName() + "의 Value 값은 " + this.value +" 입니다.");
        }
    }
    
    public int getValue() {
        return value;
    }
}

 

동기화 메소드 또는 동기화 블록을 사용하여 공유 객체에는 하나의 스레드가 작업을 모두 완료한 다음에 다른 스레드가 접근하도록 할 수 있습니다.

 

public static void main(String args[]){
    ShareThread shareTread = new ShareThread();
    Thread thredA = new Thread(()->{
        shareTread.setValue(100);
    });
    
    Thread thredB = new Thread(()->{
        shareTread.setValue(10);
    });
    
    thredA.setName("ThreadA");
    thredB.setName("ThreadB");
    thredA.start();
    thredB.start();
}


// 결과
// ThreadA의 Value 값은 100 입니다.
// ThreadB의 Value 값은 10 입니다.

 

하지만 synchronized 키워드를 너무 남발하면 오히려 프로그램 성능 저하를 일으킬 수 있기 때문에 적절한 위치에 사용해야 합니다.

 

데드락 (DeadLock)

멀티 스레드 프로그래밍에서 동기화를 통해 락을 획득하여 동일한 자원을 여러 곳에서 함부로 사용하지 못하도록 할 수 있습니다. 하지만 두 개의 스레드가 서로 가지고 있는 락이 해제되기를 기다리는 상태가 생길 수 있으며 이러한 상태를 교착 상태 (DeadLock)이라고 합니다.

 

교착 상태(DeadLock)가 발생하는 조건은 다음과 같습니다.

 

  • 상호 배제 (Mutual Exclusion): 한 자원에 대해 여러 쓰레드 동시 접근 불가
  • 점유와 대기 (Hold and Wait): 자원을 가지고 있는 상태에서 다른 쓰레드가 사용하고 있는 자원 반납을 기다리는 것
  • 비선점 (Non Preemptive): 다른 쓰레드의 자원을 실행 중간에 강제로 가져올 수 없음
  • 환형 대기 (Circle Wait): 각 쓰레드가 순환적으로 다음 쓰레드가 요구하는 자원을 가지고 있는 것

위 4가지 조건을 모두 충족할 경우 데드락이 발생합니다. 반대로 말하면, 위 4가지 조건 중 하나라도 충족하지 않을 경우 데드락을 해결할 수 있다는 뜻이기도 합니다.

 

데드락이 발생하는 예제를 살펴보겠습니다.

 

package study.sync;

public class DeadLockTest {
    private static Object objectA = new Object();
    private static Object objectB = new Object();

    public static void main(String[] args) {
        ThreadA threadA = new ThreadA();
        ThreadB threadB = new ThreadB();

        threadA.start();
        threadB.start();
    }

    private static class ThreadA extends Thread {
        @Override
        public void run() {
            synchronized (objectA) {
                System.out.println("ThreadA has objectA's lock.");

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("ThreadA wants to have objectB's lock. so wait...");

                synchronized (objectB) {
                    System.out.println("ThreadA has objectB's lock too.");
                }
            }
        }
    }

    private static class ThreadB extends Thread {
        @Override
        public void run() {
            synchronized (objectB) {
                System.out.println("ThreadB has objectB's lock.");

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("ThreadB wants to have objectA's lock. so wait...");

                synchronized (objectA) {
                    System.out.println("ThreadB has objectA's lock too.");
                }
            }
        }
    }
}

-----------------------------------------------------
// 결과
ThreadA has objectA's lock.
ThreadB has objectB's lock.
ThreadB wants to have objectA's lock. so wait...
ThreadA wants to have objectB's lock. so wait...

 

위 예제를 조건별로 하나씩 살펴보면 다음과 같습니다.

 

✔️상호배제

: objectA, objectB 객체에 대해서 동시에 쓰레드가 사용할 수 없도록 하였습니다.

✔️점유와 대기

: ThreadA에서는 objectA에 대한 락을 가지고 있으면서 objectB에 대한 락을 원하고, ThreadB에서는 objectB에 대한 락을 가지고 있으면서 objectA에 대한 락을 원합니다.

✔️비선점

: 쓰레드의 우선 순위가 기본값인 5(NORM_PRIORITY)로 동일하게 설정되어 있습니다.

✔️환형 대기

: ThreadA는 ThreadB의 objectB 락을 기다리고, ThreadB는 ThreadA의 objectA 락을 기다리고 있습니다.

 

이 조건들 중 하나만 해결되면 데드락을 피할 수 있습니다. 아래 코드는 환형 대기를 없애서 데드락을 해결한 것입니다.

 

package study.sync;

public class DeadLockTest {
    private static Object objectA = new Object();
    private static Object objectB = new Object();

    public static void main(String[] args) {
        ThreadA threadA = new ThreadA();
        ThreadB threadB = new ThreadB();

        threadA.start();
        threadB.start();
    }

    private static class ThreadA extends Thread {
        @Override
        public void run() {
            synchronized (objectA) {
                System.out.println("ThreadA has objectA's lock.");

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("ThreadA wants to have objectB's lock. so wait...");

                synchronized (objectB) {
                    System.out.println("ThreadA has objectB's lock too.");
                }
            }
        }
    }

    private static class ThreadB extends Thread {
        @Override
        public void run() {
            synchronized (objectA) {
                System.out.println("ThreadB has objectA's lock.");

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("ThreadB wants to have objectB's lock. so wait...");

                synchronized (objectB) {
                    System.out.println("ThreadB has objectB's lock too.");
                }
            }
        }
    }
}


-------------------------------------------------------
// 결과
ThreadA has objectA's lock.
ThreadA wants to have objectB's lock. so wait...
ThreadA has objectB's lock too.
ThreadB has objectA's lock.
ThreadB wants to have objectB's lock. so wait...
ThreadB has objectB's lock too.

 

참고

- 쓰레드: wikidocs.net/230

- 쓰레드 상태: blog.naver.com/PostView.nhn?blogId=qbxlvnf11&logNo=220945432938&parentCategoryNo=&categoryNo=12&viewDate=&isShowPopularPosts=true&from=search

- 쓰레드 우선순위: coding-factory.tistory.com/569 / deftkang.tistory.com/56 

- 동기화: honbabzone.com/java/java-thread/

- 데드락: math-coding.tistory.com/175

'JAVA' 카테고리의 다른 글

[Java Study 12] 애노테이션  (0) 2021.05.13
[Java Study 11] Enum  (0) 2021.05.11
[Java Study 08] 인터페이스  (0) 2021.05.05
[Java Study 07] 패키지  (0) 2021.05.04
[Java Study 06] 상속  (0) 2021.04.27
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
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 26 27 28 29 30
글 보관함