728x90

스트림

스트림 : 다양한 데이터소스를 표준화된 방법으로 다루기 위한 것.

스트림은 데이터를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았다.

 

스트림은 스트림 생성 -> 중간연산(0~n번) ->최종연산(1번) 으로 사용된다.

스트림의 특징

1.스트림은 데이터 소스를 변경하지 않는다.

스트림은 데이터를 읽기만 할 뿐, 데이터 소스를 변경하지 않는다. 데이터를 정렬하거나 중복제거 등은 가능하지만 데이터 값을 변경하는것은 불가능하다.(Read only)

 

2.스트림은 1회용이다.

스트림의 경우 최종연산을 하게되면 해당 스트림이 닫힌다. 닫힌 스트림은 사용할 수 없으며 사용시 에러가 발생한다.

 

3. 스트림의 작업은 내부 반복으로 처리한다.

내부 반복 : 반복문을 메서드의 내부에 숨길 수 있다.

forEach()의 경우 스트림에 정의된 메서드로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용시켜주는 메서드이다.

 

4.스트림의 연산

1.중간연산 : 연산 결과가 스트림을 반환하는 연산. 스트림에 연속적으로 연산가능

2.최종연산 : 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하기 때문에 스트림당 한번만 수행이 가능

최종연산 이후에는 스트림이 닫히기 때문에 최종연산은 한번만 가능하다.

 

연산목록

중간연산

Stream<T> distinct() : 중복 제거

Stream<T> filter(Predicate<T> predicate) : 조건에 안맞는 요소 제외

Stream<T> limit(long maxSize) : 스트림의 개수 지정

Stream<T> skip(long n) : 스트림의 일부를 건너 뛴다.

Stream<T> peek(Consumer<T> action) : 스트림의 요소에 작업 수행

Stream<T> sorted()

Stream<T> sorted(Comparator<T> comparator)

:스트림의 요소 정렬

 

Stream<R> map(Function<T,R> mapper) : 스트림의 요소 변환

 

최종연산

void forEach(Consumer<? super T> action) : 각 요소에 지정된 작업 수행

long count() : 스트림 요소 개수 반환

Optional<T> max(Comparator<? super T> comparator) : 스트림의 최대값 반환

Optional<T> min(Comparator<? super T> comparator) : 스트림의 최소값을 반환

Optional<T> findAny(), Optional<T> findFirst() : 스트림의 요소 하나를 반환

boolean allMatch(Predicate<T> p) : 모든 요소가 조건을 만족하는지 확인

boolean anyMatch(Predicate<T> p) : 하나의 요소라도 해당 조건을 만족하는지 확인

boolean noneMatch(Predicate<> p) : 모든 요소가 조건을 만족하지 않으면 true

Object[] toArray(), A[] toArray(IntFunction<A[ ]> generator) : 스트림의 모든 요소를 배열로 변환

Optional<T> reduce(BinaryOperator<T> accumulator)

T reduce(T identity, BinaryOperator<T> accumulator)

U reduce(U identity, BiFunction<U, T, U> accumulator, BinaryOperator<U> combiner)

 : 스트림의 요소를 하나씩 줄여 가면서 계산한다.

R collect(Collector<T,A,R> collector)

R collect(Supplier<R> supplier, BiConsumer<R, T> accumulator, BiConsumer<R, R> combiner)

: 스트림의 요소를 수집한다. 주로 요소를 그룹화 하거나 분할한 결과를 컬렉션에 담아 반환하는데 사용된다.

 

스트림 연산에서 중요한점은 최종연산이 수행되기 전까지 중간 연산은 수행되지않는다.

중간연산을 호출하더라도 즉각적인 연산이 수행되는것이 아니다.

최종 연산이 수행되어야 스트림의 요소들이 중간연산을 거쳐 최종 연산에서 소모된다.

 

5.기본형 스트림

스트림은 기본적으로 Stream<T> 형태이지만, 오토박싱&언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림, IntStream, LongStream, DoubleStream이 제공된다. 

일반적으로 Stream<Integer> 보다 IntStream을 사용하는것이 더 성능이 좋다. 또한 IntStream과 같이 기본형이 적용된 스트림의 경우 기본형에 대한 작업에 유용한 메서드들이 더 존재한다.

 

6.병렬 스트림

스트림으로 데이터를 처리할 때 장점이 병렬처리가 쉽다는 것이다. 

병렬스트림으로 전환하는 경우 parallel()이라는 메서드를 사용하는데 해당 메서드를 호출하면 스트림이 병렬 스트림으로 전환된다. 병렬로 처리되지 않게 하려면 sequential()을 호출하면 된다.

 

 

스트림 만들기

스트림의 소스가 될 수 있는 대상은 배열, 컬렉션, 임의의 수 등이 있다. 이러한 소스들을 이용하여 스트림을 만들 수 있다.

 

컬렉션

Collection에 stream()메서드라 정의되어 있어 해당 메소드를 호출하면 스트림을 생성할 수 있다.

stream()메서드는 해당 컬렉션을 소스로 하는 스트림을 반환한다.

Stream<T> Collection.stream()

ex)

List<Integer> list = Arrays.asList(1,2,3,4,5);

Stream<Integer> intStream = list.stream();

 

배열

배열을 소스로하는 스트림을 생성할 때는 Stream과 Arrays에 정의되어있는 static메서드로 생성할 수 있다.

Stream<T> Stream.of(T ... values)

Stream<T> Stream.of(T[])

Stream<T> Arrays.stream(T[])

Stream<T> Arrays.stream(T[ ] array, int startInclusive, int endExclusive)

 

특정 범위의 정수

IntStream과 LongStream은 지정한 범위의 연속된 정수를 스트림으로 생성해서 반환할 수 있다.

