STUDY/JAVA

[JAVA] 자바 스레드(Thread) 총정리

ReCode.B 2023. 5. 16. 10:32
728x90

프로세스와 스레드 설명

프로세스 

운영체제에서는 실행중인 하나의 애플리케이션을 프로세스라고 부른다

사용자가 애플리케이션을 실행하면 운영체제로부터 실행에 필요한 메모리를 할당받아 애플리케이션의 코드를 실행

스레드

스레드의 사전적의미로는 한가닥의 실이라는 뜻. 하나의 스레드는 하나의 코드실행흐름을 뜻함. 

한프로세스내에 스레드가 두개라면? 두개의 코드 실행흐름이 생긴다는 의미

 

프로세스와 스레드의 주요 차이점

프로세스 독립적인 실행 단위이고, 각각 고유한 주소 공간, 메모리, CPU 및 파일을 가지며,

프로세스 간에 이러한 자원을 공유할 수 없음.

스레드프로세스 내에서 실행되는 실행 흐름으로, 프로세스의 메모리, CPU 및 파일을 공유함.

스레드는 프로세스 내에서 동시에 실행될 수 있으므로 프로그램의 성능을 향상시키는 데 사용할 수 있음. 

 

예) 멀티 프로세스인 워드와 엑셀을 동시에 사용하던 도중 워드에 오류가 나 먹통이 되도 엑셀은 멀쩡함

그러나 멀티 스레드로 동작하는 메신저의 경우 파일을 전송하는 스레드에서 예외가 발생하면

메신저 프로세스 자체가 종료되기 때문에 채팅스레드도 같이 종료됨 그렇기에 멀티스레드는 예외처리 매우 중요!

 

멀티 스레드의 활용

  • 대용량 데이터의 분할 병렬 처리
  • 애플리케이션의 UI 네트워크 통신
  • 다수의 클라이언트 요청을 받는 서버

메인 메소드와 스레드

모든 자바 애플리케이션은 메인스레드가 main()메소드를 실행하면서 시작됨

main() 메소드를 실행시키는 주체는 메인 스레드이다.

메인스레드는 필요에따라 작업스레드를 만들어 병렬로 코드를 실행할수있다.

싱글스레드 애플리케이션 : 메인스레드가 종료하면 프로세스도 종료된다.

멀티스레드 애플리케이션 : 실행중인 스레드가 하나라도 있다면 프로세스는 종료되지 않는다.


작업스레드 생성방법

1. Runnable 인터페이스를 상속한 클래스를 만들기

2. Thread 클래스를 상속한 클래스를 만들기

둘 다 결국 run() 메소드를 오버라이드 해야 함

 

 

Runnable 인터페이스를 상속한 클래스를 만들기

public class MyRunnable implements Runnable {
    public void run() {
        // 스레드가 실행할 코드 작성
    }
}

MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();

Runnable 인터페이스를 구현한 클래스를 만들기 Runnable 인터페이스를 구현한 클래스를 만들고, 이 클래스의 인스턴스를 Thread 클래스의 생성자에 전달하여 Thread 객체를 만듭니다. 이때, Thread 객체를 start() 메서드로 시작시키면 Runnable 인터페이스의 run() 메서드가 호출되어 작업 스레드가 실행됩니다.

 

 

Thread 클래스를 상속한 클래스를 만들기

public class MyRunnable implements Runnable {
    public void run() {
        // 스레드가 실행할 코드 작성
    }
}

MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();

 

Thread 클래스를 상속한 클래스를 만들기 Thread 클래스를 상속한 클래스를 만들고, 이 클래스의 run() 메서드를 오버라이드하여 작업 스레드가 실행할 코드를 작성합니다. 이때, 이 클래스의 인스턴스를 만들어 start() 메서드로 시작시키면 Thread 클래스의 run() 메서드가 호출되어 작업 스레드가 실행됩니다.


스레드의 이름

스레드는 이름을 가진다. 디버그할 때 어떤 스레드가 어떤 작업을 하는지 알기 좋다.

스레드는 자동으로 Thread-n 이라는 이름으로 명명된다.

  • n : 스레드의 번호
  • main : main 스레드의 이름

 

스레드 이름 관련한 메소드 사용방법

