[Java]스레드(Thread)

Wintering·2022년 5월 23일
0

이펙티브 자바

목록 보기
8/18

메인 스레드

  • 모든 자바 어플리케이션은 메인 스레드(main thread)가 main() 메소드를 실행하면서 시작
  • 메인 스레드는 필요에 따라 작업 스레드들을 만들어서 병렬로 코드 실행이 가능하다.
    싱글 스레드 어플리케이션에서는 메인 스레드를 종료하면 프로세스도 종료되지만, 멀티 스레드 애플리케이션처럼 실행 중인 스레드가 하나라도 있다면, 프로세스는 종료되지 않는다.
    (메인 스레드가 먼저 종료되더라도 상관X)

작업 스레드의 생성과 실행

어떤 자바 어플리케이션이든 메인 스레드는 반드시 존재
메인 작업 이외에 추가적인 병렬 작업의 수만큼 스레드를 생성하면 된다.
java.lang.Thread 클래스를 직접 객체화하거나, Thread 클래스를 상속하여 하위 클래스를 만들어 생성한다.

Thread 클래스로부터 직접 생성

java.lang.Thread 클래스로부터 작업 스레드 객체를 직접 생성
= Runnable을 매개값으로 갖는 생성자를 호출

Thread thread = new Thread(Runnalbe target);
  • Runnable : 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체
    인터페이스 타입이므로, 구현 객체를 만들어서 대입해야 한다. (run() 메소드를 재정의)
class Task implements Runnalbe{
	public void run(){
     스레드가 실행할 코드;
    }
}

단, 이때까지 Runnable은 작업 내용을 가지고 있는 객체일 뿐 실제 스레드가 아니다.
Runnable 구현 객체를 생성한 후, 이것을 매개값으로 하여 Thread 생성자를 호출하면 비로소 작업 스레드 생성!

Runnable task = new Task();				// 객체만 만들어짐 (스레드가 생성된 건 X)
Thread thread = new Thread(task);		// 이 시점에 스레드 생성 (실행되진 X)
thread.start()							// 이 시점에 스레드 실행

ex) 비프음을 들려주며, '띵'을 문자열로 출력하기
1. main스레드만 이용 main 스레드의 코드는 위에서부터 아래로 읽히므로, 첫번째 beep() 가 실행되는 스레드를 따라서 비프음이 5번 들린 후에, 이어서 "띵"이 5번 출력된다 (동시 진행 불가능)

2. 프린팅은 메인스레드 / 비프음은 작업스레드
🔻비프음을 낼 작업스레드를 따로 만들기 위한 클래스 (BeppTask.java)🔻메인 클래스 (BeepTask 객체를 생성해서, 스레드 생성)

🔻위와 같은 방법이지만, 클래스를 따로 생성하지 않고, 람다식 이용

Thread의 하위 클래스로부터 생성

Thread의 하위클래스로 작업스레드를 정의하면서 작업 내용을 포함시킴다.
: Thread 클래스를 상속한 후 run 메소드를 재정의(overriding)해서 스레드가 실행할 코드를 작성

pulbic class WorkerThread extends Thread{
@Override
public void run(){
	//스레드가 실행할 코드
	}
}

람다식으로 표현 가능하다 (위의 예제)

Thread thread = new Thread(){
	public void run(){
    // 스레드가 표현할 코드
    }
}

🔻Thread를 상속받는 클래스 생성(BeeaThread.java)

🔻메인에서 BeepThread 호출


스레드 우선순위

멀티스레드는 동시성 또는 병렬성으로 실행 된다.
동시성 : 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 성질
병렬성 : 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질

스레드의 개수가, 코어의 개수보다 많을 경우 스레드를 어떤 순서에 의해 동시성으로 실행할 것인가를 결정해야한다.
= 스레드 스케줄링

우선순위(Priority) 방식

우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케줄링
개발자가 코드로 제어 가능 (개발자가 우선순위 코드를 부여 : thread.setPriority(index))

순환할당(Round-Robin) 방식

시간할당량을 정하여 하나의 스레드를 정해진 시간만큼 실행하고, 다른 스레드를 실행하는 방식
자바 가상 기계에 의해서 정해지므로 코드로 제어 불가능


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

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

