>  기사  >  Java  >  Java에서 잠금을 구현하는 방법은 무엇입니까?

Java에서 잠금을 구현하는 방법은 무엇입니까?

WBOY
WBOY앞으로
2023-05-12 08:37:051613검색

1. 비관적 잠금

이름에서 알 수 있듯이 데이터 수정에 대한 보수적인 태도와 다른 사람도 데이터를 수정할 것이라는 믿음을 의미합니다. 따라서 데이터를 조작하면 해당 조작이 완료될 때까지 데이터가 잠깁니다. 대부분의 경우 비관적 잠금은 작업의 최대 배타성을 보장하기 위해 데이터베이스의 잠금 메커니즘을 사용합니다. 잠금 시간이 너무 길면 다른 사용자가 오랫동안 접근할 수 없게 되어 프로그램의 동시 접근에도 영향을 미치게 되며, 특히 데이터베이스 성능 오버헤드에도 큰 영향을 미치게 됩니다. 긴 트랜잭션의 경우 이러한 오버헤드는 종종 견딜 수 없습니다.

독립형 시스템인 경우 JAVA 고유의 동기화 키워드를 사용하여 메서드나 동기화 블록에 추가하여 리소스를 잠글 수 있습니다. 분산 시스템인 경우 데이터베이스 자체 잠금 메커니즘을 사용하여 이를 달성할 수 있습니다.

select * from 表名 where id= #{id} for update

비관적 잠금을 사용할 때는 잠금 수준에 주의해야 합니다. MySQL innodb가 잠길 때는 기본 키 또는 (인덱스 필드)만 명시적으로 지정됩니다. 그렇지 않으면 테이블 잠금이 실행됩니다. 전체 테이블이 잠기므로 현재 성능이 저하됩니다. 비관적 잠금을 사용할 때 mysql은 기본적으로 자동 커밋 모드를 사용하므로 MySQL 데이터베이스의 자동 커밋 속성을 꺼야 합니다. 비관적 잠금은 쓰기가 많은 시나리오에 적합하며 높은 동시성 성능이 필요하지 않습니다.

2. 낙관적 잠금

낙관적 잠금은 문자 그대로의 의미에서 짐작할 수 있듯이 데이터를 조작할 때 매우 낙관적이며, 다른 사람이 동시에 데이터를 수정하지 않을 것이라고 생각하므로 낙관적 잠금은 업데이트를 제출할 때만 잠기지 않습니다. . 데이터 충돌이 공식적으로 감지됩니다. 충돌이 발견되면 오류 메시지가 반환되고 사용자는 빠른 실패 메커니즘을 통해 무엇을 할지 결정할 수 있습니다. 그렇지 않으면 이 작업을 수행하십시오.

데이터 읽기, 쓰기 검증, 데이터 쓰기의 세 단계로 나누어집니다.

독립형 시스템이라면 JAVA의 CAS를 기반으로 구현할 수 있습니다. CAS는 하드웨어 비교 및 ​​교환을 통해 구현되는 원자적 연산입니다.

분산 시스템인 경우 버전과 같은 버전 번호 필드를 데이터베이스 테이블에 추가할 수 있습니다.

update 表 
set ... , version = version +1 
where id= #{id} and version = #{version}

작업 전 먼저 레코드의 버전 번호를 읽어보세요. 업데이트 시 SQL 문을 통해 버전 번호를 비교하여 일치하는지 확인하세요. 일관성이 있으면 데이터를 업데이트하세요. 그렇지 않으면 버전을 다시 읽고 위 작업을 다시 시도합니다.

3. JAVA의 분산 잠금

synchronized, ReentrantLock 등은 모두 단일 애플리케이션의 단일 시스템 배포의 리소스 상호 배제 문제를 해결합니다. 비즈니스의 급속한 발전에 따라 단일 애플리케이션이 분산 클러스터로 진화하면 다중 스레드와 다중 프로세스가 서로 다른 시스템에 분산되어 원래의 단일 시스템 동시성 제어 잠금 전략이 무효화됩니다

이 시점에서 우리는 머신 간 동시성 제어 문제를 해결하기 위해 분산 잠금을 도입합니다. 머신의 상호 배제 메커니즘은 공유 리소스에 대한 액세스를 제어합니다.

분산 잠금에 필요한 조건:

  • 잠금의 기반이 되는 독립형 시스템과 동일한 리소스 상호 배제 기능

  • 고성능 잠금 획득 및 해제

  • 높음 가용성

  • 재진입 가능

  • 교착 상태를 방지하기 위한 잠금 실패 메커니즘이 있습니다

  • Non-blocking, 잠금 획득 여부에 관계없이 빠르게 반환할 수 있어야 합니다

있습니다. 데이터베이스, Redis 및 Zookeeper 등을 기반으로 하는 다양한 구현 방법. 여기서는 주류 Redis 기반 구현에 대해 이야기합니다.

