Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

forDevLife

[Java] join과 Atomic 처리 본문

Java

[Java] join과 Atomic 처리

JH_Lucid 2022. 1. 27. 11:34
문제

아래 예제는 ThreadEx20_3(데몬 쓰레드 - gc 역할)를 생성한 후 메인 쓰레드의 for문 내에서 랜덤으로 memory를 소모하는 도중 memory의 사용량을 체크 > gc를 수행하는 방식이다.

 

기존에는 gc.interrupt()가 발생하게 되면 sleep 된 데몬 쓰레드가 깨어나 gc 처리를 수행하는 도중 컨텍스트 스위칭이 발생하여 메인 쓰레드에서 memory를 깎아 먹어 마이너스 메모리가 되는 문제가 발생했다. 

따라서 main의 try ~ catch를 통해 gc 쓰레드가 수행을 마칠 때 까지 기다리는 join을 통해 문제를 해결했다.

 

이 방법외에 다른 방법을 찾고자 AtomicInteger를 사용해봤다. 결과는 우선 원하는대로 나오지는 않았다.

 

 

코드
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadEx20_2 {
    public static void main(String[] args) {
        ThreadEx20_3 gc = new ThreadEx20_3();
        gc.setDaemon(true);
        gc.start(); // daemon Thread로 실행

        int requiredMemory = 0;

        for (int i = 0; i < 20; i++) {
            requiredMemory = (int)(Math.random() * 10) * 20;

            // 필요한 메모리가 사용할 수 있는 양보다 크거나 전체 메모리 60퍼 이상 사용했을 경우 gc를 깨운다.
            if (gc.freeMemory() < requiredMemory
                || gc.freeMemory() < gc.totalMemory() * 0.4) {
                gc.interrupt();
                // try {
                //    gc.join(200); // 시간을 통해 이 쓰레드가 작업을 마칠 수 있도록 한다. 전달하지 않으면 gc가 끝날때까지 기다린다.
                //} catch (InterruptedException e) {
                //    e.printStackTrace();
                //}
            }
            gc.usedMemory.set(gc.usedMemory.get() + requiredMemory);
            System.out.println("usedMemory : " + gc.usedMemory);
        }

    }
}

class ThreadEx20_3 extends Thread {
    static final int MAX_MEMORY = 1000;
    AtomicInteger usedMemory = new AtomicInteger();

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(10 * 1000); // 10초마다 gc 실행, 하지만 interrupt 발생 시 바로 gc 실행
            } catch (InterruptedException e) {
                System.out.println("Awaken by interrupt().");
            }
            gc(); // 주기적으로 gc 실행, 인터럽트 걸리면 걸린 후 바로 실행
            System.out.println("Garbage Collected. Free Memory : " + freeMemory());
        }
    }

    public void gc() {
        System.out.println("before usedMemory = " + usedMemory);
        usedMemory.set(usedMemory.get() - 300);
        if (usedMemory.get() < 0) {
            usedMemory.set(0);
        }
        System.out.println("after usedMemory = " + usedMemory);
    }

    public int totalMemory() {
        return MAX_MEMORY;
    }

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

 

결과
usedMemory : 180
usedMemory : 300
usedMemory : 360
usedMemory : 520
usedMemory : 700
usedMemory : 760
usedMemory : 760
usedMemory : 940
usedMemory : 980
usedMemory : 1000
usedMemory : 1020
usedMemory : 1040
usedMemory : 1140
usedMemory : 1280
usedMemory : 1300
usedMemory : 1400
usedMemory : 1420
usedMemory : 1520
usedMemory : 1560
usedMemory : 1740 // Main에서 먼저 for문 20회 반복
Awaken by interrupt().
before usedMemory = 1740
after usedMemory = 1440
Garbage Collected. Free Memory : -440
Awaken by interrupt().
before usedMemory = 1440
after usedMemory = 1140
Garbage Collected. Free Memory : -140

AtomicInteger는 int 자료형을 갖고 있는 wrapping 클래스이며, 멀티쓰레드 환경에서 동시성을 보장한다.

Atomic 클래스는 CAS(compare-and-swap)를 이용하여 동시성을 보장하며, 여러 쓰레드에서 데이터를 write해도 문제가 없다.

 

위에서는 gc내의 usedMemory를 두 쓰레드에서 동시에 접근하게 되어 발생하게 된다. 따라서 한 쪽 쓰레드(GC)가 사용 중일 경우, 다른 쓰레드(main)를 block 처리하면 문제가 해결될 거라고 생각했다.

하지만 이미 Main 쓰레드의 for문 내에서 gc.usedMemory.set()을 통해 AtomicInteger 객체에 접근하고 있으므로, 인터럽트가 발생해도 gc가 주도권을 잡지 못한다. 

 

위의 결과는 Main Thread의 "usedMemory 출력" 20회 이후 interrupt 처리가 발생한 결과이다. run 할 때마다 interrupt된 gc가 먼저 처리될 때도 있지만, 결과를 예상할 수 없다.

 

