English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

java에서 잠금의 성능 향상 방법

java에서 잠금의 성능 향상 방법

우리는 제품이 겪는 문제에 대해 해결책을 생각하려고 노력하지만, 이 기사에서는 분리 락, 병행 데이터 구조, 데이터 보호 대신 코드 보호, 락의 작용 범위를 축소하는 등 몇 가지 일반적인 기술을 공유하겠습니다. 이러한 기술은 우리가 도구 없이도死锁을 검출할 수 있게 합니다.

문제의 원인은 락이 아니라 락 간의 경쟁입니다

일반적으로 다중 스레드 코드에서 성능 문제를 겪을 때, 대부분의 경우 락 문제를 불만을 내는 경향이 있습니다. 결국 락은 프로그램의 실행 속도를 저하시키고, 저 확장성이 잘 알려져 있습니다. 따라서 이러한 '통상적인 지식'을 가지고 코드 최적화를 시작하면, 나중에 싫어하는 동기화 문제가 발생할 가능성이 매우 높습니다.

따라서 경쟁 락과 비경쟁 락의 차이를 이해하는 것은 매우 중요합니다. 하나의 스레드가 다른 스레드가 실행 중인 동기화 블록이나 메서드에 진입하려고 할 때, 락 경쟁이 발생합니다. 이 스레드는 강제로 대기 상태로 들어가고, 첫 번째 스레드가 동기화 블록을 실행하고 모니터를 해제할 때까지 대기합니다. 동시에 하나의 스레드만이 동기화 코드 영역을 실행하려고 시도할 때, 락은 비경쟁 상태를 유지합니다.

실제로 비경쟁 상황과 대부분의 응용 프로그램에서 JVM은 동기화를 최적화했습니다. 비경쟁 락은 실행 중에 추가적인 비용을 초래하지 않습니다. 따라서 성능 문제로 락을 불만을 내지 말고, 락의 경쟁을 불만을 내야 합니다. 이 이해가 되면, 경쟁 가능성을 줄이거나 경쟁 지속 시간을 줄이기 위해 무엇을 할 수 있는지 살펴보겠습니다.

데이터보다 코드보다 보호

스레드 보안 문제를 해결하는 빠른 방법은 전체 메서드의 접근성에 락을 추가하는 것입니다. 예를 들어 아래의 예제에서, 온라인 포커 게임 서버를 만들기 위해 이 방법을 시도합니다:

class GameServer {
 public Map<<String, List<Player>> tables = new HashMap<String, List<Player>>();
 public synchronized void join(Player player, Table table) {
  if (player.getAccountBalance() > table.getLimit()) {
   List<Player> tablePlayers = tables.get(table.getId());
   if (tablePlayers.size() < 9) {
    tablePlayers.add(player);
   }
  }
 }
 public synchronized void leave(Player player, Table table) {/*단축을 위해 본문을 건너뛰었습니다*/}
 public synchronized void createTable() {/*단축을 위해 본문을 건너뛰었습니다*/}
 public synchronized void destroyTable(Table table) {/*단축을 위해 본문을 건너뛰었습니다*/}
}

저자의 의도는 좋습니다. 새로운 플레이어가 테이블에 참여할 때, 테이블에 있는 플레이어 수가 테이블이 수용할 수 있는 총 플레이어 수를 초과하지 않도록 보장해야 합니다.9.

하지만 이 해결책은 실제로 언제든지 플레이어가 테이블에 참여하는 것을 제어해야 합니다. 서버 접근량이 적을 때도 마찬가지입니다. 락 해제를 기다리는 스레드는 시스템의 경쟁 이벤트를 자주 유발하게 됩니다. 계정 잔액과 테이블 제한 검사를 포함한 락 블록은 호출 작업의 비용을 크게 증가시키며, 이는 경쟁 가능성과 지속 시간을 증가시킵니다.

처음으로 해결하는 것은 데이터를 보호하는지 확인하는 것입니다. 메서드 선언에서 메서드 본체로 이동된 동기화 선언이 아닙니다. 이 간단한 예제에서는 큰 변화가 없을 수 있지만, 우리는 전체 게임 서비스 인터페이스 위에서 고려해야 하며, 단순한 join() 메서드만을 고려하지 않습니다.

class GameServer {
 public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();
 public void join(Player player, Table table) {
  synchronized (tables) {
   if (player.getAccountBalance() > table.getLimit()) {
    List<Player> tablePlayers = tables.get(table.getId());
    if (tablePlayers.size() < 9) {
     tablePlayers.add(player);
    }
   }
  }
 }
 public void leave(Player player, Table table) {/* 단축을 위해 본문을 건너뛰었습니다 */}
 public void createTable() {/* 단축을 위해 본문을 건너뛰었습니다 */}
 public void destroyTable(Table table) {/* 단축을 위해 본문을 건너뛰었습니다 */}
}

처음에는 작은 변화로 보일 수 있지만, 이는 전체 클래스의 동작 방식에 영향을 미칠 수 있습니다. 플레이어가 언제든지 테이블에 참여하면, 이전의 동기화 메서드는 전체 GameServer 인스턴스에 락을 추가하며, 이는 테이블을 떠나려는 플레이어와의 경쟁을 유발합니다. 락을 메서드 선언에서 메서드 본체로 이동시키면 락의 로드를 지연시키며, 락 경쟁의 가능성을 줄입니다.