Locking

SET key unique_value  [EX seconds] [PX milliseconds] [NX|XX]

원자적 명령을 통해 실행이 성공하고 1이 반환되면 잠금에 성공했습니다. 참고: Unique_value는 잠금 작업을 다른 클라이언트와 구별하기 위해 클라이언트에서 생성한 고유 식별자입니다. 잠금 해제에 특히 주의하세요. 그렇다면 잠금 해제 및 삭제가 허용됩니다. 결국 다른 클라이언트가 추가한 잠금은 삭제할 수 없습니다.

잠금 해제: 잠금 해제에는 원자성을 보장하기 위해 Lua 스크립트를 사용해야 하는 두 가지 명령 작업이 있습니다.

// 先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Redis의 고성능을 바탕으로 Redis는 현재 주류 구현 방법이기도 한 분산 잠금을 구현합니다. 그러나 모든 것에는 장단점이 있습니다. 잠긴 서버가 다운되고 슬레이브 노드가 데이터를 백업할 시간이 없으면 다른 클라이언트도 잠금을 얻을 수 있습니다.

이 문제를 해결하기 위해 Redis는 공식적으로 분산 잠금 Redlock을 설계했습니다.

기본 아이디어: 클라이언트가 여러 독립 Redis 노드를 사용하여 병렬로 잠금을 적용하도록 합니다. 잠금 작업이 절반 이상의 노드에서 성공적으로 완료되면 클라이언트가 분산 잠금을 성공적으로 획득한 것으로 간주하고, 그렇지 않으면 잠금이 실패했습니다. .

4. 재진입 잠금

재진입 잠금은 재귀 잠금이라고도 하며, 동일한 스레드가 잠금을 획득하기 위해 외부 메서드를 호출할 때 내부 메서드에 들어갈 때 자동으로 잠금을 획득한다는 의미입니다.

객체 잠금 또는 클래스 잠금 내부에 카운터가 있습니다. 스레드가 잠금을 획득할 때마다 잠금 해제 시 카운터가 +1됩니다.

잠금 수는 잠금 해제 수에 해당합니다. 잠금 및 잠금 해제는 쌍으로 나타납니다.

ReentrantLock과 Java의 동기화는 모두 재진입 잠금입니다. 재진입 잠금의 한 가지 이점은 교착 상태를 어느 정도 방지할 수 있다는 것입니다.

5、自旋锁

自旋锁是采用让当前线程不停地在循环体内执行,当循环的条件被其他线程改变时才能进入临界区。自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不断增加时,性能下降明显,因为每个线程都需要执行,会占用CPU时间片。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

自旋锁缺点:

  • 可能引发死锁。

  • 可能占用 CPU 的时间过长。

我们可以设置一个 循环时间 或 循环次数,超出阈值时,让线程进入阻塞状态,防止线程长时间占用 CPU 资源。JUC 并发包中的 CAS 就是采用自旋锁,compareAndSet 是CAS操作的核心,底层利用Unsafe对象实现的。

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

如果内存中 var1 对象的var2字段值等于预期的 var5,则将该位置更新为新值(var5 + var4),否则不进行任何操作,一直重试,直到操作成功为止。

CAS 包含了Compare和Swap 两个操作,如何保证原子性呢?CAS 是由 CPU 支持的原子操作,其原子性是在硬件层面进行控制。

特别注意,CAS 可能导致 ABA 问题,我们可以引入递增版本号来解决。

6、独享锁

独享锁,也有人叫它排他锁。无论读操作还是写操作,只能有一个线程获得锁,其他线程处于阻塞状态。

缺点:读操作并不会修改数据,而且大部分的系统都是 读多写少,如果读读之间互斥,大大降低系统的性能。下面的 共享锁 会解决这个问题。

像Java中的 ReentrantLock 和 synchronized 都是独享锁。

7、共享锁

共享锁是指允许多个线程同时持有锁,一般用在读锁上。读锁的共享锁可保证并发读是非常高效的。读写,写读 ,写写的则是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。

8、读锁/写锁

如果对某个资源是读操作,那多个线程之间并不会相互影响,可以通过添加读锁实现共享。如果有修改动作,为了保证数据的并发安全,此时只能有一个线程获得锁,我们称之为 写锁。读读是共享的;而 读写、写读 、写写 则是互斥的。

像 Java中的 ReentrantReadWriteLock 就是一种 读写锁。

9、公平锁/非公平锁

公平锁:多个线程按照申请锁的顺序去获得锁,所有线程都在队列里排队,先来先获取的公平性原则。

优点:所有的线程都能得到资源,不会饿死在队列中。

缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,CPU 唤醒下一个阻塞线程有系统开销。

Java에서 잠금을 구현하는 방법은 무엇입니까?

