Java 디자인을 사용하여 효율적이고 확장 가능한 계산 결과 캐시 구현

개요

현재 소프트웨어 개발의 거의 모든 응용 프로그램은 일종의 캐싱을 사용하며, 이전 계산 결과를 재사용하면 대기 시간을 줄이고 시스템 처리량을 향상시킬 수 있지만 더 많은 메모리를 소비하므로 공간을 시간과 교환하는 방법입니다. 여럿이 그렇듯 重复造的轮子캐쉬는 아주 간단해 보이는데 계산 결과를 전부 저장하는 정도에 불과하고 다음 사용시에는 캐쉬에 저장된 결과를 먼저 사용하고 계산이 없을 시 재계산을 하게 됩니다. 하나. 그러나 비합리적인 캐싱 메커니즘 설계는 프로그램의 성능에 영향을 미치게 되는데, 이 글에서는 계산 결과 캐시의 반복 ​​설계를 소개하고 각 버전의 동시성 결함을 분석하고 이러한 결함을 수정하는 방법을 분석하여 최종적으로 효율적이고 확장 가능한 계산 결과 캐시.

1. 캐시 구현

시연을 위해 Computable<A,V> 컴퓨팅 인터페이스를 정의하고 입력 값이 A 유형이고 반환 값이 V 유형인 compute(A arg) 함수를 인터페이스에서 선언합니다. 인터페이스 정의는 다음과 같습니다. 다음과 같습니다.

 
 

자바

코드 복사

public interface Computable<A,V> { V compute(A arg) throws InterruptedException; }

1.1 HashMap+Synchronized를 사용하여 캐싱 구현

첫 번째 방법은 HashMap이 스레드로부터 안전하지 않기 때문에 HashMap을 캐시 컨테이너로 사용하는 것입니다. 따라서 데이터 액세스 보안을 보장하기 위해 동기화된 동기화 메커니즘을 추가해야 합니다.

아래와 같이 코드 쇼:

 
 

자바

코드 복사

public class HashMapMemoizer<A,V> implements Computable<A,V>{ private final Map<A,V> cache = new HashMap<>(); private final Computable<A,V> computable; private HashMapMemoizer(Computable<A,V> computable){ this.computable = computable; } @Override public synchronized V compute(A arg) throws InterruptedException { V res = cache.get(arg); if (res == null) { res = computable.compute(arg); cache.put(arg,res); } return res; } }

위의 코드와 같이 HashMap을 사용하여 이전 계산 결과를 저장하고 결과를 계산할 때마다 먼저 캐시에 존재하는지 확인하고 존재하면 결과를 캐시에 반환하고 그렇지 않으면 결과를 다시 계산합니다. 결과를 반환하기 전에 캐시에 넣습니다. HashMap은 스레드로부터 안전하지 않기 때문에 두 스레드가 동시에 HashMap에 액세스하지 않도록 보장할 수 없으므로 동기화된 키워드를 전체 컴퓨팅 메서드에 추가하여 메서드를 동기화합니다. 이 방법은 스레드 안전성을 보장할 수 있지만 명백한 문제가 있습니다. 즉, 계산에 시간이 많이 걸리기 때문에 다른 스레드가 결과를 계산하는 경우 한 번에 하나의 스레드만 계산을 실행할 수 있습니다. compute method 스레드가 오랫동안 차단될 수 있습니다. 아직 계산되지 않은 결과를 위해 여러 스레드가 대기 중인 경우 계산 방법의 계산 시간이 캐싱 작업이 없는 계산 시간보다 길어질 수 있으며 캐시는 의미를 잃습니다.

1.2 HashMap 대신 ConcurrentHashMap을 사용하여 캐시 동시성 향상

ConcurrentHashMap은 스레드로부터 안전하기 때문에 기본 Map에 액세스할 때 동기화할 필요가 없으므로 계산 방법을 동기화할 때 아직 계산되지 않은 결과를 위해 여러 스레드가 대기하는 문제를 피할 수 있습니다.

개선된 코드는 다음과 같습니다.

 
 

자바

코드 복사

public class ConcurrentHashMapMemoizer<A,V> implements Computable<A,V>{ private final Map<A,V> cache = new ConcurrentHashMap<>(); private final Computable<A,V> computable; private ConcurrentHashMapMemoizer(Computable<A,V> computable){ this.computable = computable; } @Override public V compute(A arg) throws InterruptedException { V res = cache.get(arg); if (res == null) { res = computable.compute(arg); cache.put(arg,res); } return res; } }

注意:这种方式有着比第一种方式更好的并发行为,多个线程可以并发的使用它,但是它在做缓存时仍然存在一些不足,这个不足就是当两个线程同时调用compute方法时,可能会导致计算得到相同的值。因为缓存的作用就是避免相同的数据被计算多次。对于更通用的缓存机制来说,这种情况将更严重。而假设用于只提供单次初始化的对象来说,这个问题就会带来安全风险。

1.3 확장 가능하고 효율적인 캐싱을 위한 최종 솔루션 완성

ConcurrentHashMap 사용의 문제점은 스레드가 비용이 많이 드는 계산을 시작하고 다른 스레드가 계산이 진행 중임을 알지 못하는 경우 이 계산을 반복할 가능성이 매우 높다는 것입니다. 그래서 우리는 "스레드 X가 f(10)의 시간 소모적인 계산을 하고 있다"라고 표현하는 방법이 있기를 바랍니다. 그래서 다른 스레드가 f(10)을 찾을 때, 그것이 무엇을 계산하고 있는지 이미 스레드가 있다는 것을 알 수 있습니다. 현재 가장 효율적인 방법은 스레드 X의 계산이 완료될 때까지 기다린 다음 캐시를 확인하여 f(10)의 결과를 찾는 것입니다. 그리고 FutureTask는 이 기능을 실현할 수 있습니다. FutureTask를 사용하여 계산되었거나 진행 중인 계산 프로세스를 나타낼 수 있습니다. 사용 가능한 결과가 있는 경우 FutureTask.get() 메서드는 결과를 즉시 반환하고, 그렇지 않으면 결과가 계산될 때까지 차단한 다음 반환합니다.