- 스레드의 이름을 변경하고 싶다?

스레드 객체의 .setName() 메소드를 이용

- 스레드의 이름을 알고 싶다?

스레드 객체의 .getName() 메소드를 이용

- 현재 수행되고 있는 스레드가 궁금하다? 

Thread.currentThread() 메소드로 현재 실행되고 있는 스레드의 참조를 얻을 수 있다.

 

스레드 이름 예제

public class ThreadA extends Thread{
    public ThreadA() {
        this.setName("ThreadA"); //스레드이름변경
    }

    @Override
    public void run() {
        for (int i = 0; i < 2; i++) {
            System.out.println(this.getName() + "가 출력한 내용");
        }
    }
}

public class ThreadB extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 2; i++) {
            System.out.println(this.getName() + "가 출력한 내용");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Thread mainThread = Thread.currentThread(); //현재수행스레드확인
        System.out.println("프로그램 시작 스레드 이름: " + mainThread.getName());

        Thread threadA = new ThreadA();
        System.out.println("작업 스레드 이름: " + threadA.getName()); //스레드이름확인
        threadA.start();

        Thread threadB = new ThreadB();
        System.out.println("작업 스레드 이름: " + threadB.getName());
        threadB.start();
    }
}

출력결과


스레드의 우선순위

멀티 스레드의 동시성과 병렬성

멀티스레드의 동시성

동시성(Concurrency)은 여러 개의 작업이 동시에 실행되는 것처럼 보이지만, 실제로는 여러 개의 작업이 번갈아가며 실행되는 것을 말합니다. 즉, CPU의 코어가 하나일 때, 스레드들이 시분할되어 번갈아가며 실행되면서 동시성을 제공합니다.

 

멀티스레드의 병렬성

병렬성(Parallelism)은 여러 개의 작업이 실제로 동시에 실행되는 것을 말합니다. 즉, CPU의 코어가 여러 개일 때, 각각의 코어에서 스레드들이 병렬적으로 실행되면서 병렬성을 제공합니다.

 

멀티 코어 CPU를 사용할 때는 병렬성을 이용하여 작업을 처리하여 더 빠른 처리 속도를 얻을 수 있습니다. 

하지만, 멀티 코어가 아닌 CPU에서는 동시성을 이용하여 작업을 처리하여 더 효율적인 작업 처리가 가능합니다.

 

동시성에서의 스케쥴링

스레드의 개수가 코어의 수보다 많을 경우 어떤 스레드에게 CPU 제어권을 주어야 하는지 결정해야 하는데,

이를 스레드 스케줄링이라고 한다.

자바의 스레드 스케줄링은 주로 우선 순위 방식과 라운드로빈방식(순환할당방식)을 사용하며,

전자는 프로그래머가 특정 스레드에게 우선 순위를 코드로 제어할 수 있지만

후자는 JVM에 의해 정해지므로 코드로 제어할 수 없다. 

 

우선순위(Priority)

개발자가 코드로 스레드에 우선순위를 부여할 수 있다.

우선 순위는 1에서부터 10까지 주어지는데, 숫자가 클수록 우선 순위가 높다.

Thread의 .setPriority() 메소드를 이용하면 된다.

가독성을 위해 

Thread.MAX_PRIORITY = 10, 

Thread.NORM_PRIORITY = 5, 

Thread.MIN_PRIORITY = 1 등의 상수도 정의되어 있다.

 

동시성 우선순위 예제

public class CalcThread extends Thread{
    public CalcThread(String name) {
        setName(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 200000000; i++) {

        }
        System.out.println(getName());
    }
}

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new CalcThread("thread" + i);
            if(i != 9) {
                thread.setPriority(Thread.MIN_PRIORITY);
            } else {
                thread.setPriority(Thread.MAX_PRIORITY);
            }
            thread.start();
        }
    }
}

멀티스레드 객체공유 문제점과 해결방법 - 동기화 

공유 객체를 사용할 때의 주의할 점

멀티 스레드 환경에서 객체를 공유해서 사용하면 문제가 생길 수 있다. 예제 코드를 바로 살펴 보자.

public class Main {