IntStream IntStream.range(int begin, int end)

IntStream IntStream.rangeClosed(int begin, int end)

range()는 begin ~ end-1까지의 수를 스트림으로 생성하고 rangeClosed()는 begin ~ end 까지의 수를 스트림으로 생성한다.

 

forEach()는 매개변수로 들어온 람다식의 작업을 스트림의 모든요소에 대해 수행한다.

intStream.forEach(System.out::println);

foroEach()가 스트림의 요소를 소모하면서 작업을 수행하므로 같은 스트림에 forEach()를 두 번 호출할 수 없다.

스트림의 요소를 한번 더 출력하려면 스트림을 새로 생성해야 한다.

 

임의의 수

난수를 생성하는데 사용하는 Random클래스에 난수들로 이루어진 스트림을 반환하는 메서드가 있다.

IntStream ints()

LongStream longs()

DoubleStream doubles()

 

위 메서드들이 반환하는 스트림은 크기가 정해지지 않은 '무한 스트림(infinite stream)' 이다.

스트림의 개수를 지정해주는 limit()을 함께 사용하여 무한스트림을 유한 스트림으로 만들어 주어야한다.

ex)

IntStream intStream = new Random().ints();

intStream.limit(5).forEach(System.out::println);

위처럼 무한스트림을 유한스트림으로 변경하여 5개의 정수형 난수를 출력 할수 있다.

 

IntStream ints(long streamSize)

LongStream longs(long streamSize)

DoubleStream doubles(long streamSize)

 

위 메서드들은 지정된 크기에 해당하는 '유한 스트림'을 생성하여 반환한다.

 

람다식 - iterate(), generate()

iterate()와 generate()는 람다식을 매개변수로 받아 람다식에 해당하는 결과를 요소로하는 무한 스트림을 생성하여 반환한다.

static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)

: seed값부터 시작해서 람다식 f에 의해 계산된결과를 스트림에 요소에 포함시키며 해당 결과를 다시 seed값으로 넣어 계산을 반복한다.

static <T> Stream<T> generate(Supplier<T> s)

: seed값 없이 s의 결과로만 무한 스트림을 생성한다.

 

728x90

'Programming > JAVA' 카테고리의 다른 글

Optional  (0) 2021.08.25
스트림(Stream)의 중간연산  (0) 2021.08.25
다양한 함수형 인터페이스 & 메서드 참조  (0) 2021.08.22
람다식(Lambda expression)  (0) 2021.08.21
fork & join  (0) 2021.08.20
728x90

다양한 함수형 인터페이스

java.util.function 패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 두었다. 일반적으로 많이쓰이는 이패키지의 인터페이스를 활용하면 재사용성이나 유지보수 측면에서도 좋을것이다.

 

목록

Supplier<T> : 메서드 T get() - 매개변수는 없고, 반환값만 있다. (공급자)

Consumer<T> : 메서드 void accept(T t) - Supplier와 반대로 매개변수만 있고, 반환값이 없다. (소비자)

Function<T, R> : 메서드 R apply(T t) - 일반적인 하나의 매개변수와 반환값이 있는 함수. 입력타입 T 반환타입 R (입출력 함수)

Predicate<T> : 메서드 boolean test(T t) - 조건식을 표현하는데 사용됨. 매개변수 하나, 반환타입은 boolean(조건식)

 

매개변수가 두개인 함수형 인터페이스

매개변수가 두개인 함수형 인터페이스의 이름에는 'Bi'가 붙는다.

 

BiConsumer<T, U> : 메서드 void accept(T t, U u) - 두개의 매개변수만 있고 반환값은 없는 소비자

BiPredicate<T, U> : 메서드 boolean test(T t, U u) - 조건식을 표현하고 수행하는데 사용된다. 매개변수는 두개이고 반환값은 boolean이다.

BiFunction<T,U,R> : 메서드 R apply(T t, U u) - 두개의 매개변수 t, u를 받아서 결과를 반환한다. 반환 타입은 R

 

3개이상의 매개변수를 사용하는 함수형 인터페이스를 사용하고싶다면 직접 만들어서 사용해야한다.

ex)

@FunctionalInterface

interface TriFunction<T, U, V, R>{

     R apply(T t, U u, V v);

}

 

켤렉션 프레임웍과 함수형 인터페이스

컬렉션 프레임워크의 인터페이스에 함수형인터페이스가 존재한다.

 

UnaryOperator<T> : T apply(T t) - Function의 자손, Function과 달리 매개변수와 결과의 타입이 같다.

BinaryOperator<T> : T apply(T t, T t) - BiFunction의 자손, BiFunction과 달리 매개변수와 결과의 타입이 같다.

 

메서드 목록

Collection

boolean removeIf(Predicate<E> filter) : Predicate 조건식에 일치하는 값들을 삭제

List

void replaceAll(UnaryOperator<E> operator)  : 모든 요소 UnaryOperator의 함수의 반환값으로 변환하여 대체

Iterator 

void forEach(Consumer<T> action) : 모든 요소에 작업 action을 수행

Map

V comput(K key, BiFunction<K,V> f) : 지정된 키의 값에 작업 f를 수행

V computeIfAbsent(K key, Function<K,V> f) : 키가 없으면, 작업 f를 수행 후 추가

V computeIfPresent(K key, BiFunction<K,V,V> f ) : key가 있을때, 작업 f를 수행

V merge(K key, V value, BiFunction<V,V,V> f) : 모든 요소에 병합작업 f를 수행

void forEach(BiConsumer<K, V> action) : 모든 요소에 작업 action을 수행

void replaceAll(BiFunction<K,V,V> f) : 모든요소에 치환작업 f를 수행

 

list.forEach를 사용하고 매개변수로 람다식을 주었다. list의 모든 요소를 출력한다.

