본문 바로가기

프로그래밍/Java

멀티쓰레드 프로그래밍

Thread 클래스와 Runnable 인터페이스

Thread 클래스와 RUnnable인터페이스를 알아보기 전에 프로세스와 쓰레드에 대해 알아보자

프로세스(process)

OS로부터 메모리를 할당받아 실행중인 프로그램을 프로세스라고 한다.
프로그램에 사용되는 데이터와 메모리 쓰레드로 구성된다.

쓰레드(thread)

프로세스 내에서 실제로 작업을 수행하는 것으로 모든 프로세스는 하나 이상의 쓰레드를 가진다.
두 개 이상의 쓰레드를 가지는 프로세스는 멀티 쓰레드 프로세스라고 한다.

쓰레드의 구현

자바에서 쓰레드를 구현하는 방법에는 크게 두 가지가 있다.

1. Thread 클래스를 상속받아 구현

public class MyThread extends Thread {
    public void run() {
        //작업
        System.out.println("MyThread run");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("MyThread end");
    }
}

2. Runnable 인터페이스를 구현

public class MyThread2 implements Runnable{
    @Override
    public void run() {
        //작업
        System.out.println("MyThread2 run");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("MyThread2 end");
    }
}
public class Test {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        Thread thread2 = new Thread(new MyThread2());

        thread.start();
        thread2.start();
    }

실행 결과

MyThread run
MyThread2 run
MyThread end
MyThread2 end

순서대로 Mythread가 종료된 후 실행되는 게 아니며 실행결과는 다를 때도 있다.
두 방법 모두 쓰레드를 통해 작업하고 싶은 내용을 run()메서드에 작성하면 되지만 Thread 클래스를 상속받아 구현할 경우 다른 클래스를 상속받을 수 없어서 그럴 때는 Runnable 인터페이스를 구현하면 된다.

run() start()

위에서 run()메서드를 구현하고 run()을 호출하지 않고 start()를 호출하였는데 이 둘은 차이가 있다.

  • run()실행: 스레드를 실행하는 게 아닌 단순히 메서드를 호출하는 것으로 호출하게 되어 스레드가 아닌 순서대로 실행이 된다.
  • start()실행: 쓰레드가 작업에 필요한 call stack을 생성하고 run()을 호출하여 독립적으로 수행한다.

Main 쓰레드

call stack을 보면 main메서드도 올라가 있는데 main메서드의 작업을 수행하는 쓰레드를 main쓰레드라고 한다.
main 메서드가 종료되더라도 다른 쓰레드에서 작업이 이루어지고 있다면 프로그램은 종료되지 않고 모든 작업이 끝난 후에 프로그램이 종료되게 된다.

public class MyThread extends Thread {
    public void run() {
        //작업
        System.out.println("MyThread run");
        try {
           Thread.sleep(3000);  //3초간 sleep하도록 변경
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("MyThread end");
    }
}
public class Test {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        Thread thread2 = new Thread(new MyThread2());

        thread.start();
        thread2.start();

        System.out.println("메인메서드 종료");
    }
}

실행 결과

메인메서드 종료
MyThread2 run
MyThread run
MyThread2 end
MyThread end

Process finished with exit code 0

3초 후에 MyThread end가 출력되고 프로세스가 종료된다.

쓰레드의 상태

쓰레드는 총 5가지의 상태가 존재한다.

  • NEW: 쓰레드가 생성되고 start()메서드가 호출되지 않은 상태
  • RUNNABLE: 실행 중이거나 실행 가능한 상태
  • BLOCKED: 동기화 블럭에 의해 일시 정지된 상태
  • WAITING, TIMED_WAITING: 쓰레드의 작업이 종료되지 않았지만 실행가능하지않은 일시정지 상태
  • TERMINATED: 쓰레드의 작업이 종료된 상태

쓰레드의 우선순위

쓰레드는 우선순위(priority)라는 멤버변수를 가지고 있으며 쓰레드의 중요도에 따라 우선순위를 다르게 지정하여 우선순위가 높은 쓰레드가 더 많은 시간동안 작업할수 있도록 설정할 수 있다.
우선순위의 범위는 1~10이며 숫자가 클수록 높은 우선순위를 가지게 된다.
또한 쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받는다.(Main쓰레드의 우선순위는 5이므로 main메서드에서 생성하는 쓰레드는 5가 된다.)

public class Test {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        System.out.println(thread.getPriority());   //5
    }
}

동기화

싱글쓰레드 프로세스와 멀티쓰레드 프로세스의 차이점은 싱글쓰레드의 경우 프로세스 내에서 하나의 쓰레드만 작업을 하여 자원을 가지고 작업하는데 문제될게 없지만, 멀티쓰레드의 경우 여러 쓰레드가 프로세스 내의 공유자원을 가지고 작업하기 때문에 서로의 작업에 영향을 준다. 이를 방지하기 위해 임계 구역(critical section)과 잠금(lock)개념이 등장하였다.

임계 구역(critical section)과 잠금(lock)

