컴퓨터 프로그램에서는 잠금을 획득한 경우에만 해당 리소스를 사용할 수 있습니다.
컴퓨터 하단의 잠금 구현은 CPU에서 제공하는 CAS 명령(비교 및 swsp)에 따라 메모리 주소에 대해 원래 값과 수정하려는 값을 비교합니다. 값이 성공적으로 수정되었는지 여부는 잠금이 포착되었는지 여부를 나타냅니다.
jvm에는 일반적으로 사용되는 두 가지 잠금이 있습니다
synchronized는 Java에서 제공하는 키워드 잠금으로 개체, 클래스 및 메서드를 잠글 수 있습니다.
JDK1.6 이후에는 동기화가 최적화되었으며 바이어스 잠금 및 경량 잠금 모드가 추가되었습니다. 이제 동기화 잠금의 작동 논리는 다음과 같습니다.
초기 잠금 시 바이어스 잠금이 추가됩니다. "biased" 마지막으로 잠금을 획득한 스레드"는 Biased 잠금 아래에서 CAS를 통해 직접 잠금을 획득하게 됩니다. 이 모드는 동일한 잠금을 반복적으로 획득하는 단일 스레드의 처리량을 크게 향상시킵니다. Java 관계자에 따르면 대부분의 잠금 경합은 동일한 스레드에서 발생합니다.
바이어스 잠금 CAS 획득에 실패하면 현재 스레드가 바이어스 잠금에 의해 바이어스된 스레드와 다르다는 의미이며, 바이어스 잠금은 경량 잠금으로 업그레이드하는 것입니다. spin CAS를 통해 잠금을 획득하세요.
스핀 획득이 실패하면 잠금은 가중치 잠금으로 업그레이드되고 잠금을 기다리는 모든 스레드는 JVM에 의해 일시 중단됩니다. 잠금이 해제된 후 통합 알림을 통해 깨어납니다. JVM을 시도한 다음 CAS 잠금을 시도합니다. 실패하면 계속 정지됩니다.
분명히 편향된 잠금 설계의 목적은 "공식 Java 관점에서 볼 때 동일한 잠금에 대한 대부분의 경합은 동일한 스레드에서 발생합니다."
경량 잠금 설계의 목적은 "단기적으로 스핀 CAS를 통해 잠금 경합을 얻을 수 있으며, 단시간에 CPU 스핀 소모가 스레드 정지 및 웨이크업 소모보다 적다"는 것입니다.
Weight Lock은 초기 최적화 전의 동기화 로직입니다.
ReentrantLock이라고 하면 JUC의 AQS에 대해 이야기해야 합니다.
AQS의 전체 이름은 AbstractQueueSynchronizer입니다. JUC의 거의 모든 도구 클래스는 구현을 위해 AQS를 사용합니다.
AQS는 Java의 추상 클래스이지만 본질적으로 Java의 아이디어를 구현한 것입니다.
AQS의 구현 논리는 다음과 같습니다.
대기열 구성
대기열은 잠금을 기다려야 하는 스레드를 유지합니다.
헤드 노드는 항상 잠금을 보유하는(또는 잠금을 보유하는) 노드입니다. 리소스), 대기 노드는 헤드 노드 다음에 순차적으로 연결됩니다.
헤드 노드가 잠금을 해제한 후 대기 중인 노드를 순서대로 깨우고 해당 노드는 다시 잠금을 획득하려고 시도합니다.
동기화 잠금 최적화 이후 AQS의 본질은 동기화와 크게 다르지 않으며 둘의 성능도 크게 다르지 않으므로 현재 AQS의 특징은 다음과 같습니다.
은 Java API에서 구현됩니다. 레벨 잠금이므로 다양한 동시 도구 클래스를 구현할 수 있으며 작업이 더 유연합니다. 타임아웃 및 기타 메커니즘을 제공하므로 작업이 유연하므로 교착 상태가 발생하기 쉽지 않습니다. 교착 상태가 발생하면 jstack이 교착 상태 표시기를 표시하지 않기 때문에 문제 해결이 더 어려워집니다.
공정한 잠금을 달성할 수 있지만 동기화는 불공정한 잠금이어야 합니다.
JavaApi 레이어에서 구현한 잠금이므로 인터럽트에 응답할 수 있습니다.
여기서 ReentrantLock은 실제로 JavaApi 계층에서 동기화된 구현이라고 할 수 있습니다.
공유 잠금(S) 및 배타적 잠금(X)
동작 범위
의도 잠금 프로토콜은 다음과 같습니다. 트랜잭션이 테이블 행에 대한 공유 잠금을 획득하기 전에 먼저 테이블에 대한 IS 잠금 또는 더 강력한 잠금을 획득해야 합니다.
트랜잭션이 테이블 행에 대한 배타적 잠금을 획득하려면 먼저 테이블에 대한 IX 잠금을 획득해야 합니다.
테이블 잠금에 대한 공유 또는 배타적 잠금을 획득하기 전에 테이블의 공유 잠금을 확인해야 합니다.
테이블 잠금과 의도 잠금의 상호 배제 규칙은 다음과 같습니다.
X IX S IS기능은: 테이블 잠금 획득 시 의도 잠금을 통해 획득 가능 여부를 빠르게 확인할 수 있습니다.
행 레벨 잠금 획득 시 해당 의도 잠금을 먼저 획득하게 되기 때문에, 다른 트랜잭션은 각 행을 스캔하지 않고도 테이블 잠금 획득 시 의도 잠금을 통해 신속하게 판단할 수 있습니다.
특별히 주의할 점은 의도 잠금이 중첩될 수 있다는 것입니다. 즉, 여러 잠금이 있을 수 있다는 것입니다. 예를 들어 T1 트랜잭션은 의도 잠금 IX1과 행 수준 잠금 X1을 획득하고 T2 트랜잭션은 여전히 의도 잠금을 획득할 수 있습니다. IX2 및 행 수준 잠금 X2. 따라서 테이블 수준 잠금이 획득될 때까지 의도 잠금만 확인되지 않습니다.
레코드 잠금은 SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE일 때 다른 트랜잭션에 의해 행 데이터가 변경되는 것을 방지하기 위해 인덱스에 적용됩니다.
innodb는 각 테이블에 대해 숨겨진 인덱스를 생성하므로 인덱스가 없어도 레코드 잠금이 계속 적용됩니다.
레코드 잠금은 가장 기본적인 행 잠금입니다.
Gap 잠금은 인덱스 값 뒤에 있는 행을 잠그는 데 사용되며 업데이트를 위해 index=?인 테이블에서 선택할 때 적용됩니다. 1인덱스 노드 관련 행이 잠기므로 다른 트랜잭션이 데이터를 삽입하는 것을 방지.
그러나 업데이트 데이터가 존재하지 않더라도 업데이트 문을 방지하지는 않습니다.
이 잠금은 레코드 잠금과 간격 잠금을 결합한 것입니다. 즉, 업데이트를 위해 인덱스가?인 테이블에서 선택하면 삽입 및 레코드 잠금을 방지하는 간격 잠금이 발생합니다. 인덱스에서 이 데이터를 삭제합니다. 이 다음 키는 업데이트를 선택할 때 일반적으로 두 잠금이 함께 나타나기 때문에 이 두 잠금을 일반화한 것입니다.
의도 잠금과 유사한 의도 잠금을 삽입합니다. 이는 업데이트를 위한 선택에서는 발생하지 않지만 삽입이 동시에 발생할 때 발생하는 특수한 간격 잠금입니다. 예를 들어 두 트랜잭션이 인덱스 범위 [4,7]을 동시에 삽입하면 의도를 얻습니다. 동시에 범위를 잠급니다. 예를 들어 A: insert-5, B: insert-7과 같이 트랜잭션이 차단되면 두 트랜잭션은 이때 차단되지 않습니다.
삽입 의도 잠금은 A: 삽입-5, B: 삽입-7과 같은 일반적인 Gap 잠금 잠금 간격에서 삽입이 자주 차단되는 것을 방지하도록 설계된 특수 Gap 잠금입니다. 5와 7은 이때 두 번째 트랜잭션이 차단되지만 의도 잠금을 삽입하면 삽입된 행이 충돌하는 경우에만 두 번째 트랜잭션이 차단됩니다. 차단하다.
자동 증가 잠금인 이 잠금은 자동 증가 기본 키가 있는 테이블의 기본 키가 원자 자동 증가를 유지하도록 보장하기 위한 테이블 수준 삽입 잠금입니다.
자물쇠와 관련하여 모든 사람은 다양한 자물쇠 디자인과 작동의 원리와 모델에 대해 더 많이 이해해야 합니다. 그래야 이해가 깊어진 후 더 깊고 철저하게 사용할 수 있습니다.
우리 모두 알고 있듯이 mysql 트랜잭션은 반복 삽입을 방지하는 데 아무런 도움이 되지 않으며 고유 인덱스는 단점이 많으므로 일반적으로 비즈니스에서는 사용하지 않는 것이 가장 좋습니다. , 중복을 방지하기 위해 삽입하는 일반적인 방법은 보다 일반적인 작성 방법인 분산 잠금을 사용하는 것입니다.
final WeekendNoticeReadCountDO weekendNoticeReadCountDO = weekendNoticeReadRepositoryService.selectByNoticeId(noticeRequestDTO.getNoticeId()); if (weekendNoticeReadCountDO == null) { final String lockKey = RedisConstant.LOCK_WEEKEND_READ_COUNT_INSERT + ":" + noticeRequestDTO.getNoticeId(); ClusterLock lock = clusterLockFactory.getClusterLockRedis( RedisConstant.REDIS_KEY_PREFIX, lockKey ); if (lock.acquire(RedisConstant.REDIS_LOCK_DEFAULT_TIMEOUT)) { //double check final WeekendNoticeReadCountDO weekendNoticeReadCountDO = weekendNoticeReadRepositoryService.selectByNoticeId(noticeRequestDTO.getNoticeId()); if (weekendNoticeReadCountDO == null) { try { lock.execute(() -> { WeekendNoticeReadCountDO readCountDO = new WeekendNoticeReadCountDO(); readCountDO.setNoticeId(noticeRequestDTO.getNoticeId()); readCountDO.setReadCount(1L); readCountDO.setCreateTime(new Date()); readCountDO.setUpdateTime(new Date()); weekendNoticeReadRepositoryService.insert(readCountDO); return true; }); } catch (ApiException err) { throw err; } catch (Exception e) { log.error("插入", e); throw new ApiException(ErrorEnum.SERVER_ERROR.getCode(), "服务端出错"); } } else { weekendNoticeReadRepositoryService.noticeCountAdd(weekendNoticeReadCountDO); } } else { log.warn("redis锁获取超时,key:{}", lockKey); throw new ApiException(ErrorEnum.SERVER_ERROR.getCode(), "服务器繁忙,请稍后重试"); } }
잠금을 획득한 후 대기 후에 획득할 수도 있습니다. 이때 잠금을 해제한 이전 스레드가 데이터를 삽입했을 수도 있으므로 잠금 내부에 데이터가 다시 존재하는지 확인이 필요합니다.
이 쓰기 방법은 고유성을 요구하는 대부분의 쓰기 시나리오에 적합합니다.
교착 상태를 피하는 방법은 무엇인가요? 가장 간단하고 효과적인 방법은 **자물쇠를 자물쇠 안에 넣지 않는 것입니다. 즉, 자물쇠를 중첩 인형으로 사용하는 것이 아니라 단독으로 사용하는 것이 가장 좋습니다.
데이터베이스와 같은 일부 암시적 잠금에도 주의하세요.
트랜잭션 A:
[5,7]을 삽입하고 인텐션 잠금을 삽입합니다.
업데이트 업데이트 [100,150] 선택, 갭락.
거래 B:
업데이트 [90,120] 선택, 갭 잠금.
[4,6]을 삽입하고 의도 잠금을 삽입합니다.
이때, 동시 시나리오에서 A가 [5,7]의 gap lock을 보유하고 있고 트랜잭션 B [90,120]의 gap lock을 기다리고 있는 상황이 발생할 수 있습니다. 교착 상태로 이어집니다.
**
비즈니스 코드를 작성하고 일부 도구 클래스나 캐시 클래스를 정의할 때 부주의하기 쉽고 유사한 문제가 발생합니다.
예를 들어 정적 캐시를 구축할 때 ConcurrentHashMap의 putIfAbsent 등의 메소드를 사용하지 않고, 구축 시 잠금도 사용하지 않기 때문에 아래 스레드는 위 스레드가 넣자마자 바로 삭제되거나, 캐시가 삭제됩니다. 두 번 건설됩니다.
이는 Redis 잠금의 예제 코드에서도 언급됩니다.
스레드 A가 이때 잠금을 획득하고 A의 실행 시간이 너무 길어서 시간 초과로 인해 잠금이 자동으로 해제됩니다. 그러다가 A가 실행을 완료한 후 잠금이 해제되는데 이때는 아직 자체적으로 보유하고 있는지 판단하지 못하여 B가 보유하고 있던 잠금이 삭제되고 이때 C는 다시 잠금을 획득하게 된다. 동시에 처형됐다.
위 내용은 Java 및 Mysql 잠금과 관련된 지식 포인트는 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!