[프로그래머스] 자바 중급 - 쓰레드

정현우·2021년 4월 7일
2

Java Basic

목록 보기
7/8

자바 쓰레드(Thread)

Thread는 무엇인가? OS적 관점과 자바에서 다루는 쓰레드에 대해알아보자!
OS에서 process, thread에 대한 기본 지식은 생략한다.

쓰레드란

정의

  • 일단 세부적인 내용을 process, thread, 자원, 할당, 교착 등 다루기 시작하면 끝이 없다,, 그리고 무조건 '제대로', '깊게' 알아야 하기 때문에 java를 통해 역으로 깨닫는 방식은 너무나 옳지 않다! CS지식 찍어먹기 멈춰!
  • 가볍게 읽어보자, 그리고 이것 또한, 그리고 이것도
  • 프로세스는 현재 실행되고 있는 프로그램의 단위이고, 이 프로그램 단위(프로세스 안)에 쓰레드가 존재한다!
  • 여러개 쓰레드가 하나의 프로세스 안에서 작동이 된다! 멀티 스레딩이 된다!!

자바의 메인쓰레드?

  • 쓰레드는 동시성과 병렬성이 있어야 한다.
  • 프로세스 시작 시 최초 실행되는 static main 메서드는 JVM에 의해 생성된 main() 스레드에서 실행된다.
  • 자바 어플리케이션 자체는 운영체제에게 자원을 할당받은 프로세스. -> 컴파일된 자바 클래스는 JVM 위에서 동작
  • JVM은 프로그램의 시작점인 main() 메서드를 찾아서 메인 스레드를 생성하고 메인 스레드의 run() 메서드를 통해 해당 main() 메서드를 실행합니다.
  • 이때 main(String[] args) 매개변수인 String 배열에 명령행에서 입력한 파라미터를 넣어 호출하는 것! -> 이 시점에서 스레드는 메인 스레드 하나이고 당연히 처리 흐름도 하나입니다.

자바에서 쓰레드 만들기