2번째줄 출력은 list.removeIf()를 수행한 이후이다. x가 2의배수거나 3의배수라면 모두 삭제하는 연산이다.

3번째줄 출력은 replaceAll을 통해 모든 요소들을 10배하여 요소값을 바꾸는 것이다.

4번째줄은 forEach를 통해 맵의 모든 요소들을 출력한 것이다.

 

Function의 합성과 Predicate의 결합

Function과 Predicate에 정의된 메서드

 

Function

default <V>Function<T,V> andThen(Function<? super R, ? extends V> after)

default <V>Function<V,R> compose(Function<? super V, ? extends T> before)

static <T> Function<T,T> identity()

 

andThen의 사용예시 Function f, g가 있을 때, f.andThen(g)는 함수 f를 적용한 후 g함수를 적용한다는 의미이다.

compose는 반대로 f.compose(g)의 경우 g함수를 먼저 적용한후 f함수를 적용한다.

identity()는 항등함수로 x->x 를 나타낸다. 잘 사용하지않는다.

 

Predicate

default Predicate<T> and(Predicate<? super T> other)

default Predicate<T> or(Prerdicate<? super T> other)

default Predicate<T> negate()

static <T> Predicate<T> isEqual(Object targetRef)

 

and, or 메서드는 내부에 또다른 조건식을 필요로한다. and(조건식)의 경우 두 조건식의 and결합(&&)이고 or(조건식)의 경우 두 조건식의 or결합(||)이다.

negate()는 부정연산자(!)가 결합되는것이다.

isEqual()은 매개변수로 대상하나를 지정하고 또다른 비교대상은 test()의 매개변수로 지정하는 방식으로 사용하는데 각각의 대상이 같은지 비교한다.

 

메서드참조

람다식을 더욱 간결하게 표현하는 방법으로 람다식이 하나의 메서드만 호출하는 경우 '메서드 참조(method reference)'라는 방법으로 람다식을 간략히 할 수 있다.

 

Function<String, Integer> f = (String s) -> Integer.parseInt(s);

위같은 함수형인터페이스가 정의되었다고 가정할 때, 위식을 아래와 같이 간단하게 나타낼 수 있다.

Function<String, Integer> f = Integer::parseInt;

람다식의 일부가 생략되었다. 생략된정보들은 좌변의 함수형인터페이스 형식으로부터 얻거나 우변의 parseInt메서드호출로 알 수 있다. Function<String, Integer>는 String을 매개변수로 받고 반환값은 Integer로 반환한다는 의미이다.

해당 Function의 메서드로는 Integer클래스의 parseInt를 사용하는것을 알 수 있다.

 

3가지 경우의 메서드 참조

static메서드 참조

(x) -> ClassName.method(x) == ClassName::method

인스턴스메서드 참조

(obj, x) -> obj.method(x) == ClassName::method

특정 객체 인스턴스 메서드 참조

(x) -> obj.method(x) == obj::method

 

생성자 매서드 참조

생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있는데 위와같이 하면된다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90

'Programming > JAVA' 카테고리의 다른 글

스트림(Stream)의 중간연산  (0) 2021.08.25
스트림(Stream)  (0) 2021.08.24
람다식(Lambda expression)  (0) 2021.08.21
fork & join  (0) 2021.08.20
쓰레드의 동기화  (0) 2021.08.20
728x90

람다식 : 메서드를 하나의 식(expression)으로 표현한 것. '익명 함수(anonymous function)'이라고도 한다.

 

람다식 작성

ex)

(매개변수 선언) -> {

   ...

}

 

람다식에서는 return이나 {}, 매개변수 타입등을 생략할 수 있다.

매개변수 타입의 경우 매개변수타입이 추론 가능한경우 생략이 가능하다.

{}내에 함수의 코드 즉 문장이 하나뿐인 경우 중괄호({})도 생략이 가능하다.

 

함수형 인터페이스(Functional Interface)

람다식은 메서드와같은 역할을 하지만 익명클래스의 객체와 같은것이다.

즉 람다식은 객체이다.

 

함수형 인터페이스를 통해 Lamdba 표현식을 사용한 경우이다.

람다식의 선언부와 함수형 인터페이스의 추상메서드의 선언부가 일치한다.

그렇기 떄문에 해당 함수를 정의하여 사용할 수 있으며 MyFunction2라는 함수형 인터페이스의 max함수가 람다식의 코드로 정의되는 것이다.

함수형 인터페이스는 오직 하나의 추상메서드만 정의되어야 한다는 제약이있다. 이러한 제약 덕분에 람다식과 인터페이스의 메서드가 1대1 매칭이 가능하다.

 

함수형 인터페이스 타입의 매개변수와 반환타입

메서드의 매개변수가 함수형 인터페이스 타입이라면, 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해주어야한다.

ex)

void aMethod(MyFunction f){

  f.myMethod();

}

 

MyFunction f = () -> System.out.println("myMethod()");

aMethod(f);

 

또는 참조변수 없이 람다식을 바로 대입해줄 수 있다.

aMethod(()-> System.out.println("myMethod()"));

 

또한 메서드의 반환타입이 함수형 인터페이스 타입이라면, 이 함수형 인터페이스의 추상메서드와 동등한 람다식을 가리키느 참조변수를 반환하거나 람다식을 직접 반환할 수 있다.

ex)

MyFunction myMethod(){

   MyFunction f = () ->{};

}

 

함수형 인터페이스를 통해 람다식을 바로 받아오거나, 메서드를 전달하는 등이 가능하다.

728x90

'Programming > JAVA' 카테고리의 다른 글

스트림(Stream)  (0) 2021.08.24
다양한 함수형 인터페이스 & 메서드 참조  (0) 2021.08.22
fork & join  (0) 2021.08.20
쓰레드의 동기화  (0) 2021.08.20
쓰레드의 상태 및 제어  (0) 2021.08.16
728x90

