[자바의 정석]

YJS·2023년 12월 3일
0

1. Thread 클래스와 Runnable 인터페이스


프로세스란

-실행 중인 프로그램

-사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당 받아 실행 중인 것으로 프로그램에 사용되는 데이터와 메모리 등의 자원 그리고 쓰레드로 구성.

쓰레드란?

-프로세스 내에서 실제로 작업을 수행하는 주체

-모든 프로세스에는 1개 이상의 쓰레드가 존재

-두 개 이상의 쓰레드를 가지는 프로세스를 멀티 쓰레드 프로세스

-경량 프로세스라고 불리며 가장 작은 실행 단위

쓰레드 구현의 2가지 방법(Thread 클래스 vs Runnable 인터페이스)

public class ThreadDemo {

    public static void main(String[] args) {
        // 상속으로 구현
        ThreadByInheritance thread1 = new ThreadByInheritance();

        //인터페이스로 구현
        Runnable r = new ThreadByImplement();
        Thread thread2 = new Thread(r);    //생성자: Thread(Runnable target)
        // 아래 코드로 축약 가능
        // Thread thread2 = new Thread(new ThreadByImplement());

        thread1.start();
        thread2.start();
    }
}

class ThreadByInheritance extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            System.out.print(0);
        }
    }
}

class ThreadByImplement implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            System.out.print(1);
        }
    }
}

Thread 클래스가 다른 클래스를 확장할 필요가 있을 경우에는 Runnable 인터페이스를 구현하면 되며, 그렇지 않은 경우는 Thread 클래스를 사용하는 것이 좋음.

⇒ 쓰레드를 생성했다고 자동을 실행되지 않음. start()를 호출해야만 쓰레드가 실행됨.

⇒ start()가 호출되어도 바로 실행되는 것이 아니라, 실행대기 상태에 있다가 자신의 차례가 되어야 실행됨.

⇒ 쓰레드의 실행 순서는 OS의 스케쥴러가 작성한 스케쥴에 의해 결정됨.

⇒ 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없음 (하나의 쓰레드에 start()가 한 번만 호출될 수 있음.)

⇒ 쓰레드를 start로 호출 시 자신만의 스택을 만들어서 사용.

2. 쓰레드의 상태


sleep(long millis)

:지정된 시간 동안 쓰레드를 멈춤.

→ static 메서드임. (자기 자신만 재울 수 있음. )

→반드시 try-catch 블록으로 예외처리를 해줘야함.

public class ThreadDemo {

    public static void main(String[] args) {
        Thread t1 = new Thread(new ThreadEx_1());
        Thread t2 = new Thread(new ThreadEx_2());

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

        try {
            Thread.sleep(2000);          // t1이 아닌 실행중인 자기자신 Thread여야함
        } catch (InterruptedException e) { }

        System.out.print("<메인 쓰레드 종료>");
    }

}

class ThreadEx_1 implements Runnable {

    @Override
    public void run() {
        System.out.print("<t1 시작>");
        for (int i = 0; i < 100; i++) {
            System.out.print("-");
        }
        System.out.print("<t1 종료>");
    }
}

class ThreadEx_2 implements Runnable {

    @Override
    public void run() {
        System.out.print("<t2 시작>");
        for (int i = 0; i < 100; i++) {
            System.out.print("|");
        }
        System.out.print("<t2 종료>");

    }
}

interrupt()

:쓰레드에게 작업을 멈추라고 요청. (강제 종료는 아님.)

→interrupted()는 쓰레드에 대해 interrupt()가 호출되었는지 알려줌.

(호출되지 않았다면 false, 호출 되었다면 true 반환)

→쓰레드가 sleep(), wait(), join()에 의해 일시정지(waiting) 상태에 있을 때, 해당 쓰레드에 대해 interrupt()를 호출하면sleep(), wait(), join()에서 Interrupted Exception이 발생하고 쓰레드는 실행대기(Runnable) 상태로 바뀜.즉, 멈춰있던 쓰레드를 깨워서 실행가능한 상태로 만드는 것.

void interrupt()                //  쓰레드의 interrupted 상태를 false에서 true로 바꾼다
boolean isInterrupted()         //  쓰레드의 interrupted 상태를 반환한다
static boolean interrupted()    //  쓰레드의 interrupted 상태를 반환하고, false로 초기화한다
public class ThreadDemo {

    public static void main(String[] args) {
        ThreadEx_1 t1 = new ThreadEx_1();

        t1.start();

        String input = JOptionPane.showInputDialog("게임을 다시 진행하시겠습니까? [Y/N]");
        System.out.println(input);
        t1.interrupt();
    }

}