Thread 클래스를 상속받기

  • java.lang.Thread클래스를 상속받는다. 그리고 Thread가 가지고 있는 run()메소드를 오버라이딩한다.
    public class MyThread1 extends Thread {
        String str;
        public MyThread1(String str){
            this.str = str;
        }

        public void run(){
            for(int i = 0; i < 10; i ++){
                System.out.print(str);
                try {
                    // 너무 빠르기 때문에 수행결과를 잘 확인 할 수 없어서 Thread.sleep() 메서드를 이용해서 조금씩 
                    // 쉬었다가 출력할 수 있게한다. 
                    Thread.sleep((int)(Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } 
        } 
    }
  • 위 class의 인스턴스(object)를 만들어서 메인쓰레드에서 사용해보자!
    public class ThreadExam1 {
        public static void main(String[] args) {
            // MyThread인스턴스를 2개 만듭니다. 
            MyThread1 t1 = new MyThread1("*");
            MyThread1 t2 = new MyThread1("-");

            t1.start();
            t2.start();
            System.out.print("!!!!!");  
        }   
    }
  • Thread 클래스를 상속받은 MyThread1을 사용하는 클래스
    • Thread를 상속 받았으므로 MyThread1은 Thread 이다.
    • 쓰레드를 생성하고, Thread 클래스가 가지고 있는 start() 메소드를 호출 한다.
    • start메소드를 호출하지 않으면 run하지 않는다!
  • 메인 스레드가 죽는다고 프로세스가 죽는 건 아니다! -> 하나의 쓰레드라도 run 중이면 종료되지 않는다! -> 모든 쓰레드가 죽어야 프로세스도 죽는다!

Runnable인터페이스를 구현하기

  • Runable 인터페이스가 가지고 있는 run()메소드를 구현한다.
    public class MyThread2 implements Runnable {
        String str;
        public MyThread2(String str){
            this.str = str;
        }
		
        @Override
        public void run(){
            for(int i = 0; i < 10; i ++){
                System.out.print(str);
                try {
                    Thread.sleep((int)(Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } 
        } 
    }
  • 위 class의 인스턴스(object)를 만들어서 메인쓰레드에서 사용해보자!
    public class ThreadExam2 {  
        public static void main(String[] args) {
            MyThread2 r1 = new MyThread2("*");
            MyThread2 r2 = new MyThread2("-");

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

            t1.start();
            t2.start();
            System.out.print("!!!!!");  
        }   
    }
  • Runable 인터페이스를 구현한 MyThread2을 사용하는 클래스
    • MyThread2는 Thread를 상속받지 않았기 때문에 Thread가 아니다.
    • Thread를 생성하고, 해당 생성자에 MyThread2를 넣어서 Thread를 생성한다. -> thread를 상속받고 만드는 방법과 차이 체크!
    • Thread 클래스가 가진 start()메소드를 호출한다.
  • 자바는 단일 상속만 지원하기 때문에 Runable 인터페이스를 지원한다!
    • 즉 다중상속이 안되기 때문에 다른 class를 extend하고 있으면 인터페이스를 사용하면 된다!
    • public class Bus extends Car implements Runnable
    • 생성자도 따로 이용할 수 있다는게 장점이다!
    • 좀 더 다양하고 많이 사용되는 방법이다.

쓰레드와 공유객체

공유객체?

공유 객체 사진

    public class MusicBox { 
        //신나는 음악!!! 이란 메시지가 1초이하로 쉬면서 10번 반복출력
        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();
                }
            } // for        
        } //playMusicA

        //슬픈 음악!!!이란 메시지가 1초이하로 쉬면서 10번 반복출력
        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();
                }
            } // for        
        } //playMusicB
        
        //카페 음악!!! 이란 메시지가 1초이하로 쉬면서 10번 반복출력
        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();
                }
            } // for        
        } //playMusicC  
    }
    
    // MusicBox를 가지는 Thread객체 MusicPlayer
    public class MusicPlayer extends Thread{
        int type;
        MusicBox musicBox;  // "공유 객체" 
        
        // 생성자로 부터 musicBox와 정수를 하나 받아들여서 필드를 초기화
        public MusicPlayer(int type, MusicBox musicBox){
            this.type = type;
            this.musicBox = musicBox;
        }       
        
        // type이 무엇이냐에 따라서 musicBox가 가지고 있는 메소드가 다르게 호출
        public void run(){
            switch(type){
                case 1 : musicBox.playMusicA(); break;
                case 2 : musicBox.playMusicB(); break;
                case 3 : musicBox.playMusicC(); break;
            }
        }       
    }    
    
    // 메인 쓰레드가 있는 클래스로 위 클래스 인스턴스를 쓰레드화 
    public class MusicBoxExam1 {

        public static void main(String[] args) {
            // 공유 객체로 쓸 MusicBox 인스턴스
            MusicBox box = new MusicBox();
			
            // MusicPlayer object는 모두 동일한 MusicBox obj를 가진다
            MusicPlayer kim = new MusicPlayer(1, box);
            MusicPlayer lee = new MusicPlayer(2, box);
            MusicPlayer kang = new MusicPlayer(3, box);

            // MusicPlayer쓰레드를 실행합니다. 
            kim.start();
            lee.start();
            kang.start();           
        }   
    }    
  • 위 흐름은 아래와 같다
    • 공통으로 사용할 데이터를 클래스로 정의하고 공통으로 사용할 클래스의 인스턴스를 만든다.
    • 그리고 이 인스턴스를 각각의 쓰레드에 넘겨 준다.
    • 각각의 쓰레드는 이 인스턴스의 참조값을 저장한 변수를 이용하여 공통 데이터를 사용한다.
  • 값이 어떻게 바뀔까? 예측이 가능한가? 어떻게 실행이 되는가?
  • ps) 원래 쓰레드 환경에서는 기본적으로 예측이 불가능하다 -> 가능하게 하는 방법도 있지만, 멀티쓰레딩 목적은 '예측 가능과 불가능'이 아니라, 서로 간 실행 순서가 상관 없을때 사용되어야 하는 것!

그래서 뭐? volatiole!

  • 왜 공유 객체? -> 메모리 절약과 최적화 관점
  • volatile에 좀 더 자세히
    • 당연히 하나의 쓰레드가 아닌 여러 쓰레드가 R/W하는 상황에서는 적합하지 X
    • 그렇게 하기 위해서는 synchronized를 통해 변수 R/W(읽고 쓰기라고 하자)의 원자성(atomic)을 보장해야한다!

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

동기화

  • 서로간 영향을 주고받는 데이터들간에 데이터의 일관성이 유지될 수 있도록 해주는 것이 동기화이다.
  • 위 예제에서 music box를 생각하자. 3개의 메소드가 '동시에' 호출이 되어서 고장이 난다면?!
  • 하나의 메서드가 실행되고 있을때는 다른 메서드를 사용하지 않도록 대기 시켜야한다.
  • 실행이 끝나면 대기하던 메서드를 실행시키면 된다.
  • 즉, 간단하게 공유객체가 가진 메소드를 동시에 호출 되지 않도록 하는 방법
    • 메소드 앞에 synchronized 를 붙힌다.
    • 여러개의 Thread들이 공유객체의 메소드를 사용할 때 메소드에 synchronized가 붙어 있을 경우 먼저 호출한 메소드가 객체의 사용권(Monitoring Lock)을 얻는다.
  • 위 예제에서 메서드에 synchronized 붙여서 실행해보자! => 메소드 하나가 모두 실행된 후에 다음 메소드가 실행된다.

