Java 101 : 고통없는 Java 동시성, Part 1

동시 애플리케이션의 복잡성이 증가함에 따라 많은 개발자는 Java의 저수준 스레딩 기능이 프로그래밍 요구 사항에 충분하지 않다는 사실을 알게되었습니다. 이 경우 Java 동시성 유틸리티를 검색 할 때가 될 수 있습니다. java.util.concurrentExecutor 프레임 워크, 동기화 기 유형 및 Java Concurrent Collections 패키지에 대한 Jeff Friesen의 자세한 소개를 통해을 시작하십시오 .

Java 101 : 차세대

이 새로운 JavaWorld 시리즈의 첫 번째 기사에서는 Java Date and Time API를 소개합니다 .

Java 플랫폼은 개발자가 서로 다른 스레드가 동시에 실행되는 동시 애플리케이션을 작성할 수있는 저수준 스레딩 기능을 제공합니다. 그러나 표준 Java 스레딩에는 몇 가지 단점이 있습니다.

  • 자바의 낮은 수준의 동시성 프리미티브 ( synchronized, volatile, wait(), notify(),과 notifyAll()) 쉽게 제대로 사용되지 않습니다. 프리미티브를 잘못 사용하여 발생하는 교착 상태, 스레드 고갈 및 경쟁 조건과 같은 스레딩 위험도 감지하고 디버깅하기가 어렵습니다.
  • synchronized스레드 간의 액세스 조정 에 의존하면 많은 최신 애플리케이션의 요구 사항 인 애플리케이션 확장성에 영향을 미치는 성능 문제가 발생합니다.
  • Java의 기본 스레딩 기능이 너무 낮습니다. 개발자는 종종 자바의 저수준 스레딩 기능이 제공하지 않는 세마포어 및 스레드 풀과 같은 상위 수준 구조가 필요합니다. 결과적으로 개발자는 시간이 많이 걸리고 오류가 발생하기 쉬운 자체 구조를 구축합니다.

JSR 166 : Concurrency Utilities 프레임 워크는 높은 수준의 스레딩 기능에 대한 요구를 충족하도록 설계되었습니다. 2002 년 초에 시작된이 프레임 워크는 공식화되어 2 년 후 Java 5에서 구현되었습니다. Java 6, Java 7 및 곧 출시 될 Java 8에서 개선 사항이 이어졌습니다.

2 부로 구성된이 Java 101 : 차세대 시리즈는 Java 동시성 유틸리티 패키지 및 프레임 워크에 대한 기본 Java 스레딩에 익숙한 소프트웨어 개발자를 소개합니다. Part 1에서는 Java Concurrency Utilities 프레임 워크에 대한 개요를 소개하고 Executor 프레임 워크, 동기화 유틸리티 및 Java Concurrent Collections 패키지를 소개합니다.

자바 스레드 이해

이 시리즈를 시작하기 전에 스레딩의 기본 사항을 잘 알고 있어야합니다. Java의 저수준 스레딩 기능에 대한 Java 101 소개부터 시작 합니다.

  • 1 부 : 스레드 및 실행 가능 항목 소개
  • 2 부 : 스레드 동기화
  • 파트 3 : 스레드 스케줄링, 대기 / 알림 및 스레드 인터럽트
  • Part 4 : 스레드 그룹, 변동성, 스레드 로컬 변수, 타이머 및 스레드 종료

Java 동시성 유틸리티 내부

Java 동시성 유틸리티 프레임 워크는 동시 클래스 또는 애플리케이션을 작성하기위한 빌딩 블록으로 사용되도록 설계된 유형 의 라이브러리입니다 . 이러한 유형은 스레드로부터 안전하고 철저한 테스트를 거쳤으며 고성능을 제공합니다.

Java 동시성 유틸리티의 유형은 작은 프레임 워크로 구성됩니다. 즉, Executor 프레임 워크, 동기화 기, 동시 컬렉션, 잠금, 원자 변수 및 Fork / Join입니다. 기본 패키지와 한 쌍의 하위 패키지로 추가 구성됩니다.

  • java.util.concurrent 는 동시 프로그래밍에서 일반적으로 사용되는 고급 유틸리티 유형을 포함합니다. 예를 들면 세마포어, 장벽, 스레드 풀 및 동시 해시 맵이 있습니다.
    • java.util.concurrent.atomic의 서브 패키지는 하나의 변수에 대한 지원 락 프리로 thread 세이프 인 프로그래밍하는 것이 낮은 수준의 유틸리티 클래스가 포함되어 있습니다.
    • java.util.concurrent.locks의의 서브 패키지는 잠금 및 자바의 낮은 수준의 동기화 및 모니터를 사용하는 다른 조건, 대기에 대한 낮은 수준의 유틸리티 유형이 포함되어 있습니다.