非公平锁:多个线程不按照申请锁的顺序去获得锁,而是同时以插队方式直接尝试获取锁,获取不到(插队失败),会进入队列等待(失败则乖乖排队),如果能获取到(插队成功),就直接获取到锁。

优点:可以减少 CPU 唤醒线程的开销,整体的吞吐效率会高点。

缺点:可能导致队列中排队的线程一直获取不到锁或者长时间获取不到锁,活活饿死。

Java 多线程并发操作,我们操作锁大多时候都是基于 Sync 本身去实现的,而 Sync 本身却是 ReentrantLock 的一个内部类,Sync 继承 AbstractQueuedSynchronizer。

像 ReentrantLock 默认是非公平锁,我们可以在构造函数中传入 true,来创建公平锁。

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

10、可中断锁/不可中断锁

可中断锁:指一个线程因为没有获得锁在阻塞等待过程中,可以中断自己阻塞的状态。不可中断锁:恰恰相反,如果锁被其他线程获取后,当前线程只能阻塞等待。如果持有锁的线程一直不释放锁,那其他想获取锁的线程就会一直阻塞。

内置锁 synchronized 是不可中断锁,而 ReentrantLock 是可中断锁。

ReentrantLock获取锁定有三种方式:

  • lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于阻塞状态,直到该线程获取锁。

  • tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false。

  • tryLock(긴 시간 초과, TimeUnit 단위), 잠금이 획득되면 즉시 true를 반환합니다. 다른 스레드가 잠금을 보유하고 있으면 대기 프로세스 중에 매개변수에 지정된 시간 동안 대기합니다. 잠금이 획득되면 true를 반환하고, 대기 시간이 초과되면 false를 반환합니다.

  • lockInterruptible()은 잠금이 획득되면 즉시 반환하고, 잠금이 획득되지 않으면 잠금이 획득되거나 스레드가 다른 스레드에 의해 중단될 때까지 스레드가 차단됩니다.

11. 분할 잠금

분할 잠금은 실제로 잠금의 세분성을 개선하는 것입니다. ConcurrentHashMap의 경우 분할 잠금을 통해 동시성이 달성됩니다. 효율적인 동시 작업을 달성합니다.

ConcurrentHashMap의 세그먼트 잠금은 Segment라고 하며 이는 HashMap(JDK7의 HashMap 구현)과 유사한 구조입니다. 즉, 내부적으로 Entry 배열을 가지며 배열의 각 요소는 연결된 목록입니다. 또한 ReentrantLock(세그먼트는 ReentrantLock을 상속함)입니다.

요소를 넣어야 할 때 전체 HashMap을 잠그는 것이 아니라 먼저 해시코드를 통해 어떤 세그먼트에 넣을지 알고 나서 이 세그먼트를 잠급니다. 넣지 않기 때문에 동일한 세그먼트 내에서 병렬 인서트가 지원됩니다.

12. 잠금 업그레이드(잠금 없음 | 바이어스 잠금 | 경량 잠금 | 중량 잠금)

JDK 1.6 이전에는 동기화가 여전히 상대적으로 효율성이 낮은 중량 잠금이었습니다. 그러나 JDK 1.6 이후 JVM에서는 잠금 획득 및 해제의 효율성을 높이기 위해 동기화를 최적화하고 바이어스 잠금 및 경량 잠금을 도입했습니다. 이후에는 잠금 없음, 바이어스 잠금 및 경량 수준 잠금의 네 가지 잠금 상태가 있습니다. , 헤비급 잠금 장치. 이 네 가지 상태는 경쟁을 통해 점차적으로 업그레이드되며 다운그레이드될 수 없습니다.

Java에서 잠금을 구현하는 방법은 무엇입니까?

Lock-free

Lock-free는 리소스를 잠그지 않습니다. 모든 스레드는 동일한 리소스에 액세스하고 수정할 수 있지만 동시에 하나의 스레드만 성공적으로 수정할 수 있습니다. 이것이 우리가 흔히 낙관적 잠금이라고 부르는 것입니다.

편향된 잠금

동기화된 코드 블록이 처음 실행될 때 CAS를 통해 개체 헤더의 잠금 플래그가 수정되고 잠금 개체가 편향된 잠금이 됩니다.

스레드가 동기화된 코드 블록에 접근하여 잠금을 획득하면 잠금 바이어스 스레드 ID가 Mark Word에 저장됩니다. 스레드가 동기화된 블록에 들어가고 나갈 때 더 이상 CAS 작업을 통해 잠금 및 잠금 해제되지 않지만 Mark Word가 현재 스레드를 가리키는 바이어스 잠금을 저장하는지 여부를 감지합니다. 경량 잠금의 획득 및 해제는 여러 CAS 원자 명령에 의존하는 반면 편향 잠금은 ThreadID를 대체할 때 하나의 CAS 원자 명령에만 의존하면 됩니다.

