Java

멀티스레드 프로그래밍

donggi 2022. 9. 27. 12:06

백기선 자바 스터디 10주차 멀티스레드 프로그래밍에 대해 학습하면서 정리한 내용입니다.

Java의 멀티스레드

멀티 스레드 프로그램에서는 여러 스레드가 동시에 실행됩니다. 각 스레드는 리소스를 최적으로 사용하여 다른 작업을 동시에 처리합니다.

자바에서 스레드를 생성하는 방법은 두 가지가 있습니다.

 

1. Thread 클래스를 상속 받거나

2. Runnable 인터페이스를 구현하는 방법이 있습니다.

 

1. Thread class

Thread class를 사용하는 방법은 아래와 같습니다.

 

1. Thread class를 상속받는 class를 만듭니다.

2. run() 메서드를 overriding하여 수행할 코드를 작성합니다.

3. main 메서드에서 Thread class를 상속 받은 class 객체를 만들고 start() 메서드로 해당 스레드를 실행시킵니다.

public class ThreadEx {
    
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();

        int cnt = 1;
        int sum = 1;
        while(cnt <= 10) {
            System.out.println("main thread: " + sum * cnt);
            cnt++;
        }
    }
}
class MyThread extends Thread {
    
    @Override
    public void run() {
        int cnt = 0;
        char x = 'a';
        while(cnt <= 25) {
            System.out.println("new thread: " + (char) (x + cnt));
            cnt++;
        }

    }
}

 

2. Runnable interface

Runnable interface를 사용하는 방법은 아래와 같습니다.

 

1. Runnable interface를 구현하는 class를 만듭니다.

2. run() 메서드를 overriding하여 수행할 일을 작성합니다.

3. Runnable interface 구현체를 스레드 생성시 생성자 파라미터로 넣어줍니다.

4. main 메서드에서 Runnable interface 구현체를 파라미터로 한 스레드 객체를 만들고 start() 메서드로 스레드를 실행시킵니다.

public class RunnableEx {

    public static void main(String[] args) {
        Runnable r = new MyRunnable();
        Thread t = new Thread(r);
        t.start();

        int cnt = 0;
        char x = 'a';
        while(cnt < 10) {
            System.out.println("main thread: " + (char) (x + cnt));
            cnt++;
        }
    }
}

class MyRunnable implements Runnable {

    @Override
    public void run() {
        int cnt = 1;
        int sum = 1;
        while(cnt <= 10) {
            System.out.println("runnable thread: " + sum * cnt);
            cnt++;
        }
    }
}

스레드의 상태

스레드 객체를 생성하고 start() 메서드를 호출하면 스레드는 실행 대기 상태가 됩니다.

 

실행 대기 상태에 있는 스레드 중 스레드 스케줄링으로 선택된 스레드는 CPU를 점유하고 run() 메서드를 실행합니다. 이때를 실행 상태라고 합니다.

 

실행 상태의 스레드는 run() 메서드를 모두 실행하기 전 스레드 스케줄링에 의해 다시 실행 대기 상태로 돌아갈 수 있습니다.

그리고 실행 대기 상태에 있는 다른 스레드가 선택되어 실행 상태가 됩니다. 이렇게 스레드는 실행 대기 상태와 실행 상태를 번갈아가며 자신의 run() 메서드를 실행합니다.

 

실행 상태에서 run() 메서드가 종료되면 더이상 실행할 코드가 없기 때문에 스레드의 실행은 멈춥니다. 이 상태를 종료 상태라고 합니다.


스레드 우선순위(Thread Priority)

멀티스레드로 동작할 때 스레드마다 우선순위(Priority)를 설정할 수 있습니다. 

우선순위가 높은 스레드는 먼저 CPU가 할당됩니다.

 

우선순위는 1부터 10까지의 숫자로 설정할 수 있으며, Thread class는 다음 3개의 static 변수를 제공합니다.

 

스레드를 생성하면 우선순위는 5로 설정되어 있습니다.

 

어떤 스레드가 5보다 큰 우선순위를 갖고 있다면 해당 스레드가 먼저 CPU를 할당 받게 됩니다.