    public static void main(String[] args) {
        Calculator calculator = new Calculator();

        User1 user1 = new User1();
        user1.setCalculator(calculator);
        user1.start();

        User2 user2 = new User2();
        user2.setCalculator(calculator);
        user2.start();
    }
}

public class Calculator {

    private int memory;

    public int getMemory() {
        return memory;
    }

    public void setMemory(int memory) {
        this.memory = memory;
        try {
            Thread.sleep(2000); 
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ": " + this.memory);
    }

}

public class User1 extends Thread {

    private Calculator calculator;

    public void setCalculator(Calculator calculator) {
        this.setName("User1");
        this.calculator = calculator;
    }

    @Override
    public void run() {
        calculator.setMemory(100);
    }
}

public class User2 extends Thread {

    private Calculator calculator;

    public void setCalculator(Calculator calculator) {
        this.setName("User2");
        this.calculator = calculator;
    }

    @Override
    public void run() {
        calculator.setMemory(50);
    }
}

User1이 계산기에 100을 입력하고 잠시 화장실을 다녀 올 동안 User2가 계산기에 50을 입력하게 되면, User1이 보는 계산기는 50이므로 의도한 결과와 다르게 연출이 된다.

출력결과

 

임계영역과 동기화처리

아래 링크에 정리 ▼

https://rebornbb.tistory.com/entry/JAVA-sychronized%EB%9E%80

 

[JAVA] 동기화 sychronized

스레드동기화와 상호배제란? 동기화 : 여러 스레드나 프로세스가 공유 자원에 접근할 때, 동시에 접근하는 것을 막고 순서를 조정하여 데이터 일관성을 유지하는 것을 의미합니다. 멀티 스레드

rebornbb.tistory.com


스레드 상태

getState() 메소드를 통해 스레드의 상태를 확인할 수 있다.

위 그림과 같이 스레드 객체가 만들어져서 실행 대기하다가 실행도 하고 일시 정지 상태가 되다가 최종적으로 종료 상태가 된다.

 

스레드 상태에 따른 열거 상수 정리

객체생성 NEW 스레드 객체가 생성되었고, 아직 start() 메소드가 호출되지 않은 상태
실행대기 RUNNABLE 실행 상태로 언제든지 갈 수 있는 상태
일시정지 WAITING 스레드가 특정 조건이 충족될 때까지 기다리는 상태
종료 TIMED_WAITING 스레드가  주어진 특정 시간 동안 기다리는 상태
BLOCKED 사용하고자 하는 객체의 lock이 풀릴때까지 기다리는 상태
TERMINATED 스레드가 실행을 마친 상태

 

스레드 상태를 확인하는 예제

public class Main {

    public static void main(String[] args) {
        StatePrintThread statePrintThread = new StatePrintThread(new TargetThread());
        statePrintThread.start();
    }
}

public class StatePrintThread extends Thread{

    private Thread targetThread;

    public StatePrintThread(Thread targetThread) {
        this.targetThread = targetThread;
    }