fork & join 프레임웤 : 하나의 작업을 작은 단위로 나눠서 여러 쓰레드가 동시에 처리하는것을 쉽게 만들어 준다.

수행할 작업에 따라 RecursiveAction, RecursiveTask 두 클래스중 하나를 상속받아 구현해야 한다.

 

RecursiveAction : 반환값이 없는 작업을 구현할때

RecursiveTask : 반환값이 있는 작업을 구현할 때

두 클래스 모두 compute()라는 추상메서드를 가지고있는데 상속을 통해 compute()메소드를 구현하고 작동시키면된다.

compute()를 구현한 후 쓰레드 풀과 수행할 작업(compute()가 구현된 객체)을 생성해 준다.

이후 start가 아닌 invoke()를 통해 작업을 시작한다.

ex)

ForkJoinPool pool = new ForkJoinPool();

SumTask task = new SumTask(from, to);

 

Long result = pool.invoke(task); //invoke()를 통해 생성한 작업시작

 

fork()와 join()

fork() : 작업을 쓰레드의 작업 큐에 넣는것이다. 작업큐에 들어간 작업은 더이상 나눌 수 없을때까지 나뉜다. 비동기 메서드이다.

join() : 해당 작업의 수행이 끝날 때까지 기다렸다가 수행이 끝나면 그 결과를 반환한다. 동기메서드이다.

 

rightSum.compute를 통해 계속 반으로 나눈 계산의 왼쪽 계산을 fork한다. 그리고 size가 5이하가되면 계산을 실행한다.

쓰레드를 많이사용한것이 시간은 더오래걸린다. 작업을 나누고 합치는데 시간이 걸리기 때문이다. 

항상 멀티쓰레드가 빠른것이 아니기 때문에 항상 테스트해보고 더빠를경우에만 멀티쓰레드를 사용하는것이 좋다.

728x90

'Programming > JAVA' 카테고리의 다른 글

다양한 함수형 인터페이스 & 메서드 참조  (0) 2021.08.22
람다식(Lambda expression)  (0) 2021.08.21
쓰레드의 동기화  (0) 2021.08.20
쓰레드의 상태 및 제어  (0) 2021.08.16
데몬 쓰레드(daemon thread)  (0) 2021.08.13
728x90

쓰레드 동기화 : 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는것

 

쓰레드는 자원을 공유하기 때문에 공유데이터를 사용할 수 있다.

공유데이터에 여러 쓰레드가 동시에 접근하게 되면 프로그래머의 의도와 다르게 동작할 수 있기 때문에 동기화를 통해 공유 데이터에 대한 동시접근 문제를 해결 할 수 있다.

 

공유데이터를 사용하는 코드 영역을 임계 영역 이라한다. 공유데이터(객체)가 가지고 있는 lock을 획득한 쓰레드만 영역내의 코드를 수행하고 해당 영역 코드실행을 완료했을경우 lock을 반납할 수 있다. lock은 하나만 존재하며 임계영역에는 하나의 쓰레드만 접근이 가능하다.

 

synchronized를 사용한 동기화

synchronized 키워드를 사용하여 동기화가 가능하다.

1.메서드를 임계영역으로 지정

ex)

public synchronized void calcSum(){

...

}

 

메서드에 synchronized 키워드를 붙일 경우 메서드 전체가 임계영역으로 설정되고, 쓰레드는 synchronized메서드가 호출된 시점부터 메서드의 객체 lock을 얻어 작업을 수행한다. 이후 해당 메서드의 작업이끝나면 메서드의 객체 lock을 반환한다.

 

2.특정한 영역을 임계영역으로 지정

ex)

synchronized(객체의 참조변수){

...

}

 

메서드 내의 임의의 코드를 블럭{ }으로 감싸고 블럭 앞에 synchronized(참조변수)를 붙이는 것이다. 매개변수형태로 되어있는 참조변수에는 락을 걸고자 하는 객체를 참조하는 것이여야한다. 일반적으로 this를 사용한다.

이러한 블럭을 synchronized블럭이라 하며 이 블럭 영역 내의 코드를 실행하면서 지정한 객체의 lock을 얻고 블럭내의 코드를 모두 실행한 후 블럭을 빠져나갈때 lock을 반환한다. 

 

synchronized키워드를 이용한 쓰레드 동기화의 경우 영역만 지정해주면 자동으로 lock을 얻고 반환한다.

모든 객체는 lock을 하나씩 가지고 있으며, 해당 객체의 lock을 가진 쓰레드만 객체내의 임계영역 코드를 실행할 수 있다. 그리고 다른 쓰레드들은 lock을 얻기위해 기다리게된다. 임계영역은 멀티쓰레드 프로그램에서 프로그램의 성능을 좌우할 수 있기 때문에 임계영역을 최소화하거나 동기화를 잘 조절하여 효율적인 프로그램이 되도록 해야한다.

 

임계영역에서 사용되는 공유데이터는 private여야 한다. synchronized는 지정된 코드를 한번에 하나의 쓰레드만 접근 가능하도록 했을 뿐이기 때문에 다른방면에서의 데이터접근을 막기 위해서는 private로 설정해야한다.

 

wait()과 notify() 

특정쓰레드가 객체 lock을 가진상태에서 오랜시간이 지나지않도록, 임계영역코드를 수행하다가 작업을 더이상 수행하지 못하는 상황을 해결하기위해 wait()과 notify()가 고안되었다.

 