동기화된 코드 블록을 실행한 후 스레드는 바이어스 잠금을 적극적으로 해제하지 않습니다. 스레드는 두 번째로 동기화된 코드 블록을 실행할 때, 이때 잠금을 보유하고 있는 스레드가 자신인지(잠금을 보유하고 있는 스레드의 ID도 객체 헤더에 있음) 여부를 확인하고, 그렇다면 정상적으로 계속 실행됩니다. 이전에 잠금이 해제된 적이 없기 때문에 여기서 다시 잠글 필요가 없습니다. 바이어스 잠금은 추가 오버헤드가 거의 없으며 성능이 매우 높습니다.

바이어스 잠금은 다른 스레드가 바이어스 잠금을 놓고 경쟁하려고 할 때만 잠금을 해제합니다. 스레드는 바이어스 잠금을 적극적으로 해제하지 않습니다. 편향된 잠금의 취소와 관련하여 전역 안전 지점을 기다려야 합니다. 즉, 특정 시점에 바이트코드가 실행되지 않을 때 먼저 편향된 잠금을 소유한 스레드를 일시 중지한 다음 여부를 결정합니다. 잠금 개체가 잠겨 있습니다. 스레드가 활성화되지 않은 경우 개체 헤더는 잠금 없는 상태로 설정되고 편향된 잠금이 취소되어 잠금 없는(플래그 비트는 01) 또는 경량 잠금(플래그 비트는 00) 상태로 돌아갑니다.

편향된 잠금은 동기화된 코드 조각이 항상 동일한 스레드에 의해 액세스될 때, 즉 여러 스레드 간에 경쟁이 없을 때 스레드가 후속 액세스에서 자동으로 잠금을 획득하여 코드 획득 비용을 줄이는 것을 의미합니다. 잠그다. .

경량 잠금

현재 잠금은 바이어스 잠금입니다. 여러 스레드가 동시에 잠금을 놓고 경쟁하는 경우 바이어스 잠금이 경량 잠금으로 업그레이드됩니다. 경량 자물쇠는 경쟁이 존재하더라도 이상적으로는 경쟁 정도가 매우 낮으며 회전을 통해 자물쇠를 획득한다고 믿습니다.

경량 잠금 장치를 획득하는 상황은 두 가지입니다.

  • 바이어스 잠금 기능이 꺼진 경우.

  • 바이어스 잠금을 놓고 경쟁하는 여러 스레드로 인해 바이어스 잠금이 경량 잠금으로 업그레이드됩니다. 두 번째 스레드가 잠금 경쟁에 참여하면 편향된 잠금이 경량 잠금(스핀 잠금)으로 업그레이드됩니다.

경량 잠금 상태에서 잠금 경쟁을 계속합니다. 잠금을 잡지 못한 스레드는 잠금을 성공적으로 획득할 수 있는지 확인하기 위해 회전하고 계속 루프됩니다. 잠금을 획득하는 작업은 실제로 CAS를 통해 개체 헤더의 잠금 플래그를 수정하는 것입니다. 먼저 현재 잠금 플래그가 "해제"되었는지 비교하고, 그렇다면 "잠금"으로 설정합니다. 이 프로세스는 원자적입니다. 잠금이 확보되면 스레드는 현재 잠금 보유자 정보를 자체적으로 수정합니다.

重量级锁

如果线程的竞争很激励,线程的自旋超过了一定次数(默认循环10次,可以通过虚拟机参数更改),将轻量级锁升级为重量级锁(依然是 CAS  修改锁标志位,但不修改持有锁的线程ID),当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。

重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资。

13、锁优化技术(锁粗化、锁消除)

锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

举个例子:有个循环体,内部。

for(int i=0;i<size;i++){
    synchronized(lock){
        ...业务处理,省略
    }
}

经过锁粗化的代码如下:

synchronized(lock){
    for(int i=0;i<size;i++){
        ...业务处理,省略
    }
}

锁消除指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。

锁消除的依据是逃逸分析的数据支持,如 StringBuffer 的 append() 方法,或 Vector 的 add() 方法,在很多情况下是可以进行锁消除的,比如以下这段代码:

public String method() {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < 10; i++) {
        sb.append("i:" + i);
    }
    return sb.toString();
}

以上代码经过编译之后的字节码如下:

Java에서 잠금을 구현하는 방법은 무엇입니까?

从上述结果可以看出,之前我们写的线程安全的加锁的 StringBuffer 对象,在生成字节码之后就被替换成了不加锁不安全的 StringBuilder 对象了,原因是 StringBuffer 的变量属于一个局部变量,并且不会从该方法中逃逸出去,所以我们可以使用锁消除(不加锁)来加速程序的运行。

위 내용은 Java에서 잠금을 구현하는 방법은 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제