[JAVA]멀티쓰레드 프로그래밍

Thread 클래스와 Runnable 인터페이스

- 메모리를 할당받아 실행 중인 프로그램을 프로세스라고 한다.

- 프로세스 내의 명령어 블록으로 시작점과 종료점을 가진다.

- 실행 중에 멈출 수 있으며 동시에 수행 가능하다.

어떠한 프로그램내에서 특히 프로세스 내에서 실행되는 흐름의 단위.

작업 Thread 생성과 실행

Thread 클래스로부터 직접 생성

class Task implements Runnable {
	public void run() {
		// run code
    }
}

java.lang.Thread 클래스로부터 작업 쓰레드 객체를 직접 생성 하려면 Runnable을 매개값으로 갖는 생성자를 호출해야 한다. Runnable은 작업 쓰레드가 실행할 수 있는 코드를 가지고 있는 객체라고 해서 붙여진 이름이다. Runnable에는 run() 메소드 하나가 정의되어 있는데, 구현 클래스는 run()을 재정의해서 작업 쓰레드가 실행할 코드를 작성해야 한다.

// Runnable 구현 객체를 생성한 후, 이것을 매개값으로 Thread 생성자를 호출
Runnable task = new Task();

Thread thread = new Thread(task);

// Thread 생성자를 호출할 때 Runnable 익명 객체를 매개값으로 사용할 수 있다. 이 방법이 더 많이 사용된다.
Thread thread = new Thread( new Runnable() {
  public void run() {
    쓰레드가 실행할 코드;
  }
});

// 람다식을 매개값으로 사용하는 방법
Thread thread = new Thread( () -> {
    쓰레드가 실행할 코드;
});

Thread 하위 클래스로부터 생성

작업 Thread가 실행할 작업을 Runnable로 만들지 않고, Thread의 하위 클래스로 작업 Thread를 정의하면서 작업 내용을 포함시킬 수도 있따. Thread 클래스를 상속한 후 run 메소드를 overriding해서 쓰레드가 실행할 코드를 작성하면된다.

public class WorkerThread extends Thread {
  @Override
  public void run() {
      //스레드가 실행할 코드
  }
}

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

thread.start()

쓰레드의 상태

1. 쓰레드 객체를 생성하고, start() 메소드를 호출하면 쓰레드는 실행 대기 상태가 된다.

    - 실행 대기 상태 : 아직 스케줄링이 되지 않아 실행을 기다리고 있는 상태

2. 실행 대기 상태에 있는 메소드 중 쓰레드 스케줄링으로 선택된 쓰레드가 CPU를 점유하고 run() 메소드를 실행할 때 실행(Running) 상태라고 한다.

3. 실행 상태의 쓰레드는 run() 메소드를 모두 실행하기 전에 쓰레드 스케줄링에 의해 다시 실행 대기 상태로 돌아갈 수 있다. 그리고 실행 대기 상태에 있는 다른 쓰레드가 선택되어. 실행 상태가 된다. 이렇게 쓰레드는 실행 대기 상태와 실행 상태를 번갈아가며 자신의 run() 메소드를 조금씩 실행한다.

4. 실행 상태에서 run() 메소드가 종료되면, 더 이상 실행할 코드가 없기 때문에 쓰레드의 실행은 멈춘다. 이 상태를 종료 상태라고 한다.

쓰레드가 실행 상태에서 실행 대기 상태로 가지 않고 일시 정시 상태로 가기도 한다.

  - 일시 정지 상태 : 쓰레드가 실행할 수 없는 상태다.

  - 일시 정지 상태의 종류 : WATING, TIMED_WATING, BLOCKED

 

getState()

Thread 클래스의 getState() 메소드를 통해 쓰레드의 상태를 코드로 확인할 수 있다.

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

쓰레드 상태 제어

취소선으로 그어진 함수는 Deprecated된 함수이다.

