복잡한뇌구조마냥

[JAVA] 쓰레드 ( Thread ) 본문

BE/JAVA

[JAVA] 쓰레드 ( Thread )

지금해냥 2025. 6. 18. 01:17

운영체제 ( OS )

- 컴퓨터의 하드웨어를 사용하게 해주는 프로그램

프로세스 ( Process )

- 현재 실행중인 프로그램

쓰레드 ( Thread )

여러가지 작업을 동시에 수행할 수 있게 하는 것

  • 자바 프로그램은 JVM에 의해 실행됨
  • 자바 프로그램이 여러개의 작업을 동시에 하게 만들려면 Thread를 알아야함.
  • 운영체제에서 프로세스가 여러개 동작하듯이 하나의 프로세스에서도 여러개의 동작을 할 수 있음.
  • 여러개의 동작 각각을 Thread라고 생각하면 됨.

쓰레드 생성

- 자바에서 Thread를 만드는 방법은 크게 Thread 클래스를 상속받는 방법과 Runnable인터페이스를 구현하는 방법이 있음.

 

extend Thread

- Thread 클래스를 상속받는 방법

// MyThread1.java
package Thread;

public class MyThread1 extends Thread {
    String str;
    public MyThread1(String str){
        this.str = str;
    }

    @Override
    public void run() {
        super.run();
        for (int i = 0; i< 10; i++){
            System.out.println(str);

            try {
                Thread.sleep((int)(Math.random() *1000));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

- Thread 클래스가 가지고 있는 run 클래스를 오버라이딩해서 동작 정의

// ThreadExam1.java
package Thread;

public class ThreadExam1 {
    public static void main(String[] args) {
        MyThread1 mth1 = new MyThread1("*");
        MyThread1 mth2 = new MyThread1("-");

        mth1.start();
        mth2.start();

        System.out.println("끝!!!");
    }
}

- Thread 클래스를 상속받은클래스를 선언하고 run이 아닌 start 메소드를 호출하여 동작함 

- console에서 보듯이 main 함수는 끝났는데 Thread 클래스를 상속받은 클래스들이 동작하고 있는것을 볼 수 있음.

- 즉, Thread는 main과 별도로 기능이 동작하고 있음.

- 내부의 Thread들이 동작이 끝나야 프로세스가 종료됨.

 

implements Runnable

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

package Thread;

public class MyThread2 implements Runnable {
    String str;
    MyThread2(String str){
        this.str = str;
    }

    @Override
    public void run() {
        for (int i = 0; i< 10; i++){
            System.out.println(str);
        }

        try{
            Thread.sleep((int)(Math.random() *1000));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

- Runnable 인터페이스를 구현하도록 만들어줬음.

- Runnable 에서 run 메소드를 가지고있음.

- Runnable 인터페이스를 이용하는 방법은 이용하는 이유는 클래스가 단일 상속만 가능하기 때문임.

package Thread;

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

        Thread t1 = new Thread(mth1);
        Thread t2 = new Thread(mth2);

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

        System.out.println("끝 !!!");
    }
}

- Runnable 인터페이스는 start 메소드를 가지고 있지 않기 때문에, Thread 생성자를 통해서 넣어줘야 사용가능

- Thread를 상속받았을 때와 마찬가지로, main 클래스가 끝나는 것과 별개로 Thread가 수행됨.

 

쓰레드와 공유객체

- 하나의 객체를 여러개의 Thread가 사용하기

package Thread;

public class MusicBox {
    private String MusicA = "신나는 음악";
    private String MusicB = "슬픈 음악";
    private String MusicC = "카페 음악";
    
    public void playMusicA(){
        for (int i = 0; i< 10; i++){
            System.out.println(MusicA);

            try{
                Thread.sleep((int)(Math.random() *1000));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void playMusicB(){
        for (int i = 0; i< 10; i++){
            System.out.println(MusicB);

            try{
                Thread.sleep((int)(Math.random() *1000));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void playMusicC(){
        for (int i = 0; i< 10; i++){
            System.out.println(MusicC);

            try{
                Thread.sleep((int)(Math.random() *1000));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
// MusicPlayer.java
package Thread;

public class MusicPlayer extends Thread{
    int type;
    MusicBox mb;

    public MusicPlayer(int type, MusicBox mb){
        this.type = type;
        this.mb = mb;
    }

    @Override
    public void run(){
        switch (type){
            case 1: mb.playMusicA(); break;
            case 2: mb.playMusicB(); break;
            case 3: mb.playMusicC(); break;
            default: break;
        }
    }
}
// MusicBoxExam1.java
package Thread;

public class MusicBoxExam1 {
    public static void main(String[] args) {
        MusicBox mb = new MusicBox();

        MusicPlayer mp1 = new MusicPlayer(1, mb);
        MusicPlayer mp2 = new MusicPlayer(2, mb);
        MusicPlayer mp3 = new MusicPlayer(3, mb);

        mp1.start();
        mp2.start();
        mp3.start();
    }
}

- 특정 객체를 공유하여 각각의 Thread가 다른 역할을 하도록 만들 수 있음.

 

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

  • 공유객체의 메소드가 동시에 호출되서 공유객체가 고장날 수 있음
  • 공유객체가 가진 메소드를 동시에 호출되지 않도록 해야함.
    • 메소드 앞에 synchronized를 붙이기.
    • 여러개의 Thread들이 공유객체의 메소드를 사용할 때 메소드에 synchronized가 붙어 있을 경우 호출한 메소드가 객체의 사용권(Monitoring Lock)을 얻음
package Thread;

public class MusicBox {
    private String MusicA = "신나는 음악";
    private String MusicB = "슬픈 음악";
    private String MusicC = "카페 음악";
    
    public synchronized void playMusicA(){
        for (int i = 0; i< 10; i++){
            System.out.println(MusicA);

            try{
                Thread.sleep((int)(Math.random() *1000));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void playMusicB(){
        for (int i = 0; i< 10; i++){
            System.out.println(MusicB);

            try{
                Thread.sleep((int)(Math.random() *1000));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void playMusicC(){
        for (int i = 0; i< 10; i++){
            System.out.println(MusicC);

            try{
                Thread.sleep((int)(Math.random() *1000));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

  • 동일한 코드에 synchronized를 붙여서 실행하면, 메소드 하나가 모두 실행된 후에 다음 메소드가 실행됨
  • 해당 모니터링 락은 메소드 실행이 종료되거나, wait()같은 메소드를 만나기 전까지 유지됨.
  • 다른 쓰레드들이 모니터링 락을 놓을때까지 대기
  • synchronized를 붙이지 않은 메소드는 다른 쓰레드들이 synchronized 메소드를 실행하면서 모니터랑 락을 하더라도 상관없이 실행함.
public void playMusicC(){
        for (int i = 0; i< 10; i++){
            synchronized (this){
                System.out.println(MusicC);
            }

            try{
                Thread.sleep((int)(Math.random() *1000));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
  • synchronized를 메소드에 붙여서 사용할 경우, 메소드의 코드가 길어지면, 마지막에 대기하는 쓰레드가 너무 오래 기다림
  • 메소드 전체가 아닌 부분만 synchronized로 동기화 시킬 수 있음.
  • 메소드에 synchronized를 붙이지 않고, 문제가 있을 것 같은 부분만 synchronized 블록을 사용함
  • 백그라운드로 동작이 필요한 기능에는 synchronized를 빼고 사용하면 된다고 이해하면 됨.

쓰레드 상태제어

상태

- New Thread는 쓰레드가 생성되는 상태

- Runnable은 동작 가능한 상태

- Running은 실행중인 상태

- Block은 일시 정지된 상태

- Dead는 쓰레드의 run 메소드가 종료된 상태

 

메소드

- 삭선 표시된 메소드는 deprecated 된 메소드이므로 사용하지 않는게 좋음.

- sleep 메소드는 특정시간이 지나면 block 상태가 해제

- wait 메소드가 되면 모니터링 락을 놓게됨.

- wait 메소드는 noitify나 notifyAll이 동작되면 wait이 종료됨.

- yield 메소드가 호출되면 thread는 다른 thread에게 자원을 양보

- join 메소드가 호출되면 해당 쓰레드가 종료될 때까지 대기함.

 

// MyThread3.java
package Thread;

public class MyThread3 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i< 5; i++){
            System.out.println("MyThread3 : " + i);

            try {
                Thread.sleep(500);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
// ThreadExam3
package Thread;

public class ThreadExam3 {
    public static void main(String[] args) {
        MyThread3 mth1 = new MyThread3();

        mth1.start();

        System.out.println("시작");

        try {
            mth1.join();
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("종료!");
    }
}

- 실행 결과를 비교하면 join을 사용하면 thread가 종료될 때까지 기다리는 것을 확인 할 수 있음.

// MyThread4.java
package Thread;

public class MyThread4 extends Thread {
    int total = 0;

    @Override
    public void run() {
        synchronized (this){
            for(int i = 0; i<5; i++){
                    System.out.println(i + "를 더합니다.");
                    total += i;

                try{
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            notify();
        }
    }
}
ThreadExam4.java
package Thread;

public class ThreadExam4 {
    public static void main(String[] args) {
        MyThread4 mth1 = new MyThread4();
        mth1.start();

        synchronized (mth1){
            try{
                System.out.println("완료될때까지 대기");
                mth1.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }

            System.out.println(mth1.total);
        }
    }
}

- wait이 없을 때는 메인스레드가 먼저 동작하므로 total이 계산하기 전 값이 나옴.

- wait이 있기 때문에 메인스레드가 notify를 만나기 전까지 정지함.

- 스레드가 모든 값을 더하면 notify가 실행되면서 main 스레드가 동작함

 

데몬스레드 ( Daemon Thread )

- 자바에서 데몬과 유사하게 동작하는 쓰레드

- 자바프로그램을 만들 때, 백그라운드에서 특별한 작업을 처리하게 하는 용도로 만듬.

- 데몬쓰레드는 일반 쓰레드(main 등)가 모두 종료되면 강제적으로 종료되는 특징을 가지고 있음.

 

* 데몬(Daemon)

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

- 윈도우에서는 주로 서비스 라고 이야기함.

 

package Thread;

public class DaemonThread implements Runnable {
    @Override
    public void run() {
        while (true){
            System.out.println("데몬 쓰레드가 실행중입니다.");

            try {
                Thread.sleep(500);
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
        }
    }

    public static void main(String[] args) {
        Thread t = new Thread(new DaemonThread());
        t.setDaemon(true);
        t.start();

        try {
            Thread.sleep(6000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("메인쓰레드가 종료됩니다.");
    }
}

- 무한루프로 0.5초마다 문구가 출력되는 메소드 생성

- 데몬쓰레드 적용을 위해서 setDaemon() 메소드를 사용해서 적용해야함.

- 메인 쓰레드가 종료되면 데몬쓰레드도 종료됨.

LIST