java - Thread

이유석·2022년 11월 24일
0
post-thumbnail

Thread 란?

  • 프로세스(process) 내에서 실제로 작업을 수행하는 주체를 의미합니다.
  • 모든 프로세스에는 한 개 이상의 스레드가 존재하여 작업을 수행합니다.

    Process 란? : 단순히 실행 중인 프로그램(program)이라고 할 수 있습니다.
    즉, 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행 중인 것을 말합니다.

  • Java 프로그램도 JVM 이라는 프로그램에 의해 실행된다.
    즉, 운영체제 입장으로 보면, Java는 JVM 이라는 프로그램의 프로세스가 실행된 것 으로 볼 수 있다.
  • Java 프로그램이 여러개의 작업을 동시에 할 수 있도록 하는 것이 : 멀티 스레드

Thread 만들기

방법

  1. Thread 클래스를 상속받는 방법
  2. Runnable 인터페이스를 구현하는 방법

Thread 클래스를 상속받는 방법

  • 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();
            }
        } 
    } 
}
  • MyThread1 클래스를 사용하는 예제
public class ThreadExam1 {
    public static void main(String[] args) {
        // MyThread인스턴스를 2개 만듭니다. 
        MyThread1 t1 = new MyThread1("*");
        MyThread1 t2 = new MyThread1("-");

        t1.start(); // MyThread1 의 상태를 ready 로 전환
        t2.start();
        System.out.print("!!!!!");  
    }   
}

Runnable 인터페이스를 구현하는 방법

  • Runnable 인터페이스를 구현하여서, Runnable 인터페이스가 가지고 있는 run() 메서드를 구현한다.
public class MyThread2 implements Runnable {
    String str;
    public MyThread2(String str){
        this.str = str;
    }

    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();
            }
        } 
    } 
}
  • MyThread2 는 Thread를 상속받지 않았기 때문에 Thread 가 아니다.
  • Thread 를 생성하고, 해당 생성자에 MyThread2 를 넣어서 Thread를 생성한다.
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("!!!!!");  
    }   
}

Thread 와 공유 객체

  • 하나의 객체를 여러 Thread 가 공유하여 사용한다는 것을 의미.

  • MusicBox라는 클래스가 있다고 가정하겠습니다. 해당 클래스는 3개의 메소드를 가지고 있습니다.
    각각의 메소드는 1초 이하의 시간동안 10번 반복하면서, 어떤 음악을 출력합니다.
    이러한 MusicBox를 사용하는 MusicPlayer를 3명 만들어 보도록 하겠습니다.

  • MusicPlayer3명은 하나의 MusicBox를 사용할 것입니다. 이때 어떤 일이 발생하는지 살펴보도록 하겠습니다.

공유 객체 Music Box

 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;
        }
    }       
}

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

public class MusicBoxExam1 {

    public static void main(String[] args) {
        // MusicBox 인스턴스
        MusicBox box = new MusicBox();

        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();           
    }   
}

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

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

    동기화 문제

    • 다중 프로세스 환경에서 동일한 자원에 한 프로세스만이 접근가능하도록 하는 것입니다.
    • 프로세스 동기화를 하지 않으면 데이터의 일관성이 깨져서 잘못된 연산결과를 반환할 가능성이 있기 때문에 주의해야 합니다.
  • 메서드 앞에 synchronized 키워드를 붙인다.

  • 여러개의 Thread들이 공유객체의 메서드를 사용할 때, 메서드에 Synchronized가 붙어 있을 경우
    먼저 호출한 메서드가 객체의 사용권 (Monitoring Lock)을 얻는다.

public synchronized 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
  • 메소드 앞에 synchronized 를 붙혀서 실행해 보면, 메소드 하나가 모두 실행된 후에 다음 메소드가 실행된다.
  • 해당 모니터링 락은 메소드 실행이 종료되거나, wait()와 같은 메소드를 만나기 전까지 유지된다.
  • 다른 쓰레드들은 모니터링 락을 놓을때까지 대기한다.
  • synchronized를 붙히지 않은 메소드는 다른 쓰레드들이 synchronized메소드를 실행하면서 모니터링 락을 획득했다 하더라도, 그것과 상관없이 실행된다.
  • synchronized를 메소드에 붙혀서 사용 할 경우, 메소드의 코드가 길어지면, 마지막에 대기하는 쓰레드가 너무 오래 기다리는것을 막기위해서 메소드에 synchronized를 붙이지 않고, 문제가 있을것 같은 부분만 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();
        }
    } // for        
} //playMusicB

