(5) Thread

Chaeyun·2024년 1월 29일
0

Java

목록 보기
5/6

Thread 객체 생성 방법은?

Java에서 Thread를 구현하는 방법은 크게 두 가지가 있다.

  1. Thread 클래스를 extends
  2. Runnable 인터페이스를 implements

그 후 run() 메소드를 오버라이딩하는 것이다.

1. Thread 클래스 상속

public class ThreadTest1 extends Thread {
	@Override
    public void run(){
    	// 작업 내용
    }
}

2. Runnable 인터페이스 구현

public class ThreadTest2 implements Runnable {
	@Override
    public void run(){
    	// 작업 내용
    }
}

위 방법 중 Thread를 상속하는 방법은 다른 클래스를 상속받을 수 없기에(*Java는 하나의 클래스만 상속 가능) Runnable 인터페이스를 구현하는 방법을 좀 더 권장하는 편이다.

Thread로 실행 흐름 생성

위처럼 만든 Thread 자식 클래스 또는 Runnable 인터페이스 구현 클래스를 이용해 실제 실행 흐름을 생성하는 방법에 대해 알아보자.

1. Thread 클래스 상속

public class ThreadTest1 extends Thread {
	@Override
    public void run(){
    	// ...
    }
    
    public static void main(String[] args){
    	Thread th = new ThreadTest1();
        th.start();
    }
}

2. Runnable 인터페이스 구현

public class ThreadTest2 implements Runnable {
	@Override
    public void run(){
    	// ...
    }
    
    public static void main(String[] args){
    	Runnable r = new ThreadTest2();
        Thread th = new Thread(r);
        th.start();
    }
}

Thread를 상속한 클래스의 경우 바로 new 키워드를 이용해 새로운 스레드 객체를 생성할 수 있다.
Runnable 인터페이스를 상속한 클래스의 경우 새로운 Thread의 인자로 인스턴스를 생성해 넘겨줘야 한다.

Thread의 run() 메소드

위와 같이 스레드를 구현해야 하는 이유는 Thread 클래스의 내부 구조가 다음과 같기 때문이다.

private Runnable target;
   ...
@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

Thread를 상속받아 run() 메소드를 오버라이딩하면 해당 메소드 로직이 변경되고,
Runnable 구현 클래스를 만들어 Thread 생성자로 인스턴스를 넘겨주면 위의 코드에서 target 변수에 해당 인스턴스가 담겨 스레드 실행시 Runnable의 run() 메소드가 호출된다.

run() VS start()

위의 예시 코드에서는 Thread 객체를 생성 후 start() 메소드를 호출해 실행 흐름을 생성했다.

이때 run() 메소드를 사용하면 안되는 걸까?


안 된다.
둘의 차이는 다음과 같다.

run() 메소드는 일반 메소드처럼 작용하며 독립적인 실행 흐름이 생성되지 않는다.
start() 메소드는 별개의 새로운 실행 흐름(스레드)을 생성한다.

아래 예시 코드를 보자.

1. run() 호출

public class ThreadTest2 implements Runnable {
	@Override
    public void run(){
    	while(true){
        	System.out.println("Running");
            sleep(100);
        }
    }
    
    public static void main(String[] args){
    	Runnable r = new ThreadTest2();
        Thread th = new Thread(r);
        th.run();
        
        System.out.println("Terminated");
    }
}

위와 같이 run() 호출 시 "Running"만 무한히 찍히며 "Terminated"는 출력되지 않는다.

2. start() 호출

public class ThreadTest2 implements Runnable {
	@Override
    public void run(){
    	while(true){
        	System.out.println("Running");
            sleep(100);
        }
    }
    
    public static void main(String[] args){
    	Runnable r = new ThreadTest2();
        Thread th = new Thread(r);
        th.start();
        
        System.out.println("Terminated");
    }
}

단, 위와 같이 start() 호출 시 "Terminated"가 출력되는 것을 볼 수 있다.

이는 start()로 호출 시 이를 호출한 스레드(부모 스레드)와 호출로 생성된 스레드(자식 스레드)의 실행 흐름이 독립적으로 흘러가기 때문이다.
단, run()을 호출 시 main 스레드는 해당 메소드가 수행되기를 기다린 뒤 남은 작업을 이어서 진행하게 된다.

스레드 상태 전이도란


