본문 바로가기
CS(Computer Science)/20) 자바

자바를 자바 26 : Multithreaded Programming with Java (2)

by tonyhan18 2020. 12. 30.
728x90

Thread States


서버와 통신하는 스레드의 상태는 위와 같이 구성되게 된다.

new : 지금 막 생성된 상태
runnable : 운영될 준비됨
blocked : 어떤 이유로 스레드가 코드를 실행하지 않고 막혀있음(어떤 코드 블락이나 리소스를 접근해야 하는 상황에서 다른 스레드가 해당 블락이나 리소스를 접근한 상태)
waiting : 스레드가 기다리고 있는 상태이지만 다른 스레드가 notify나 notifyAll로 깨워줘야 한다.
TIMED_WAITING : timeout이 걸려 있는 waiting으로 시간이 지나면 자동으로 깨어난다.
TERMINATED : 스레드가 끝날때의 상태로 예전에는 stop을 사용해서 Deprecated 상태로 만들곤 하였다. 현재는 run이 끝나면 자동으로 TERMINATED 상태가 된다.

Controlling Threads


이와 같은 상태를 만들어 주는 함수들은 위와 같이 구성된다.

sleep()

static void sleep(long millis)
static void sleep(long millis, int nanos)

try {
//1 mili + 500000 nano sec
    Thread.sleep(1, 500000);
//여기에서 InterruptedException을 사용해야 한다.
} catch(InterruptedException e) {}

만약 sleep 함수가 호출되면 TIMED_WAITING 상태가 되고 이 상태에서 Interrupt를 부르게 되면 InterruptedException이 발생하게 된다. 그리고 스레드는 RUNNABLE state로 바뀌게 된다.

그렇기 때문에 InterruptedException은 진짜 예외일 수도 있지만 일부로한 것일 수도 있다.

class ThreadEx12_1 extends Thread {
    public void run() {
        try {
            Thread.sleep(1000);
        } catch(InterruptedException e) {}
        for(int i=0; i<300; i++) {
            System.out.print("-"); 
        }
        System.out.print("<<end of th1>>"); 
    } 
}

class ThreadEx12_2 extends Thread {
    public void run() {
        try {
            Thread.sleep(2000);
        } catch(InterruptedException e) {}
        for(int i=0; i<300; i++) {
            System.out.print("|"); 
        }
        System.out.print("<<end of th2>>"); 
    } 
}

public class Lecture {
    public static void main(String args[]) {
        ThreadEx12_1 th1 = new ThreadEx12_1();
        ThreadEx12_2 th2 = new ThreadEx12_2();\

        //1초 sleep
        th1.start();
        //2초 sleep
        th2.start();

        try {
        //3초 정지후 end of main 출력
            Thread.sleep(3000);
        } catch(InterruptedException e) {}
        System.out.print("<<end of main>>");
    }
}

interrupt() and interrupted()

• interrupt() : 다른 스레드를 인터럽트
• isInterrupted() : 스레드가 인터럽트 된 상태인지 체크
• interrupted() : current thread가 인터럽트되었는지 확인

class ThreadEx13_1 extends Thread {
    public void run() {
        int i = 10;

        //인터럽트가 걸리지 않으면 시간을 그냥 버린다.
        while(i != 0 && !isInterrupted()) {
            System.out.println(i--);
            for(long x = 0; x<2500000000L; x++); // waste time
        }
    }
}

public class Lecture {
    public static void main(String[] args) {
        ThreadEx13_1 th1 = new ThreadEx13_1();
        th1.start();
        String input = JOptionPane.showInputDialog("Enter any string.");
        System.out.println("You entered " + input);

        //여기에서 인터럽트를 걸게 된다.
        th1.interrupt();
        System.out.println("isInterrupted(): " + th1.isInterrupted());
    }
}

class ThreadEx14_1 extends Thread {
    public void run() {
        int i=10;

        //여기에서 !isInterrupted 인 것은 sleep 상태에서 interrupted Exception 발생시 interrupted Flag가 Flase로 바뀐다.
        //그래서 countdown이 지속된다.
        while(i!=0 && !isInterrupted()) {
            System.out.println(i--);

            //카운트하고 1초간 sleep
            try {
                Thread.sleep(1000);
            } catch(InterruptedException e) {}
        }
        System.out.println("countdown complete.");
    }
}

