본문 바로가기

JAVA

JAVA의 정석 - Thread

쓰레드.html

JAVA의 정석

2018-12-16

Thread

9.4 volatile

    멀티 코어 프로세서에서는 코어마다 별도의 캐시를 가지고 있다. 
    코어는 메모리에서 일겅온 값을 캐시에 저장하고 캐시에서 값을 읽어서 작업한다. 다시 같은 값을 읽어올 때는 먼저 캐시에 있는지 확인하고 없을 때만 메모리에서 읽어온다. 
    그러다보니 도중에 메모리에 저장된 변수의 값이 변경되었는데도 캐시에 저장된 값이 갱신되지 앟아서 메모리에 저장된 값이 다른 경우가 발생한다.

    volatile boolean suspended = false;
    volatile boolean stopped = false;

위와 같이 타입 옆에 volatile 을 붙이면 캐시에서 읽어오지 않고 메모리에서 읽어온다. 
    public synchronized void stop() {
        stopped = trune;
    }

쓰레드가 synchronized 블럭으로 들어가고 나올 때, 캐시와 메모리간의 동기화가 이뤄지기 때문에 값의 불일치가 해소되므로 volatile 과 같은 효과가 발생한다.  

volatile로 long과 double을 원자화

    JVM은 데이터를 4 byte(=32bit) 단위로 처리하기 떄문에, int와 int보다 작은 타입들은 한 번에 읽거나 쓰는 것이 가능하다. 즉, 단 하나의 명령어로 일걱나 쓰기가 가능하다는 뜻이다.
    그러나, 크기가 8byte인 long과 double 타입의 변수는 하나의 명령어로 값을 읽거나 쓸 수 없기 때문에, 변수의 값을 읽는 과정에 다른 쓰레드가 끼어들 여지가 있다. 다른 쓰레드가 끼어들지 못하게 하려고 변수를 읽고 쓰는 모든 문장을 synchronized 블럭으로 감쌀 수도 있지만, 더 간단한 방법은 변수를 선언할 때 volatile을 붙이는 것이다.



9.5 fork & join 프레임웍

    JDK1.7 부터 추가되었고, 이 프레임웍은 하나의 작업을 작은 단위로 나눠서 여려 쓰레드가 동시에 처리하는 것을 쉽게 만들어 준다.
    먼저 수행할 작업에 따라 RecursiveAction과 RecursiveTask 두 클래스 중에서 하나를 상속받아 구현해야 한다.

    RecursiveAction            반환값이 없는 작업을 구현할 때 사용
    RecursiveTask            반혼값이 있는 작업을 구현할 때 사용

    public bastract class RecursiveAction extends ForkJoinTask<Void> {
        protected abstract void compute();
    }

    public abstract class RecursiveTask<V> extends ForkJoinTask<T> {
        V result;
        protected abstract V compute();
    }

    class SumTask extends RecursiveTask<Long> {
        long from, to;

        SumTask(long from, long to) {
            this.from = from;
            this.to = to;
        }

        publicLong compute() {
            // 처리할 작업을 수행하기 위한 문장을 넣는다.
        }
    }

    쓰레드를 시작할 때 run()이 아니라 start()를 호출하는 것처럼, fork&join 프레임웍으로 수행할 작업도 compute()가 아닌 invoke()로 시작한다.

    ForkJoinPool pool = new ForkJoinPool();    // 쓰레드 풀을 생성
    SumTask task = new SumTask(from, to);    // 수행할 작업을 생성
    Long result = pool.invoke(task);        // invoke()를 호출해서 작업을 시작

    ForkJoinPool 은 fork & join 프레임웍에서 제공하는 쓰레드 풀로, 지정된 수의 쓰레드를 생성해서 미리 만들어 놓고 반복해서 재사용할 수 있게 한다. 그리고 쓰레드를 반복해서 생성하지 않아도 된다는 장점과 너무 많은 쓰레드가 생성되어 성능이 저하되는 것을 막아준다는 장점이 있다. 
    쓰레드 풀은 쓰레드가 수행해야하는 작업이 담긴 큐를 제공하며, 각 쓰레드는 자신의 작업 큐에 담긴 작업을 순서대로 처리한다.

쓰레드 풀은 기본적으로 코어의 개수와 동일한 개수의 쓰레드를 생성한다.


    public Long compute() {
        long szie = to - from + 1;

        if(size <= 5) {
            return sum();
        }

        long half = (from + to) / 2;

        SumTask leftSum = new SumTask(from, half);
        SumTask rightSum = new SumTask(half + 1, to);

        leftSum.fork();

        return rightSum.compute() + leftSum.join();
    }

compute() 의 구조는 일반적인 재귀호출 메서드와 동일하다.

다른 쓰레드의 작업 훔쳐오기

    자신의 작업 큐가 비어있는 쓰레드는 다른 쓰레드의 작업 큐에서 작업을 가져와서 수행한다. 이것을 작업 훔쳐오기(work stealing)라고 하며, 이 과정은 모두 쓰레드풀에 의해 자동적으로 이루어진다.

fork() 와 join()

    fork()와 join()의 중요한 차이점은 fork()는 비동기 메서드(asynchronous method)이고, join()은 동기 메서드(synchronous method)라는 것이다.

    fork()        해당 작업을 쓰레드 풀의 작업 큐에 넣는다. 비동기 메서드
    join()        해당 작업의 수행이 끝날 때까지 기다렸다가, 수행이 끝나면 그 결과를 반환한다. 동기 메서드

    return 문에서 compute()가 재귀호출될 때, join()은 호출되지 않는다. 그러다가 작업을 더 이상 나눌 수 없게 되었을 때, compute()의 재귀호출은 끝나고 join()의 결과를 기다렸다가 더해서 결과를 반환한다. 재귀호출된 compute()가 모두 종료될 때, 최종 결과를 얻는다.

멀티 쓰레드로 처리한다고 해서 for 문보다 빠르다고 생각할 수 있는데, 작업을 나누고 합치는데 시간이 더 걸리기 때문에 for 문으로 하는 작업이 더 빠르다.



지네릭 타입의 형변환

    지네릭 타입과 넌지네릭 타입간의 형변환은 경고가 발생하지만 항상 가능하다.
    다만, 대입된 타입이 다른 지네릭 타입 간에는 형변환이 불가능하다.

    <? extends Object> 인 경우 지네릭으로 형변환이 가능하다.

    Optional<?> EMPTY = new Optional<?>();
    -> Optional<? extends Object> EMPTY = new Optional<>();
    -> Optional<? extends Object> EMPTY = new Optional<Object>();

일반적으로 <?>는 <? extends Object>를 줄여 쓴것이며, <> 안에 생략된 타입은 ‘?’가 아니라 ‘Object’이다.
다만, class Box의 경우 <>는 이다.



지네릭 타입의 제거

    컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다. 그리고 지네릭 타입을 제거한다. 즉, 컴파일된 파일(*.class)에는 지네릭 타입에 대한 정보가 없는 것이다.
    이렇게 하는 이유는 이전 버전의 소스코드와의 호환성을 유지하기 위해서이다.

'JAVA' 카테고리의 다른 글

[JAVA] Reflection 을 이용하여 Method 호출하기  (0) 2020.01.08
[Java] POJO 와 Beans 의 차이점  (0) 2019.08.24
JAVA의 정석 - Generics  (0) 2019.01.13
JAVA의 정석 - 열거형(enums)  (0) 2019.01.13