자바(JAVA)

자바 - Thread / Lambda / Stream

BlueNoa 2023. 11. 5. 20:20
728x90
반응형

<목차>

 


• Tread란?

컴퓨터에 대해 잘 아는 사람이라면 스레드에 대해 들어봤을 것이다. 보통 CPU에서 작업을 할 때 '프로세스'라고 하는 데 이를 나눠서 병렬 처리 하는 것을 스레드라고 한다. 자바를 비롯한 다른 언어에서 사용하는 스레드도 이와 마찬가지로 두 가지 이상의 일을 동시에 처리하는 것을 'Tread'라고 한다.

 

자바에서 스레드를 생성하고 관리하는 주요 클래스는 'java.lang.Thread' 클래스다. 일반적으로 메인 스레드에서 시작하며, 이후에 필요한 경우 추가 스레드를 생성하여 병렬 작업을 수행한다.

 

스레드를 생성하려면 'Thread' 클래스의 하위 클래스를 만들거나 'Runnable' 인터페이스를 구현하는 클래스를 사용한다.

스레드 객체를 생성한 후에는 'start()' 메서드를 호출하여 실행한다.

 

※ 참고로 파이썬에서의 스레드도 해당 개념 및 원리가 동일하다.

 

<예제 - 간단한 스레드 예시>

더보기
더보기
import java.lang.Thread;

public class sample extends Thread {
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println("스레드 : " + i);
        }
    }
    public static void main (String[] args) {
        sample sm = new sample();
        sm.start();
    }
}

<결과>

스레드 : 1
스레드 : 2
스레드 : 3
스레드 : 4
스레드 : 5
스레드 : 6
스레드 : 7
스레드 : 8
스레드 : 9
스레드 : 10

Process finished with exit code 0
반응형

• Thread의 작동 원리

<예시1 - 작동 원리>

더보기
더보기

해당 예시는 교재 '점프 투 자바'의 예시를 사용하였습니다.

import java.lang.Thread;

public class sample extends Thread {
    int count;

    public sample(int count) {
        this.count = count;
    }

    public void run() {
        System.out.println("작업 시작 : " + this.count);
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.getStackTrace();
        } System.out.println("작업 종료 : "+ this.count);
    }
    public static void main (String[] args) {
        for (int i = 1; i<=10; i++) {
            sample sm = new sample(i);
            sm.start();
        }
        System.out.println("main end");
    }
}

<결과>

시작과 종료가 순차적이지 않고 제각각이다. 즉 순서와는 관계없이 작업이 진행된다는 것을 알 수 있다. 

이는 파이썬(Python)에서 스레드를 구현해도 동일하게 작동한다.

작업 시작 : 3
작업 시작 : 6
작업 시작 : 2
작업 시작 : 8
작업 시작 : 4
작업 시작 : 1
작업 시작 : 5
작업 시작 : 10
작업 시작 : 9
main end
작업 시작 : 7
작업 종료 : 9
작업 종료 : 1
작업 종료 : 10
작업 종료 : 4
작업 종료 : 8
작업 종료 : 5
작업 종료 : 7
작업 종료 : 3
작업 종료 : 6
작업 종료 : 2

Process finished with exit code 0

 

<예시2 - 작동 원리(배열과 join)>

더보기
더보기
import java.lang.Thread;
// 배열 생성
class Create {
    public int[] array = new int[10];
    public void Array() {
        for (int i = 0; i < 10; i++) {
            array[i] = i+1;
        }
    }
}