lock을 가지고있는 쓰레드가 wait()을 호출하면, 작업중이던 쓰레드는 lock을 반납하고 해당 객체의 대기실(waiting pool : 객체의 lock을 얻기위해 대기하는 쓰레드 대기실)로 이동하여 notify()를 기다린다. notify()가 호출되면, 해당 객체 대기실에 있던 쓰레드중 임의의 쓰레드만 통지(lock)을 받아 임계영역에 접근한다.

notifyAll()은 객체의 대기실에 존재하는 모든 쓰레드들에게 통보를 하지만 lock을 얻을 수 있는 쓰레드는 한개 뿐이다.

 

main메소드

위처럼 코드를 짜면 각 메소드에 동시접근 문제를 해결할 수 있다.

그러나 객체의 lock은 하나이기 때문에 COOK쓰레드가 add를 하거나 CUST1, CUST2가 remove를 할때 각각 쓰레드들은 대기를 해야한다. 즉 COOK이 호출하는 table.add()와 CUST의 table.remove()는 동시에 작동할 수 없다.

또한 각 작업을 마치고 notify()가 호출되어도 CUST와 COOK중 어느쓰레드가 lock얻게되어 작업을 수행하게 될지 알 수 없다. CUST쓰레드가 많고 COOK쓰레드는 하나라면 최악의 경우  COOK은 lock을 오랜시간 얻지못하여 프로그램이 '기아(starvation) 현상'에 빠질 수 있다.

이러한 경우를 막기위해 notifyAll()을 사용하여 모든 쓰레드에게 공정하게 lock을 얻을 기회를 줄 수 있는데 이또한 문제가 발생할 수 있다. 하나의 COOK쓰레드가 많은수의 CUST쓰레드와 lock을 얻기위한 '경쟁 상태(race condition)'에 빠질 수 있기 때문이다. 

두개의 쓰레드는 같은 객체의 다른메소드를 호출하기 때문에 경쟁상대가 되어서는 안된다. 하지만 객체의 lock은 하나이기 때문에 wait()과 notify()를 이용한 lock제어에서는 경쟁상대가 될 수 밖에없다.

Lock과 Condition을 이용하면 wait()과 notify()에서는 불가능한 선별적인 통지가 가능하다.

 

Lock & Condition

synchronized키워드가 아닌 'java.util.concurrent.locks' 패키지에서 제공하는 lock클래스를 이용하여 동기화를 하는 방법이 있다. lock클래스를 통해 동기화를 할경우 같은객체내에서 메소드별로 lock을 생성할 수 있다.

 

lock클래스의 종류

ReentrantLock : 재진입이 가능한 lcok, 가장 일반적인 lock. lock이 있어야만 임계영역 코드를 수행할 수 있다.

ReentrantReadWriteLock : 읽기에는 공유적이고 쓰기에는 배타적인 lock.

StampedLock : lock을 걸거나 해지할 때 '스탬프(long 타입의 정수값)'를 사용한다.

 

ReentrantLock의 생성자

ReentrantLock()

ReentrantLock(boolean fair)

생성자 매개변수를 true로 주면, lock이 풀렸을 때 가장 오래 기다린 쓰레드가 lock을 얻을 수 있도록, 공정(fair)하게 처리한다. 공정하게 처리하기 위해 쓰레드들을 확인하는 과정이 생겨나므로 성능은 좀 떨어진다.

 

void lock() : lock을 잠근다.

void unlock() : lock을 해지한다.

boolean isLocked() : lock이 잠겼는지 확인한다.

 

synchronized동기화와 달리, ReentrantLock과 같은 lock클래스들은 수동으로 lock을 잠구고 해제해야한다.

임계영역 내에서 예외가 발생하거나 return으로 빠져나가게 되면 unlock()을 수행하지 못하는 경우가 생길 수 있으므로 unlock()은 try-finally문으로 감싸는것이 일반적이다.

 

ex)

lock.lock();

try{

   ...

} finally{

    lock.unlock();

}

 

try블럭내에서 어떤 코드를 실행하더라도 마지막엔 반드시 finally블록 코드를 실행하기 때문에 finally블럭에 lock.unlock()을 넣어주는 것이다.

 

선별적인 통지를 위해서는 Condition이 필요하다. 위예제의 문제점을 해결하기 위해서는 손님쓰레드를 위한 Condition과 요리사 쓰레드를 위한 Condition을 만들어서 각각의 waiting pool에서 따로 기다리게 하는것이다.

 

Condition은 이미 생성된 lock으로 부터 newCondition()을 호출하여 생성한다.

 

private ReentrantLock lock = new ReentrantLock();

 

private Condition forCook = lock.newCondition();

private Condition forCust = lock.newCondition();

 

위코드에서 두개의 Condition을 생성하였다.

이후 wait(), notify()대신 await() & signal()을 사용하면된다.

 Table클래스의 코드를 보면 ReentrantLock클래스를 통해 lock생성과 두개의 Condition생성을 볼 수 있다.

또한 add메서드에서 음식이 꽉찼다면 forCook.await()을 통해 Cook 쓰레드를 대기시키고 forCust.signal()을 통해 CUST쓰레드를 작업을 하도록 만든다. remove()메서드 또한 음식의 개수가 0개일때 CUST쓰쓰레드를 forCust.await()을 통해 대기시키고 forCook.signal()을 통해 COOK쓰레드를 실행시킨다. 

 

위처럼 reentrantLock클래스와 Condition을 사용할경우 필요한 상황에 맞춰 작업을 수행하는 쓰레드들에 대해 선별적인 동기화를 적용해줄 수 있다. 기아현상이나 경쟁현상이 많이 줄어든 예제이다.

728x90

'Programming > JAVA' 카테고리의 다른 글

람다식(Lambda expression)  (0) 2021.08.21
fork & join  (0) 2021.08.20
쓰레드의 상태 및 제어  (0) 2021.08.16
데몬 쓰레드(daemon thread)  (0) 2021.08.13
쓰레드 우선순위와 쓰레드 그룹  (0) 2021.08.13
728x90