    @Override
    public void run() {
        while(true) {
            Thread.State state = targetThread.getState();
            System.out.println("타켓 스레드 상태: " + state);

            if (state == Thread.State.NEW) {
                targetThread.start();
            }

            if (state == Thread.State.TERMINATED) {
                break;
            }

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

public class TargetThread extends Thread {

    @Override
    public void run() {
        for (long i = 0; i < 1_000_000_000; i++) {
        }

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

        for (long i = 0; i < 1_000_000_000; i++) {
        }
    }
}

StatePrintThread

- 0.3초마다 현재 스레드의 상태를 출력한다.

- 스레드가 NEW 상태면 RUNNABLE 상태로 만들어 주고, 스레드가 TERMINATED 상태면 무한 루프를 종료한다.

TargetThread

- 10억 번 루프를 돌고, 1초 동안 스레드를 TIMED_WATING 상태에 빠지게 하고, 다시 10억 번 루프를 돈다.


스레드 상태 제어

sleep()
현재 실행 중인 스레드를 일정 시간 동안 일시 정지 상태로 만듭니다.
이 메소드는 예외를 발생시키므로 try-catch 문으로 처리해야 합니다.
join() 현재 실행 중인 스레드가 다른 스레드의 종료를 기다리도록 합니다.
다른 스레드가 종료되기 전까지는 현재 스레드는 일시 정지 상태에 머무릅니다.
interrupt() 다른 스레드를 일시 정지 상태에서 깨우기 위해 인터럽트 신호를 보냅니다.
이 메소드를 호출하면 InterruptedException이 발생하므로 이에 대한 예외 처리도 필요합니다.
yield() 다른 스레드에게 실행 기회를 양보하고, 현재 스레드는 실행 대기 상태로 돌아갑니다.
wait() 스레드를 일시 정지 상태로 만들고, 다른 스레드가 notify() 또는 notifyAll() 메소드를 호출하여 일시 정지된 스레드를 다시 실행할 수 있도록 합니다. 이 메소드는 synchronized 블록 내에서 호출되어야 합니다.
notify()
wait() 메소드에 의해 일시 정지된 스레드 중 하나를 실행 가능한 상태로 변경합니다.
이 메소드는 synchronized 블록 내에서 호출되어야 합니다.
notifyAll() wait() 메소드에 의해 일시 정지된 모든 스레드를 실행 가능한 상태로 변경합니다.
이 메소드는 synchronized 블록 내에서 호출되어야 합니다.

 

sleep() 예제

public class Main {
    public static void main(String[] args) {
        try {
            System.out.println("Thread is going to sleep for 5 seconds...");
            Thread.sleep(5000); // 5초 동안 스레드 일시 정지
            System.out.println("Thread has woken up.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

매개 변수로 들어온 시간만큼 스레드를 일시 정지 상태로 만들고, 시간이 다 지나면 실행 대기 상태로 변경한다

 

 

join() 예제

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 1: " + i);
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 2: " + i);
            }
        });

        t1.start();
        try {
            t1.join(); // t1 스레드가 종료될 때까지 t2 스레드를 일시 정지
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

 t2스레드가  t1스레드의 일이 종료할 때까지 기다렸다가 일을 수행해야 할 수 있다.

이때 사용하는 메소드가 바로 join() 이다.

 

interrupt()  예제

public class Main {

    public static void main(String[] args) {
        Thread thread = new PrintThread();
        thread.start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        thread.interrupt();
    }
}

public class PrintThread extends Thread {

    @Override
    public void run() {
        while (true) {
            System.out.println("실행 중");
            if (Thread.interrupted()) {
                break;
            }
        }

        System.out.println("자원 정리");
        System.out.println("실행 종료");
    }
}

현재 스레드에 대해 interrupt() 메소드가 실행 되었는지 확인하고,

실행 된 상태라면 true를 반환하여 무한 루프를 탈출하는 로직이다.

 

yield() 예제

public class Main {
    public static void main(String[] args) {
    	   Thread t1 = new Thread(() -> {
               for (int i = 1; i <= 5; i++) {
                   System.out.println("Thread 1: " + i);
                   Thread.yield(); // 다른 스레드에게 실행 기회 양보
               }
           });

           Thread t2 = new Thread(() -> {
               for (int i = 1; i <= 5; i++) {
                   System.out.println("Thread 2: " + i);
                   Thread.yield(); // 다른 스레드에게 실행 기회 양보
               }
           });

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

yield() 메소드를 호출한 스레드는 실행 대기 상태로 돌아가고

동일한 우선 순위 또는 높은 우선 순위를 갖는 다른 스레드가 실행 기회를 가질 수 있도록 한다.

 

wait() 와 notify() 예제

package javaStudy;


public class Main {
    public static void main(String[] args) {
        Object lock = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Thread 1 is waiting...");
                    lock.wait(); // 다른 스레드에 의해 notify() 호출될 때까지 일시 정지
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 1 has been notified.");
            }
        });
        
        //다른스레드
        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Thread 2 is running...");
                lock.notify(); // lock 객체에 대한 wait() 메소드가 일시 정지 상태에서 깨어나도록 함
            }
        });

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

 

t2 스레드가 lock 객체에 대한 notify() 메소드를 호출하여 t1 스레드를 일시 정지 상태 wati() 에서 깨웁니다.

 


데몬스레드

데몬스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다.

주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료된다.

 

아래 링크에 정리 ▼

https://rebornbb.tistory.com/entry/JAVA-%EB%8D%B0%EB%AA%AC%EC%8A%A4%EB%A0%88%EB%93%9C%EB%9E%80

 

[JAVA] 데몬스레드란?

deamon thread 데몬스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다. 주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료된다. (그이유는 주 스레드의 보조 역할을

rebornbb.tistory.com


스레드 그룹

스레드 그룹은 스레드를 묶어서 관리하기 위한 개념입니다. 

스레드 그룹을 이용하면 스레드를 논리적으로 묶어서 그룹 단위로 관리할 수 있다.

이를 통해 스레드의 우선순위, 중요도, 예외처리 등을 일괄적으로 처리할 수 있다.

 

스레드 그룹 이름 얻기

public static void main(String[] args) {

    Thread t1 = new Thread();
    System.out.println("main : " + Thread.currentThread().getThreadGroup().getName());
    System.out.println("t1 :" + t1.getThreadGroup().getName());
}

특정 스레드가 어떤 그룹에 속해있는지를 알기위해서는 'getThreadGroup()' 메서드를 사용하면 된다.

 

스레드 그룹 생성

스레드 그룹을 생성하기 위해 ThreadGroup 클래스의 생성자를 사용합니다. 

ThreadGroup 클래스는 다음과 같은 생성자를 제공합니다:

 

ThreadGroup(String name)

지정된 이름으로 스레드 그룹을 생성합니다. 부모 그룹은 현재 스레드가 속한 그룹이 됩니다.
ThreadGroup(ThreadGroup parent, String name)

지정된 부모 그룹과 이름으로 스레드 그룹을 생성합니다. 부모 그룹은 parent 매개변수로 전달되는 스레드 그룹이 됩니다.

 

public class Main {
    public static void main(String[] args) {
        ThreadGroup myThreadGroup = new ThreadGroup("MyThreadGroup");
        ThreadGroup subThreadGroup = new ThreadGroup(myThreadGroup, "SubThreadGroup");
    }
}

위의 예제에서는 "MyThreadGroup"이라는 이름으로 부모 스레드 그룹을 생성하고, 

이를 이용하여 "SubThreadGroup"이라는 이름으로 자식 스레드 그룹을 생성합니다.

스레드 그룹을 생성하면 부모-자식 관계가 형성되며, 부모 스레드 그룹에 속한 스레드 그룹은 자식 스레드 그룹으로서 동작합니다. 자식 스레드 그룹은 부모 그룹에 대한 참조를 가지고 있습니다.

따라서, ThreadGroup 클래스의 생성자를 사용하여 스레드 그룹을 생성할 수 있습니다. 

생성된 스레드 그룹은 스레드를 관리하고 제어하는 데 사용될 수 있습니다.

 

public class Main {
    public static void main(String[] args) {
    	        ThreadGroup group = new ThreadGroup("New thread group");

    	        Thread t1 = new WorkerThread(group, "Thread1");
    	        Thread t2 = new WorkerThread(group, "Thread2");
    	        Thread t3 = new WorkerThread(group, "Thread3");

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

    	        try {
    	            Thread.sleep(5000);
    	        } catch (InterruptedException e) {
    	            /* No-op */
    	        }

    	        System.out.println("Call interrupt");
    	        
    	        // 일괄 Interrupt
    	        group.interrupt();
    	        try {
    	            t1.join();
    	            t2.join();
    	            t3.join();
    	        } catch (InterruptedException e) {
    	            /* No-op */
    	        }
    	        System.out.println("Finished");
    	    
    }
}

 

스레드 그룹 interrupt()

public class ThreadGroupInterruptExample {
    public static void main(String[] args) {
        ThreadGroup myThreadGroup = new ThreadGroup("MyThreadGroup");

        Thread thread1 = new Thread(myThreadGroup, () -> {
            while (!Thread.currentThread().isInterrupted()) {
                // 스레드 동작
            }
        });

        Thread thread2 = new Thread(myThreadGroup, () -> {
            while (!Thread.currentThread().isInterrupted()) {
                // 스레드 동작
            }
        });

        // 스레드 실행
        thread1.start();
        thread2.start();

        // 스레드 그룹의 모든 스레드 인터럽트
        myThreadGroup.interrupt();
    }
}

참고 : 이것이자바다 - 신용권

 

 

 

728x90