// 스레드 작업
public class sample extends Thread {
    public static void main(String[] args) {
        Create cr = new Create();
        cr.Array();
        int mid = cr.array.length / 2;
        int[] sum = new int[2];
        // 1~10까지의 값을 반으로 나눈 후 각자 더하여 합산을 구한는 스레드
        
        // 1~5까지 합산
        Thread th1 = new Thread() {
            public void run() { // 스레드는 'run' 메서드 안에서 코드를 작성해야 한다.
                for (int j = 0; j < mid; j++) {
                    sum[0] += cr.array[j];
                }
                System.out.println("1번 스레드 완료");
            }
        };
        // 6~10까지 합산
        Thread th2 = new Thread() {
            public void run() {
                for (int k = mid; k < cr.array.length; k++) {
                    sum[1] += cr.array[k];
                }
                System.out.println("2번 스레드 완료");
            }
        };
        // 스레드 실행
        th1.start();
        th2.start();

        try {
            th1.join(); // join은 해당 스레드가 끝나야지만 다음 스레드가 작동하게끔 해줌
            th2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        int total = sum[0] + sum[1];
        System.out.println("총 합 : " + total);
    }
}

join은 작동 중인 스레드가 종료되어야만 다음 스레드가 실행되게끔 해준다. 일종의 작업 '순서'를 부여해 준다고 보면 된다.

<결과>

1번 스레드 완료
2번 스레드 완료
총 합 : 55

 

<예시3 - Runnable>

더보기
더보기

Thread 클래스를 상속하여 사용하기도 하지만 Runnable 인터페이스를 사용하여 구현하기도 한다.

왜냐하면 Thread 클래스를 상속하면 다른 클래스를 상속할 수가 없기 때문이다.

즉, 해당 방법은 클래스가 다른 클래스를 상속해야 하는 경우 유용하다.

 

아래 코드는 예시2를 변형하여 Runnable을 사용하였다.

import java.lang.Thread;

// 배열 생성
class Create {
    public int[] array = new int[10];
    public void Array() {
        for (int i = 0; i < 10; i++) {
            array[i] = i+1;
        }
    }
}

// 스레드 작업
public class sample implements Runnable {
    private Create cr;
    private int mid;
    private int[] sum;

    public sample(Create cr, int mid, int[] sum) {
        this.cr = cr;
        this.mid = mid;
        this.sum = sum;
    }

    @Override
    public void run() {
        if (mid == 0) {
            System.out.println("mid(중간) 값이 제대로 설정되지 않았습니다.");
            return; // 중단
        }

        // 1~5 까지 합산
        if (Thread.currentThread().getName().equals("Thread-0")) {
            for (int j = 0; j < mid; j++) {
                sum[0] += cr.array[j];
            }
            System.out.println("1번 스레드 완료");
        }
        // 6~10 합산
        else if (Thread.currentThread().getName().equals("Thread-1")) {
            for (int k = mid; k < cr.array.length; k++) {
                sum[1] += cr.array[k];
            }
            System.out.println("2번 스레드 완료");
        }
    }

        public static void main(String[] args) {
            Create cr = new Create();
            cr.Array();
            // 확인용 System.out.println(Arrays.toString(cr.array));
            int mid = cr.array.length / 2;
            int[] sum = new int[2];

            Thread thread1 = new Thread(new sample(cr, mid, sum));
            Thread thread2 = new Thread(new sample(cr, mid, sum));

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

            try {
                thread1.join();
                thread2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int total = sum[0] + sum[1];
            System.out.println("총 합 : " + total);
        }
    }

 

<결과>

1번 스레드 완료
2번 스레드 완료
총 합 : 55

 

728x90

• Lambda

람다(Lambda) 표현식은 함수형 프로그래밍 기법을 지원하는 기능 중 하나다.

람다 표현식은 간결하고 읽기 쉬운 방식으로 익명 함수(무명 함수)를 표현할 수 있게 해 준다. 

즉, 함수를 변수처럼 다룰 수 있게 해 주어 코드를 간결하게 만들어 준다.

※ 람다 표현식은 Python에서도 지원한다.

 

람다 표현식은 함수형 인터페이스와 함께 사용되는 것이 일반적이다. 함수형 인터페이스는 하나의 추상 메서드만 가지는 인터페이스를 말하며, 스트림(Stream) API와 병렬 프로그래밍 및 함수형 프로그래밍의 기본 요소로 활용된다.

 

 

일반적인 람다 표현식 구문

(parameter) -> expression

parameter : 람다 함수의 매개변수, 필요한 경우 생략이 가능하다.

-> : 람다 연산자, 매개변수와 함수 본문을 구분을 구분한다.

expression : 람다 함수의 본문으로, 작동할 코드를 나타낸다.

 

<예제1 - 일반 코드 vs 람다 사용>

더보기
더보기

- 일반 코드

import java.util.Arrays;
import java.util.List;
import java.util.Collections;
import java.util.Comparator;

public class sample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("김철수", "조준", "다람이", "판다", "나람단");
        // 정렬하기 위해 Collections, Comparator 사용
        Collections.sort(names, new Comparator<String>() {
            @Override
            public int compare(String s1, String s2) { // 두 문자열을 비교
                return s1.compareTo(s2);
            }
        });
        for (String name : names) {
            System.out.println(name);
        }
    }
}

 

- 람다 사용