public class Lecture {
    public static void main(String[] args) throws Exception {
        ThreadEx14_1 th1 = new ThreadEx14_1();
        th1.start();
        String input = JOptionPane.showInputDialog("Enter any string.");
        System.out.println("You entered " + input);
        th1.interrupt();
        System.out.println("isInterrupted(): " + th1.isInterrupted());
    }
}

yield()

thread th1, th2, th3가 RUNNABLE 상태로 바뀐다. 그래서 th1이 동작중에 yield를 만나면 th1은 내려오게 된다. 그래서 그 다음 스레드는 스케쥴러가 결정하게 된다.

join()

내가 동작중 th1.join()을 만나게 되면 current thread가 waiting 상태로 들어가고 th1이 종료 될때까지 그 상태로 기다리게 된다.

void join()
//성분을 주면 현재 thread가 기다리는 시간을 넘기어 준다.
void join(long millis)
void join(long millis, int nanos)

try {
    th1.join(); // current thread waits until thread th1 finishes execution (and dies).
} catch(InterruptedExecution e) {}
class ThreadEx19_1 extends Thread {
    public void run() {
        for(int i=0; i<300; i++) { System.out.print(new String("-")); }
    }
}

class ThreadEx19_2 extends Thread {
    public void run() {
        for(int i=0; i<300; i++) { System.out.print(new String("|")); }
    }
}

public class Lecture {
    static long startTime = 0;
    public static void main(String args[]) {
        ThreadEx19_1 th1 = new ThreadEx19_1();
        ThreadEx19_2 th2 = new ThreadEx19_2();
        th1.start();
        th2.start();
        startTime = System.currentTimeMillis();
        try {
            th1.join();
            th2.join();
        } catch(InterruptedException e) { }
        //두 thread가 걸린 시간을 계산하게 된다.
            System.out.print("elapsed time: " + (System.currentTimeMillis() - Lecture.startTime));
    }
}


자바의 가비지 컬렉션을 따라한 것이다.

class ThreadEx20_1 extends Thread {
    final static int MAX_MEMORY = 1000;
    int usedMemory = 0;
    public void run() {
        while(true) {
            try {
            //10초간 sleep
                Thread.sleep(10*1000); // sleep for 10 seconds
            } catch(InterruptedException e) {
                System.out.println("Awaken by interrupt()");
            }

        //gc 함수 호출
        gc();
        System.out.println("Garbage Collected. Free Memory: " + freeMemory());
        }
    }

    public void gc() {
        usedMemory -= 300;
        if(usedMemory < 0) usedMemory = 0;
    }

    public int totalMemory() { return MAX_MEMORY; }
    public int freeMemory() { return MAX_MEMORY - usedMemory; }
}

public class Lecture {
    public static void main(String args[]) {
        ThreadEx20_1 gc = new ThreadEx20_1();
        gc.setDaemon(true); // set this thread as a daemon thread.
        gc.start();
        int requiredMemory = 0;
        for(int i=0; i<20; i++) {
            //0~20 사이의 값
            requiredMemory = (int)(Math.random()*10) * 20;

            //메모리가 부족한 경우 usedMemory를 빼준다음 채워줌
            if(gc.freeMemory() < requiredMemory || gc.freeMemory() < gc.totalMemory() * 0.4) {
                gc.interrupt();
            }
            gc.usedMemory += requiredMemory;
            System.out.println("usedMemory: " + gc.usedMemory);
        }
    }
}

이렇게 했을 때 메모리가 어찌되었든 넘어가는 일이 없어야 하겠지만 존재한다. 그래서 아래와 같이 코드를 짜주는 것이 최선이다.