쓰레드 프로그래밍이 어려운 이유는 동기화와 스케줄링 때문이다.

우선순위를 통해 어느정도 스케줄링에 영향을 줄 수 있지만 부족하다.

효율적인 멀티쓰레드 프로그램은 스케줄링을 적절하게 조절하여 자원과 시간의 낭비 없이 잘 동작하게 하는 프로그램이다.

쓰레드의 스케줄링을 위한 쓰레드 상태와 관련 메서드이다.

쓰레드 상태

NEW : 쓰레드가 생성되고 start()가 호출되지 않은 상태

RUNNABLE : start()가 호출되고 쓰레드가 실행중 또는 실행 대기인 상태

BLOCKED : 동기화 블럭에 의해 쓰레드가 일시정지된 상태

WAITING, TIMED_WAITING : 쓰레드의 작업이 종료되지는 않았으나 실행대기 또는 실행중(RUNNALBE)이 아닌 일시정지 상태.

TERMINATED : 쓰레드의 작업이 종료된 상태

 

쓰레드 메서드

static void sleep(long millis), static void sleep(long millis, int nanos) : 지정된 시간(천분의 일초 단위)동안 쓰레드를 일시정지시킨다. 지정한 시간이 지나고 나면 실행대기상태가 된다.

void join(), void join(long millis), void join(long millis, int nanos) : 지정된 시간동안 쓰레드가 일시정지상태가 되도록한다. 지정한 쓰레드가 종료되면 join()을 호출한 쓰레드로 돌아와 실행을 계속한다.

void interrupt() : sleep()이나 join()에 의해 일시정지 상태인 쓰레드를 깨워서 실행대기 상태로 만든다. 해당 쓰레드에서는 InterruptedException이 발생 함으로써 일시정지 상태를 벗어나게 한다.

void stop() : 쓰레드를 즉시 종료시킨다.

void suspend() : 쓰레드를 일시정지 시킨다. resume()을 호출하면 다시 실행대기상태가 된다.

void resume() : suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기상태로 만든다.

static void yield() : 실행중에 자신의 실행시간을 다른쓰레드에게 양보(yield)하고 자신은 실행대기 상태가 된다.

 

suspend(), resum(), stop() 모두 교착상태(deadlock)을 유발할 위험이 있어 Deprecated(사용을 권장하지 않음)로 처리되어있다.

 

 

쓰레드를 생성하고 start()를 호출하면 바로 실행되는것이아니라 실행대기 상태가 되어 실행대기열에 저장된다.

실행대기열은 큐(queue)구조로 되어있으며 먼저 실행대기열에 들어온 순서대로 실행된다.

실행대기열에 있는것이 실행대기상태이다. 실행대기상태에서 자신의 차례가 되면 실행상태가된다.

OS 스케줄러에서 부여한 실행시간이 다되거나 yield()를 호출하면 실행대기상태가 되어 실행대기열에 다시 들어간다. 이후 실행대기열에 있던 다음쓰레드가 실행된다.

실행중에 suspend(), sleep(), wait(), join(), I/O block에 의해 쓰레드가 일시정지 상태가 될 수 있다. I/O block의 경우 사용자의 입력을 기다리는 경우가 있는데 사용자 입력을 마치면 다시 실행대기상태가 된다.

wait, sleep, suspend, join 일경우 일시정지시간이 끝나거나, notify(), resum(), interrupt()가 호출되어 일시정지상태를 벗어나 실행대기상태가 될 수 있다. 실행대기열에 들어가 실행을 기다린다.

쓰레드의 작업을 모두 끝내거나 stop()호출에 의해 쓰레드가 소멸된다.

 

sleep(long millis) - 일정시간동안 쓰레드를 멈추게 한다.

sleep()은 try-catch문을 필요로한다.

ex)

try{

     Thread.sleep(1, 500000);

    }catch(InterruptedException e) {}

}

 

sleep()으로 쓰레드를 0.0015초 멈춘 예제이다. sleep은 본인 쓰레드만 정지가 가능하며 sleep()에 의해 일시정지된 쓰레드는 지정된 시간이 다되거나 interrupt()가 호출되면 InterruptedException이 발생하며 깨어나 실행대기상태가 된다.

sleep()을 호출할경우 항상 try-catch문으로 예외처리를 해주어야한다.

 

interrupt(), interrupted() - 쓰레드의 작업을 취소한다.

진행중인 쓰레드의 작업이 완료되기전에 해당 쓰레드를 취소하는 경우가 발생할 수 있다.

interrupt()는 쓰레드에게 작업을 멈추라고 요청한다. 해당 메서드를 통해 쓰레드를 멈출 수 는 있지만 멈추라고 요청만 할 뿐 쓰레드를 강제 종료시키지는 못한다. interrupt()는 interrupted상태로 변경하는 메서드이다.

 

void interrupt() : 쓰레드의 interrupted상태를 false에서 true로 변경한다.

boolean isInterrupted() : 쓰레드의 현재 interrupt상태를 반환한다.

static boolean interrupted() : 현재 쓰레드의 interrupted상태를 반환하고 false로 변경한다.

 

쓰레드가 sleep(), wait(), join()등에 의해 일시정지 상태(WAITING)일 경우 interrupt()를 호출하면 InterruptedException이 발생하고 쓰레드는 실행대기 상태(RUNNABLE)상태로 변경된다. 즉. 멈춰있던 쓰레드를 실행가능한 상태로 만드는 것이다.

 

ThreadEx13_1에 의해 10부터 0까지 1씩 감소하지만 main Thread에서 입력을 완료할경우 쓰레드에 interrupt()를 발생시킨다.