락의 작용 범위 축소

지금, 데이터보다 프로그램을 보호해야 한다는 것을 확신했을 때, 우리는 필요한 곳에만 락을 추가해야 합니다 - 예를 들어 위의 코드가 재구조화된 후에:

public class GameServer {
 public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();
 public void join(Player player, Table table) {
  if (player.getAccountBalance() > table.getLimit()) {
   synchronized (tables) {
    List<Player> tablePlayers = tables.get(table.getId());
    if (tablePlayers.size() < 9) {
     tablePlayers.add(player);
    }
   }
  }
 }
 //간략성을 위해 건너뛴 메서드
}

이렇게 해서 플레이어 계정 잔액 검사(IO 작업을 유발할 수 있는 가능성이 있습니다)와 시간이 걸릴 수 있는操作을 일으킬 수 있는 코드는 락 제어 범위 밖으로 이동되었습니다. 주의하세요, 지금은 락은 플레이어 수가 테이블이 수용할 수 있는 인원 수를 초과하지 않도록 방지하는 데 사용되며, 계정 잔액 검사는 이 보호 조치의 일부가 아닙니다.

분리된 락

위 예제의 마지막 줄 코드에서 명확하게 볼 수 있듯이: 전체 데이터 구조는 동일한 락에 보호되고 있습니다. 이 데이터 구조에서 수천 개의 패테이블이 있을 수 있으며, 우리는 패테이블의 인원수가 용량을 초과하지 않도록 보호해야 하는 경우에도 경쟁 사건이 발생할 가능성이 여전히 높습니다.

이에 대한 간단한 방법은 각 패테이블에 분리된 락을 도입하는 것입니다. 다음 예제와 같이:

public class GameServer {
 public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();
 public void join(Player player, Table table) {
  if (player.getAccountBalance() > table.getLimit()) {
   List<Player> tablePlayers = tables.get(table.getId());
   synchronized (tablePlayers) {
    if (tablePlayers.size() < 9) {
     tablePlayers.add(player);
    }
   }
  }
 }
 //간략성을 위해 건너뛴 메서드
}

이제, 우리는 모든 패테이블 대신 단일 패테이블의 접근성을 동기화하여 경쟁 가능성이 显著적으로 감소합니다. 구체적인 예로, 현재 데이터 구조에는 다음과 같습니다100개의 패테이블 인스턴스가 있을 때, 이제 경쟁 가능성은 이전보다 작습니다100배.

스레드 안전한 데이터 구조 사용

다른 개선할 수 있는 방법 중 하나는 전통적인 단일 스레드 데이터 구조를 버리고 명확히 스레드 안전으로 설계된 데이터 구조를 사용하는 것입니다. 예를 들어, ConCurrentHashMap을 사용하여 패테이블 인스턴스를 저장할 때, 코드는 다음과 같을 수 있습니다:

public class GameServer {
 public Map<String, List<Player>> tables = new ConcurrentHashMap<String, List<Player>>();
 public synchronized void join(Player player, Table table) {/*메서드 본문은 간략화를 위해 건너뜀*/}
 public synchronized void leave(Player player, Table table) {/*메서드 본문은 간략화를 위해 건너뜀*/}
 public synchronized void createTable() {
  Table table = new Table();
  tables.put(table.getId(), table);
 }
 public synchronized void destroyTable(Table table) {
  tables.remove(table.getId());
 }
}

join()와 leave() 메서드 내의 동기 블록은 이전 예제와 동일하게 유지되며, 이는 단일 테이블 데이터의 완전성을 보장하기 위해서입니다. ConcurrentHashMap은 이 점에서 어떤 도움도 제공하지 않습니다. 그러나 우리는 increaseTable()와 destroyTable() 메서드에서 ConcurrentHashMap을 사용하여 새 테이블을 생성하고 제거하는 데 사용하며, 모든 이러한 작업은 ConcurrentHashMap에 대해 완전 동기화되어 있으며, 이는 우리가 테이블의 수를 병행적으로 추가하거나 감소할 수 있게 합니다.

기타 권장 사항과 팁

락의可见성을 낮춥니다. 위의 예제에서 락은 public으로 선언되어 있어, 다른 의도 있는 사람들이 여러분의 신중하게 설계된 모니터에 락을 걸어 여러분의 작업을 파괴할 수 있을 수 있습니다.

java.util.concurrent.locks의 API를 통해 기타 이미 구현된 락 전략이 있는지 확인하고, 이를 통해 위의 솔루션을 개선해 보세요.

원자 연산을 사용합니다. 위의 단순한 증가 카운터는 락을 요구하지 않습니다. 위의 예제에서 AtomicInteger를 Integer 대신 사용하는 것이 더 적합합니다.

마지막으로, Plumber의 자동 dead lock 검출 솔루션을 사용하든지, 수동으로 스레드 드롭에서 해결 방법의 정보를 얻든지, 이 문서가 여러분의 락 경쟁 문제를 해결하는 데 도움이 되길 바랍니다.

읽어주셔서 감사합니다. 여러분의 이 사이트에 대한 지원에 감사합니다!

추천 합니다