public class Lecture {
    public static void main(String args[]) {
        ThreadEx20_1 gc = new ThreadEx20_1();
        gc.setDaemon(true); // set this thread as a daemon thread.
        gc.start();
        int requiredMemory = 0;
        for(int i=0; i<20; i++) {
            requiredMemory = (int)(Math.random()*10) * 20;
            if(gc.freeMemory() < requiredMemory || gc.freeMemory() < gc.totalMemory() * 0.4) {
                gc.interrupt();

                //gc호출하고 join하는데 반드시 매개변수 넣자. 코드상 while문으로 끝나지 않기 때문
                try {
                    gc.join(100);
                } catch(InterruptedException e) {}
            }
            gc.usedMemory += requiredMemory;
            System.out.println("usedMemory: " + gc.usedMemory);
        }
    }
}


왜 이렇게 해야 하나? 이전 코드에서는 스레드를 깨우는 과정이 조금 늦어서 코드를 넘어가고 그냥 메모리를 더하는 경우가 생길 수 있는 위험이 존재한다.

Daemon Thread

public class Lecture implements Runnable {
    static boolean autoSave = false;
    public static void main(String[] args) {
        Thread t = new Thread(new Lecture());
//데몬스레드라고 부르는 것인데 보통은 무한으로 작동한다.
//다른 스레드가 다 죽으면 자동으로 데몬스레드도 죽는다.
        t.setDaemon(true);
        t.start();
        for (int i = 1; i <= 10; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
        System.out.println(i);
//또 다른 예제로 DemonThread를 autoSave로 만드는 것이다.
if (i == 5)
autoSave = true;
}
System.out.println("Terminating program.");
}

//autoSave가 되면 3초간 sleep 하고 autoSave가 True이면 autoSave 함수를 호출한다.
    public void run() {
        while (true) {
        try {
            Thread.sleep(3 * 1000);
        } catch (InterruptedException e) {}
            if (autoSave) autoSave();
        }
    }
    public void autoSave() {
        System.out.println("Your work is saved to a file.");
    }
}

Daemon Thread로 설정 안한 경우

Thread Synchronization

  • Critical Section
    Section은 코드 블락으로 여러 스레드가 해당 섹션에 들어가면 문제가 된다. 그래서 한 스레드가 섹션에 존재시 다른 스레드가 접근하지 못하도로고 만드는 것이다.
class Account {
    private int balance = 1000;
    public int getBalance() {
        return balance;
    }
    public void withdraw(int money) {
        if(balance >= money) {
            try { Thread.sleep(1000); } catch(InterruptedException e) {}
            balance -= money;
        }
    }
}
class RunnableEx21 implements Runnable {
    Account acc = new Account();
    public void run() {
        while(acc.getBalance() >= 200) {
            int money = 200;
            acc.withdraw(money);
            System.out.println("balance: " + acc.getBalance());
        }
    }
}

// 서로 다른 스레드가 작동한다.
public class Lecture {
    public static void main(String[] args) {
        Runnable r = new RunnableEx21();
        new Thread(r).start();
        new Thread(r).start();
    }
}


그런데 이러한 스레드들의 문제점은 스레드가 여러개 접근하면서 balance에 있는 값을 계속 무너트린다.

class Account {
    ...
    //이 영역을 Critical Section으로 만드는 것이다.
    public synchronized void withdraw(int money) {
        if(balance >= money) {
            try { Thread.sleep(1000); } catch(InterruptedException e) {}
            balance -= money;
        }
    }
}
class Account {
    ...
    //혹은 아래와 같은 방법이 존재한다.
    public void withdraw(int money) {
        synchronized(this) {
            if(balance >= money) {
                try { Thread.sleep(1000); } catch(InterruptedException e) {}
                balance -= money;
            }
        }
    }
}

하지만 이렇게 synchronized를 막쓰면 멀티스레드의 장점이 사라지기 때문에 적당히 쓰자.

728x90

'CS(Computer Science) > 20) 자바' 카테고리의 다른 글

자바를 자바 fp  (0) 2020.12.30
자바를 자바 25  (0) 2020.12.30
자바를 자바 24 (Networking with Java(3))  (0) 2020.12.30
자바를 자바 23  (0) 2020.12.30
자바를 자바 22  (0) 2020.12.03