👉이렇게 스레드가 사용중인 객체를 다른 스레드가 변경할 수 없도록 하기 위해서, 스레드 작업이 끝날 때까지 객체에 잠금을 걸어서 다른 스레드가 사용할 수 없도록 해야한다.

  • 단 하나의 스레드만 실행할 수 있는 코드 영역(임계영역)을 지정하기 위해서
    동기화(Synchronized)메소드와 동기화 블록을 제공
  • synchronized 키워드는 인스턴스와 정적 메소드 어디에든 붙일 수 있음
    ex. user1과 user2 스레드를 사용할 공유객체가 Calculator의 setMemory()메소드 였다면
public void setMemory(int memory){
	this.memory = memory;
    try{
    	Thread.sleep(2000);
    }catch(Exception e){}
}

에서

public synchronized void setMemory(int memroy){
	this.memory = memory;
    try{
    	Thread.sleep(2000);
    }catch(Exception e){}
}

이렇게 바꿔줄 수 있다.


스레드 상태

  • Thread.getState() 메소드를 사용해서 아래의 열거 상수 리턴 가능
상태열거상수설명
객체생성NEW스레드 객체 생성 / 아직 start() 메소드가 호출되지 않은 상태
실행대기RUNNABLE실행 상태로 언제든지 갈 수 있는 상태
일시정지WAITING다른 스레드가 통지할 때까지 기다리는 상태
TIME_WATINING주어진 시간 동안 기다리는 상태
BLOCKED사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태
종료TERMINATED실행을 마친 상태

스레드상태제어

메소드설명
interrupt()일시정지 상태의 스레드에서 InterruptedExecption 예외를 발생시켜, 예외 처리 코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 한다.
notify()동기화 블록 내에서 wait() 메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만든다.
notifyAll()
sleep()주어진 시간동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행대기상태가 된다.
join()join() 메소드를 호출한 스레드는 일시정지 상태가 된다. 실행 대기 상태로 가려면, join() 메소드를 멤버로 가지는 스레드가 종료되거나, 매개값으로 주어진 시간이 지나야한다.
wait동기화(Synchronized) 블록 내에서 스레드를 일시 정지 상태로 만든다. 매개값으로 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. 시간이 주어지지않으면, notify(), nofifyAll() 메소드에 의해 실행 대기 상태로 갈 수 있다.
  • wait(), notify(), notifyAll() : Object 클래스의 메소드
  • 그 외 : Thread 클래스의 메소드

sleep() : 주어진 시간 동안 일시정지

public class SleepExample {
    public static void main(String[] args) {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        for(int i=0; i<10; i++){
            toolkit.beep();
            try{
                Thread.sleep(3000);
            }catch (InterruptedException e){}
        }
    }
}
  • 3초 주기로 beep음을 10번 발생
try{
    Thread.sleep(3000);
 }catch (InterruptedException e){}

: 메인 스레드를 3초동안 일시 정지 상태로 보내고, 3초가 지나면 다시 실행 준비 상태가 돌아오도록 함

yield() : 다른 스레드에게 실행 양보

yield() 메소드를 호출한 스레드는 실행 대기 상태로 돌아가고, 동일한 우선순위 또는 높은 우선순위를 갖는 다른 스레드가 실행 기회를 가질수 있도록 함

//ThreadA.java
public class ThreadA extends Thread {
    public boolean stop = false;    //종료 플래그
    public boolean work = true;     //작업 진행 여부

    public void run() {
        while (!stop) {
            if (work) {
                System.out.println("ThreadA 작업 내용");
            } else {
                Thread.yield();
            }
        }
        System.out.println("ThreadA 종료");
    }
}
//ThreadB.java
public class ThreadB extends Thread {
    public boolean stop = false;    //종료 플래그
    public boolean work = true;     //작업 진행 여부

    public void run() {
        while (!stop) {
            if (work) {
                System.out.println("ThreadB 작업 내용");
            } else {
                Thread.yield();
            }
        }
        System.out.println("ThreadB 종료");
    }
}
//YieldExample.java
public class YieldExample {
    public static void main(String[] args) {
        ThreadA threadA = new ThreadA();
        ThreadB threadB = new ThreadB();

        threadA.start();
        threadB.start();

        try{Thread.sleep(3000);} catch (InterruptedException e){}
        threadA.work =false;

        try{Thread.sleep(3000);} catch (InterruptedException e){}
        threadA.work = true;

        try{Thread.sleep(3000);} catch (InterruptedException e){}
        threadA.stop =true;
        threadB.stop =true;
    }
}