호출 주체 메소드 설명
Thread interrupt() 일시 정지 상태의 쓰레드에 InterruptedException 예외를 발생시켜, 예외 처리 코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있돌고 한다.
Object notify() / notifyAll() 동기화 블록 내에서 wait() 메소드에 의해 일시 정지 상태에 있는 쓰레드를 실행 대기 상태로 만든다.
Thread sleep(long millis) / sleep(long millis, int nanos) 주어진 시간 동안 스레드를 일시 정지 상태로 만든다.
주어진 시간이 지남녀 자동저긍로 실행 대기 상태가 된다.
Thread join() / join(long mills) / join(long millis, int nanos) join() 메소드를 호출한 쓰레드는 일시 정지 상태가 된다. 실행 대기 상태로 가려면 join() 메소드를 멤버로 가지는 쓰레드가 종료되거나, 매개값으로 주어진 시간이 지나야한다.
Object wait() / wait(long mills) / wait(long millis, int nanos) 동기화(synchronized) 블록 내에서 쓰레드를 일시 정지 상태로 만든다.
매개값으로 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. 시간이 주어지지 않으면 notify(), notifyAll() 메소드에 의해 실행 대기상태로 갈 수 있다.
Thread yield() 실행 중에 우선순위가 동일한 다른 쓰레드에게 실행을 양보하고, 실행 대기 상태가 된다.

Interrupt()를 이용한 Thread의 종료

public class PrintThread2 extends Thread {
    
    public void run(){
        
        try{
            while (true){
                System.out.println("실행 중");
                Thread.sleep(1);
                // 쓰레드가 실행 대기 또는 실행 상태에서는 interrupt가 발생하지 않으므로
                // sleep을 이용해 일시 정지 시켜준다.
            }
        }catch (InterruptedException e){}
        
        System.out.println("자원 정리");
        System.out.println("실행 종료");
    }
}



public class InterruptExam {
    
    public static void main(String[] atgs){
        
        Thread thread = new PrintThread2();
        thread.start();
        
        try{Thread.sleep(1000);}catch (InterruptedException e){}
        
        thread.interrupt(); //스레드를 종료시키기 위해 InterruptedException을 발생시킴
    }
}

Thread간 협업 - wait(), notify(), notifyAll()

두개의 쓰레드를 교대로 번갈아가며 실행해야 할 경우 자신의 작업이 끝나면 상대방 쓰레드를 일시 정지 상태에서 풀어주고, 자신은 일시 정지 상태로 만들어야 한다.

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

notifyAll()의 경우는 notify() 메소드와 동일한 역할을 하지만 notifyAll()은 wait으로 일시 정지 상태인 모든 쓰레드들을 실행 대기 상태로 만든다.

위 메소드들은 모두 동기화 메소드 또는 동기화 블록 내에서만 사용 가능하다.

public class WorkObject {
    
    public synchronized void methodA(){
        
        System.out.println("ThreadA의 methodA() 작업 실행");
        
        notify(); //일시 정지 상태의 ThreadB를 실행 대기 상태로 만듦
        
        try{
            
            wait(); //ThreadA를 일시 정지 상태로 만듦
            
        }catch (InterruptedException e){}
        
    }
    
    
    
    public synchronized void methodB(){
        
        System.out.println("ThreadB의 methodB() 작업 실행");
        
        notify(); //일시 정지 상태의 ThreadA를 실행 대기 상태로 만듦
        
        try{
            
            wait(); //ThreadB를 일시 정지 상태로 만듦
            
        }catch (InterruptedException e){}
        
    }
    
}

public class ThreadA extends Thread{
    
    private WorkObject workObject;
    
    
    
    public ThreadA(WorkObject workObject){
        
        this.workObject = workObject; //공유 객체를 매개값으로 받아 필드에 저장
        
    }
    
    
    
    @Override
    
    public void run(){
        
        for(int i=0; i<10; i++){ //공유 객체의 methodA()를 10번 반복 호출
            
            workObject.methodA();
            
        }
        
    }
    
}
public class ThreadB  extends Thread{
    
    private WorkObject workObject;
    
    
    
    public ThreadB(WorkObject workObject){
        
        this.workObject = workObject; //공유 객체를 매개값으로 받아 필드에 저장
        
    }
    
    
    
    @Override
    
    public void run(){
        
        for(int i=0; i<10; i++){ //공유 객체의 methodB()를 10번 반복 호출
            
            workObject.methodB();
            
        }
        
    }
    
}
 
public class Main {
    
    public static void main(String[] args){
        
        WorkObject sharedObject = new WorkObject();
        
        Thread threadA = new ThreadA(sharedObject);
        Thread threadB = new ThreadB(sharedObject);
        
        
        //ThreadA와 ThreadB를 실행
        threadA.start();
        threadB.start();
    }
}

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

호출한 쓰레드는 주어진 시간 동안 일시 정지 상태가 되고, 다시 실행 대기 상태로 돌아간다.

public class SleepExam {
    
    public static void main(String[] args){
        
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        
        for(int i=0; i<0; i++){
            toolkit.beep();
            
            try{
                Thread.sleep(3000);
            }catch(InterruptedException e){}
            
        }
    }
}

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

다른 쓰레드가 종료될 때까지 기다렸다가 쓰레들르 실행할 경우 join() 메소드를 사용한다.

public class SumThread extends Thread {
    
    private long sum;
    
    public long getSum(){
        return sum;
        
    }
    
    public void setSum(long sum){
        this.sum = sum;
    }
    
    public void run(){
        for(int i=1; i<=100; i++){
            sum+=i;
        }
    }
}

public class Main {
    
    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());
    }
}

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

불필요한 반복문 등이 실행될 경우, 무의미한 반복을 피하기 위해 yield()를 사용하여 동일한 또는 보다 높은 우선 순위를 가진 다른 쓰레드에게 실행을 양보하고 자신은 실행 대기 상태로 갈 수 있다.

public class ThreadA extends Thread{
    
    public boolean stop = false;    //종료 플래그
    public boolean work = true;     //작업 진행 여부 플래그
    
    public void run(){
        while(!stop){    //stop이 true가 되면 while 종료
            if(work){
                System.out.println("ThreadA 작업 내용");
            }else{
                Thread.yield(); //work가 false가 되면 다른 스레드에게 실행 양보
            }
        }
    }
}
public class ThreadB  extends Thread{
    
    public boolean stop = false;    //종료 플래그
    public boolean work = true;     //작업 진행 여부 플래그
    
    public void run(){
        while(!stop){    //stop이 true가 되면 while 종료
            if(work){
                System.out.println("ThreadB 작업 내용");
            }else{
                Thread.yield(); //work가 false가 되면 다른 스레드에게 실행 양보
            }
        }
    }
}
public class YieldExam {
    
    public static void main(String[] args){
        
        ThreadA threadA = new ThreadA();
        ThreadB threadB = new 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.stop = true; // ThreadA, ThreadB 모두 종료
        threadB.stop = true;
    }
}

쓰레드의 우선순위

동시성과 병렬성

동시성(Concurrency) : 멀티 작업을 위해 하나의 코어에서 멀티 쓰레드가 번갈아 가며 실행하는 성질

병렬성(Parallelism) : 멀티 작업을 위해 멀티 코어에서 개별 쓰레드를 동시에 실행하는 성질

※ 싱클 코어에서의 멀티 쓰레드 작업은 병렬적으로 실행되는 것처럼 보이지만, 사실은 번갈아가며 실행하는 동시성 작업이다. 번갈아 실행하는 것이 워낙 빨라서 병렬성으로 보일 뿐이다.

Thread Scheduling

Thread의 수가 코어의 수보다 많을 경우

    - 쓰레드를 어떤 순서에 의해 동시성으로 실행할 것인가를 결정해야 하는데, 이것을 쓰레드 스케줄링이라고 한다.

    - 쓰레드 스케줄링에 의해 쓰레드들은 아주 짧은 시간에 번갈아가면서 그들의 run() 메소드를 조금씩 실행한다.

JAVA의 Thread Scheduling

    - 우선 순위(priority) 방식과 순환 할당(Round-Robin) 방식을 사용

      - 우선 순위 방식(코드로 제어 가능) : 우선 순위가 높은 쓰레드가 실행 상태를 더 많이 가지도록 스케줄링

      - 순환 할당 방식(코드로 제어할 수 없음) : 시간 할당량(Time Slice)를 정해서 하나의 쓰레드를 정해진 시간만큼 실행하는 방식