class ThreadEx_1 extends Thread {

    @Override
    public void run() {
        int i = 10;

        while (i != 0 && !isInterrupted()) {
				    try {
				        System.out.println(i--);
				        Thread.sleep(1000);
				    } catch (InterruptedException e) {
				        interrupt();
				    }
}
}

suspend(), resume(), stop()

: stop은 쓰레드를 완전히 종료시키는 메서드

suspend는 쓰레드를 일시정지 상태로 만드는 메서드

resume은 정지상태의 쓰레드를 다시 실행 대기 상태로 만드는 메서드

→이 세 개의 메서드는 교착상태(deadlock)를 일으킬 가능성이 있어서 deprecated되었음. 따라서 사용하지 않는 것이 권장됨.

join()

:join 메서드는 일정 시간 동안 특정 쓰레드가 작업하는 것을 기다리게 만드는 메서드.

→sleep과 마찬가지로 try-catch 블록으로 예외처리를 해야 함.

ublic class ThreadDemo {

    public static void main(String[] args) {
        Thread t1 = new Thread(new ThreadEx_1());
        Thread t2 = new Thread(new ThreadEx_2());

        long startTime = System.currentTimeMillis();

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

        try {
            t1.join();    // t1의 작업이 끝날 때까지 기다린다.
            t2.join();    // t2의 작업이 끝날 때까지 기다린다.

        } catch (InterruptedException e) {}

        System.out.println("소요시간 : " + (System.currentTimeMillis() - startTime));
        // 메인쓰레드가 종료된다.
    }

}

class ThreadEx_1 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            System.out.print("-");
        }

    }
}

class ThreadEx_2 implements Runnable {

    boolean suspended = false;
    boolean stopped = false;

    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            System.out.print("|");
        }
    }
}

yield()

: 자신에게 할당된 시간이 남았더라도 다음 쓰레드에게 작업을 넘기도록 하는 메서드

→static 메서드이므로 자기 자신에게만 사용할 수 있음.

→ 쓰레드가 busy-waiting 상태(작업할 내용이 없는데 작업 시간이 할당되어 쓰레드가 돌아가는 상태) 일 때 yield를 호출하도록 설계하면 프로그램의 응답성과 효율을 높일 수 있음.

3. 쓰레드의 우선순위


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

!https://blog.kakaocdn.net/dn/MFHjS/btqTPzhtqy7/bqJqzVIKyMB1XUGf0xZvZ1/img.png


-getPriority()와 setPrioritiy()메소드를 통해 쓰레드의 우선순위를 반환하거나 변경 가능.

-쓰레드의 우선순위가 가질 수 있는 범위는 1부터 10까지이며, 숫자가 높을수록 우선순위 또한 높아짐

-BUT, 쓰레드의 우선순위는 비례적인 절댓값이 아닌 어디까지나 상대적인 값

(=우선순위가 10인 쓰레드가 우선순위가 1인 쓰레드보다 10배 더 빨리 수행되는 것이 아님. 단지 우선순위가 10인 쓰레드는 우선순위가 1인 쓰레드 보다 좀 더 많이 실행 큐에 포함되어, 좀 더 많은 작업 시간을 할당받는 것.)

4. Main 쓰레드


-main 메서드도 하나의 쓰레드 = 메인 쓰레드(Main Thread)

-메인 쓰레드는 프로그램이 시작하면 가장 먼저 실행되는 쓰레드이며, 모든 쓰레드는 메인 쓰레드로부터 생성됨.

-다른 쓰레드를 생성해서 실행하지 않으면, 메인 메서드, 즉 메인 쓰레드가 종료되는 순간 프로그램도 종료됨. (여러 쓰레드를 실행하면, 메인 쓰레드가 종료되어도 다른 쓰레드가 작업을 마칠 때까지 프로그램이 종료되지 않음.)

-쓰레드는 '사용자 쓰레드(user thread)'와 '데몬 쓰레드(daemon thread)'로 구분되는데,실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램이 종료됨.

cf. Daemon Thread

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

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

-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();
}
--------------------------------------------------------------------
public void runDaemonThread() {
    DaemonThread thread = new DaemonThread();
    thread.setDaemon(true);
    thread.start();
}

5. 동기화


동기화란?

어떤 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 하는 작업.

멀티 쓰레드 프로세스에서는 여러 프로세스가 메모리를 공유하기 때문에, 한 쓰레드가 작업하던 부분을 다른 쓰레드가 간섭하는 문제가 생길 수 있음.

따라서 다른 쓰레드가 간섭해서는 안 되는 부분을 임계 영역(critical section)으로 설정해야함.

→임계 영역 설정은 synchronized 키워드를 사용