join() : 다른 스레드의 종료를 기다림

스레드는 다른 스레드와 독립적으로 실행하는 것이 기본!
다만, 계산 작업을 하는 스레드가 모든 계산 작업을 마쳤을 때, 계산 결과값을 받아 이용해야하는 경우 등 다른 스레드가 종료될때까지 기다렸다가 실행해야 하는 경우가 발생할 수도 있음.

//SumThread.java
public class SumThread extends Thread {
    private long sum;

    public long getSum() {return sum;}
    public void setSum(long sum) {this.sum = sum;}

    public void run() {
        for (int i = 1; i <= 100; i++) {
            sum += 1;
        }
    }
}
//JoinExample.java
public class JoinExample {
    public static void main(String[] args) {
        SumThread sumThread = new SumThread();
        sumThread.start();

        try {
            sumThread.join();       // sumThread가 종료할 때 까지 메인 스레드를 일시정지
        } catch (InterruptedException e) {
        }
        System.out.println("1~100의 합:" + sumThread.getSum());
    }
}

wait(), notify(), notifyAll() : 스레드 간 협업

👉동기화 메소드 또는 블록에서만 호출 가능한 Object 메소드

  • 정확한 교대작업이 필요할 경우, 자신의 작업이 끝나면 상대방의 스레드를 일시 정지 상태에서 풀어주고,
    자신은 일시정지 상태로 만드는 것
  • 공유객체 : 두 스레드가 작업할 내용을 각각 동기화 메소드로 구분해놓는다.
    한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업하지 않도록 wait() 메소드를 호출하여 일시 정지 상태로 만든다.
    (이 때, wait(long timeout)처럼 매개변수를 넘기면, nofity() 메소드를 호출하지 않아도, 지정된 시간이 지나면 스레드가 자동적으로 실행 대기 상태가 됨)
  • 이 메소드들은 Object 클래스에서 선언된 메소드이므로 모든 공유객체에서 호출이 가능하다
//ThreadA,B의 공유객체 WorkObject
public class WorkObejct {
    public synchronized void methodA(){
        System.out.println("ThreadA의 methodA() 작업 실행");
        notify();       //일시 정지 상태에 있는 ThreadB를 실행 대기 상태로 만듦
        try{
            wait();    // ThreadA를 일시정지 상태로 만듬
        }catch(InterruptedException e){}
    }

    public synchronized void methodB(){
        System.out.println("ThreadB의 methodB() 작업 실행");
        notify();       //일시 정지 상태에 있는 ThreadA를 실행 대기 상태로 만듦
        try{
            wait();     //공유 객체의 methodA()를 10번 반복 호출
        }catch(InterruptedException e){}
    }
}
//ThreadA
public class ThreadA extends Thread{
    private WorkObejct workObejct;

    public ThreadA(WorkObejct workObejct){
        this.workObejct = workObejct;       // 공유 객체를 매개값으로 받아 필드에 저장
    }

    @Override
    public void run(){
        for(int i=0; i<1; i++){
            workObejct.methodA();       //공유 객체의 methodA()를 10번 반복 호출
        }
    }
}

//ThreadB
package ThisIsJava.chap12.section6;

public class ThreadB extends Thread{
    private WorkObejct workObejct;

    public ThreadB(WorkObejct workObejct){
        this.workObejct = workObejct;       // 공유 객체를 매개값으로 받아 필드에 저장
    }

    @Override
    public void run(){
        for(int i=0; i<1; i++){
            workObejct.methodB();       //공유 객체의 methodA()를 10번 반복 호출
        }
    }
}
//스레드 실행 메소드
public class WaitNotifyExample {
    public static void main(String[] args) {
        WorkObejct shareObject = new WorkObejct();      //공육객체생성

        ThreadA threadA = new ThreadA(shareObject);
        ThreadB threadB = new ThreadB(shareObject);     //ThreadA,B 생성

        threadA.start();
        threadB.start();
    }
}

stop플래그, interrupt() : 스레드의 안전한 종료

👉 실행 중인 스레드를 즉시 종료할 필요가 있을 때

interrupt()

interrupt()메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다. 자체로 정지의 의미를 가지는 게 아니기 때문에, 예외를 발생 시킨 뒤, catch문에서 조절할 수 있음

0개의 댓글