Java Concurrency Utilities 프레임 워크는 또한 최신 프로세서에서 일반적으로 지원되는 변형 인 저수준 CAS (비교 및 교체) 하드웨어 명령을 제공합니다. CAS는 Java의 모니터 기반 동기화 메커니즘보다 훨씬 가볍고 확장 성이 뛰어난 일부 동시 클래스를 구현하는 데 사용됩니다. java.util.concurrent.locks.ReentrantLock예를 들어 CAS 기반 클래스는 동등한 모니터 기반 synchronized기본 요소 보다 성능이 뛰어납니다 . ReentrantLock잠금에 대한 더 많은 제어를 제공합니다. (2 부에서는 CAS가에서 작동하는 방식에 대해 자세히 설명합니다 java.util.concurrent.)

System.nanoTime ()

Java 동시성 유틸리티 프레임 워크에는 클래스 long nanoTime()의 멤버 인이 포함 java.lang.System됩니다. 이 방법을 사용하면 상대적 시간 측정을 위해 나노초 단위 시간 소스에 액세스 할 수 있습니다.

다음 섹션에서는 Java 동시성 유틸리티의 세 가지 유용한 기능을 소개하고, 먼저 현대 동시성에 왜 중요한지 설명하고 동시 Java 애플리케이션의 속도, 안정성, 효율성 및 확장 성을 높이기 위해 작동하는 방법을 보여줍니다.

Executor 프레임 워크

스레딩에서 작업작업 단위입니다. Java에서 저수준 스레딩의 한 가지 문제점은 Listing 1에서 보여 주듯이 태스크 제출이 태스크 실행 정책과 밀접하게 결합되어 있다는 것입니다.

목록 1. Server.java (버전 1)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; class Server { public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; new Thread(r).start(); } } static void doWork(Socket s) { } }

위의 코드는 간단한 서버 애플리케이션을 설명합니다 ( doWork(Socket)간결성을 위해 비어 있음). 서버 스레드 socket.accept()는 들어오는 요청을 기다리도록 반복적으로 호출 한 다음이 요청이 도착하면 서비스를 제공하기 위해 스레드를 시작합니다.

이 애플리케이션은 각 요청에 대해 새로운 스레드를 생성하기 때문에 엄청난 수의 요청에 직면했을 때 제대로 확장되지 않습니다. 예를 들어, 생성 된 각 스레드에는 메모리가 필요하고 너무 많은 스레드가 사용 가능한 메모리를 모두 사용하여 응용 프로그램을 강제 종료 할 수 있습니다.

태스크 실행 정책을 변경하여이 문제를 해결할 수 있습니다. 항상 새 스레드를 만드는 대신 고정 된 수의 스레드가 들어오는 작업을 처리하는 스레드 풀을 사용할 수 있습니다. 그러나 이렇게 변경하려면 응용 프로그램을 다시 작성해야합니다.

java.util.concurrent작업 실행 정책에서 작업 제출을 분리하는 작은 유형의 프레임 워크 인 Executor 프레임 워크가 포함됩니다. Executor 프레임 워크를 사용하면 코드를 크게 다시 작성하지 않고도 프로그램의 작업 실행 정책을 쉽게 조정할 수 있습니다.

Executor 프레임 워크 내부

Executor 프레임 워크는 작업 을 실행할 수있는 개체로 실행기Executor 를 설명하는 인터페이스를 기반으로 합니다. 이 인터페이스는 작업 을 실행하기 위해 다음과 같은 단독 메서드를 선언합니다 .java.lang.RunnableRunnable

void execute(Runnable command)

Runnable작업을에 전달하여 제출합니다 execute(Runnable). 실행 프로그램이 어떤 이유로 든 작업을 실행할 수없는 경우 (예 : 실행 프로그램이 종료 된 경우)이 메서드는 RejectedExecutionException.

핵심 개념은 작업 제출이Executor 구현에 의해 설명되는 작업 실행 정책에서 분리 된다는 입니다. 따라서 실행 가능한 작업은 새 스레드, 풀링 된 스레드, 호출 스레드 등을 통해 실행할 수 있습니다.

참고 Executor매우 제한됩니다. 예를 들어 실행기를 종료하거나 비동기 작업이 완료되었는지 여부를 확인할 수 없습니다. 또한 실행중인 작업을 취소 할 수 없습니다. 이러한 이유와 다른 이유 때문에 Executor 프레임 워크는 Executor.

의 다섯 가지 ExecutorService방법은 특히 주목할 만합니다.

  • boolean awaitTermination (long timeout, TimeUnit unit) 은 종료 요청 후 모든 작업이 실행을 완료하거나 시간 초과가 발생하거나 현재 스레드가 중단 될 때까지 호출 스레드를 차단합니다. 최대 대기 시간은에 의해 지정 timeout되며이 값은 열거 형에 unit지정된 단위 로 표시됩니다 TimeUnit. 예 : TimeUnit.SECONDS. 이 메서드는 java.lang.InterruptedException현재 스레드가 중단 될 때 발생합니다. executor가 종료되면 true를 반환 하고 종료 전에 timeout이 경과하면 false를 반환 합니다 .
  • boolean isShutdown () 은 실행기가 종료되었을 때 true를 반환 합니다 .
  • void shutdown () 은 이전에 제출 된 작업이 실행되지만 새 작업이 수락되지 않는 순서대로 종료를 시작합니다.
  • Future submit (Callable task) 은 실행을 위해 값을 반환하는 작업을 제출하고 작업 Future의 보류중인 결과를 나타내는를 반환합니다 .
  • Future submit (Runnable task)Runnable 은 실행할 작업을 제출하고 해당 작업을 Future나타내는 반환합니다 .

Future인터페이스는 비동기 계산의 결과를 나타낸다. 결과는 일반적으로 미래의 어느 순간까지 사용할 수 없기 때문에 미래 라고합니다. 메서드를 호출하여 작업을 취소하고, 작업 결과를 반환하고 (무기한 대기 또는 작업이 완료되지 않은 경우 시간 초과가 경과 할 때까지) 작업이 취소되었는지 또는 완료되었는지 확인할 수 있습니다.

Callable인터페이스는 비슷 Runnable가 실행하는 작업을 설명하기위한 하나의 방법을 제공하는 것을 인터페이스. 달리 Runnablevoid run()방법 CallableV call() throws Exception방법은 값을 반환하고, 예외를 던질 수있다.

실행자 팩토리 방법

언젠가는 실행자를 구하고 싶을 것입니다. Executor 프레임 워크는 Executors이를 위해 유틸리티 클래스를 제공합니다 . Executors특정 스레드 실행 정책을 제공하는 여러 종류의 실행기를 얻기위한 여러 팩토리 방법을 제공합니다. 다음은 세 가지 예입니다.

  • ExecutorService newCachedThreadPool () 은 필요에 따라 새 스레드를 생성하지만 이전에 생성 된 스레드가 사용 가능할 때 재사용하는 스레드 풀을 생성합니다. 60 초 동안 사용되지 않은 스레드는 종료되고 캐시에서 제거됩니다. 이 스레드 풀은 일반적으로 많은 단기 비동기 작업을 실행하는 프로그램의 성능을 향상시킵니다.
  • ExecutorService newSingleThreadExecutor () 는 제한되지 않은 대기열에서 작동하는 단일 작업자 스레드를 사용하는 실행기를 만듭니다. 작업이 대기열에 추가되고 순차적으로 실행됩니다 (한 번에 하나 이상의 작업이 활성화되지 않음). 이 스레드가 실행기 종료 전에 실행 중에 실패로 종료되면 후속 작업을 실행해야 할 때 대신 할 새 스레드가 생성됩니다.
  • ExecutorService newFixedThreadPool (int nThreads) 는 제한되지 않은 공유 큐에서 작동하는 고정 된 수의 스레드를 재사용하는 스레드 풀을 만듭니다. 대부분의 nThreads스레드가 적극적으로 작업을 처리하고 있습니다. 모든 스레드가 활성 상태 일 때 추가 작업이 제출되면 스레드를 사용할 수있을 때까지 대기열에서 대기합니다. 종료 전에 실행 중에 실패로 인해 스레드가 종료되면 후속 작업을 실행해야 할 때 대신 할 새 스레드가 생성됩니다. 풀의 스레드는 실행 프로그램이 종료 될 때까지 존재합니다.

(예 : 같은 집행자 프레임 워크를 제공 추가 유형 ScheduledExecutorService이 가장 자주 함께 작동하도록 가능성이 인터페이스)하지만, 유형 ExecutorService, Future, Callable,와 Executors.

java.util.concurrent추가 유형을 탐색 하려면 Javadoc을 참조하십시오 .

Executor 프레임 워크 작업

Executor 프레임 워크는 작업하기가 매우 쉽다는 것을 알 수 있습니다. 목록 2에서, 나는 사용한 적이 ExecutorExecutors더 확장 스레드 풀 기반의 대안 목록 1에서 서버 예를 대체 할 수 있습니다.

목록 2. Server.java (버전 2)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.Executor; import java.util.concurrent.Executors; class Server { static Executor pool = Executors.newFixedThreadPool(5); public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; pool.execute(r); } } static void doWork(Socket s) { } }

목록 2는 newFixedThreadPool(int)5 개의 스레드를 재사용하는 스레드 풀 기반 실행기를 얻기 위해 사용 합니다. 또한 대체 new Thread(r).start();와 함께 pool.execute(r);이러한 스레드의를 통해 실행 가능한 작업을 실행합니다.

목록 3은 애플리케이션이 임의의 웹 페이지의 내용을 읽는 또 다른 예를 보여줍니다. 최대 5 초 이내에 내용을 사용할 수없는 경우 결과 줄 또는 오류 메시지를 출력합니다.