1) 메서드 전체를 임계영역으로 설정

public synchronized void method1 () { ...... }

→ 쓰레드는 synchronized 키워드가 붙은 메서드가 호출된 시점부터 해당 메서드가 포함된 객체의 lock을 얻어 작업을 수행하다가 메서드가 종료되면 lock을 반환

2) 특정한 영역을 임계영역으로 설정

synchronized(객체의 참조변수) {......}

→이 영역으로 들어가면서부터 쓰레드는 지정된 객체의 lock을 얻게 되고 블록을 벗어나면 lock을 반납.(단, 이때 참조 변수는 락을 걸고자 하는 객체를 참조하는 것이어야 함)

lock이란?

일종의 자물쇠 개념으로 모든 객체는 lock을 하나씩 가지고 있음.

해당 객체의 lock을 가지고 있는 쓰레드만 임계 영역의 코드를 수행할 수 있으며 한 객체의 lock은 하나밖에 없기 때문에 다른 쓰레드들은 lock을 얻을 때까지 기다리게 됨.

public class ThreadDemo {

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

class Account {
    private int balance = 1000; //잔고

    public int getBalance() {
        return balance;
    }

    public void withdraw(int money) {
        synchronized (this) {
            if (balance >= money) {
                try {
                    // 문제 상황을 만들기 위해 고의로 쓰레드를 일시정지
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }

                balance -= money;
            }
        }
    }
}

class ThreadEx_1 implements Runnable {

    Account account = new Account();

    @Override
    public void run() {
        while (account.getBalance() > 0) {
            // 100, 200, 300 중 임의의 값을 선택해서 출금
            int money = (int) (Math.random() * 3 + 1) * 100;
            account.withdraw(money);
            System.out.println("balance: " + account.getBalance());
        }
    }
}

wait() & notify()

메서드설명
void wait()void wait(long timeout)void wait(long timeout, int nanos)객체의 락을 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다.
void notify()waiting pool에서 대기 중인 쓰레드 하나를 깨운다.
void notifyAll()waiting pool에서 대기 중인 모든 쓰레드를 깨운다.
  1. 동기화된 임계 코드 영역의 작업을 수행하다가 작업을 더 이상 진행할 상황이 아니면, 일단 wait()을 호출하여 쓰레드가 락을 반납하고 기다리게 함.
  2. 그러면 다른 쓰레드가 락을 얻어서 해당 객체에 대한 작업을 수행할 수 있게 됨.
  3. 나중에 작업을 진행할 수 있는 상황이 되면 notify()를 호출해서 작업을 중단했던 쓰레드가 다시 락을 얻어 작업을 진행할 수 있게 됨.
class Account {
    private int balance = 1000;

    public synchronized void withdraw(int money) {
        //잔고가 출금액보다 적어 인출을 할 수 없다.
        while (balance < money) {
            try {
                // 해당 객체의 락을 풀고 waiting pool에서 대기
                wait();
            } catch (InterruptedException e) {}
        }
        balance -= money;
    }

    public synchronized void deposit(int money) {
        // 돈을 입금하고 waiting pool의 쓰레드에 통보 
        balance += money;
        notify();
    }
}

6. 데드락


데드락이란?

한 자원을 여러 시스템이 사용하려고 할 때 발생할 수 있는 교착상태

서로 원하는 리소스가 상대방에게 할당되어 있기 때문에 두 프로세스는 무한히 대기 상태에 있게됨.

<데드락 발생 조건>

  1. 상호 배제(Mutual exclusion)

    • 자원은 한 번에 한 프로세스만이 사용할 수 있어야 한다.
  2. 점유 대기(Hold and wait)

    • 최소한 하나의 자원을 점유하고 있으면서 다른 프로세스에 할당되어 사용하고 있는 자원을 추가로 점유하기 위해 대기하는 프로세스가 있어야 한다.
  3. 비선점(No preemption)

    • 다른 프로세스에 할당된 자원은 사용이 끝날 때까지 강제로 빼앗을 수 없어야 한다.
  4. 순환 대기(Circular wait)

    • 프로세스의 집합{P0, P1, ,…Pn}에서P0는P1이 점유한 자원을 대기하고P1은P2가 점유한 자원을 대기하고P2…Pn-1은Pn이 점유한 자원을 대기하며Pn은P0가 점유한 자원을 요구해야 한다.

    ⇒ 4가지 조건이 모두 성립할때만 데드락 발생. 하나라도 성립하지 않으면 교착 상태 해결 가능.

    (출처:https://jwprogramming.tistory.com/12[개발자를 꿈꾸는 프로그래머])

profile
우당탕탕 개발 일기

0개의 댓글