Java

[Java] 쓰레드(Thread)

ukkkk7 2024. 5. 12. 21:52
728x90
반응형

학습할 것 (필수)

  • Thread 클래스와 Runnable 인터페이스
  • 쓰레드의 상태
  • 쓰레드의 우선순위
  • Main 쓰레드
  • 동기화
  • 데드락

 

 

Thread 클래스와 Runnable 인터페이스

 

Process

  • 단순히 실행중인 프로그램
  • 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행중인 것
  • 프로그램에 사용되는 데이터, 메모리, 쓰레드 등으로 구성된다.

 

Thread

  • 프로세스 내에서 실제로 작업을 수행하는 주체
  • 모든 프로세스에는 1개 이상의 쓰레드가 존재
  • 두 개 이상의 쓰레드를 가지는 프로세스를 멀티 쓰레드 프로세스 라고 한다.

 

쓰레드를 생성하는 방법 2가지

  1. Runnable 인터페이스 사용
  2. Thread 클래스 사용

Thread 클래스는 Runnable 인터페이스를 구현한 클래스이며 Runnable과 Thread 모두 java.lang 패키지에 포함되어 있다.

 

Thread 클래스

package java.lang;

class Thread implements Runnable {
    private static native void registerNatives();
    static {
        registerNatives();
    }
        (생략)
}

 

 

Runnable 인터페이스

package java.lang;

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

 

둘 중 어느것을 사용할지는 Thread 클래스가 다른 클래스를 확장할 필요가 있을 경우에는 Runnable 인터페이스를 구현하면 되고, 그렇지 않은 경우에는 Thread클래스를 사용하는 것이 편리하다.

 

 

Thread의 동작순서

 

RunnableSample

public class RunnableSample implements Runnable{
    @Override
    public void run() {
        System.out.println("This is RunnableSample's run() method.");
    }
}

 

ThreadSample

public class ThreadSample extends Thread{
    @Override
    public void run() {
        System.out.println("This is ThreadSample's run() method.");
    }
}

 

public class RunMultiThreads {
    public static void main(String[] args) {
        runMultiThread();
    }

    public static void runMultiThread() {
        RunnableSample[] runnable = new RunnableSample[5];
        ThreadSample[] thread = new ThreadSample[5];
        for (int loop = 0; loop < 5; loop++) {
            runnable[loop] = new RunnableSample();
            thread[loop] = new ThreadSample();

            new Thread(runnable[loop]).start();
            thread[loop].start();
        }

        System.out.println("RunMultiThreads.runMultiThread() method is ended");
    }
}

 

 

결과

This is RunnableSample's run() method.
This is ThreadSample's run() method.
This is RunnableSample's run() method.
This is ThreadSample's run() method.
This is ThreadSample's run() method.
This is ThreadSample's run() method.
This is ThreadSample's run() method.
This is RunnableSample's run() method.
This is RunnableSample's run() method.
RunMultiThreads.runMultiThread() method is ended
This is RunnableSample's run() method.

 

실행결과 순서대로 실행하지 않으며 컴퓨터의 성능에 따라 달라 질 수도 있고 매번 결과가 다르다

run() 메소드가 끝나지 않으면 애플리케이션은 종료되지 않는다.

 

 

Thread sleep메소드

 

sleep메소드는 주어진 시간만큼 대기한다.

 