만약 우선순위가 같은 스레드가 있다면 어떤 스레드가 먼저 우선시 될지 알 수 없습니다.

 

우선순위 설정 및 확인

Thread에 우선순위를 설정할 때는 setPriority() 메서드를 사용합니다.

설정된 값을 확인하고자 할 때는 getPriority() 메서드를 사용합니다.

 

public class ThreadPriorityEx extends Thread {

    public void run() {
        System.out.println("Inside the run() method");
    }

    public static void main(String[] args) {
        ThreadPriorityEx th1 = new ThreadPriorityEx();
        ThreadPriorityEx th2 = new ThreadPriorityEx();
        ThreadPriorityEx th3 = new ThreadPriorityEx();

        System.out.println("Priority of the thread th1 is : " + th1.getPriority());
        System.out.println("Priority of the thread th2 is : " + th2.getPriority());
        System.out.println("Priority of the thread th2 is : " + th2.getPriority());

        th1.setPriority(6);
        th2.setPriority(3);
        th3.setPriority(9);

        System.out.println("Priority of the thread th1 is : " + th1.getPriority());
        System.out.println("Priority of the thread th2 is : " + th2.getPriority());
        System.out.println("Priority of the thread th3 is : " + th3.getPriority());

        System.out.println("Currently Executing The Thread : " + Thread.currentThread().getName());
        System.out.println("Priority of the main thread is : " + Thread.currentThread().getPriority());
        
        Thread.currentThread().setPriority(10);

        System.out.println("Priority of the main thread is : " + Thread.currentThread().getPriority());
    }
}

예제 코드 출처 : https://www.geeksforgeeks.org/java-thread-priority-multithreading/

Output


Main 스레드

 

모든 Java 애플리케이션은 Main Thread가 main() 메서드를 실행하면서 시작됩니다. main() 메서드에서 마지막 코드를 실행하거나 return 문을 만나면 해당 프로세스는 종료됩니다.

public static void main(String[] args) {    
    // ...
}

 

싱글 스레드 애플리케이션에서는 Main Thread가 종료되면 프로세스도 종료되지만 멀티 스레드 애플리케이션에서는 실행 중인 스레드가 하나라도 있으면 프로세스는 종료되지 않습니다. Main Thread가 작업 스레드보다 먼저 종료되어도 작업 스레드가 실행 중이라면 프로세스는 종료되지 않습니다.


동기화

멀티스레드를 잘 사용하면 애플리케이션이 좋은 성능을 낼 수 있습니다.

하지만 멀티스레드 환경에서 스레드간 동기화라는 문제를 꼭 해결해야 합니다.

 

둘 이상의 스레드가 동시에 공유 자원에 접근하면 예기치 않은 동작이 발생할 수 있습니다. 스레드간 동기화가 되지 않은 상태에서 멀티스레드 애플리케이션을 실행시키면 데이터의 안정성과 신뢰성을 보장할 수 없습니다.

 

이것을 경쟁 조건(race condition)이라고 합니다. 프로그램에서 공유 자원을 접근하는 부분을 크리티컬 섹션(Critical Section)이라고 합니다. 이 경쟁 조건을 피하기 위해서는 크리티컬 섹션에 대한 접근을 동기화해야 합니다.

 

스레드 동기화는 상호 배제(mutual exclusive)스레드 간 통신(inter-thread communication)이 있습니다.

상호 배제(Mutual Exclusive)

상호 배제는 한 번에 하나의 스레드만 크리티컬 섹션을 실행할 수 있도록 합니다. 스레드가 크리티컬 섹션을 실행 중이면 다른 스레드는 크리티컬 섹션을 간섭할 수 없습니다.

 

크리티컬 섹션 접근에 대한 동기화를 하지 않았을 때의 예시를 보겠습니다.

public class SequenceGenerator {
    
    private int currentValue = 0;

    public int getNextSequence() {
        currentValue = currentValue + 1;
        return currentValue;
    }
}

 

 

@Test
public void givenUnsafeSequenceGenerator_whenRaceCondition_thenUnexpectedBehavior() throws Exception {
    int count = 1000;
    Set<Integer> uniqueSequences = getUniqueSequences(new SequenceGenerator(), count);
	assertThat(uniqueSequences.size()).isEqualTo(count);
}