공유자원을 사용하는 코드를 임계 구역으로 하고 lock을 획득한 하나의 쓰레드만 해당 영역에서 작업을 수행하고 lock을 반납하도록 하여 다른 쓰레드가 임계 구역의 코드를 수행할 수 없도록 한다.
lock은 의미 그대로 잠근다는 행위로 자원을 사용하고 있는 동안은 잠가서 다른 쓰레드가 작업할 수 없도록 하는 개념이다.

자바에서는 동기화를 여러 방법으로 동기화를 제공하지만 Synchronized를 통한 동기화에 대해 알아보자.
동기화가 적용되지 않은 계좌에서 출금하는 간단한 코드가 있다.

public class Account {
    private int balance;

    public Account(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public int withdraw(int money) {
        if (this.balance < money) {
            System.out.println("잔액부족");
            return 0;
        }
        try {
            Thread.sleep(1000); //인출 지연시간
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.balance -= money;
        System.out.println("찾으신 금액: " + money);
        System.out.println("출금후 잔고: " + balance);

        return balance;
    }
}
package synchronize;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Account account = new Account(100_000);
        Runnable runnable = () -> account.withdraw(50_000);
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        Thread t3 = new Thread(runnable);

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

        Thread.sleep(2000);
        System.out.println("작업후 잔액"+account.getBalance());
    }
}

실행 결과

찾으신 금액: 50000
찾으신 금액: 50000
찾으신 금액: 50000
출금후 잔고: -50000
출금후 잔고: -50000
출금후 잔고: -50000
작업후 잔액-50000

계좌에 10만원이 있고 3곳에서 5만원씩 출금하였다. 잔액보다 출금하려는 금액이 크면 출금이 안되야 하지만 출금되어 -가 찍힌 것을 볼 수 있다.
출금하는 메소드에 synchronized키워드를 붙여 하나의 쓰레드만 자원에 접근할 수 있도록 하였다.

public class Account {
    private int balance;

    public Account(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public synchronized int withdraw(int money) {
        if (this.balance < money) {
            System.out.println("잔액부족");
            return 0;
        }
        try {
            Thread.sleep(1000); //인출 지연시간
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.balance -= money;
        System.out.println("찾으신 금액: " + money);
        System.out.println("출금후 잔고: " + balance);

        return balance;
    }
}

실행 결과

찾으신 금액: 50000
출금후 잔고: 50000
작업후 잔액50000
찾으신 금액: 50000
출금후 잔고: 0
잔액부족

이처럼 메소드 전체를 임계 구역으로 설정할 수도 있고 아래와 같이 특정 블럭만 임계 영역으로 지정할 수도 있다.

public class Account {
    private int balance;

    public Account(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public int withdraw(int money) {
        synchronized (this) {
            if (this.balance < money) {
                System.out.println("잔액부족");
                return 0;
            }
            try {
                Thread.sleep(1000); //인출 지연시간
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.balance -= money;
        }

        System.out.println("찾으신 금액: " + money);
        System.out.println("출금후 잔고: " + balance);

        return balance;
    }
}

데드락(Deadlock)

교착상태라고도 하며 두 쓰레드가 공유자원을 점유한 상태에서 서로 상대방이 작업을 끝내기를 기다리고 있어 멈춘 상태이다.

public class DeadlockTest {
    public static void main(String[] args) {
        final DeadLock deadLock = new DeadLock();
        Thread t1 = new Thread(() -> deadLock.methodA());
        Thread t2 = new Thread(() -> deadLock.methodB());
        t1.start();
        t2.start();
    }
}

class DeadLock {
    Object lock1 = new Object();
    Object lock2 = new Object();

    public void methodA() {
        System.out.println(Thread.currentThread().getName());
        System.out.println("methodA wait for lock1");
        synchronized (lock1) {
            System.out.println("methodA acquired lock1");
            try {
                Thread.sleep(100);
                methodB();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void methodB() {
        System.out.println(Thread.currentThread().getName());
        System.out.println("methodB wait for lock2");
        synchronized (lock2) {
            System.out.println("methodB acquired lock2");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            methodA();
        }
    }

}

실행 결과

Thread-0
methodA wait for lock1
methodA acquired lock1
Thread-1
methodB wait for lock2
methodB acquired lock2
Thread-0
methodB wait for lock2
Thread-1
methodA wait for lock1

서로의 lock을 얻으려고 호출하며 기다리게 된다.

참고 문서

Java의 정석 - 남궁 성
http://www.tcpschool.com/java/intro
https://riptutorial.com/ko/java/example/9135/%EA%B8%B0%EB%B3%B8-%EA%B5%90%EC%B0%A9-%EC%83%81%ED%83%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0

'프로그래밍 > Java' 카테고리의 다른 글

junit5 사용하기  (0) 2021.02.03
Enum 클래스  (0) 2021.01.26
예외처리  (0) 2021.01.13
조건문과 반복문  (0) 2021.01.11
인터페이스  (0) 2021.01.09