본문 바로가기
DevelopmentTools/Java

[programmers-java 중급] 쓰레드(Thread)

by 수짱수짱 2022. 7. 21.

< 쓰레드란? >

쓰레드(Thread) : 동시에 여러가지 작업을 수행할 수 있게 하는 것

  • 자바 프로그램은 JVM위에서 실행되며 JVM도 하나의 프로그램
  • 하나의 프로세스(현재 실행되고 있는 프로그램, Process)안에서 여러개의 흐름(Thread)이 발생할 수 있다.
  • 자바 프로그램이 여러 개의 작업을 동시에 작업하도록 하려면 Thread를 이용해야 한다.

< 쓰레드 만들기(extend Thread) > 

자바에서 Thread 만드는 방법

  • Thread 클래스를 상속받는 방법
  • Runnable 인터페이스를 구현하는 방법
Thread 클래스를 상속받아 Thread 만들기

 

  1.  java.lang.Thread 클래스 상속받기
  2. 해당 Thread 클래스가 가지고 있는 run() 메소드를 오버라이딩

 

ex) Thread 클래스를 상속받은 MyThread1을 사용하는 클래스

public class MyThread1 extends Thread{   // 상속은 extends
    String str;
    public MyThread1(String str){  // 생성자
   		this.str = str;
    }
    
    public void run(){
	   for(int i=0; i<10; i++){
        
        	System.out.println(str);
            
                try{
                    Thread.sleep( (int)(Math.random() * 1000) );    // 받아온 str이 천천히 출력되도록 조절
                }
                catch(InterruptedException e){
                    e.printStackTrace();
                }
             }
	}
}

Thread를 상속받아 run() 메소드를 구현하는 예제

* sleep() 메소드 때문에 예외처리가 필요하다.

 

=> MyThread1은 Thread를 상속받았으므로 Thread가 된다. 해당 쓰레드를 호출하기 위해 start 메소드를 호출해보자.

public class ThreadExam1{
	public static void main(String[] args){
    	MyThread1 t1 = new MyThread("*");   // 쓰레드 객체 생성
        MyThread1 t2 = new MyThread("-");
        
        t1.start(); // 쓰레드 시작
        t2.start();
        System.out.println("main thread end!!!!");
    }
}

쓰레드를 생성하고 start 메소드를 호출하는 예제

 

=> 수행흐름이 3가지가 되며 Main Thread는 다 실행되면 "!!!" 를 출력하게 되면서 Main Thread가 종료한다. 

 

단, Main Thread가 종료된다해도 프로그램이 종료되지 않는다! 

모든 쓰레드가 종료되어야 프로그램이 종료된다.


< 쓰레드 만들기(implements Runnable) >

자바에서 Thread 만드는 방법

  • Thread 클래스를 상속받는 방법
  • Runnable 인터페이스를 구현하는 방법
Runnable 인터페이스를 구현하여 Thread 만들기

 

Q> 이 방법을 자바가 제공해주는 이유?

A. 자바는 단일 상속만 지원하기 때문이다. 이미 다른 클래스를 상속받고 있다면 Thread 클래스를 상속받지 못하지만

인터페이스는 여러 개를 구현하여 사용이 가능하기 때문에 이 방법을 사용하는 것이다.

 

참고* 상속과 인터페이스 구현은 동시에 가능하다

 

  • Runnable 인터페이스가 가지고 있는 run() 메소드를 구현한다.
public class MyThread2 implements Runnable {   // 인터페이스를 구현하는 것이므로 implements 사용
	String str;
    public MyThread2(String str){
    	this.str = str;
    }
    
    public void run(){
		for(int i=0; i<10; i++){
        	System.out.println(str);
            try{
            	Thread.sleep((int)(Math.random() * 1000));
            }
            catch(InterruptedException e){
            	e.printStackTrace();
            }
        }
	}
}

 

Thread가 run() 메소드를 직접호출하는 것이 아니라 start() 메소드를 호출해야 하지만 MyThread2는 Thread 클래스를 상속받은 것이 아니기 때문에 run 메소드는 존재하나 start 메소드가 존재하지 않는다. run 메소드밖에 없고 Thread는 오직 start메소드를 통해 실행될 수 있다. 즉, 해당 방법으로는  start 메소드를 호출할 수가 없어진다. 

 