Thread 우선 순위

    - 쓰레드들이 동시성을 가질 경우 우선적으로 실행할 수 있는 순위

    - 우선 순위는 1(낮음)에서부터 10(높음)까지로 부여

      - 모든 스레드들은 기본적으로 5의 우선 순위를 할당

우선 순위 변경 방법

thread.setPriority(우선순위);

thread.setPriority(Thread.MAX_PRIORITY);
thread.setPriority(Thread.NORM_PRIORITY);
thread.setPriority(Thread.MIN_PRIORITY);

우선 순위 효과

    - 싱글 코어

      - 우선 순위가 높은 쓰레드가 실행 기회를 더 많이 가지기 때문에 우선 순위가 낮은 스레드보다 계산 작업을 빨리 끝낸다.

    - 멀티 코어

      - 코어의 수보다 많은 쓰레드가 실행되어야 우선 순위의 영향을 받는다.

Main 쓰레드

메인 스레드 이름 : main

작업 스레드 이름 : Thread-n

작업 쓰레드의 이름 변경

thread.setName("Thread Name");

코드를 실행하는 쓰레드의 참조 얻기

Thread thread = Thread.currentThread();

동기화

싱글 쓰레드 프로그램에서는 한 개의 쓰레드가 객체를 독차지해서 사용하면 되지만, 멀티 쓰레드 프로그램에서는 쓰레드들이 객체를 공유해서 작업해야 하는 경우가 있다. 이 경우, 쓰레드 A를 사용하던 객체가 쓰레드 B에 의해 상태가 변경될 수 있기 때문에 쓰레드 A가 의도했던 것과는 다른 결과를 산출할 수도 있다.

동기화 메소드 및 동기화 블록

임계 영역(critical section) : 멀티 쓰레드 프로그램에서 단 하나의 쓰레드만 실행할 수 있는 코드 영역

JAVA에서의 임계 영역 : 동기화(synchronized) 메소드와 동기화 블록을 제공. 스레드가 객체 내부의 동기화 메소드 또는 블록에 들어가면 즉시 객체에 잠금을 걸어 다른 쓰레드가 임계 영역 코드를 실행하지 못 하도록 한다.

동기화 메소드

동기화 메소드는 메소드 전체 내용이 임계 영역이므로 쓰레드가 동기화 메소드를 실행하는 즉시 객체에는 잠금이 일어나고, 쓰레드가 동기화 메소드를 실행 종료하면 잠금이 풀린다.

public synchronized void method() {
  임계 영역; //단 하나의 스레드만 실행
}

동기화 블록

등기화 블록의 외부 코드들은 여러 쓰레드가 동시에 실행할 수 있지만, 동기화 블록의 내부 코드는 임계 영역이므로 한번에 한 쓰레드만 실행할 수 있고 다른 쓰레드는 실행할 수 없다. 만약 동기화 메소드와 동기화 블록이 여러개 있을 경우, 쓰레드가 이들 중 하나를 실행할 때 다른 쓰레드는 해당 메소드는 물론이고 다른 동기화 메소드 및 블록도 실행할 수 없다. 하지만 일반 메소드는 실행이 가능하다.

public vod method () {
  //여러 스레드가 실행 가능한 영역
  ...
    
  synchronized(공유객체) {
    임계 영역; //단 하나의 스레드만 실행
  }
  
  //여러 스레드가 실행 가능한 영역
  ...
}

데드락

Thread DeadLock

멀티 쓰레드 프로그래밍에서 동기화를 통해 락을 획득하여 동일한 자원을 여러 곳에서 사용하지 못 하도록 하였을 때,

서로 다른 쓰레드가 서로가 가지고 있는 락이 해제되기를 기다리는 상태가 생길 수 있으며 이러한 상태를 교착상태(deadlock)이라 한다.