따라서 주석처리됐던 gc.join()을 통해 Main 쓰레드가 gc 쓰레드의 메서드가 끝나기 전까지 대기하도록 해서, 자원 접근을 막도록 하자.

gc.join() 주석 해제한 결과는 아래와 같다.

 

* join 하게 되면 atomic 처리는 필요 없다.

usedMemory : 120
usedMemory : 120
usedMemory : 180
usedMemory : 340
usedMemory : 340
usedMemory : 500
usedMemory : 580
usedMemory : 600
usedMemory : 620
Awaken by interrupt(). // 정상 시점(메모리가 60퍼 이상 사용된 시점)에 Interrupt가 발생해 Gc가 일어난다.
before usedMemory = 620
after usedMemory = 320
Garbage Collected. Free Memory : 680
usedMemory : 360
usedMemory : 440
usedMemory : 560
usedMemory : 720
Awaken by interrupt().
before usedMemory = 720
after usedMemory = 420
Garbage Collected. Free Memory : 580
usedMemory : 520
usedMemory : 640
Awaken by interrupt().
before usedMemory = 640
after usedMemory = 340
Garbage Collected. Free Memory : 660
usedMemory : 500
usedMemory : 540
usedMemory : 580
usedMemory : 720
Awaken by interrupt().
before usedMemory = 720
after usedMemory = 420
Garbage Collected. Free Memory : 580
usedMemory : 480

 

추가

join도 안하고, usedMemory에도 Atomic 처리를 하지 않으면 어떻게 될까?

 

package thread;

public class ThreadEx20_2 {
    public static void main(String[] args) {
        ThreadEx20_3 gc = new ThreadEx20_3();
        gc.setDaemon(true);
        gc.start(); // daemon Thread로 실행

        int requiredMemory = 0;

        for (int i = 0; i < 20; i++) {
            requiredMemory = (int)(Math.random() * 10) * 20;

            // 필요한 메모리가 사용할 수 있는 양보다 크거나 전체 메모리 60퍼 이상 사용했을 경우 gc를 깨운다.
            if (gc.freeMemory() < requiredMemory
                || gc.freeMemory() < gc.totalMemory() * 0.4) {
                gc.interrupt();
                // try {
                //     gc.join(200); // 시간을 통해 이 쓰레드가 작업을 마칠 수 있도록 한다. 전달하지 않으면 gc가 끝날때까지 기다린다.
                // } catch (InterruptedException e) {
                //     e.printStackTrace();
                // }
            }
            gc.usedMemory += requiredMemory;
            System.out.println("usedMemory : " + gc.usedMemory);
        }
        // System.out.println("main is ended");

    }
}

class ThreadEx20_3 extends Thread {
    static final int MAX_MEMORY = 1000;
    int usedMemory = 0;

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(10 * 1000); // 10초마다 gc 실행, 하지만 interrupt 발생 시 바로 gc 실행
            } catch (InterruptedException e) {
                System.out.println("Awaken by interrupt().");
            }
            gc(); // 주기적으로 gc 실행, 인터럽트 걸리면 걸린 후 바로 실행
            System.out.println("Garbage Collected. Free Memory : " + freeMemory());
        }
    }

    public void gc() {
        System.out.println("before usedMemory = " + usedMemory);
        usedMemory -= 300;
        if (usedMemory < 0) {
            usedMemory = 0;
        }
        System.out.println("after usedMemory = " + usedMemory);
    }

    public int totalMemory() {
        return MAX_MEMORY;
    }

    public int freeMemory() {
        return MAX_MEMORY - usedMemory;
    }
}
usedMemory : 100
usedMemory : 240
usedMemory : 260
usedMemory : 380
usedMemory : 400
usedMemory : 460
usedMemory : 480
usedMemory : 480
usedMemory : 580
usedMemory : 640
usedMemory : 660
usedMemory : 820
usedMemory : 960
usedMemory : 980
Awaken by interrupt().
usedMemory : 1140
usedMemory : 1220
before usedMemory = 1140 // Gc 실행
usedMemory : 1060        // main 으로 넘어감
after usedMemory = 1060  // 다시 gc 실행 (원하는 결과는 1140 - 300 이지만, main에서 메모리를 한번 더 소모하게 됨)
Garbage Collected. Free Memory : -60
usedMemory : 1080
usedMemory : 1240
Awaken by interrupt().
usedMemory : 1400
before usedMemory = 1240
after usedMemory = 1100
Garbage Collected. Free Memory : -100
Awaken by interrupt().
before usedMemory = 1100
after usedMemory = 800
Garbage Collected. Free Memory : 200

gc에서 usedMemory를 사용하는 도중에도 컨텍스트 스위칭이 발생하여 main 쓰레드가 실행된다.

따라서 Gc메서드가 온전히 실행되지 않는다.

'Java' 카테고리의 다른 글

[Java] notify() vs notifyAll()  (1) 2022.01.26
[Java] Call back(Listener)  (0) 2021.11.18
[Java] Recursive Type Bound  (0) 2021.11.14
[정규식] 문자열 계산기  (0) 2021.09.30
[java] import, static import 문  (0) 2021.06.09
Comments