import java.util.Arrays;
import java.util.List;
import java.util.Collections;
import java.util.Comparator;

public class sample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("김철수", "조준", "다람이", "판다", "나람단");
        // compare 객체를 생성한 것과 같은 역할을 한다.
        Collections.sort(names, (s1, s2) -> s1.compareTo(s2));

        for (String name : names) {
            System.out.println(name);
        }
    }
}

두 코드의 실행 결과는 동일하다. 하지만 람다를 사용함으로써 코드를 간소화시킬 수 있었다. 

 

<결과>

김철수
나람단
다람이
조준
판다

 

하지만 인터페이스(interface)를 사용하는 경우 메서드가 1개 이상이라면 람다 함수를 사용할 수 없다.

그렇기 때문에 람다 함수를 사용할 인터페이스는 '@FunctionalInterface'라는 어노테이션을 사용한다.

위 어노테이션을 사용하면 2개 이상의 메서드를 가진 인터페이스를 작성하는 것은 불가능하다.

 

<예시2 - @FunctionalInterface>

더보기
더보기
@FunctionalInterface
interface Calculator {
    // 메소드를 하나만 작성 가능하게 해준다.
    int operation(int a, int b);
}

public class sample {
    public static void main(String[] args) {
        Calculator add = (a,b) -> a+b;
        Calculator sub = (a,b) -> a-b;

        System.out.println("합 : "  + add.operation(4, 5));
        System.out.println("차 : "  + sub.operation(10, 2));
    }
}

<결과>

합 : 8
차 : 8

 

<예제3 - 축약함수 : 리스트 반복문>

더보기
더보기

람다를 사용하면 반복문도 간편하게 작성할 수 있다.

- 일반 코드

import java.util.Arrays;
import java.util.List;



public class sample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("김두한", "송하나", "한지우");
        for (String name : names) {
            System.out.println("안녕하세요. " + name + "님");
        }
    }
}

 

- 축약 함수 사용

import java.util.Arrays;
import java.util.List;



public class sample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("김두한", "송하나", "한지우");
        names.forEach (name -> System.out.println("안녕하세요. " + name + "님"));
    }
}

 

<결과>

안녕하세요. 김두한님
안녕하세요. 송하나님
안녕하세요. 한지우님

• Stream

스트림이란 '흐름'이라는 뜻을 가지고 있으며 데이터 요소의 시퀀스(순차적으로 나열되어 있는 것을 의미)를 처리하기 위한 시퀀셜 한 요소의 모음을 나타내는 개념이다.

스트림은 람다와 같이 Java8부터 소개되었다. 컬렉션, 배열, I/O 등 다양한 데이터 소스를 처리하는 데 사용된다.

또한 데이터를 다루는 함수형 스타일의 연산을 지원하여 코드를 간결하고 가독성 있게 만들어준다.

 

스트림의 주요 특징 및 개념

1. 시퀀셜 한 데이터 처리

데이터 요소를 연속적으로 처리하고 연속적으로 연산을 수행할 수 있음을 의미한다.

 

2. 데이터 소스

스트림은 다양한 데이터 소스로부터 생성될 수 있다. 예를 들어 컬렉션과 배열, 파일과 함수 등이 스트림 데이터 소스가 될 수 있다.

 

3. 중간 연산과 최종 연산

스트림의 연산은 중간 연산과 최종 연산으로 구분된다. 중간 연산은 스트림을 다른 스트림으로 변환하거나 필터링하며, 최종 연산은 스트림의 처리를 시작하고 결과를 반환하는 연산이다.

 

4. 지연 연산

스트림은 지연 연산을 지원한다. 최종 연산을 호출하기 전까지 중간 연산이 실제로 수행되지 않았을 경우 필요한 연산만을 수행하게 되어 효율적인 처리를 가능하게 한다.

 

5. 함수형 프로래밍

스트림은 함수형 프로그래밍 스타일을 지원하며, 람다 표현식과 함께 사용된다.

스트림을 사용하면 데이터 처리 코드를 간결하게 작상할 수 있으며, 병렬 처리를 간단하게 수행할 수 있어 멀티코어 프로세서에서의 성능을 향상할 수 있다. 데이터 필터링, 매핑, 정렬, 그룹화 및 집계를 할 수 있다.

 

<예시1 - 문자열 필터링 매핑 후 반환>

더보기
더보기
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;