교착 상태는 어떤 작업도 실행되지 못하고 서로 상대방의 작업이 끝나기만 바라는 무한정 대기 상태이다.

DeadLock 발생 조건

    - 상호 배제 (Mutual Exclusion) : 한 자원에 대해 여러 쓰레드 동시 접근 불가

    - 점유와 대기 (Hold and Wait) : 자원을 가지고 있는 상태에서 다른 스레드가 사용하고 있는 자원 반납을 기다리는 것

    - 비선점 (Non Preemptive) : 다른 쓰레드의 자원을 실행 중간에 강제로 가져올 수 없음

    - 환형대기(Circle Wait) : 각 쓰레드가 순환적으로 다음 쓰레드가 요구하는 자원을 가지고 있는 것

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

public class Main {

    public static Object object1 = new Object();
    public static Object object2 = new Object();

    public static void main(String[] args) {
        FirstThread thread1 = new FirstThread();
        SecondThread thread2 = new SecondThread();

        thread1.start();
        thread2.start();

    }

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

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("First Thread want to have object2's lock. so wait");

                synchronized (object2){
                    System.out.println("First Thread has object2's lock too");
                }
            }
        }
    }

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

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Second Thread want to have object1's lock, so wait");

                synchronized (object1){
                    System.out.println("Second Thread has object1's lock too");
                }
            }
        }
    }
}


/*************************************************
출력 결과
First Thread has object1`s lock
Second Thread has object2`s lock
First Thread want to have object2`s lock. so wait
Second Thread want to have object1`s lock. so wait
**************************************************

    - 상호 배제 : object1과 object2 객체에 대해서 동시에 쓰레드가 사용할 수 없도록 하였다.

    - 점유와 대기 : FirstThread에서는 object1의 락을 가지고 있으면서 object3에 대한 락을 원하고, SecondThread는 object2에 대한 락을 가지고 있으면서 object1의 락을 획득하기를 원한다.

    - 비선점 : 쓰레드의 우선순위의 기본값은 NORM_PRIORITY로 동일하게 설정되어 있습니다.

    - 환형대기 : FirstThread는 SecondThread의 object2 객체의 락을 대기하고 SecondThread는 FirstThread의 object1 객체의 락을 대기하고 있다.

public class Main {

    public static Object object1 = new Object();
    public static Object object2 = new Object();

    public static void main(String[] args) {
        FirstThread thread1 = new FirstThread();
        SecondThread thread2 = new SecondThread();

        thread1.start();
        thread2.start();

    }

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

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("First Thread want to have object2's lock. so wait");

                synchronized (object2){
                    System.out.println("First Thread has object2's lock too");
                }
            }
        }
    }

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

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Second Thread want to have object1's lock, so wait");

                synchronized (object2){
                    System.out.println("Second Thread has object1's lock too");
                }
            }
        }
    }
}

/*****************************
First Thread has object1`s lock
First Thread want to have object2`s lock. so wait
First Thread has object2`s lock too
Second Thread has object2`s lock
Second Thread want to have object1`s lock. so wait
Second Thread has object21`s lock too

Process finished with exit cod 0
*****************************/

위 코드는 환형대기 조건을 만족하지 않기 때문에 데드락이 발생하지 않는다.

ref {

  https://ict-nroo.tistory.com/41#recentEntries

 

[JAVA] 자바의 멀티 스레드

멀티 스레드 '이것이 자바다 - 신용권' 12장 학습 소스코드 repo 1절. 멀티 스레드 개념 2절. 작업 스레드 생성과 실행 3절. 스레드 우선순위 4절. 동기화 메소드와 동기화 블록 5절. 스레드 상태 6절.

ict-nroo.tistory.com

  https://math-coding.tistory.com/175

  https://scshim.tistory.com/243

}

'Programming > Java' 카테고리의 다른 글

[JAVA] Annotation  (0) 2022.03.28
[JAVA] ENUM  (0) 2022.03.21
[JAVA] 예외 처리  (0) 2022.03.04
[JAVA]인터페이스  (0) 2022.02.28
[JAVA] 패키지  (0) 2022.02.21

댓글