진행중인 쓰레드는 Interrupted()함수 호출을 통해 계속해서 interrupt상태값을 검사하다가 interrupt()호출에 의해 상태값이 변화하면 반복을 멈추고 카운트를 종료시킨다.

이처럼 Interrupt()와 Interrupted()를 통해 진행중인 쓰레드를 중지시킬 수 있다.

interrupted() 호출 시 interrupted상태값이 false로 변경되는것도 알 수 있다.

위예제의 경우 입력을 완료하더라도 계속해서 카운트를 진행시키는데 코드는 파란색 동그라미 부분이 추가되었다.

위 코드에서 Interrupt가 발생하여 반복을 멈추지 않은 이유는 InterruptException이 발생했기 때문이다.

sleep()상태에서 Interrupt()호출시 InnterruptedException 처리가 되면 쓰레드의 interrupted상태값은 false로 자동 초기화된다. 출력이 true로 된것은 쓰레드에서 InterruptedException처리가 되기 전에 출력해서 true가 출력되는것 같다.

정상적으로 모두 반복되는것을 보면 InterruptedException시 쓰레드의 interrupted상태는 false가 되는것을 알 수 있다.

 

interrupt발생시 프로그램을 멈추고 싶다면

try{

   Thread.sleep(1000);

}catch(InterruptedException e){

   interrupt();

}

로 수정하면 InterruptedException예외가 발생하더라도 interrupt상태값을 수정할 수 있다.

 

suspend(), resume(), stop()

 

void suspend() : 쓰레드를 sleep()처럼 일시정지 시킨다.

void resume() : suspend()에 의해 일시정지된 쓰레드를 실행대기상태로 만든다.

void stop() : 쓰레드를 즉시 종료시킨다.

위 메서드들은 교착상태(deadlock)를 일으키기 쉽게 작성되어있어 사용을 권장하지않는다.(Deprecated)

 

yield() - 다른 쓰레드에게 양보한다.

yield()는 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보(yield)한다.

출력으로는 확인할 수 없지만 실행시켜보면 yield()를 통해 프로그램의 응답성이 증가하였다.

효율적인 멀티쓰레드 프로그램을 위해서는 yield()를 적절히 사용해주어야 할것이다.

 

join() - 다른 쓰레드의 작업을 기다린다.

join() : 쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 join()을 사용한다.

 

void join()

void join(long millis)

void join(long millis, int nanos)

시간을 지정해주지 않을경우 쓰레드가 작업을 모두 마칠 때까지 기다리게된다. 

 

ex)

try{

   th1.join();

}catch(InterruptedException e){}

 

join()도 sleep()처럼 interrupt()에 의해 대기상태에서 벗어날 수 있으며, join()이 호출되는 부분또한 sleep()처럼 try-catch구문으로 감싸야 한다.

join()이 sleep()과 다른점은 static 메서드가 아니기때문에 현재 쓰레드가 아닌 특정 쓰레드에대해 동작한다는 것이다.

join()을 사용하지 않았다면 main 쓰레드가 소요시간을 출력하고 바로 종료됐겠지만 join()에 의해 쓰레드가 모두 끝난후 출력하고 main 쓰레드가 종료하게 된다.

728x90

'Programming > JAVA' 카테고리의 다른 글

fork & join  (0) 2021.08.20
쓰레드의 동기화  (0) 2021.08.20
데몬 쓰레드(daemon thread)  (0) 2021.08.13
쓰레드 우선순위와 쓰레드 그룹  (0) 2021.08.13
쓰레드(Thread)  (0) 2021.08.13
728x90

데몬쓰레드 : 일반쓰레드의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다.

일반쓰레드가 모두 종료되면 데몬쓰레드는 강제로 종료된다.

그 이유는 데몬쓰레드는 일반 쓰레드의 보조역할을 수행하는데 일반 쓰레드가 모두 종료되었다면 데몬쓰레드의 존재는 의미가 없어지기 때문이다. 데몬쓰레드의 예로는 가비지 컬렉터, 워드프로세스 자동저장, 화면 자동갱신 등이 있다.

 

데몬쓰레드는 무한루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.

 

데몬쓰레드 메소드

boolean isDaemon() : 쓰레드가 데몬쓰레드인지 확인한다. 데몬쓰레드라면 true를 반환한다.

void setDaemon(boolean on) : 쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경하는 메서드이다. 매개변수 on을 true로 지정하면 데몬쓰레드가 되고 false로 지정하면 사용자용 쓰레드가된다.

 

사용자쓰레드가 종료되면 데몬쓰레드도 자동으로 종료되는것을 알수있다.

또한 데몬쓰레드는 무한 반복문과 특정 조건문을 통해서 작성하는것을 알 수 있다.

getAllStackTraces()를 이용하면 실행 중 또는 대기상태의(작업이 완료되지않은) 모든 쓰레드의 호출스택을 출력할 수 있다.

보면 생성한 쓰레드 thread1, thread2는 main 그룹에 속해있고 가비지컬렉션, 이벤트처리 등과 같이 보조작업을 수행하는 데몬쓰레드들은 system그룹에 속하는것을 알 수 있다.

자바 프로그램 실행시 JVM에서 많은 데몬 쓰레드를 생성한다.

728x90

'Programming > JAVA' 카테고리의 다른 글

쓰레드의 동기화  (0) 2021.08.20
쓰레드의 상태 및 제어  (0) 2021.08.16
쓰레드 우선순위와 쓰레드 그룹  (0) 2021.08.13
쓰레드(Thread)  (0) 2021.08.13
에너테이션(annotation)  (0) 2021.08.11
728x90

쓰레드의 우선순위

쓰레드는 내부적으로 우선순위(priority)에 해당하는 속성값을 가지고 있다.