public class sample {
    public static void main(String[] args) {
        List<String> animals = Arrays.asList("dog", "cat", "pig", "horse", "rabbit");
        // 'r'로 시작하는 동물을 필터링하고, 대문자로 변환한다.
        List<String> filteredAnimal = animals.stream()
                .filter(animal -> animal.startsWith("r")) // 필터 메서드 사용
                .map(animal -> animal.toUpperCase()) // 일치하는 문자열을 변환
                .collect(Collectors.toList()); // collect 메서드로 최종 결과를 리스트로 수집

        System.out.println(filteredAnimal);
    }
}

 

<결과>

[RABBIT]

 

<예시2 - 짝수 필터링>

더보기
더보기
import java.util.*;
import java.util.stream.Collectors;

class Create {
    public List<Integer> Create() {
        List<Integer> numbers = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            numbers.add(i);
        }
        // System.out.println(numbers);
        return numbers;
    }
}


public class sample {
    public static void main(String[] args) {
        Create cr = new Create();
        List<Integer> num = cr.Create();

        List<Integer> filteredNumbers = num.stream()
                .filter(n -> n % 2 == 0) // 중간 연산 : 짝수
                .sorted() // 정렬 : 오름차순
                .collect(Collectors.toList()); // 최종 연산 : 리스트화

        System.out.println(filteredNumbers);
    }
}

 

<결과>

[2, 4, 6, 8, 10]

 

<예시3 - 중복 값을 제거하여 출력 비교하기>

더보기
더보기

- 일반 코드

import java.util.*;


class Create {
    public List<Integer> Create() {
        int numberCount = 10; // 원하는 랜덤 값의 개수
        int minValue = 2; // 랜덤 값의 최소값
        int maxValue = 8; // 랜덤 값의 최대값

        Random random = new Random();
        List<Integer> randomList = new ArrayList<>();

        for (int i = 0; i < numberCount; i++) {
            // 0 ~ n-1까지 값 생성, 예를 들어 최대 8, 최소 2라고 하면 8-2+1 = 7
            // 0 ~ 6까지 생성하므로 최소값인 2를 더해줘서 2~8 범위가 된다.
            int randomValue = random.nextInt((maxValue - minValue) + 1) + minValue;
            randomList.add(randomValue);
        }
        System.out.println("랜덤 리스트 : " + randomList);
        return randomList;
    }
}


public class sample {
    public static void main(String[] args) {
        Create cr = new Create();
        List<Integer> randomSet = cr.Create();
        // 중복을 제거()
        HashSet<Integer> numSet = new HashSet<>(randomSet);
        // Set -> List 변경
        ArrayList<Integer> distinctList = new ArrayList<>(numSet);

        // 역순 정렬
        distinctList.sort(Comparator.reverseOrder());
        
        // 정수 배열 변환
        int[] result = new int[distinctList.size()]; // 크기 설정
        for (int i = 0; i < distinctList.size(); i++) {
            result[i] = distinctList.get(i);
        }
        System.out.println("결과 : " + Arrays.toString(result));
    }
}

 

- 스트림을 이용한 축약 함수

import java.util.stream.Collectors;
import java.util.*;


class Create {
    // 생성 개수, 최소값, 최대값을 받아온다.
    public List<Integer> Create(int numberCount, int minValue, int maxValue) {
        Random random = new Random();
        // 생성값, 최소값, 최대값(n-1) 범위에 +1
        List<Integer> randomList = random.ints(numberCount, minValue, maxValue + 1)
                .boxed() // int, double, float -> Integer 형태로 박싱
                .collect(Collectors.toList());
        System.out.println("랜덤 리스트 : " + randomList);
        return randomList;
    }
}


public class sample {
    public static void main(String[] args) {
        Create cr = new Create();
        List<Integer> randomSet = cr.Create(10, 2, 8);

        int[] result = randomSet.stream()
                .distinct() // 중복제거
                .sorted(Comparator.reverseOrder()) // 정렬(오름차순)
                .mapToInt(Integer::intValue) // 스트림 요소를 정수 변환, Stream<Integer> -> IntStream
                .toArray(); // 배열로 반환

        System.out.println("결과 : " + Arrays.toString(result));
    }
}

 

 단, 결과는 매번 다를 수 있다. 매 순간 랜덤으로 생성되는 값이 고정되어 있지 않기 때문이다.

<결과>

랜덤 리스트 : [7, 3, 5, 7, 5, 3, 2, 2, 6, 3]
결과 : [7, 6, 5, 3, 2]

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90
반응형