Java로 프로그래밍할 때 피해야 할 9가지 실수
Java로 프로그래밍할 때 어떤 실수를 피해야 할까요? 다음 글에서 이 질문에 대한 답을 찾아보세요.
Java의 동시성에 관한 블로그 시리즈의 첫 번째 글을 읽어보세요. 다음 글에서는 스레드와 프로세스, 스레드 풀, 실행자 등의 차이점에 대해 자세히 살펴보겠습니다!
일반적으로 기존의 프로그래밍 방식은 순차적입니다. 프로그램의 모든 작업은 한 번에 한 단계씩 진행됩니다.
하지만 사실 병렬은 두 가지 이상의 작업을 동시에 실행할 수 있는 능력으로, 전 세계가 돌아가는 방식입니다.
다음과 같은 고급 주제를 논의하려면 의 동시성 Java 또는 멀티스레딩에 대한 몇 가지 공통된 정의에 합의하여 같은 페이지에 있는지 확인해야 합니다.
기본부터 시작하겠습니다. 비순차적 세계에서 동시성 표현자는 두 가지 종류가 있습니다.
스레드입니다. 프로세스는 실행 중인 프로그램의 인스턴스입니다. 일반적으로 다른 프로세스와 격리되어 있습니다.
운영 체제는 각 프로세스에 리소스를 할당하는 역할을 담당합니다. 또한 다음과 같은 지휘자 역할을 합니다.
예약하고 제어합니다.
스레드는 일종의 프로세스라고 할 수 있지만 그보다 낮은 수준이기 때문에 라이트 스레드라고도 합니다. 하나의 스레드에서 여러 스레드를 실행할 수 있습니다.
프로세스. 여기서 프로그램은 스레드에 대한 스케줄러 및 컨트롤러 역할을 합니다. 이렇게 하면 개별 프로그램이 다음과 같은 작업을 수행하는 것처럼 보입니다.
동시에 여러 작업을 수행할 수 있습니다.
스레드와 프로세스의 근본적인 차이점은 격리 수준입니다. 프로세스에는 고유한
리소스를 사용하는 반면, 스레드는 다른 스레드와 데이터를 공유합니다. 오류가 발생하기 쉬운 접근 방식처럼 보일 수 있으며 실제로 그렇습니다. 예를 들어
이 글의 범위를 벗어나므로 여기서는 다루지 않겠습니다.
프로세스, 스레드 - 좋아요... 하지만 동시성이란 정확히 무엇일까요? 동시성이란 여러 작업을 동시에 실행할 수 있다는 뜻입니다.
시간입니다. 이러한 작업이 동시에 실행되어야 한다는 의미는 아니며, 이것이 바로 병렬 처리입니다. Javay의 동시 실행 또한
를 사용하려면 여러 개의 CPU 또는 여러 개의 코어가 필요합니다. 단일 코어 환경에서는 다음을 활용하여 이를 달성할 수 있습니다.
컨텍스트 전환.
동시성과 관련된 용어는 멀티스레딩입니다. 이는 한 번에 여러 작업을 실행할 수 있는 프로그램의 기능입니다. 모든 프로그램이 이 방식을 사용하는 것은 아니지만 멀티스레드라고 할 수 있습니다.
정의 하나만 더 내리면 거의 모든 준비가 끝났습니다. 비동기란 프로그램이 비차단 연산을 수행한다는 의미입니다.
작업을 시작한 다음 응답을 기다리는 동안 다른 작업을 계속 진행합니다. 응답을 받으면 응답에 반응할 수 있습니다.
기본적으로 각 Java 애플리케이션 는 하나의 프로세스에서 실행됩니다. 이 프로세스에는 다음과 관련된 하나의 스레드가 있습니다. main()
방법의
를 사용할 수 있습니다. 그러나 앞서 언급했듯이 하나의 애플리케이션 내에서 여러 스레드의 메커니즘을 활용할 수 있습니다.
프로그램입니다.
스레드
는 Java 클래스에서 마법이 일어납니다. 앞서 언급한 스레드의 객체 표현입니다. To
나만의 스레드를 만들면 스레드
클래스를 사용할 수 있습니다. 그러나 권장되는 접근 방식은 아닙니다. 스레드
를 태스크를 실행하는 메커니즘으로 사용해야 합니다. 작업은 코드 를 동시 모드로 실행할 수 있습니다. 이를 정의하려면 실행 가능
인터페이스.
하지만 이론은 그만하고, 실제로 코드를 적용해 보겠습니다.
두 개의 숫자 배열이 있다고 가정해 보겠습니다. 각 배열에 대해 배열에 있는 숫자의 합을 알고 싶습니다. 배열의
이러한 배열이 많고 각 배열이 상대적으로 크다고 가정해 보겠습니다. 이러한 조건에서는 동시성을 활용하고 각 배열을 별도의 작업으로 합산하고 싶습니다.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
실행 가능한 task1 = () -> {
int sum = Arrays.stream(a1).sum();
System.out.println("1. 합계는 : " + sum);
};
실행 가능한 task2 = () -> {
int sum = Arrays.stream(a2).sum();
System.out.println("2. 합계는 : " + sum);
};
실행 가능한 task3 = () -> {
int sum = Arrays.stream(a3).sum();
System.out.println("3. 합계는 : " + sum);
};
new Thread(task1).start();
new Thread(task2).start();
new Thread(task3).start();
위의 코드에서 볼 수 있듯이 실행 가능
는 함수형 인터페이스입니다. 여기에는 하나의 추상 메서드가 포함되어 있습니다. run()
논쟁의 여지가 없습니다. The 실행 가능
인터페이스는 인스턴스가 의도된 모든 클래스에서 구현되어야 합니다.
스레드에 의해 실행됩니다.
작업을 정의한 후에는 스레드를 만들어 실행할 수 있습니다. 이 작업은 다음을 통해 수행할 수 있습니다. 새로운 스레드()
생성자에서
취하다 실행 가능
를 인수로 사용합니다.
마지막 단계는 다음과 같습니다. start()
새로 생성된 스레드입니다. API에는 다음과 같은 기능도 있습니다. run()
메서드의 실행 가능
그리고스레드
. 그러나 이는 Java에서 동시성을 활용하는 방법이 아닙니다. 이러한 각 메서드를 직접 호출하면 다음과 같은 결과가 발생합니다.
동일한 스레드에서 작업을 실행하면 main()
메서드가 실행됩니다.
작업이 많을 때는 각 작업에 대해 별도의 스레드를 만드는 것은 좋은 생각이 아닙니다. 하나의 스레드
는
무거운 작업이며 새 스레드를 만드는 것보다 기존 스레드를 재사용하는 것이 훨씬 낫습니다.
프로그램에서 수명이 짧은 스레드를 많이 생성하는 경우 스레드 풀을 사용하는 것이 좋습니다. 스레드 풀에는
실행할 준비가 되었지만 현재 활성화되지 않은 스레드입니다. 스레드에 실행 가능
를 풀에 추가하면 스레드 중 하나가run()
주어진 메서드 실행 가능
. 작업을 완료한 후에도 스레드는 여전히 존재하며 유휴 상태에 있습니다.
알겠습니다. 수동 생성 대신 스레드 풀을 선호하시는군요. 하지만 스레드 풀을 어떻게 활용할 수 있을까요? 스레드 풀의 실행자
클래스에는 스레드 풀을 구성하기 위한 여러 정적 팩토리 메서드가 있습니다. 예를 들어 새로운 캐시된 스레드 풀()
생성
필요에 따라 새 스레드가 생성되고 유휴 스레드가 60초 동안 유지되는 풀입니다. 반대로새로운 고정 스레드 풀()
에는 유휴 스레드가 무기한 유지되는 고정 스레드 집합이 포함되어 있습니다.
이 예제에서 어떻게 작동하는지 살펴봅시다. 이제 스레드를 수동으로 만들 필요가 없습니다. 대신실행자 서비스
를 생성하여 스레드 풀을 제공합니다. 그런 다음 작업을 할당할 수 있습니다. 마지막 단계는 스레드를 닫는 것입니다.
풀을 사용하여 메모리 누수를 방지합니다. 이전 코드의 나머지 부분은 동일하게 유지됩니다.
실행자 서비스 실행자 = Executors.newCachedThreadPool();
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.shutdown();
실행 가능
는 동시 작업을 생성하는 멋진 방법처럼 보이지만 한 가지 큰 단점이 있습니다. 이 방법은
값으로 설정할 수 없습니다. 또한 작업이 완료되었는지 여부도 확인할 수 없습니다. 또한 작업이 완료되었는지 여부도 알 수 없습니다.
정상적으로 또는 예외적으로. 이러한 문제에 대한 해결책은 다음과 같습니다. 호출 가능
.
호출 가능
은 실행 가능
와 비슷한 방식으로 비동기 작업도 래핑합니다. 주요 차이점은 다음을 수행할 수 있다는 것입니다.
값을 반환합니다. 반환 값은 (프리미티브가 아닌) 모든 유형이 될 수 있습니다. 호출 가능
인터페이스는 매개변수화된 유형입니다.호출 가능
는 다음과 같은 기능을 갖춘 인터페이스입니다. call()
메서드를 던질 수 있는 예외
.
이제 어떻게 활용할 수 있는지 살펴보겠습니다. 호출 가능
를 배열 문제에 적용합니다.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Callable task1 = () -> Arrays.stream(a1).sum();
Callable task2 = () -> Arrays.stream(a2).sum();
Callable task3 = () -> Arrays.stream(a3).sum();
ExecutorService executor = Executors.newCachedThreadPool();
Future future1 = executor.submit(task1);
Future future2 = executor.submit(task2);
Future future3 = executor.submit(task3);
System.out.println("1. 합계는: " + future1.get());
System.out.println("2. 합계는: " + future2.get());
System.out.println("3. 합계는: " + future3.get());
executor.shutdown();
자, 이제 어떻게 호출 가능
를 생성한 다음 실행자 서비스
. 하지만 도대체 미래
?미래
는 스레드 사이의 다리 역할을 합니다. 각 배열의 합계는 별도의 스레드에서 생성되므로 다음과 같은 방법이 필요합니다.
그 결과를 다시 main()
.
다음에서 결과를 검색하려면 미래
전화해야 합니다. get()
메서드를 호출합니다. 여기서 두 가지 중 하나가 발생할 수 있습니다. 먼저
에 의해 수행된 계산 결과 호출 가능
를 사용할 수 있습니다. 그런 다음 즉시 가져옵니다. 둘째, 결과는
아직 준비되지 않았습니다. 이 경우 get()
메서드는 결과를 사용할 수 있을 때까지 차단됩니다.
의 문제 미래
는 '푸시 패러다임'에서 작동한다는 것입니다. 사용 시 미래
다음과 같은 상사가 되어야 합니다.
는 결과를 제공할 때까지 끊임없이 '작업이 완료되었나요? 준비되었는가?'라고 끊임없이 질문합니다. 지속적인 압박감 속에서 행동하는 것은
비싸다. 더 좋은 방법은 다음과 같이 주문하는 것입니다. 미래
작업이 준비되면 어떻게 해야 하는지 알려드립니다. 안타깝게도미래
는 그렇게 할 수 없지만 계산 가능한 미래
할 수 있습니다.
계산 가능한 미래
는 '풀 패러다임'으로 작동합니다. 작업을 완료하면 결과를 어떻게 처리할지 알려줄 수 있습니다. It
는 비동기식 접근 방식의 예입니다.
계산 가능한 미래
와 완벽하게 작동합니다. 실행 가능
하지만 호출 가능
. 대신 다음 주소로 작업을 제공할 수 있습니다.계산 가능한 미래
의 형태로 공급업체
.
위의 내용이 우리의 문제와 어떤 관련이 있는지 살펴봅시다.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
CompletableFuture.supplyAsync(() -> Arrays.stream(a1).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a2).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a3).sum())
.thenAccept(System.out::println);
가장 먼저 눈에 띄는 것은 이 솔루션이 얼마나 더 짧은지입니다. 그 외에도 깔끔하고 깔끔해 보입니다.
작업 대상 완성 가능한 미래
에서 제공할 수 있습니다. 공급 비동기화()
메서드를 사용하는 공급업체
또는 runAsync()
그
취하다 실행 가능
. 작업 완료 시 실행되어야 하는 코드 조각인 콜백은 다음과 같이 정의됩니다. thenAccept()
메서드를 사용합니다.
Java 는 동시성에 대한 다양한 접근 방식을 제공합니다. 이 글에서는 이 주제에 대해서는 거의 다루지 않았습니다.
그럼에도 불구하고 다음과 같은 기본 사항을 다루었습니다. 스레드
, 실행 가능
, 호출 가능
및 콜러블 퓨처
좋은 지적을 제기합니다.
를 클릭하면 더 자세한 주제 조사를 할 수 있습니다.