이 우선순위 값에 따라 쓰레드가 얻는 실행시간이 달라진다. 즉 우선순위는 중요도 이며 우선순위 값이 높을 수록 해당 쓰레드가 작업시간을 많이 갖게된다.

예를 들어 카카오톡 같은 메신저 프로그램에서 파일전송과 메시지전송중 메시지전송이 우선순위가 더높다.

파일전송보다 채팅의 메시지 전송의 우선순위가 더 높아야 원활한 의사소통이 가능하기 때문이다.

 

쓰레드 우선순위 관련 메서드와 상수

void setPriority(int newPriority) : 쓰레드의 우선순위를 지정한 값으로 변경한다.

int getPriority() : 쓰레드의 우선순위를 반환한다.

public static final int MAX_PRIORITY = 10

public static final int MIN_PRIORITY = 1

public static final int NORM_PRIORITY = 5

 

쓰레드가 가질 수 있는 우선순위의 범위는 1~10이며 숫자가 높을수록 우선순위가 높다.

쓰레드의 우선순위 값은 쓰레드를 생성한 쓰레드로부터 상속받는다. main메서드를 실행시키는 main쓰레드의 경우 우선순위 값이 5이다. 그러므로 main메서드에서 생성하는 쓰레드의 경우 우선순위 값은 5가된다.

 

setPriority(MAX_PRIORITY)로 변경하고 각각 출력 횟수를 1000회로 하였다. 우선순위가 높은 th2가 먼저 끝나는것을 알수 있다.

 

그러나 실행시킬 때마다 다른결과가 나오는것을 확인했다. 그이유는 멀티코어환경에서 쓰레드의 우선순위에 따른 차이가 전혀없다는 것이다.

이론적으로만 "쓰레드에 높은 우선순위를 줄경우 더많은 실행시간과 실행기회를 갖게된다"는 것을 인지하고 있어야할것 같다.

우선순위에 차등을 두어 쓰레드를 실행하려면 OS의 스케줄링 정책과 JVM의 구현을 직접 확인해봐야한다고 한다...

자바의 쓰레드의 우선순위와 관련된 구현이 JVM마다 차이가 있을 수 있기 때문이다. JVM을 확인하더라도 쓰레드는 OS의 스케줄러에 종속적이기 떄문에 어느정도 예측만 가능하고 정확히 알수는 없다고한다.

 

쓰레드 그룹(thread group)

쓰레드그룹 : 관련된 쓰레드를 그룹으로 묶어 다루기 위함

 

쓰레드그룹 관련 메서드

ThreadGroup(String name) : 지정된 이름의 새로운 쓰레드 그룹을 생성

ThreadGroup(ThreadGroup parent, String name) : 지정된 쓰레드 그룹에 포함되는 새로운 쓰레드 그룹 생성

int activeCount() : 쓰레드 그룹에 포함된 활성상태(작업이 덜끝난, 쓰레드가 종료되지않은) 쓰레드의 수를 반환

int activeGroupCount() : 쓰레드 그룹에 포함된 활성상태에 있는 쓰레드 그룹의 수를 반환

void checkAccess() : 현재 실행중인 쓰레드가 쓰레드 그룹을 변경할 권한이 있는지 체크한다.

void destroy() : 쓰레드 그룹과 하위 쓰레드 그룹까지 모두 삭제한다.

int getMaxPriority() : 쓰레드 그룹의 최대 우선순위를 반환

String getName() : 쓰레드 그룹의 이름을 반환

ThreadGroup getParent() : 쓰레드 그룹의 상위 쓰레드그룹을 반환

void interrupt() : 쓰레드 그룹에 속한 모든 쓰레드를 interrupt

void list() : 쓰레드 그룹에 속한 쓰레드와 하위 쓰레드그룹에 대한 정보를 출력

void setMaxPriority(int pri) : 쓰레드 그룹의 최대 우선순위를 설정

 

쓰레드를 쓰레드 그룹에 포함시키려면 Thread의 생성자를 사용해야한다.

Thread(ThreadGroup group, String name)

Thread(ThreadGroup group, Runnable target)

Thread(ThreadGroup group, Runnable target, String name)

Thread(ThreadGroup group, Runnable target, String name, long stackSize)

 

모든 쓰레드는 쓰레드그룹에 포함되어있다.

쓰레드 그룹을 지정해주지않는경우 생성한 쓰레드와 같은 쓰레드그룹에 속하게된다.

 

자바프로그램을 실행시키면 JVM은 main과 system이라는 쓰레드 그룹을 만들고 각각 쓰레드그룹에 쓰레드를 포함시킨다.

main쓰레드의 경우 main 쓰레드 그룹에 속한다.

가비지 컬렉션을 수행하는 Finalizer쓰레드는 system쓰레드 그룹에 속하게 된다.

코드로 생성한 모든 쓰레드는 main메서드에서 생성되기 떄문에 일반적으로 main 쓰레드 그룹에 속하게 된다.

쓰레드 그룹을 지정할 경우 main 쓰레드 그룹이아닌 지정한 쓰레드 그룹에 포함된다.

 

main.list()를 통해 main쓰레드 그룹의 정보를 출력한다.

th1의 경우 setMaxPriority를 3으로 주었기 때문에 main쓰레드에서 생성했지만 Priority가 3으로나온다.

th3은 main쓰레드의 우선순위 값을 받아와 5로 설정되어있다.

 

 

728x90

'Programming > JAVA' 카테고리의 다른 글

쓰레드의 상태 및 제어  (0) 2021.08.16
데몬 쓰레드(daemon thread)  (0) 2021.08.13
쓰레드(Thread)  (0) 2021.08.13
에너테이션(annotation)  (0) 2021.08.11
열거형(Enums)  (0) 2021.08.10

+ Recent posts