(출처: https://mangkyu.tistory.com/309)

아래와 같이 Thread는 상태가 변화한다.

  1. 객체 인스턴스 생성 시 new 또는 create 상태가 된다.
  2. start() 메소드 호출 시 프로세서에 의해 실행되기를 기다리는 Runnable 상태가 된다.
  3. 스레드 스케쥴러가 스레드 풀에서 실행하기 위해 스레드를 가져오면 Running 상태가 된다. 이때 run() 메소드가 실행된다.
  4. Running 중 sleep(), wait(), join(), I/O block 등으로 대기하는 경우 Wating/Blocked 상태 (위 이미지에서는 Non-Runnable)가 된다.
    (suspend()는 deprecated.)
  5. 또는 Running 중 yield() 메소드 호출 시 다른 스레드에게 실행을 양보하고 Runnable 상태가 된다.
  6. Wating/Blocked 상태에서 sleep time-out, notify(), interrupt(), I/O 처리 완료 등으로 다시 Runnable 상태로 돌아간다.
    (resume()은 deprecated.)
  7. Running 상태에서 run() 메소드 완료 시 Done 또는 Terminated 상태가 된다.

스레드 상태 제어 메소드에는 어떤 것들이 있는가

위에서 Wating/Blocked 상태가 되는 상태 제어 메소드와 다시 Runnable 상태로 돌아가는 메소드들에 대해 좀 더 자세히 알아보자.

1. sleep

sleep()은 알다시피 인자로 넘겨진 시간(ms)만큼 일시정지한다.

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

일시 정지 상태에서 interrupt()가 호출되면 InterruptedException가 발생한다.

2. interrupt()

interrupt() 메서드는 작업 취소 요청을 보내는 역할이다.
interrupt()가 호출되는 경우 isInterrupted() 반환값이 true가 된다.
이 때 isInterrupted()는 결과를 반환하고 false로 초기화한다.
따라서, true는 interrupt()가 호출된 후 최초로 호출된 isInterrupted()에 한정해서 반환된다.

public class ThreadTest2 implements Runnable {
	@Override
    public void run(){
    	while(!isInterrupted()){
        	System.out.println("Running");
            sleep(100);
        }
        System.out.println("Terminated");
    }
    
    public static void main(String[] args){
    	Runnable r = new ThreadTest2();
        Thread th = new Thread(r);
        th.start();
        
        sleep(1000);
        
        try{
        	th.interrupt();
        } catch(InterruptedException e) {
    		e.printStackTrace();
		}
    }
}

위와 같은 경우 Running이 열번만 출력된 후 Terminated가 출력될 것이다.

(만약 sleep 중 interrupt가 호출되면 InterruptedException가 발생하여 Terminated는 출력되지 않음)

3. join()

join()은 다른 스레드가 종료될 때까지 대기하는 메소드이다.

public class ThreadTest2 implements Runnable {
	@Override
    public void run(){
    	try{
            while(!isInterrupted()){
                System.out.println("Running");
                sleep(100);
            }
       	} catch(InterruptedException e) {
    		e.printStackTrace();
		}
        
        sleep(1000);
        System.out.println("Terminating");
    }
    
    public static void main(String[] args){
    	Runnable r = new ThreadTest2();
        Thread th = new Thread(r);
        th.start();
        
        sleep(1000);
      	th.interrupt();
        th.join();
        
        System.out.println("Terminated");
    }
}

위에서 th.join()이 빠지면 "Terminated" > "Terminating" 순으로 출력된다.
하지만 join()을 호출하면 th 스레드가 종료되기까지 대기하다 그 뒤를 수행하므로 "Terminating" > "Terminated" 순으로 출력이 이루어진다.

이외에도 wait()(일시정지), notify()(wait으로 대기 상태에 있는 스레드를 다시 수행 가능한 상태로 깨움), notifyAll()(특정 스레드 그룹에 있는 모든 스레드들을 notify), yield()(running 상태인 스레드를 runnable로 변경) 등이 있다.

Thread 동기화 구현 방법에 대해 설명해보시오

멀티 스레드에서 스레드 동기화를 하는 목적은 "공유 자원을 보호"하기 위함이다.

공유 자원에 상호 배타적으로 접근하게 함으로써 공유 자원을 보호하고 동기화를 구현한다.

Thread의 동기화는 synchronized스레드 Lock을 이용해 작성할 수 있다.

synchronized는 메소드 synchronized와 블록 synchronized 두 가지로 작성할 수 있다.

1. 메소드 synchronized

public synchronized void function_name(){ //작업 내용 }

2. 블록 synchronized

public class ThreadLock(){
	private Object lockObj = new Object();
    public void testLock(string name) {
    	synchronized(lockObj) { // 작업 내용 }
    }
}
  1. 메소드 synchronized
    • 내부적으로 현재 객체를 락으로 사용한다.
    • 즉, 한 객체 내의 동기화 메소드들은 동일한 락을 사용한다.
      • 임의의 스레드가 하나의 메소드에 접근 중이면 다른 스레드들은 해당 메소드뿐만 아니라 해당 객체의 다른 동기화 메소드에도 접근할 수 없다.
    • 같은 클래스지만 서로 다른 객체의 경우는 당연히 다른 락을 사용하게 된다.
      • 만약 같은 클래스 간에 동기화를 구현하고자 하는 경우, static 변수로 락 객체를 선언하여 블록 synchronized에 넘겨주는 방법이 있다.
  2. 블록 synchronized
    • 락 객체를 synchronized 구문에 넘겨주어 작성한다.
    • 락 객체는 this를 사용하기도 하지만 별도의 락 객체를 작성하는 경우가 권장된다.
    • 클래스를 락으로도 사용가능하다.
      synchronized(String.class){ // 작업 내용 }

References

  1. 자바를 다루는 기술, 2014
  2. https://kindbear.tistory.com/67
  3. https://mangkyu.tistory.com/309

0개의 댓글

관련 채용 정보