이를 해결하기 위해 실제 수행할 때 Thread 객체를 만들어 runnable을 상속받은 객체를 넣어준다.

Thread의 생성자는 runnable을 받아들일 수 있게 되어있다. MyThread2는 runnable 인터페이스를 구현하고 있기 때문에 Thread의 생성자를 이용할 수 있다. 

public class ThreadExam2{
	public static void main(String[] args){
            MyThread2 r1 = new MyThread("*");
            MyThread2 r2 = new MyThread("-");

            Thread t1 = new Thread(r1);
            Thread t2 = new Thread(r2);

            t1.start();
            t2.start();

            System.out.println("main thread end!!!!");
	}
}

 


< 쓰레드와 공유객체 >

 
 

하나의 객체를 여러개의 Thread가 사용한다.

 

예제) MusicBox 클래스는 메소드 3가지를 가진다. 각각의 메소드는 1초 이하의 시간동안 10번을 반복하면서 음악을 출력.

이러한 MusicBox를 사용하는 MusicPlayer를 3개 만들어보자.

 

=> 3개의 MusicPlayer(쓰레드) 는 1개의 MusicBox를 사용할 것이다. 

 

    public class MusicBox { 
        
        public void playMusicA(){
            for(int i = 0; i < 10; i ++){
                System.out.println("신나는 음악!!!");
                try {
                    Thread.sleep((int)(Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }        
        }

       
        public void playMusicB(){
            for(int i = 0; i < 10; i ++){
                System.out.println("슬픈 음악!!!");
                try {
                    Thread.sleep((int)(Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }        
        }
       
       
        public void playMusicC(){
            for(int i = 0; i < 10; i ++){
                System.out.println("카페 음악!!!");
                try {
                    Thread.sleep((int)(Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }    
        }   
    }

MusicBox 클래스

public class MusicPlayer extends Thread{ // MusicPlayer는 쓰레드
	int type;
    	MusicBox musicbox; // MusicBox를 가질 수 있다.
    
    public MusicPlayer(int type, MusicBox musicbox){ // 생성자
    	this.type = type;
        this.musicbox = musicbox;
    }
    
    public void run(){
    	swtich(type){
                case 1:
                    musicbox.playMusicA();
                    break;
                case 2:
                    musicbox.playMusicB();
                    break;
                case 3:
                    musicbox.playMusicC();
                    break;
        }
    }
}

 

MusicBox를 가지는 Thread 객체 MusicPlayer

 

    public class MusicBoxExam1 {

        public static void main(String[] args) {
            
            MusicBox box = new MusicBox(); // MusicBox 객체

            MusicPlayer kim = new MusicPlayer(1, box);
            MusicPlayer lee = new MusicPlayer(2, box);
            MusicPlayer kang = new MusicPlayer(3, box);   // 쓰레드 3개 생성

            // MusicPlayer쓰레드를 실행합니다. 
            kim.start();
            lee.start();
            kang.start();           
        }   
    }

MusicBox와 MusicPlayer를 이용하는 MusicBoxExam1 클래스

 

 


< 동기화 메소드와 동기화 블록 >

 
 

공유객체가 가진 메소드를 동시에 호출 되지 않도록 하는 방법

  • 메소드 반환 타입 앞에 synchronized 를 붙인다.
  • 여러개의 Thread들이 공유객체의 메소드를 사용할 때 메소드에 synchronized가 붙어 있을 경우 먼저 호출한 Thread가 객체의 사용권(Monitoring Lock)을 얻는다. 
  • Monitoring Lock은 메소드 실행이 종료되거나 wait() 메소드를 만나기 전까지 유지된다.
  • 다른 Thread들은 Lock이 풀릴때까지 대기한다.
  • synchronized가 붙지 않은 메소드는 다른 쓰레드들의 모니터링 락 여부에 상관없이 실행된다.
    • 메소드의 코드가 길 때 synchronized를 남용할 경우 마지막에 대기하는 쓰레드의 대기 시간이 길어질 수 있으므로 문제가 있을 것 같은 부분에만 synchronized 블록을 사용한다.
    • 메소드 전체에 synchronized를 붙여주는 것이 아니라 동시에 실행되면 안 되는 부분에만 적용

 

예제)

public synchronized void playMusicA(){   // 메소드에 return타입 앞에 synchronized 키워드 추가
	for(int i=0; i<10; i++){
    	Systen.out.println("신나는 음악");
        
        try{
        	Thread.sleep((int)(Math.random() * 1000));
        }
        catch(InterruptedException e){
        	e.printStackTrace();
        }
    }
}

synchronized가 붙은 메소드

 

public void playMusicB(){
	for(int i=0; i<10; i++){
    
    	synchronized(this){   // 자기 자신 객체. 이때만 모니터링락을 가지고 실행된다. => 다른 쓰레드들은 더 빠르게 실행된다.
        	System.out.println("슬픈 음악");
        }
        
        try{
        	Thread.sleep((int)(Math.random() * 1000));
        }
        catch(InterruptedException e){
        	e.printStackTrace();
        }
    }
}

메소드 전체가 아닌 부분에만 synchronized가 붙은 블럭

=> 동기화 되는 부분이 메소드 전체가 아니고 필요한 부분만 동기화된다.


< 쓰레드와 상태제어 >

쓰레드가 3개가 있을 때 JVM은 쓰레드를 동시에 실행시는 것 처럼 보이기 위해 t1, t2, t3를 번갈아 가며 실행한다.

 

  • Thread의 상태
    • Runnable : 실행가능상태
    • Running : 실행상태
    • Blocked : 블록상태
      • 쓰레드가 공유객체의 synchronized 블럭/메소드를 실행했을 때 다른 쓰레드가 이미 모니터링 락을 갖고 있을 때 걸리는 상태
      • wait() 메소드 호출, sleep() 메소드 호출 할 경우 발생하는 상태
        • sleep() : 특정시간이 지나면 스스로 블록상태에서 빠져나와 Runnable이나 Running 상태가 된다.
        • wait() : 모니터링 락을 놓게 되며 다른 대기 상태 메소드가 실행된다.
          • wait()는 다른 쓰레드가 notify(), notifyAll() 를 호출하기 전까지는 블록상태를 빠져나갈 수 없다.
    • Dead : 종료상태
      • 쓰레드의 run 메소드가 종료될 때
    • 다른 Thread에게 자원 양보
      • yeild 메소드를 호출했을 때
      • 다른 쓰레드가 더 빨리 실행될 수 있도록 한다.
    • 해당 Thread가 종료될 때 까지 대기
      • join 메소드를 호출했을 때

 

쓰레드의 상태


< 쓰레드와 상태제어 (join) >

join 메소드는 쓰레드가 멈출때까지 기다리게 하는 메소드

=> 메인쓰레드가 먼저 끝나는 경우를 해결할 수 있다.

public class MyThread5 extends Thread{
    public void run(){
        for(int i = 0; i < 5; i++){
            System.out.println("MyThread5 : "+ i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
           	}
        }
    } 
}

0.5초씩 쉬면서 숫자를 출력하는 쓰레드인 MyThread5

 

public class JoinExam { 
    public static void main(String[] args) {
    
    MyThread5 thread = new MyThread5();
    thread.start(); 
    
    System.out.println("Thread가 종료될때까지 기다립니다.");
    
        try {
            // 해당 쓰레드(MyThread5)가 멈출때까지 멈춤
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
    System.out.println("Thread가 종료되었습니다."); 
    
    }   
}

MyThread5 쓰레드가 종료될 때 까지 기다리는 JoinExam 클래스

 

* join메소드는 예외처리 필요


< 쓰레드와 상태제어 (wait, notify) >

wait, notify는 동기화된 블록 안에서만 사용 가능

  • wait 메소드를 만나게 되면 해당 객체의 모니터링 락에 대한 권한을 놓고 대기한다.
  • notify 메소드를 만나게되면 wait하고 있는 쓰레드를 깨울 수 있다.
public class ThreadB extends Thread{

       // 해당 쓰레드가 실행되면 자기 자신의 모니터링 락을 획득
       // 5번 반복하면서 0.5초씩 쉬면서 total에 값을 누적
       // 그후에 notify()메소드를 호출하여 wiat하고 있는 쓰레드를 깨움 
       
	int total;
        
	@Override
	public void run(){
        
		synchronized(this){
            
			for(int i=0; i<5 ; i++){
                
				System.out.println(i + "를 더합니다.");
				total += i;
                    
				try {
					Thread.sleep(500);
				} 
				catch (InterruptedException e) {
					e.printStackTrace();
				}
                    
			}
                
			notify();   // 쓰레드를 깨운다.
		}
	}
}

ThreadB

 

 public class ThreadA {   // ThreadB를 사용하며 wait하는 클래스 작성
 
	public static void main(String[] args){
        
        // 앞에서 만든 쓰레드 B를 만든 후 start 
        // 해당 쓰레드가 실행되면, 해당 쓰레드는 run메소드 안에서 자신의 모니터링 락을 획득(synchronize 블럭을 설정했기 때문이다.)
        ThreadB b = new ThreadB();
        b.start();

        // b에 대하여 동기화 블럭을 설정
        // 만약 main쓰레드가 아래의 블록을 위의 Thread보다 먼저 실행되었다면 wait를 하게 되면서 모니터링 락을 놓고 대기       
        synchronized(b){

            try{             
                System.out.println("b가 완료될때까지 기다립니다.");
                b.wait(); // 메인쓰레드는 정지 => 다른 쓰레드가 notify 메소드를 호출해야 깨어난다.
                // ThreadB가 5번 값을 더한 후 notify를 호출하게 되면 메인쓰레드가 wait에서 깨어남

            }catch(InterruptedException e){
                e.printStackTrace();
            }

        	System.out.println("Total is: " + b.total);  //  메인 쓰레드가 깨어난 이후결과를 출력
            }
	}        
}

ThreadB를 사용하며 wait 하는 ThreadA 클래스

 

 

 

ThreadB에서 synchronized 블럭을 설정했기 때문에 메인 쓰레드가 아래의 블럭{ synchronized(b) }을 위 쓰레드보다 먼저 실행하게 되었다면 wait가 걸리면서 모니터링 락이 걸리고 대기를 한다.

 

 


< 데몬 쓰레드 >

데몬(Daemon) : 리눅스와 같은 유닉스계열의 운영체제에서 백그라운드로 동작하는 프로그램

* 윈도우에서는 서비스(service)

 

데몬 쓰레드(Daemon Thread) :  자바에서 데몬과 유사하게 동작하는 쓰레드

Ex> 주기적으로 맞춤법 검사하는 프로그램 (자바에서 백그라운드에서 특별한 작업을 처리)

  • 데몬 쓰레드를 만들기 위해서는 쓰레드에 데몬 설정을 하면 된다.
  • 데몬 쓰레드는 자바 프로그램에서 백그라운드에서 특별한 작업을 처리하게 하는 용도
  • 데몬 쓰레드는 일반 쓰레드가 모두 종료되면 강제적으로 종료된다.
public class DaemonThread implements Runnable { // Runnable 인터페이스를 구현하는 데몬쓰레드 클래스
	
    @override
    public void run() {
    	while(true){
        	System.out.println("데몬 쓰레드 실행중");
            
            try{
            	Thread.sleep(500);  // 0.5초 동안 실행중임을 출력하는 run 메소드
            }
            catch(InterruptedException e){
            	e.printStackTrace();
                break;  // 예외발생시 while문 탈출
            }
        }
    }
    
    public static void main(String[] args){
    
    	Thread th = new Thread(new DaemonThread()); // Runnable 인터페이스를 구현하는 데몬쓰레드를 위한 Thread 생성
        th.setDaemon(true); // 데몬쓰레드로 설정 => setDaemon 메소드 사용!
        th.start(); // 데몬쓰레드 실행
        
        try{
        	Thread.sleep(1000); // 메인이 종료되면 데몬도 종료되므로 메인이 1초뒤에 종료될 수 있도록 설정
        }
        catch(InterruptedException e){
        	e.printStackTrace();
        }
        
        System.out.println("메인 쓰레드 종료");
    }
}

Runnable 인터페이스를 구현하는 DaemonThread 클래스

 

실행결과

 


강의 출처 :  https://school.programmers.co.kr/learn/courses/9/lessons/270

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr