1. 스레드 풀을 사용하는 이유
웹 서버, 데이터베이스 서버, 파일 서버 또는 메일 서버와 같은 많은 서버 애플리케이션은 일부 원격 소스에서 많은 수의 짧은 작업을 처리하는 데 중점을 두고 있습니다. 요청은 네트워크 프로토콜(예: HTTP, FTP 또는 POP), JMS 대기열 또는 데이터베이스 폴링을 통해 어떤 방식으로든 서버에 도달합니다. 요청이 어떻게 도착하는지에 관계없이 서버 응용 프로그램의 일반적인 상황은 단일 작업의 처리 시간은 매우 짧지만 요청 수는 엄청납니다.
서버 애플리케이션 구축을 위한 간단한 모델은 요청이 도착할 때마다 새 스레드를 생성한 다음 새 스레드에서 요청을 처리하는 것입니다. 이 접근 방식은 실제로 프로토타입 제작에 적합하지만 이러한 방식으로 실행되는 서버 애플리케이션을 배포하려고 하면 이 접근 방식의 심각한 단점이 명백해집니다. 요청별 스레드 접근 방식의 단점 중 하나는 각 요청에 대해 새 스레드를 생성하는 데 비용이 많이 든다는 것입니다. 각 요청에 대해 새 스레드를 생성하는 서버는 스레드를 생성하고 삭제하는 데 많은 시간을 소비합니다. 실제 사용자 요청을 처리하는 데 사용되는 리소스보다
스레드 생성 및 삭제에 따른 오버헤드 외에도 활성 스레드는 시스템 리소스를 소비합니다. JVM에서 스레드를 너무 많이 생성하면 시스템의 메모리가 부족해지거나 과도한 메모리 소비로 인해 "오버스위치"가 발생할 수 있습니다. 리소스 부족을 방지하려면 서버 애플리케이션에는 주어진 시간에 처리할 수 있는 요청 수를 제한하는 방법이 필요합니다.
스레드 풀은 스레드 수명 주기 오버헤드 문제와 리소스 부족 문제에 대한 솔루션을 제공합니다. 여러 작업에 스레드를 재사용하면 스레드 생성의 오버헤드가 여러 작업에 분산됩니다. 이점은 요청이 도착할 때 스레드가 이미 존재하기 때문에 스레드 생성으로 인한 지연도 의도치 않게 제거된다는 것입니다. 이렇게 하면 요청을 즉시 처리할 수 있어 애플리케이션의 응답성이 향상됩니다. 또한, 스레드 풀의 스레드 수를 적절하게 조정함으로써, 즉 요청 수가 특정 임계값을 초과하는 경우 다른 들어오는 요청은 스레드를 얻을 때까지 강제로 대기하여 처리함으로써 리소스 부족을 방지합니다.
2. 스레드 풀 사용의 위험
스레드 풀은 멀티 스레드 애플리케이션을 구축하기 위한 강력한 메커니즘이지만 사용에도 위험이 따릅니다. 스레드 풀로 구축된 응용 프로그램은 동기화 오류 및 교착 상태와 같이 다른 다중 스레드 응용 프로그램이 취약한 모든 동시성 위험에 취약합니다. 또한 풀 관련 교착 상태와 같은 스레드 풀과 관련된 몇 가지 다른 위험에도 취약합니다. , 리소스 부족 및 스레드 누출.
2.1 교착 상태
모든 멀티 스레드 애플리케이션에는 교착 상태가 발생할 위험이 있습니다. 각 프로세스 또는 스레드 그룹이 그룹 내의 다른 프로세스에 의해서만 발생할 수 있는 이벤트를 기다리고 있을 때 프로세스 또는 스레드 그룹이 교착 상태에 빠졌다고 말합니다. 교착 상태의 가장 간단한 경우는 스레드 A가 개체 X에 대한 배타적 잠금을 보유하고 개체 Y에 대한 잠금을 기다리고 있는 반면, 스레드 B는 개체 Y에 대한 배타적 잠금을 보유하지만 개체 X에 대한 잠금을 기다리고 있는 것입니다. 잠금 대기를 중단할 수 있는 방법이 없는 한(Java 잠금은 이 방법을 지원하지 않음) 교착 상태에 빠진 스레드는 영원히 대기합니다.
모든 다중 스레드 프로그램에는 교착 상태의 위험이 있지만 스레드 풀은 모든 풀 스레드가 차단된 대기 대기열에서 실행되는 또 다른 교착 상태 가능성을 도입합니다. 다른 작업이 있지만 비어 있는 스레드가 없기 때문에 이 작업을 실행할 수 없습니다. 이는 스레드 풀을 사용하여 상호 작용하는 많은 개체가 포함된 시뮬레이션을 구현하는 경우 발생하며, 시뮬레이션된 개체는 서로에게 쿼리를 보낼 수 있으며, 쿼리 개체가 동기적으로 응답을 기다리는 동안 이러한 쿼리는 대기열에 있는 작업으로 실행됩니다.
2.2 리소스 부족
스레드 풀의 한 가지 장점은 일반적으로 다른 대체 스케줄링 메커니즘(일부는 이미 논의함)에 비해 성능이 매우 뛰어나다는 것입니다. 그러나 이는 스레드 풀 크기가 적절하게 조정된 경우에만 해당됩니다. 스레드는 메모리 및 기타 시스템 리소스를 포함하여 많은 리소스를 소비합니다. Thread 개체에 필요한 메모리 외에도 각 스레드에는 두 개의 실행 호출 스택이 필요하며 이는 클 수 있습니다. 또한 JVM은 각 Java 스레드에 대한 기본 스레드를 생성할 수 있으며 이러한 기본 스레드는 추가 시스템 리소스를 소비합니다. 마지막으로 스레드 간 전환에 따른 스케줄링 오버헤드는 적지만 스레드 수가 많으면 컨텍스트 전환이 프로그램 성능에 심각한 영향을 미칠 수 있습니다.
스레드 풀이 너무 크면 해당 스레드가 소비하는 리소스가 시스템 성능에 심각한 영향을 미칠 수 있습니다. 스레드 간 전환은 시간을 낭비하게 되며, 실제로 필요한 것보다 더 많은 스레드를 사용하면 풀 스레드가 다른 작업에서 더 효율적으로 사용할 수 있는 리소스를 소비하기 때문에 리소스 부족 문제가 발생할 수 있습니다. 스레드 자체에서 사용하는 리소스 외에도 요청을 처리하는 작업에는 JDBC 연결, 소켓 또는 파일과 같은 다른 리소스가 필요할 수 있습니다. 이는 제한된 리소스이기도 하며 동시 요청이 너무 많으면 JDBC 연결을 할당할 수 없는 등의 오류가 발생할 수 있습니다.
2.3 동시성 오류
스레드 풀 및 기타 대기열 메커니즘은 wait() 및 inform() 메서드 사용에 의존하는데, 둘 다 사용하기 어렵습니다. 올바르게 코딩되지 않으면 알림이 손실되어 대기열에 처리할 작업이 있어도 스레드가 유휴 상태로 유지될 수 있습니다. 이러한 방법을 사용할 때는 각별한 주의가 필요합니다. 대신 util.concurrent 패키지와 같이 이미 작동하는 것으로 알려진 기존 구현을 사용하는 것이 더 좋습니다.
2.4 Thread Leak
다양한 유형의 스레드 풀에서 심각한 위험은 스레드 누출로, 작업을 수행하기 위해 풀에서 스레드를 제거하고 작업이 완료된 후 스레드가 이는 풀이 반환되지 않을 때 발생합니다. 스레드 누수가 발생하는 상황 중 하나는 작업이 RuntimeException 또는 오류를 발생시키는 경우입니다. 풀 클래스가 이를 포착하지 못하면 스레드는 단순히 종료되고 스레드 풀의 크기는 영구적으로 1만큼 줄어듭니다. 이러한 일이 여러 번 발생하면 작업을 처리하는 데 사용할 수 있는 스레드가 없기 때문에 결국 스레드 풀이 비워지고 시스템이 정지됩니다.
일부 작업은 특정 리소스나 사용자 입력을 영원히 기다릴 수 있으며 이러한 리소스는 사용 가능 여부가 보장되지 않으며 사용자가 집에 갔을 수 있으며 이러한 작업은 영구적으로 중지됩니다. 또한 스레드 누출과 동일한 문제가 발생합니다. 스레드가 이러한 작업에 의해 영구적으로 소비되면 풀에서 효과적으로 제거됩니다. 이러한 작업의 경우 자체 스레드만 제공하거나 제한된 시간 동안만 기다리도록 해야 합니다.
2.5 요청 과부하
요청만으로 서버를 압도할 수 있습니다. 이 시나리오에서는 실행을 위해 대기열에 있는 작업이 너무 많은 시스템 리소스를 소비하고 리소스 부족을 유발할 수 있으므로 들어오는 모든 요청을 작업 대기열에 대기열에 넣기를 원하지 않을 수 있습니다. 이 상황에서 무엇을 할지 결정하는 것은 귀하에게 달려 있습니다. 어떤 경우에는 단순히 요청을 포기하고 더 높은 수준의 프로토콜을 사용하여 나중에 요청을 다시 시도하거나 서버가 일시적으로 중단되었음을 나타내는 응답으로 응답할 수 있습니다. 요청을 거부하는 중입니다.
3. 스레드 풀을 효과적으로 사용하기 위한 지침
몇 가지 간단한 지침을 따르는 한 스레드 풀은 서버 애플리케이션을 구축하는 매우 효과적인 방법이 될 수 있습니다.
하지 마세요. t 다른 작업의 결과를 동기적으로 기다리는 작업을 큐에 넣습니다. 이는 위에 설명된 교착 상태의 형태로 이어질 수 있습니다. 이 경우 모든 스레드가 매우 바빠서 실행할 수 없는 대기열에 있는 작업의 결과를 기다리고 있는 작업이 모든 스레드를 점유하게 됩니다.
잠재적으로 긴 작업을 위해 풀링된 스레드를 사용할 때는 주의하세요. 프로그램이 I/O와 같은 리소스가 완료될 때까지 기다려야 하는 경우 최대 대기 시간을 지정하고 이후 실행을 위해 작업을 무효화하거나 다시 대기열에 넣을지 여부를 지정합니다. 그렇게 하면 성공적으로 완료될 수 있는 작업에 대한 스레드를 해제하여 결국 일부 진행이 보장됩니다.
작업을 이해하세요. 스레드 풀의 크기를 효과적으로 조정하려면 대기열에 있는 작업과 해당 작업이 수행 중인 작업을 이해해야 합니다. CPU에 바인딩되어 있나요? I/O 바인딩되어 있나요? 귀하의 답변은 지원서 조정 방법에 영향을 미칩니다. 특성이 매우 다른 다양한 작업 클래스가 있는 경우 각 풀을 적절하게 조정할 수 있도록 다양한 작업 클래스에 대해 여러 작업 대기열을 갖는 것이 합리적일 수 있습니다.
4. 스레드 풀 크기 설정
스레드 풀 크기를 조정하는 것은 기본적으로 스레드가 너무 적거나 너무 많은 두 가지 오류를 피하기 위한 것입니다. 다행스럽게도 대부분의 애플리케이션에서는 너무 많은 것과 너무 적은 것 사이의 차이가 상당히 넓습니다.
참고: 애플리케이션에서 스레드를 사용하면 I/O와 같은 느린 작업을 기다리더라도 처리를 계속할 수 있고 여러 프로세서를 활용할 수 있다는 두 가지 주요 이점이 있습니다. N개의 프로세서가 있는 시스템의 계산 제약 하에서 실행되는 애플리케이션에서 스레드 수가 N에 가까워지면 추가 스레드를 추가하면 전체 처리 능력이 향상될 수 있지만 스레드 수가 N을 초과하면 추가 스레드를 추가해도 아무런 효과가 없습니다. 실제로 스레드가 너무 많으면 추가 컨텍스트 전환 오버헤드가 발생하므로 성능이 저하될 수도 있습니다.
스레드 풀의 최적 크기는 사용 가능한 프로세서 수와 작업 대기열의 작업 특성에 따라 다릅니다. N개의 프로세서(모두 계산 작업)가 있는 시스템에 작업 대기열이 하나만 있는 경우 일반적으로 스레드 풀에 N 또는 N+1개의 스레드가 있을 때 최대 CPU 사용률을 얻습니다.
I/O가 완료될 때까지 기다려야 하는 작업(예: 소켓에서 HTTP 요청을 읽는 작업)의 경우 풀 크기가 사용 가능한 프로세서 수를 초과하도록 해야 합니다. 스레드는 항상 작동합니다. 프로파일링을 사용하면 일반적인 요청에 대한 대기 시간(WT)과 서비스 시간(ST)의 비율을 추정할 수 있습니다. 이 비율을 WT/ST라고 하면 N 프로세서가 있는 시스템의 경우 프로세서를 완전히 활용하려면 약 N*(1+WT/ST) 스레드를 설정해야 합니다.
스레드 풀 크기 조정 시 프로세서 활용률만 고려되는 것은 아닙니다. 스레드 풀이 커짐에 따라 스케줄러, 사용 가능한 메모리 또는 소켓 수, 열린 파일 핸들 또는 데이터베이스 연결 수와 같은 기타 시스템 리소스에 제한이 발생할 수 있습니다.
5. 일반적으로 사용되는 여러 스레드 풀
5.1 newCachedThreadPool
캐시 가능한 스레드 풀의 길이가 처리 요구 사항을 초과하는 경우 유휴 스레드를 유연하게 재활용할 수 있습니다. . 재활용이 없으면 새 스레드를 만듭니다.
이 유형의 스레드 풀의 특징은 다음과 같습니다.
• 생성되는 작업자 스레드 수에는 거의 제한이 없습니다(실제로는 제한이 있으며 개수는 Interger입니다. MAX_VALUE ), 스레드 풀을 유연하게 추가할 수 있도록 .
• 오랫동안 스레드 풀에 작업이 제출되지 않은 경우, 즉 작업자 스레드가 지정된 시간(기본값은 1분) 동안 유휴 상태인 경우 작업자 스레드가 자동으로 종료됩니다. 종료 후 새 작업을 제출하면 스레드 풀이 작업자 스레드를 다시 생성합니다.
• CachedThreadPool 사용 시 작업 개수 조절에 주의해야 합니다. 그렇지 않으면 동시에 실행되는 스레드 수가 많아 시스템이 마비될 수 있습니다.
샘플 코드는 다음과 같습니다.
package test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExecutorTest { public static void main(String[] args) { ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { final int index = i; try { Thread.sleep(index * 1000); } catch (InterruptedException e) { e.printStackTrace(); } cachedThreadPool.execute(new Runnable() { public void run() { System.out.println(index); } }); } } }
5.1 newFixedThreadPool
지정된 개수의 스레드 풀을 생성합니다. 작업자 스레드의 수입니다. 작업이 제출될 때마다 작업자 스레드가 생성됩니다. 작업자 스레드 수가 스레드 풀의 초기 최대 수에 도달하면 제출된 작업이 풀 큐에 저장됩니다.
FixedThreadPool은 스레드 풀의 대표적인 장점으로 프로그램 효율성을 높이고 스레드 생성 시 오버헤드를 줄여준다는 장점이 있습니다. 그러나 스레드 풀이 유휴 상태인 경우, 즉 스레드 풀에 실행 가능한 작업이 없는 경우 작업자 스레드를 해제하지 않고 특정 시스템 리소스도 점유하게 됩니다.
샘플 코드는 다음과 같습니다.
package test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExecutorTest { public static void main(String[] args) { ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; i++) { final int index = i; fixedThreadPool.execute(new Runnable() { public void run() { try { System.out.println(index); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } }
스레드 풀 크기가 3이므로 각 작업은 출력 후 2초 동안 Sleep 상태로 유지됩니다. 색인이므로 각 작업은 2초 안에 3개의 숫자를 인쇄합니다.
고정 길이 스레드 풀의 크기는 Runtime.getRuntime().availableProcessors()와 같은 시스템 리소스에 따라 설정하는 것이 가장 좋습니다.
5.1 newSingleThreadExecutor
단일 스레드 실행자를 생성합니다. 즉, 작업을 실행하기 위해 고유한 작업자 스레드만 생성하여 모든 작업이 실행되도록 합니다. 지정된 순서(FIFO, LIFO, 우선순위)에 따라 실행을 따릅니다. 이 스레드가 비정상적으로 종료되면 순차적 실행을 보장하기 위해 다른 스레드가 이를 대체합니다. 단일 작업자 스레드의 가장 큰 특징은 작업이 순차적으로 실행되고 특정 시간에 여러 스레드가 활성화되지 않도록 할 수 있다는 것입니다.
샘플 코드는 다음과 같습니다.
package test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExecutorTest { public static void main(String[] args) { ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) { final int index = i; singleThreadExecutor.execute(new Runnable() { public void run() { try { System.out.println(index); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } }
5.1 newScheduleThreadPool
고정 길이 스레드 풀을 생성하고 타이밍 및 주기적인 작업 실행을 지원하고 예약된 주기적인 작업 실행을 지원합니다.
3초 동안 실행을 지연합니다. 지연 실행에 대한 샘플 코드는 다음과 같습니다.
package test; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ThreadPoolExecutorTest { public static void main(String[] args) { ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); scheduledThreadPool.schedule(new Runnable() { public void run() { System.out.println("delay 3 seconds"); } }, 3, TimeUnit.SECONDS); } }
은 1의 지연 후 3초마다 실행된다는 의미입니다. 둘째, 정규 실행을 위한 샘플 코드는 다음과 같습니다.
package test; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ThreadPoolExecutorTest { public static void main(String[] args) { ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); scheduledThreadPool.scheduleAtFixedRate(new Runnable() { public void run() { System.out.println("delay 1 seconds, and excute every 3 seconds"); } }, 1, 3, TimeUnit.SECONDS); } }
Java에서 일반적으로 사용되는 여러 스레드 풀을 비교한 위의 기사는 모두 편집기에서 공유하는 내용입니다. . 참고할 수 있기를 바라며, PHP 중국어 웹사이트도 지원해 주시길 바랍니다.
Java에서 일반적으로 사용되는 여러 스레드 풀 비교에 대한 더 많은 기사를 보려면 PHP 중국어 웹사이트를 주목하세요!