public class EndlessThread extends Thread{
    public void run() {
        while (true) {
            try {
                System.out.println(System.currentTimeMillis());
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

Thread.sleep() 메소드를 사용할 때는 항상 try-catch로 묶어줘야 한다. sleep() 메소드가 InterruptedException 예외를 던질 수 있다고 선언되어 있기 때문

 

 

 

쓰레드(Thread)의 상태

 

 

 

 

 

쓰레드의 우선순위

Java에서 각 쓰레드는 우선순위(Priority)에 관한 자신만의 필드를 가지고 있다. 이러한 우선순위에 따라 특정 쓰레드가 더 많은 시간동안 작업을 할 수 있도록 설정할 수 있다.

 

  • getPriority()와 setPriority() 메소드를 통해 쓰레드의 우선순위를 반환하거나 변경할 수 있다.
  • 쓰레드의 우선순위가 가질 수 있는 범위는 1부터 10까지이며, 숫자가 높을수록 우선순위 또한 높아진다.
  • 쓰레드의 우선순위는 비례적인 절댓값이 아니며 상대적인 값이다.
  • 우선순위가 10인 쓰레드가 우선순위가 1인 쓰레드보다 10배 더 빨리 수행되는 것이 아니며 단지 10의 우선순위를 가진 쓰레드가 1의 우선순위를 가진 쓰레드보다 더 많이 실행 큐에 포함되어, 더 많은 작업시간을 할당받을 뿐이다.

 

 

class ThreadWithRunnable implements Runnable {

    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()); // 현재 실행 중인 스레드의 이름을 반환함.
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


public class Thread02 {

    public static void main(String[] args){
        Thread thread1 = new Thread(new ThreadWithRunnable());
        Thread thread2 = new Thread(new ThreadWithRunnable());
        thread2.setPriority(10); // Thread-1의 우선순위를 10으로 변경함.
        thread1.start(); // Thread-0 실행
        thread2.start(); // Thread-1 실행
        System.out.println(thread1.getPriority());
        System.out.println(thread2.getPriority());
    }
}

 

결과

5
10
Thread-1
Thread-0
Thread-1
Thread-0
Thread-1
Thread-0
Thread-1
Thread-0
Thread-1
Thread-0

 

main() 메소드를 실행하는 쓰레드의 우선순위는 언제나 5이다.

main() 메소드 내에서 생성된 쓰레드 Thread-0의 우선순위는 5로 설정되는 것을 확인할 수 있다.

 

 

 

 

 

Main 쓰레드

Java는 실행 환경인 JVM에서 돌아가게 된다. 이것이 하나의 프로세스이고 Java를 실행하기 위해 우리가 실행하는 main()메소드가 메인 쓰레드이다.

따로 쓰레드를 실행하지 않고 main() 메소드만 실행하는 것을 싱글쓰레드 애플리케이션이라고 한다.

 

아래와같이 메인쓰레드에서 쓰레드를 생성하여 작업하는것을 멀티쓰레드 애플리케이션이라고 한다.

 

 

 

Daemon Thread

Main 쓰레드의 작업을 돕는 보조적인 역할을 하는 쓰레드

Main 쓰레드가 종료되면 데몬 쓰레드는 강제적으로 자동종료 된다. (어디까지나 Main 쓰레드의 보조 역할을 수행하기 때문에, Main 쓰레드가 없어지면 의미가 없어지기 때문이다.)

 

Deamon Thread 사용

Main 쓰레드가 Daemon이 될 쓰레드의 setDaemon(true)을 호출해주면 Daemon 쓰레드가 된다.

 

public class DaemonThread extends Thread{
    public void run() {
        try {
            Thread.sleep(Long.MAX_VALUE);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}
public void runCommonThread() {
    DaemonThread thread = new DaemonThread();
    thread.start();
}

 

sleep메소으로 인해 Long의 최대값 만큼 대기하게 된다.

public void runDaemonThread() {
    DaemonThread thread = new DaemonThread();
    thread.setDaemon(true);
    thread.start();
}

 

프로그램이 대기하지 않고 그냥 끝나버린다. 즉 데몬 쓰레드는 해당 쓰레드가 종료되지 않아도 다른 실행중인 일반 쓰레드가 없다면 멈춰버리게된다. 위에서는 sleep메소드로 인해 main 쓰레드가 대기상태가 되었기 때문에 프로그램이 바로 종료될 수 있다.

 

데몬쓰레드를 만든 이유

예를들어 모니터링 하는 쓰레드를 별도로 띄워 모니터링을 하다가, Main 쓰레드가 종료되면 관련된 모니터링 쓰레드가 종료되어야 프로세스가 종료될 수 있다. 모니터링 쓰레드를 데몬 쓰레드로 만들지 않으면 프로세스가 종료할 수 없게 된다. 이렇게 부가적인 작업을 수행하는 쓰레드를 선언할 때 데몬 쓰레드를 만든다.

 

 

 

동기화(Synchronize)

여러 개의 쓰레드가 한 개의 리소스를 사용하려 할 때 사용하려는 쓰레드를 제외한 나머지들을 접근하지 못하게 막는 것

쓰레드에 안전하다 하여 Thread-safe하다고 한다.

Java에서 동기화 하는 방법은 3가지로 분류된다.

  • synchronized 키워드
  • Atomic 클래스
  • Volatile 키워드

Synchronized 키워드

  • Java 예약어 중 하나이다
  • 변수명이나, 클래스명으로 사용이 불가능하다

Synchronized 사용방법

  • 메소드 자체를 synchronized로 선언하는 방법(synchronized methods)
  • 다른 하나는 메소드 내의 특정 문장만 synchronized로 감싸는 방법(synchronized statements)이다.

Synchronized 적용하지 않은 예제

 

public class CommonCalculate {
    private int amount;
    public CommonCalculate() {
        amount=0;
    }

    public void plus(int value) {
        amount += value;
    }

    public void minus(int value) {
        amount -= value;
    }

    public int getAmount() {
        return amount;
    }

}

 

public class ModifyAmountThread extends Thread{
    private CommonCalculate calc;
    private boolean addFlag;

    public ModifyAmountThread(CommonCalculate calc, boolean addFlag) {
        this.calc = calc;
        this.addFlag = addFlag;
    }

    public void run() {
        for(int loop = 0; loop<10000;loop++){
            if (addFlag) {
                calc.plus(1);
            } else {
                calc.minus(1);
            }
        }
    }
}

 

public class RunSync {
    public static void main(String[] args) {
        RunSync runSync = new RunSync();
        runSync.runCommonCalculate();
    }

    public void runCommonCalculate() {
        CommonCalculate calc = new CommonCalculate(); //1
        ModifyAmountThread thread1 = new ModifyAmountThread(calc, true); //2
        ModifyAmountThread thread2 = new ModifyAmountThread(calc, true); //2

        thread1.start(); //3
        thread2.start(); //3
        try {
            thread1.join(); //4
            thread2.join(); //4
            System.out.println("Final value is " + calc.getAmount()); //5
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

 

결과

value가 20000이 나오지 않는다

Final value is 19511

 

 

  1. 각 쓰레드를 실행한다
  2. try-catch 블록 안에서는 join() 이라는 메소드를 각각의 쓰레드에 대해서 호출 join() 메소드는 쓰레드가 종료될 때 까지 기다리는 메소드
  3. join() 이 끝나면 calc 객체의 getAmount() 메소드를 호출한다. getAmount() 메소드의 호출 결과는 join() 메소드 수행 이후이므로, 모든 쓰레드가 종료된 이후의 결과다. join() 메소드 결과는 join() 메소드 수행이후이므로, 모든 쓰레드가 종료된 이후의 결과이다.

5번 반복 시키는 예제

RunSync runSync = new RunSync();
for (int loop = 0; loop < 5; loop++) {
    runSync.runCommonCalculate();
}

결과

Final value is 19511
Final value is 12515
Final value is 16621
Final value is 15161
Final value is 17515

 

 

Synchronized 적용 예제 ( 메소드)

public class CommonCalculate {
    private int amount;
    public CommonCalculate() {
        amount=0;
    }

    public synchronized void plus(int value) {
        amount += value;
    }

    public synchronized void minus(int value) {
        amount -= value;
    }

    public int getAmount() {
        return amount;
    }
}



결과

Final value is 20000
Final value is 20000
Final value is 20000
Final value is 20000
Final value is 20000

 

  • 원하는 결과인 20000이 정상적으로 출력이 된다
  • 블록으로 객체를 받아 락을 걸 수 있다

Synchronized 적용 예제 ( block)

synchronized 에 this를 사용하는 것은 메서드에 synchronized에 붙이는 것과 다르지 않다.

그러나 this가 아닌 다른 object 별로 lock을 걸게되면 락걸리는 것이 다르다.

 

 

public class CommonCalculate {
    private int amount;
    private int interest;
    public static Object interestLock = new Object();
    public CommonCalculate() {
        amount=0;
    }
    public void addInterest(int value) {
        synchronized (interestLock) {
            interest+=value;
        }
    }

    public void plus(int value) {
        synchronized (this){
            amount += value;
        }
    }

    public void minus(int value) {
        synchronized (this){
            amount -= value;
        }
    }

    public int getAmount() {
        return amount;
    }

}


결과


Final value is 20000
Final value is 20000
Final value is 20000
Final value is 20000
Final value is 20000

 

 

 

데드락(교착상태, Deadlock)

Thread Deadlock

  • Deadlock(교착상태) 란 , 둘 이상의 쓰레드가 lock을 획득하기 위해 대기하는데, 이 lock을 잡고 있는 쓰레드들도 똑같이 다른 lock을 기다리면서 서로 block 상태에 놓이는 것을 말한다. Deadlock은 다수의 쓰레드가 같은 lock을 동시에, 다른 명령에 의해 획득하려 할 때 발생할 수 있다.
  • 예를 들어, Thread-1 이 A의 lock 을 가지고 있는 상태에서 B의 lock을 획들하려 한다. 그리고 Thread-2는 B의 lock 을 가진 상태에서 획득하려 한다. 데드락이 생긴다. Thread-1은 절대 B의 lock을 얻을 수 없고 마찬가지로 Thread-2는 절대 A의 lock을 얻을 수 없다. 두 쓰레드 중 어느 쪽도 이 사실유무를 모르며, 쓰레드들은 각 개체 A와 B에서 영원히 차단된 상태로 유지 된다. 이를 데드락(교착상태)이라고 한다

 

public class DeadlockSample {
    public static final Object LOCK_1 = new Object();
    public static final Object LOCK_2 = new Object();

    public static void main(String args[]) {
        ThreadSample1 thread1 = new ThreadSample1();
        ThreadSample2 thread2 = new ThreadSample2();
        thread1.start();
        thread2.start();
    }

    private static class ThreadSample1 extends Thread {
        public void run() {
            synchronized (LOCK_1) {
                System.out.println("Thread 1: Holding lock 1...");
                try { 
                    Thread.sleep(10); 
                } catch (InterruptedException e) {

                }
                System.out.println("Thread 1: Waiting for lock 2...");
                synchronized (LOCK_2) {
                    System.out.println("Thread 1: Holding lock 1 & 2...");
                }
            }
        }
    }
    private static class ThreadSample2 extends Thread {
        public void run() {
            synchronized (LOCK_2) {
                System.out.println("Thread 2: Holding lock 2...");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {

                }
                System.out.println("Thread 2: Waiting for lock 1...");
                synchronized (LOCK_1) {
                    System.out.println("Thread 2: Holding lock 1 & 2...");
                }
            }
        }
    }

}



결과

Thread 1: Holding lock 1...
Thread 2: Holding lock 2...
Thread 1: Waiting for lock 2...
Thread 2: Waiting for lock 1...

 

 

 

 

references

https://sujl95.tistory.com/63

 

 

728x90
반응형