이 기사의 초점은 멀티 스레드 애플리케이션의 성능 문제에 있습니다. 우리는 먼저 성능과 확장 성을 정의한 다음 Amdahl 규칙을 신중하게 연구합니다. 다음 컨텐츠에서는 다양한 기술 방법을 사용하여 잠금 경쟁을 줄이는 방법과 코드로 구현하는 방법을 살펴 봅니다.
1. 성능
우리는 모두 멀티 스레딩이 프로그램 성능을 향상시키는 데 사용될 수 있다는 것을 알고 있으며, 그 이유는 멀티 코어 CPU 또는 다중 CPU가 있기 때문입니다. 각 CPU 코어는 그 자체로 작업을 완료 할 수 있으므로 큰 작업을 서로 독립적으로 실행할 수있는 일련의 작은 작업으로 나누면 프로그램의 전반적인 성능을 향상시킬 수 있습니다. 당신은 예를들 수 있습니다. 예를 들어, 하드 디스크의 폴더의 모든 그림의 크기를 변경하는 프로그램이 있으며 멀티 스레딩 기술의 적용은 성능을 향상시킬 수 있습니다. 단일 스레드 접근 방식을 사용하면 모든 이미지 파일을 순서대로 통과하고 수정을 수행 할 수 있습니다. 우리의 CPU에 여러 코어가있는 경우, 그 중 하나만 사용할 수 있다는 것은 의심의 여지가 없습니다. 멀티 스레딩을 사용하면 생산자 스레드가 파일 시스템을 스캔하여 각 이미지를 큐에 추가 한 다음 여러 작업자 스레드를 사용하여 이러한 작업을 수행 할 수 있습니다. 작업자 스레드 수가 총 CPU 코어 수와 동일하면 모든 작업이 실행될 때까지 각 CPU 코어가 수행 할 작업이 있는지 확인할 수 있습니다.
더 많은 IO Waits가 필요한 다른 프로그램의 경우 멀티 스레딩 기술을 사용하여 전반적인 성능을 향상시킬 수 있습니다. 특정 웹 사이트의 모든 HTML 파일을 크롤링하고 로컬 디스크에 저장 해야하는 프로그램을 작성하고 싶다고 가정 해 봅시다. 프로그램은 특정 웹 페이지에서 시작한 다음이 웹 페이지 에서이 웹 사이트에 대한 모든 링크를 구문 분석 한 다음 이러한 링크를 차례로 크롤링하여 반복 할 수 있습니다. 원격 웹 사이트에 대한 요청을 시작한 시간부터 모든 웹 페이지 데이터를 수신하는 시간까지 기다리는 데 시간이 걸리므 로이 작업을 실행을 위해 여러 스레드로 넘겨 줄 수 있습니다. 수신 된 HTML 페이지를 구문 분석하고 찾은 링크를 대기열에 넣고 다른 모든 스레드가 페이지를 요청하는 책임을 맡게하십시오. 이전 예제와 달리이 예에서는 CPU 코어 수보다 더 많은 스레드를 사용하더라도 성능 향상을 계속할 수 있습니다.
위의 두 가지 예는 고성능이 짧은 시간에 가능한 한 많은 일을하는 것임을 알려줍니다. 물론 이것은 성능이라는 용어에 대한 가장 고전적인 설명입니다. 그러나 동시에 스레드를 사용하면 프로그램의 응답 속도가 잘 향상 될 수 있습니다. 위의 입력 상자와 입력 상자 아래의 "프로세스"라는 버튼이있는 그래픽 인터페이스 응용 프로그램이 있다고 상상해보십시오. 사용자 가이 버튼을 누르면 응용 프로그램은 버튼의 상태를 다시 렌더링해야합니다 (버튼이 누르면 왼쪽 마우스 버튼이 해제되면 원래 상태로 돌아와서 사용자의 입력 처리를 시작합니다. 이 작업이 사용자 입력을 처리하는 데 시간이 걸리는 경우 단일 스레드 프로그램은 사용자가 운영 체제에서 전송 된 이벤트를 이동하는 마우스 이벤트를 클릭하거나 마우스 포인터 등과 같은 다른 사용자 입력 작업에 계속 응답 할 수 없습니다. 이러한 이벤트에 대한 응답은 응답 할 독립 스레드가되어야합니다.
확장 성은 프로그램 컴퓨팅 리소스를 추가하여 더 높은 성능을 얻을 수 있음을 의미합니다. 기계의 CPU 코어 수가 제한되어 있기 때문에 많은 이미지의 크기를 조정해야한다고 상상해보십시오. 스레드 수를 늘리면 항상 성능이 향상되지는 않습니다. 반대로, 스케줄러는 더 많은 스레드의 생성 및 종료에 대한 책임이 있어야하므로 CPU 리소스를 점유하여 성능을 줄일 수 있습니다.
1.1 Amdahl Rule
이전 단락은 경우에 따라 추가 컴퓨팅 리소스를 추가하면 프로그램의 전반적인 성능을 향상시킬 수 있다고 언급했습니다. 추가 리소스를 추가 할 때 얻을 수있는 성능 개선의 양을 계산하려면 프로그램의 어느 부분이 연속적으로 (또는 동기식), 어떤 부품이 병렬로 실행되는지 확인해야합니다. B와 동시에 실행 해야하는 코드 비율을 정량화하고 (예 : 동시에 실행 해야하는 코드 줄 수) CPU의 총 코어 수를 N으로 기록한 다음 Amdahl Law에 따르면, 우리가 얻을 수있는 성능 개선의 상한은 다음과 같습니다.
N이 무한대 인 경우 (1-B)/N이 0으로 수렴됩니다. 따라서이 표현의 값을 무시할 수 있으므로 성능 향상 비트 수는 1/b로 수렴되므로 B는 동기식으로 실행 해야하는 코드의 비율을 나타냅니다. B가 0.5와 같으면 프로그램 코드의 절반이 병렬로 실행될 수없고 0.5의 상호는 2이므로 수많은 CPU 코어를 추가하더라도 최대 2 배의 성능 향상을 얻습니다. 현재 프로그램을 수정했으며 수정 후 0.25 코드 만 동기식으로 실행해야한다고 가정 해 봅시다. 이제 1/0.25 = 4는 프로그램 프로그램이 많은 CPU로 하드웨어에서 실행되면 단일 코어 하드웨어보다 약 4 배 빠를 것임을 의미합니다.
반면, Amdahl 법을 통해 프로그램이 우리가 얻고 자하는 속도를 기반으로 해야하는 동기화 코드의 비율을 계산할 수도 있습니다. 100 배 속도를 달성하고 1/100 = 0.01을 달성하려면 프로그램이 동시에 실행하는 최대 코드 수가 1%를 초과 할 수 없음을 의미합니다.
Amdahl Law를 요약하기 위해 추가 CPU를 추가함으로써 얻는 최대 성능 향상은 프로그램의 작은 비율이 코드의 일부를 동시에 실행하는 방법에 달려 있음을 알 수 있습니다. 실제로이 비율을 계산하는 것이 항상 쉬운 것은 아니지만, 대규모 상업용 시스템 응용 프로그램에 직면하지는 않지만 Amdahl Law는 우리에게 중요한 영감을줍니다. 즉, 우리는 동기식으로 실행 해야하는 코드를 고려하고 코드 의이 부분을 줄여야합니다.
1.2 성능에 미치는 영향
기사가 여기에 썼을 때, 우리는 더 많은 스레드를 추가하면 프로그램 성능과 응답 성을 향상시킬 수 있다고 지적했습니다. 그러나 반면에 이러한 혜택을 달성하기는 쉽지 않으며 가격도 필요합니다. 스레드 사용은 성능 향상에도 영향을 미칩니다.
첫째, 첫 번째 영향은 스레드 생성 시점에서 비롯됩니다. 스레드를 생성하는 동안 JVM은 기본 운영 체제의 해당 리소스를 신청하고 스케줄러의 데이터 구조를 초기화하여 실행 스레드 순서를 결정해야합니다.
스레드 수가 CPU 코어 수와 동일하면 각 스레드는 코어에서 실행되므로 자주 중단되지 않을 수 있습니다. 그러나 실제로 프로그램이 실행될 때 운영 체제에는 CPU가 처리 해야하는 자체 운영도 있습니다. 따라서이 경우에도 스레드가 중단되고 운영 체제가 작동을 재개 할 때까지 기다립니다. 스레드 수가 CPU 코어 수를 초과하면 상황이 악화 될 수 있습니다. 이 경우 JVM의 프로세스 스케줄러는 특정 스레드를 방해하여 다른 스레드가 실행되도록합니다. 스레드가 전환되면 다음에 실행될 때 데이터 상태를 복원 할 수 있도록 러닝 스레드의 현재 상태를 저장해야합니다. 뿐만 아니라 스케줄러는 자체 내부 데이터 구조를 업데이트하고 CPU 사이클도 필요합니다. 이 모든 것이 스레드간에 컨텍스트 전환이 CPU 컴퓨팅 리소스를 소비하여 단일 스레드 케이스에서 성능 오버 헤드를 가져옵니다.
멀티 스레드 프로그램에 의해 가져 오는 또 다른 오버 헤드는 공유 데이터의 동기식 액세스 보호에서 나옵니다. 동기화 보호를 위해 동기화 된 키워드를 사용하거나 휘발성 키워드를 사용하여 여러 스레드간에 데이터를 공유 할 수 있습니다. 둘 이상의 스레드가 공유 데이터 구조에 액세스하려면 경합이 발생합니다. 현재 JVM은 어떤 프로세스가 먼저 있고 어떤 프로세스가 뒤에 있는지 결정해야합니다. 실행될 스레드가 현재 실행중인 스레드가 아닌 경우 스레드 스위칭이 발생합니다. 현재 스레드는 잠금 객체를 성공적으로 얻을 때까지 기다려야합니다. JVM 은이 "대기"를 수행하는 방법을 결정할 수 있습니다. JVM이 잠긴 객체를 성공적으로 획득 할 것으로 예상되는 경우, JVM은 성공할 때까지 잠긴 객체를 지속적으로 획득하려고 시도하는 것과 같은 공격적인 대기 방법을 사용할 수 있습니다. 이 경우,이 방법은 프로세스 컨텍스트 전환을 비교하는 것이 더 빠르기 때문에 더 효율적일 수 있습니다. 대기 스레드를 실행 대기열로 옮기면 추가 오버 헤드가 나타납니다.
따라서 잠금 경쟁으로 인한 컨텍스트 전환을 피하기 위해 최선을 다해야합니다. 다음 섹션에서는 그러한 경쟁의 발생을 줄이는 두 가지 방법을 설명합니다.
1.3 잠금 경쟁
이전 섹션에서 언급 한 바와 같이, 두 개 이상의 스레드에 의한 잠금에 대한 경쟁은 경쟁에서 스케줄러가 공격적인 대기 상태로 들어가거나 대기 상태를 수행하도록하여 두 개의 컨텍스트 스위치를 유발하기 때문에 추가 계산 오버 헤드를 가져옵니다. 자물쇠 경쟁의 결과를 다음과 같이 완화 할 수있는 경우가 있습니다.
1. 자물쇠의 범위를 줄입니다.
2. 획득 해야하는 잠금의 주파수를 줄입니다.
3. 동기화되지 않고 하드웨어에서 지원하는 낙관적 잠금 작업을 사용하십시오.
4. 가능한 한 적게 동기화 된 것을 사용하십시오.
5. 객체 캐시 사용을 줄입니다
1.3.1 동기화 도메인 감소
코드가 필요 이상으로 잠금 장치를 보유하면이 첫 번째 방법을 적용 할 수 있습니다. 일반적으로 동기화 영역에서 하나 이상의 코드 라인을 이동하여 현재 스레드가 잠금을 유지하는 시간을 줄일 수 있습니다. 적은 코드가 동기화 영역에서 실행되면, 현재 스레드가 잠금을 일찍 해제하여 다른 스레드가 잠금을 일찍 얻을 수 있습니다. 이를 수행하면 동기식으로 실행 해야하는 코드의 양이 줄어 듭니다.
더 잘 이해하려면 다음 소스 코드를 참조하십시오.
공개 클래스 ReduceLockDuration은 실행 가능한 {private static final int number_of_threads = 5; 개인 정적 최종지도 <문자열, 정수>지도 = New Hashmap <문자열, integer> (); public void run () {for (int i = 0; i <10000; i ++) {synchronized (map) {uuid randomuuid = uuid.randomuuid (); 정수 값 = integer.valueof (42); 문자열 key = randomuuid.toString (); map.put (키, 값); } thread.yield (); }} public static void main (string [] args)은 인터럽트 exception {thread [] streads = 새 스레드 [number_of_threads]; for (int i = 0; i <number_of_threads; i ++) {스레드 [i] = 새 스레드 (new redelockDuration ()); } long startMillis = System.CurrentTimeMillis (); for (int i = 0; i <number_of_threads; i ++) {스레드 [i] .start (); } for (int i = 0; i <number_of_threads; i ++) {스레드 [i] .join (); } system.out.println ((System.CurrentTimeMillis () -StartMillis)+"MS"); }}위의 예에서는 5 개의 스레드가 공유 맵 인스턴스에 액세스하기 위해 경쟁하게합니다. 하나의 스레드 만 맵 인스턴스에 동시에 액세스 할 수 있으려면 맵에 키/값을 추가하는 작업을 동기화 된 보호 코드 블록에 넣습니다. 이 코드를주의 깊게 살펴보면 키와 값을 계산하는 몇 가지 코드 문장이 동기식으로 실행될 필요가 없음을 알 수 있습니다. 키와 값은 현재이 코드를 실행하는 스레드에만 속합니다. 그것은 현재 스레드에만 의미가 있으며 다른 스레드에 의해 수정되지 않습니다. 따라서 이러한 문장을 동기화 보호에서 옮길 수 있습니다. 다음과 같이 :
public void run () {for (int i = 0; i <10000; i ++) {uuid randomuuid = uuid.randomuuid (); 정수 값 = integer.valueof (42); 문자열 key = randomuuid.toString (); 동기화 된 (map) {map.put (키, 값); } thread.yield (); }}동기화 코드를 줄이는 효과는 측정 가능합니다. 내 컴퓨터에서 전체 프로그램의 실행 시간은 420ms에서 370ms로 줄었습니다. 동기화 보호 블록에서 3 줄의 코드를 옮기면 프로그램 실행 시간을 11%줄일 수 있습니다. Thread.yield () 코드는 스레드 컨텍스트 전환을 유도하는 것입니다.이 코드는 JVM에 현재 사용 된 스레드가 현재 사용되는 컴퓨팅 리소스를 넘겨서 실행중인 다른 스레드가 실행될 수 있다고 말하기 때문입니다. 그렇지 않은 경우 스레드가 특정 코어를 더 오래 차지하므로 스레드 컨텍스트 전환이 줄어 듭니다.
1.3.2 분할 잠금
잠금 경쟁을 줄이는 또 다른 방법은 잠금 보호 코드 블록을 여러 개의 작은 보호 블록으로 전파하는 것입니다. 이 방법은 여러 다른 객체를 보호하기 위해 프로그램의 잠금을 사용하는 경우 작동합니다. 프로그램을 통해 일부 데이터를 계산하고 간단한 카운트 클래스를 구현하여 여러 다른 통계 지표를 보유하고 기본 카운트 변수 (긴 유형)로 표시하십시오. 우리의 프로그램은 다중 스레드이기 때문에 이러한 변수에 액세스하는 작업에 동기식으로 보호해야합니다. 이러한 동작은 다른 스레드에서 나오기 때문입니다. 이를 달성하는 가장 쉬운 방법은 이러한 변수에 액세스하는 각 기능에 동기화 된 키워드를 추가하는 것입니다.
공개 정적 클래스 Coun 비공개 긴 배송수 = 0; public synchronized void incrementCustomer () {customerCount ++; } public synchronized void ycrementshipping () {shippingCount ++; } public synchronized long getCustomerCount () {return CustomerCount; } public synchronized long getshippingcount () {return shippingcount; }}이는 이러한 변수의 각 수정이 다른 카운터 인스턴스로 고정 될 것임을 의미합니다. 다른 스레드가 다른 다른 변수에서 증분 메소드를 호출하려면 이전 스레드가 잠금 컨트롤을 완성하기 전에 잠금 컨트롤을 해제 할 때까지 기다릴 수 있습니다. 이 경우 각각의 다른 변수에 대해 별도의 동기화 된 보호를 사용하면 실행 효율이 향상됩니다.
공개 정적 클래스 Coun 비공개 정적 최종 객체 ShippingLock = 새 개체 (); 개인 긴 CustomerCount = 0; 비공개 긴 배송수 = 0; public void excrementcustomer () {synchronized (customerlock) {customercount ++; }} public void ycrementshipping () {synchronized (ShippingLock) {ShippingCount ++; }} public long getCustomerCount () {synchronized (customerLock) {return customerCount; }} public long getshippingcount () {synchronized (shippinglock) {return ShippingCount; }}}이 구현은 각 카운트 메트릭에 대해 별도의 동기화 된 객체를 소개합니다. 따라서 스레드가 고객 수를 늘리려면 운송 수가 완료되기 위해 다른 스레드를 기다리지 않고 고객 수를 완료하도록 고객 수를 늘리는 다른 스레드를 기다려야합니다.
다음 클래스를 사용하여 분할 잠금 장치로 가져온 성능 향상을 쉽게 계산할 수 있습니다.
공개 클래스 잠금 장치는 실행 가능한 {private static final int number_of_threads = 5; 개인 카운터 카운터; 공개 인터페이스 카운터 {void excrementCustomer (); void ycrementshipping (); Long getCustomerCount (); Long GetshippingCount (); } public static class coun } public void run () {for (int i = 0; i <100000; i ++) {if (threadlocalrandom.current (). nextBoolean ()) {counter.incrementCustomer (); } else {counter.incrementshipping (); }}} public static void main (String [] args)은 InterruptedException {thread [] streads = 새 스레드 [number_of_threads]; 카운터 카운터 = 새로운 CounterOnelock (); for (int i = 0; i <number_of_threads; i ++) {스레드 [i] = 새 스레드 (new locksplitting (counter)); } long startMillis = System.CurrentTimeMillis (); for (int i = 0; i <number_of_threads; i ++) {스레드 [i] .start (); } for (int i = 0; i <number_of_threads; i ++) {스레드 [i] .join (); } system.out.println ((System.CurrentTimeMillis () -StartMillis) + "MS"); }}내 컴퓨터에서 단일 잠금 장치의 구현 방법은 평균 56ms이며 두 개의 개별 잠금 장치 구현은 38ms입니다. 시간 소모는 약 32%감소합니다.
개선하는 또 다른 방법은 우리가 다른 자물쇠로 읽기와 쓰기를 보호하기 위해 더 나아갈 수 있다는 것입니다. 원래 카운터 클래스는 각각 계산 지표를 읽고 쓰는 방법을 제공합니다. 그러나 실제로 읽기 작업에는 동기화 보호가 필요하지 않습니다. 여러 스레드가 현재 표시기의 값을 병렬로 읽을 수 있음을 확신 할 수 있습니다. 동시에, 쓰기 작업은 동기식으로 보호되어야합니다. java.util.concurrent 패키지는 readwritelock 인터페이스의 구현을 제공하여 이러한 차이를 쉽게 달성 할 수 있습니다.
ReentrantreadWritelock 구현은 두 개의 다른 잠금 장치를 유지하며, 하나는 읽기 작동을 보호하고 다른 하나는 쓰기 작업을 보호합니다. 두 자물쇠에는 잠금 장치를 획득하고 해제하는 작업이 있습니다. 쓰기 잠금은 아무도 읽기 잠금을 얻지 못한 경우에만 성공적으로 얻을 수 있습니다. 반대로, 쓰기 잠금 장치가 획득되지 않는 한, 읽기 잠금은 동시에 여러 스레드에 의해 획득 될 수 있습니다. 이 접근법을 입증하기 위해 다음 카운터 클래스는 다음과 같이 readWritelock을 사용합니다.
공개 정적 클래스 Coun 개인 최종 잠금 고객 writelock = customerlock.writelock (); 개인 최종 잠금 CustomerReadLock = CustomerLock.ReadLock (); Private Final ReentrantreadWritelock ShippingLock = 새로운 ReentrantreadWritelock (); 비공개 최종 잠금 ShippingWritelock = ShippingLock.writelock (); 비공개 최종 잠금 ShippingReadLock = ShippingLock.ReadLock (); 개인 긴 CustomerCount = 0; 비공개 긴 배송수 = 0; public void ycrementCustomer () {customerWritElock.lock (); CustomerCount ++; CustomerWritelock.unlock (); } public void ycrementshipping () {shippingWritelock.lock (); ShippingCount ++; ShippingWritelock.unlock (); } public long getCustomerCount () {customerReadlock.lock (); 긴 카운트 = CustomerCount; customerReadlock.unlock (); 반환 수; } public long getshippingCount () {ShippingReadlock.lock (); 긴 카운트 = 배송수; ShippingReadLock.unlock (); 반환 수; }}모든 읽기 작업은 읽기 잠금으로 보호되며 모든 쓰기 작업은 쓰기 잠금으로 보호됩니다. 프로그램에서 수행 된 읽기 작업이 Writ
1.3.3 분리 잠금
위의 예는 단일 잠금을 여러 개의 별도 잠금으로 분리하는 방법을 보여 주므로 각 스레드가 수정하려는 객체의 잠금을 얻을 수 있습니다. 그러나 반면 에이 방법은 프로그램의 복잡성을 증가시키고 부적절하게 구현 된 경우 교착 상태를 유발할 수 있습니다.
분리 잠금은 분리 잠금과 유사한 방법이지만 분리 잠금은 다른 코드 스 니펫이나 물체를 보호하기 위해 잠금 장치를 추가하는 반면 분리 잠금은 다른 잠금 장치를 사용하여 다른 범위의 값을 보호하는 것입니다. JDK의 java.util.concurrent 패키지의 ConsurenthashMap 은이 아이디어를 사용하여 해시 맵에 크게 의존하는 프로그램의 성능을 향상시킵니다. 구현 측면에서 ConscurrEthashMap은 동기식 보호 된 해시 맵을 캡슐화하는 대신 내부적으로 16 개의 다른 잠금 장치를 사용합니다. 16 개의 자물쇠 각각은 버킷 비트 (버킷)의 10 분의 1에 동기식 액세스를 보호하는 데 도움이됩니다. 이러한 방식으로, 다른 스레드가 키를 다른 세그먼트에 삽입하려면 해당 작업은 다른 잠금으로 보호됩니다. 그러나 특정 작업의 완료와 같은 일부 나쁜 문제는 이제 하나의 잠금 대신 여러 자물쇠가 필요합니다. 전체 맵을 복사하려면 16 개의 잠금 장치를 모두 완료하려면 얻어야합니다.
1.3.4 원자 작동
잠금 경쟁을 줄이는 또 다른 방법은 원자 운영을 사용하는 것입니다.이 작업은 다른 기사의 원칙에 대해 자세히 설명합니다. java.util.concurrent 패키지는 일반적으로 사용되는 일부 기본 데이터 유형에 대해 원자 적으로 캡슐화 된 클래스를 제공합니다. 원자 작동 클래스의 구현은 프로세서에서 제공하는 "비교 순열"함수 (CAS)를 기반으로합니다. CAS 조작은 현재 레지스터의 값이 작업에서 제공하는 이전 값과 동일 할 때만 업데이트 작업을 수행합니다.
이 원칙은 낙관적 인 방식으로 변수의 값을 높이는 데 사용될 수 있습니다. 스레드가 현재 값을 알고 있으면 CAS 작업을 사용하여 증분 작업을 수행하려고합니다. 이 기간 동안 다른 스레드가 변수의 값을 수정 한 경우 스레드가 제공하는 소위 전류 값은 실제 값과 다릅니다. 현재 JVM은 현재 값을 되찾고 다시 시도하여 성공할 때까지 다시 반복하려고합니다. 루핑 작업은 일부 CPU주기를 낭비 할 것이지만,이를 수행하는 이점은 우리가 어떤 형태의 동기화 제어가 필요하지 않다는 것입니다.
아래 카운터 클래스의 구현은 원자 연산을 사용합니다. 보시다시피, 동기화 된 코드는 사용되지 않습니다.
공개 정적 클래스 반원 학적 구현 카운터 {private atomiclong customercount = new atomiclong (); Private AtomicLong ShippingCount = New AtomicLong (); public void ycrementCustomer () {customerCount.incrementAndget (); } public void ycrementshipping () {shippingCount.incrementAndGet (); } public long getCustomerCount () {return customerCount.get (); } public long getshippingcount () {return shippingcount.get (); }}반소 세포 클래스와 비교하여 평균 실행 시간은 39ms에서 16ms로 감소했으며 이는 약 58%입니다.
1.3.5 핫스팟 코드 세그먼트를 피하십시오
일반적인 목록 구현은 컨텐츠의 변수를 유지하여 목록 자체에 포함 된 요소 수를 기록합니다. 요소가 목록에서 삭제되거나 추가 될 때 마다이 변수의 값이 변경됩니다. 단일 스레드 응용 프로그램에서 목록을 사용하는 경우이 방법을 이해할 수 있습니다. size ()을 호출 할 때마다 마지막 계산 후 값을 반환 할 수 있습니다. 이 카운트 변수가 목록별로 내부적으로 유지되지 않으면 각 호출 Size ()로 인해 목록이 목록을 재발하고 요소 수를 계산하게됩니다.
많은 데이터 구조에서 사용되는이 최적화 방법은 다중 스레드 환경에있을 때 문제가됩니다. 여러 스레드간에 목록을 공유하고 여러 스레드가 동시에 목록에 요소를 추가하거나 삭제하고 큰 길이를 쿼리한다고 가정합니다. 현재 Count ariable 내부 목록은 공유 리소스가되므로 모든 액세스는 동기식으로 처리해야합니다. 따라서 카운트 변수는 전체 목록 구현에서 핫 스팟이됩니다.
다음 코드 스 니펫은이 문제를 보여줍니다.
공개 정적 클래스 카레 포지티브 (public static class arrrepository)는 간병을 구현합니다. 개인지도 <문자열, 자동차> 트럭 = New Hashmap <문자열, 자동차> (); 개인 객체 carcountsync = new Object (); 개인 int carcount = 0; public void addcar (Car Car) {if (car.getlicenceplate (). startSwith ( "c")) {synchronized (cars) {car foundcar = cars.get (car.getlicenceplate ()); if (foundCar == null) {cars.put (car.getLicenceplate (), car); 동기화 된 (carcountsync) {carcount ++; }}}} else {synchronized (트럭) {CAR foun if (foundCar == null) {trucks.put (car.getLicenceplate (), car); 동기화 된 (carcountsync) {carcount ++; }}}}}} public int getCarcount () {synchronized (carcountsync) {return carcount; }}}위의 간도 구현에는 내부에 두 개의 목록 변수가 있으며, 하나는 세차 요소를 배치하고 다른 하나는 트럭 요소를 배치하는 데 사용됩니다. 동시에이 두 목록의 총 크기를 쿼리하는 메소드를 제공합니다. 사용 된 최적화 방법은 자동차 요소가 추가 될 때마다 내부 카운트 변수의 값이 증가한다는 것입니다. 동시에, 증분 작업은 동기화 된 것으로 보호되며 카운트 값을 반환하는 경우에도 마찬가지입니다.
이 추가 코드 동기화 오버 헤드를 피하려면 아래의 사도 위치의 다른 구현을 참조하십시오. 더 이상 내부 카운트 변수를 사용하지 않지만 총 자동차 수를 반환하는 방법 에서이 값을 실시간으로 계산합니다. 다음과 같이 :
공개 정적 클래스 카레 포지티브 (public static class carrepository)는 카운터를 구현합니다. 개인지도 <문자열, 자동차> 트럭 = New Hashmap <문자열, 자동차> (); public void addcar (Car Car) {if (car.getlicenceplate (). startSwith ( "c")) {synchronized (cars) {car foundcar = cars.get (car.getlicenceplate ()); if (foundCar == null) {cars.put (car.getLicenceplate (), car); }}} else {Synchronized (트럭) {CAR Foun if (foundCar == null) {trucks.put (car.getLicenceplate (), car); }}}}} public int getCarcount () {synchronized (cars) {synchronized (트럭) {return cars.size () + trucks.size (); }}}}이제 getCarcount () 메소드에서 두 목록의 액세스에는 동기화 보호가 필요합니다. 이전 구현과 마찬가지로 새 요소가 추가 될 때마다 동기화 오버 헤드가 더 이상 존재하지 않습니다.
1.3.6 객체 캐시 재사용을 피하십시오
Java VM의 첫 번째 버전에서는 새 키워드를 사용하여 새 객체를 만드는 오버 헤드가 비교적 높기 때문에 많은 개발자가 객체 재사용 모드를 사용하는 데 익숙합니다. 객체의 반복적 인 생성을 반복해서 피하기 위해 개발자는 버퍼 풀을 유지합니다. 객체 인스턴스의 각 생성 후에는 버퍼 풀에 저장 될 수 있습니다. 다음에 다른 스레드가 사용해야 할 때 버퍼 풀에서 직접 검색 할 수 있습니다.
언뜻보기 에이 방법은 매우 합리적이지만이 패턴은 멀티 스레드 애플리케이션에서 문제를 일으킬 수 있습니다. 객체의 버퍼 풀은 여러 스레드간에 공유되므로 객체에 액세스 할 때의 모든 스레드 작업에는 동기 보호가 필요합니다. 이 동기화의 오버 헤드는 물체 자체의 생성보다 큽니다. 물론, 너무 많은 객체를 생성하면 쓰레기 수집의 부담이 높아지지만이를 고려하더라도 객체 캐시 풀을 사용하는 것보다 코드를 동기화하여 가져온 성능 향상을 피하는 것이 여전히 좋습니다.
이 기사에 설명 된 최적화 체계는 다시 한 번 가능한 각 최적화 방법이 실제로 적용될 때 신중하게 평가해야 함을 보여줍니다. 미성숙 최적화 솔루션은 표면에서 의미가있는 것처럼 보이지만 실제로는 성능 병목 현상이 될 것입니다.