모니터링 락(Monitoring Lock)

  • 해당 모니터링 락은 메소드 실행이 종료되거나, wait()와 같은 메소드를 만나기 전까지 유지된다.
  • 다른 쓰레드들은 모니터링 락을 놓을때까지 대기한다.
  • synchronized를 붙히지 않은 메소드는 다른 쓰레드들이 synchronized메소드를 실행하면서 모니터링 락을 획득했다 하더라도, 그것과 상관없이 실행된다.
  • synchronized를 메소드에 붙혀서 사용 할 경우, 메소드의 코드가 길어지면, 마지막에 대기하는 쓰레드가 너무 오래 기다리는것을 막기위해서 메소드에 synchronized를 붙이지 않고, 문제가 있을것 같은 부분만 synchronized블록을 사용한다.
  • 깊게 알면 알수록 좋다
    public void playMusicB(){
        for(int i = 0; i < 10; i ++){
            // 해당 객체에 대해 모니터링 락가졌니? (여기서는 this)
            synchronized (this) {
                System.out.println("슬픈 음악!!!");
            }
            try {
                Thread.sleep((int)(Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } // for        
    } //playMusicB

쓰레드와 상태제어

쓰레드의 상태

java process and thread

  • 쓰레드는 어떻게 실행될까? 시간에 지나감에 따라 여러 쓰레드가 동시다발적으로 실행이 된다. run, wait, stop 정도로 간단하게 생각해 볼 수 있다.
  • 쓰레드가 3개가 있다면 JVM은 시간을 잘게 쪼갠 후에 한번은 쓰레드1을, 한번은 쓰레드 2를, 한번은 쓰레드 3을 실행합니다. 이것에 빠르게 일어나다 보니 쓰레드가 모두 동작하는 것처럼 보이는 것이다.

thread status

  • 쓰레드는 실행가능상태인 Runnable과 실행상태인 Running상태로 나뉜다.
  • 실행되는 쓰레드 안에서 Thread.sleep()이나 Object가 가지고 있는 wait()메소드가 호출이 되면 쓰레드는 블록상태가 된다. blocking vs nonblocking
  • Thread.sleep()은 특정시간이 지나면 자신 스스로 블록상태에서 빠져나와 Runnable이나 Running상태가 된다.
  • Object가 가지고 있는 wait()메소드는 다른 쓰레드가 notify()나 notifyAll()메소드를 호출하기 전에는 블록상태에서 해제되지 않는다. -> 일이 끝났다는 걸 '알린다' 생각하자!
  • wait()메소드는 호출이 되면 모니터링 락을 놓게 된다. 그래서 대기중인 다른 메소드가 실행한다.
  • 쓰레드가 고유 객체의 sync block이나 method가 실행되었는데 이미 다른 쓰레드가 모니터링 락을 획득한 상태라면, 락 풀에서 블럭된 상태가 된다. 이 역시 block되었다고 표현 할 수 있다.
  • 쓰레드의 run메소드가 종료되면, 쓰레드는 종료된다. 즉 Dead상태가 된다.
  • Thread의 yeild메소드가 호출되면 해당 쓰레드는 다른 쓰레드에게 자원을 양보하게 된다. -> 즉 다른 쓰레드가 좀 더 빠르게 실행되게 할 수 있다.
  • Thread가 가지고 있는 join메소드를 호출하게 되면 해당 쓰레드가 종료될 때까지 대기하게 된다.

simple thread status

join

  • 쓰레드가 멈출때까지 기다리게 한다. (Thread가 가지고 있는 join메소드를 호출하게 되면 해당 쓰레드가 종료될 때까지 대기)
  • 일단 0.5초씩 쉬면서 숫자를 출력하는 MyThread5를 작성해 보자
    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();
                }
            }
        } // run
    }
    
    // 위 해당 쓰레드를 실행하고, 해당쓰레드가 종료될때까지 기다린 후, 내용을 출력하는 JoinExam클래스
    public class JoinExam { 
        public static void main(String[] args) {
            /* 
            MyThread5 thread = new MyThread5();            
            thread.start();
            System.out.println("Thread 시작.");
            System.out.println("Thread 종료.");
            */
        
        
            MyThread5 thread = new MyThread5();
            // Thread 시작 -> (Thread 상속 받았기 때문에 바로 시작 가능)
            thread.start(); 
            System.out.println("Thread가 종료될때까지 기다립니다.");
            try {
                // 해당 쓰레드가 멈출때까지 멈춤
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread가 종료되었습니다."); 
        }   
    }    
  • 실행 결과 -> 즉 main 쓰레가 해당 쓰레드 끝날때까지 wait상태가 된 것!
        [ 주석 안 코드 먼저 ]
        Thread 시작.
        Thread 종료.
        MyThread5 : 0
        MyThread5 : 1
        MyThread5 : 2
        MyThread5 : 3
        MyThread5 : 4
        
        [ join이 들어간 메인 코드 ]
        Thread가 시작되었습니다.
        Thread가 종료될때까지 기다립니다.
        MyThread5 : 0
        MyThread5 : 1
        MyThread5 : 2
        MyThread5 : 3
        MyThread5 : 4
        Thread가 종료되었습니다.

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(); // 알린다! -> wait 풀어 임마!
            }
        }
    }
    
    // ThreadB를 사용하며 wait하는 클래스 작성
    public class ThreadA {
        public static void main(String[] args){
            // 앞에서 만든 쓰레드 B를 만든 후 start 
            // 해당 쓰레드가 실행되면, 해당 쓰레드는 run메소드 안에서 자신의 모니터링 락을 획득
            ThreadB b = new ThreadB();
            b.start();

            // b에 (thread 상속받는 object) 대하여 동기화 블럭을 설정
            // 만약 main쓰레드가 아래의 블록을 위의 Thread보다 먼저 실행되었다면 wait를 하게 되면서 모니터링 락을 놓고 대기       
            synchronized(b){
                try{
                    // b.wait()메소드를 호출.
                    // 메인쓰레드는 정지
                    // ThreadB가 5번 값을 더한 후 notify를 호출하게 되면 wait에서 깨어남
                    System.out.println("b가 완료될때까지 기다립니다.");
                    b.wait(); // 잠들어서 기다려 -> 즉 다른 쓰레드가 notify 할때까지 기다린다! 
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                //깨어난 후 결과를 출력
                System.out.println("Total is: " + b.total);
            }
        }
    }    
  • 실행 결과 ->
        b가 완료될때까지 기다립니다.
        0를 더합니다.
        1를 더합니다.
        2를 더합니다.
        3를 더합니다.
        4를 더합니다.
        Total is: 10

데몬 쓰레드 (Daemon Thread)

  • 데몬? : 데몬(Daemon)이란 보통 리눅스와 같은 유닉스계열의 운영체제에서 백그라운드로 동작하는 프로그램을 말한다. -> window 계열에서는 보통 service(서비스)라고 한다.
  • 자바에서 데몬과 유사하게 동작하는 쓰레드가 데몬쓰레드다!
    • 데몬쓰레드를 만드는 방법은 쓰레드에 데몬 설정을 하면 된다.
    • 이런 쓰레드는 자바프로그램을 만들 때 백그라운드에서 특별한 작업을 처리하게 하는 용도로 만든다.
    • ex) 주기적으로 자동 저장 / 일정 시간에 맞춤법 검사 ....
  • 데몬쓰레드는 일반 쓰레드(main 등)가 모두 종료되면 강제적으로 종료되는 특징을 가지고 있다.
  • setDaemon 메서드는 반드시 start()를 호출하기 전에 실행되어야 한다. 그렇지 않으면 IllegalThreadStateException이 발생한다!
    // Runnable을 구현하는 DaemonThread클래스를 작성
    public class DaemonThread implements Runnable {

        // 무한루프안에서 0.5초씩 쉬면서 데몬쓰레드가 실행중입니다를 출력하도록 run()메소드를 작성
        @Override
        public void run() {
            while (true) {
                System.out.println("데몬 쓰레드가 실행중입니다.");
                try {
                    Thread.sleep(500); // 0.5초씩 쉬면서 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break; // Exception발생시 while 문 빠찌도록 
                }
            }
        }

        public static void main(String[] args) {
            // Runnable을 구현하는 DaemonThread를 실행하기 위하여 Thread 생성
            Thread th = new Thread(new DaemonThread());
            th.setDaemon(true); // 데몬쓰레드로 설정!!
            th.start(); // 쓰레드를 실행

            // 메인 쓰레드가 1초뒤에 종료되도록 설정. 
            // 데몬쓰레드는 다른 쓰레드가 모두 종료되면 자동종료.
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }   
            System.out.println("메인 쓰레드가 종료됩니다. ");    
        }   
    }
profile
도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결” 에 몰두하는 개발자가 되고싶습니다. 그러기 위해 항상 새로운 것에 도전하고 노력하는 개발자가 되고 싶습니다!

0개의 댓글