Thread 의 상태 제어

Thread 상태

상태

  • 프로세스 관리를 위해, 상태를 변화시킨다.
  • 생성 (New)
    • 프로세스가 생성되는 중
  • 준비 (Ready)
    • 프로세스가 설정되어 대기 중. CPU를 사용하고 있지는 않지만 언제든지 사용할 수 있는 상태로, CPU가 할당되기를 기다리고 있는 상태. 일반적으로 준비 상태의 프로세스 중 우선순위가 높은 프로세스가 CPU를 할당 받는다.
  • 실행 (Running)
    • 프로세서가 CPU를 차지하여 명령어들이 실행되는 중
  • 대기 (Waiting)
    • 보류(block)라고도 부른다. 프로세스가 입출력 완료, 시그널 수신 등 어떤 사건(Event)의 발생을 기다리고 있는 상태를 말한다.
  • 종료 (Terminated)
    • 프로세스가 실행을 종료

Thread 상태 제어

join

  • join() 메서드는 Thread가 멈출때 까지 대기하게 한다.

일단 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 시작 
        thread.start(); 
        System.out.println("Thread가 종료될때까지 기다립니다.");
        try {
            // 해당 쓰레드가 멈출때까지 멈춤
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread가 종료되었습니다."); 
    }   
}

실행 결과

Thread가 종료될때까지 기다립니다.
MyThread5 : 0
MyThread5 : 1
MyThread5 : 2
MyThread5 : 3
MyThread5 : 4
Thread가 종료되었습니다.

wait, notify

  • wait와 notify는 동기화된 블록안에서 사용해야 한다.
  • wait를 만나게 되면 해당 쓰레드는 해당 객체의 모니터링 락에 대한 권한을 가지고 있다면 모니터링 락의 권한을 놓고 대기한다.
  • notify 메서드가 실행되면, 다시 모니터링 락에 대한 권한을 요청한다.

Thread를 상속받는 ThreadB클래스를 작성

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를 사용하며 wait하는 클래스 작성

public class ThreadA {
    public static void main(String[] args){
        // 앞에서 만든 쓰레드 B를 만든 후 start 
        // 해당 쓰레드가 실행되면, 해당 쓰레드는 run메소드 안에서 자신의 모니터링 락을 획득
        ThreadB b = new ThreadB();
        b.start();

        // b에 대하여 동기화 블럭을 설정
        // 만약 main쓰레드가 아래의 블록을 위의 Thread보다 먼저 실행되었다면 wait를 하게 되면서 모니터링 락을 놓고 대기       
        synchronized(b){
            try{
                // b.wait()메소드를 호출.
                // 메인쓰레드는 정지
                // ThreadB가 5번 값을 더한 후 notify를 호출하게 되면 wait에서 깨어남
                System.out.println("b가 완료될때까지 기다립니다.");
                b.wait();
            }catch(InterruptedException e){
                e.printStackTrace();
            }

            //깨어난 후 결과를 출력
            System.out.println("Total is: " + b.total);
        }
    }
}

실행 결과

b가 완료될때까지 기다립니다.
0를 더합니다.
1를 더합니다.
2를 더합니다.
3를 더합니다.
4를 더합니다.
Total is: 10

데몬 쓰레드

  • Daemon : 리눅스, 유닉스 계열의 운영체제에서 백그라운드로 동작하는 프로그램

  • Daemon 쓰레드를 만드는 방법 : 쓰레드에 데몬 설정을 하면 된다.

    • 이런 쓰레드는 Java 프로그램을 만들 때, 백그라운드에서 특별한 작업을 처리하게 하는 용도로 만든다.
    • 데몬 쓰레드는 일반 쓰레드 (main 등)가 모두 종료되면 강제적으로 종료되는 특징을 가지고 있다.
// Runnable을 구현하는 DaemonThread클래스를 작성
public class DaemonThread implements Runnable {

    // 무한루프안에서 0.5초씩 쉬면서 데몬쓰레드가 실행중입니다를 출력하도록 run()메소드를 작성
    @Override
    public void run() {
        while (true) {
            System.out.println("데몬 쓰레드가 실행중입니다.");

            try {
                Thread.sleep(500);

            } 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
https://github.com/yuseogi0218

0개의 댓글