private Set<Integer> getUniqueSequences(SequenceGenerator generator, int count) throws Exception {
    ExecutorService executor = Executors.newFixedThreadPool(3);
    Set<Integer> uniqueSequences = new LinkedHashSet<>();
    List<Future<Integer>> futures = new ArrayList<>();

    for (int i = 0; i < count; i++) {
        futures.add(executor.submit(generator::getNextSequence));
    }

    for (Future<Integer> future : futures) {
        uniqueSequences.add(future.get());
    }

    executor.awaitTermination(1, TimeUnit.SECONDS);
    executor.shutdown();

    return uniqueSequences;
}

 

예제 코드 출처 : https://www.baeldung.com/java-mutex

3개의 스레드를 만들어 getNextSequence() 메서드를 1000번 실행하게 해보았습니다. 스레드가 동시에 메서드를 실행하면서 uniqueSequences의 크기가 1000이 아닌 999가 되었습니다.

 

우리가 기대한 것처럼 동작하기 위해서는 한 번에 하나의 스레드만 getNextSequence() 메서드를 실행할 수 있도록 해야 합니다. 상호 배제를 사용하여 스레드를 동기화해보겠습니다.

 

상호 배제는 synchronized 키워드를 사용하여 수행합니다. synchronized 키워드의 동작을 이해하기 위해서는 Monitor라는 것을 알아야 합니다. Monitor는 Object 내부에 존재합니다. Java의 모든 객체는 Object를 상속받고 있기 때문에 Java의 모든 객체에는 Monitor를 가지고 있게 됩니다. 이 Monitor의 Lock을 통해 스레드 동기화를 수행할 수 있습니다. 

 

synchronized 키워드는 세 가지 형태로 사용이 가능합니다.

  1. synchronized method
  2. synchronized block
  3. static synchronization

1. synchronized method

public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator{

    @Override
    public synchronized int getNextSequence() {
        return super.getNextSequence();
    }
}

2. synchronized block

public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator {

    private Object mutex = new Object();

    @Override
    public int getNextSequence() {
        synchronized (mutex) {
            return super.getNextSequence();
        }
    }
}

3. static synchronization

학습 예정..

 

 

스레드간 통신 (Inter-thread communication)

 

이해가 부족하여 일단은 링크로 대체 ㅜ


교착 상태

멀티 스레드 프로그래밍에서 동기화를 통해 락을 획득하여 동일한 자원을 여러 곳에서 함부로 사용하지 못하도록 하였습니다.

이 때 둘 이상의 스레드가 락이 해제되기를 기다리는 상태가 되어 상대방의 작업이 끝나기만 바라는 무한정 대기 상태를 교착 상태(Deadlock)라고 합니다.

 

데드락 발생의 4가지 조건

아래 4가지 조건이 모두 만족되는 경우 데드락이 발생할 수 있습니다.

하나라도 만족하지 않는다면 데드락은 절대 발생하지 않습니다.

 

Mutual Exclusion(상호 배제)

  • 매순간 하나의 프로세스만 자원을 사용할 수 있음
  • 독점적인 사용

No Preemption(비선점)

  • 프로세스는 자원을 스스로 내어놓을뿐 강제로 빼앗기지 않음

Hold and wait(보유 대기)

  • 자원을 가진 프로세스가 다른 자원을 얻기 위해 기다릴 때 보유 자원을 놓지 않고 계속 가지고 있음

Circular wait(순환 대기)

  • 자원을 기다리는 프로세스 간의 사이클이 형성되어야함

References

https://www.baeldung.com/java-mutex

https://math-coding.tistory.com/175

https://www.geeksforgeeks.org/java-thread-priority-multithreading/

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'Java' 카테고리의 다른 글

enum  (0) 2022.10.07
예외 처리  (0) 2022.09.15
Java의 Dynamic Method Dispatch, Abstract class, final, Object class  (0) 2022.09.01
Java Overriding  (0) 2022.08.30
Java의 상속과 super 키워드  (0) 2022.08.29