ConcurrentHashMap<A, Future<V>>원래 값을 대체하기 위해 값을 캐시하는 데 사용된 이전 맵을 재정의합니다 ConcurrentHashMap<A, V>. 코드는 다음과 같습니다.

 
 

자바

코드 복사

public class PerfectMemoizer<A, V> implements Computable<A, V> { private final ConcurrentHashMap<A, Future<V>> cache = new ConcurrentHashMap<>(); private final Computable<A, V> computable; public PerfectMemoizer(Computable<A, V> computable) { this.computable = computable; } @Override public V compute(final A arg) throws InterruptedException { while (true) { Future<V> f = cache.get(arg); if (f == null) { Callable<V> eval = new Callable<V>() { @Override public V call() throws Exception { return computable.compute(arg); } }; FutureTask<V> ft = new FutureTask<>(eval); f = cache.putIfAbsent(arg, ft); if (f == null) { f = ft; ft.run(); } } try { return f.get(); } catch (CancellationException e) { cache.remove(arg); } catch (ExecutionException e) { throw new RuntimeException(e); } } } }

위 코드와 같이 해당 계산이 시작되었는지 먼저 확인하고, 그렇지 않으면 FutureTask를 생성하여 Map에 등록한 후 계산을 시작하고, 이미 계산이 시작되었으면 다음의 결과를 기다립니다. 계산. 결과가 곧 나오거나 아직 계산 중일 수 있습니다. 그러나 Future.get() 메서드에는 투명합니다.

注意:我们在代码中用到了ConcurrentHashMap的putIfAbsent(arg, ft)方法,为啥不能直接用put方法呢?因为如果使用put方法,那么仍然会出现两个线程计算出相同的值的问题。我们可以看到compute方法中的if代码块是非原子的,如下所示:

 
 

자바

코드 복사

// compute方法中的if部分代码 if (f == null) { Callable<V> eval = new Callable<V>() { @Override public V call() throws Exception { return computable.compute(arg); } }; FutureTask<V> ft = new FutureTask<>(eval); f = cache.putIfAbsent(arg, ft); if (f == null) { f = ft; ft.run(); } }

因此两个线程仍有可能在同一时间调用compute方法来计算相同的值,只是概率比较低。即两个线程都没有在缓存中找到期望的值,因此都开始计算。而引起这个问题的原因复合操作(若没有则添加)是在底层的Map对象上执行的,而这个对象无法通过加锁来确保原子性,所以需要使用ConcurrentHashMap中的原子方法putIfAbsent,避免这个问题

1.4 테스트 코드

원래는 캐시를 사용하는 경우와 사용하지 않는 경우의 속도 비교를 보여주기 위해 동적 그래프를 만들고 싶었지만 결과 그래프가 너무 커서 업로드할 수 없으므로 테스트 코드 리더가 직접 확인하도록 합니다.

 
 

자바

코드 복사

public static void main(String[] args) throws InterruptedException { Computable<Integer, List<String>> cache = arg -> { List<String> res = new ArrayList<>(); for (int i = 0; i < arg; i++) { Thread.sleep(50); res.add("zhongjx==>" + i); } return res; }; PerfectMemoizer<Integer, List<String>> memoizer = new PerfectMemoizer<>(cache); new Thread(new Runnable() { @Override public void run() { List<String> compute = null; try { compute = memoizer.compute(100); System.out.println("zxj 第一次计算100的结果========: " + Arrays.toString(compute.toArray())); compute = memoizer.compute(100); System.out.println("zxj 第二次计算100的结果: " + Arrays.toString(compute.toArray())); } catch (InterruptedException e) { throw new RuntimeException(e); } } }).start(); System.out.println("zxj====>start===>"); }

테스트 코드에서는 Thread.sleep() 메서드를 사용하여 시간이 많이 걸리는 작업을 시뮬레이트합니다. 캐시를 사용하지 않는 경우를 테스트하려면 f = cache.putIfAbsent(arg, ft);아래 그림과 같이 이 코드에 주석을 달기만 하면 됩니다.

결론: 캐시를 사용하면 계산 결과를 빨리 얻을 수 있고 캐시를 사용하지 않으면 각 계산에 시간이 걸립니다.

2. 동시성 기술 요약

지금까지: 확장 가능하고 효율적인 캐시가 설계되었으므로 지금까지 동시 프로그래밍 기술을 다음과 같이 요약할 수 있습니다.

1. 도메인이 변경 가능한 경우가 아니면 도메인을 최종 유형으로 선언하십시오. 즉, 도메인을 설계할 때 도메인이 변경 가능한지 변경 불가능한지 고려해야 합니다. 잠금 또는 보호 복제와 같은 메커니즘을 추가합니다. 3. 각각의 가변 변수를 잠금으로 보호 4. 모든 변수를 동일한 불변 조건으로 보호할 경우 동일한 잠금을 사용 5. 복합 연산 실행 중에는 잠금을 유지 6. 설계 과정에서 스레드 안전 고려

Supongo que te gusta

Origin blog.csdn.net/BASK2312/article/details/131305725
Recomendado
Clasificación