>  기사  >  Java  >  Java의 다양한 잠금 목록

Java의 다양한 잠금 목록

爱喝马黛茶的安东尼
爱喝马黛茶的安东尼앞으로
2019-08-14 17:30:532675검색

Java의 다양한 잠금 목록

자물쇠 분류 소개

낙관적 잠금과 비관적 잠금

잠금의 매크로 분류는 낙관적 잠금과 비관적 잠금입니다. 낙관적 잠금 및 비관적 잠금은 특정 잠금을 참조하지 않지만(Java에는 낙관적 잠금 또는 비관적 잠금이라고 하는 특정 잠금 구현 이름이 없음) 동시 상황에서 두 가지 다른 전략을 참조합니다.

Optimistic Lock은 데이터를 얻으러 갈 때마다 다른 사람들이 데이터를 수정하지 않을 것이라고 생각합니다. 그러면 잠겨있지 않을 것입니다. 하지만 데이터를 업데이트하려면 업데이트하기 전에 다른 사람이 데이터를 읽고 업데이트하는 사이에 데이터를 수정했는지 확인해야 합니다. 수정되었다면 다시 읽고, 다시 업데이트를 시도하고, 업데이트가 성공할 때까지 위의 단계를 반복합니다(물론 업데이트에 실패한 스레드도 업데이트 작업을 포기할 수 있습니다).

비관적 잠금은 매우 비관적입니다. 데이터를 얻으러 갈 때마다 다른 사람이 데이터를 수정할 것이라고 생각합니다. 따라서 데이터를 검색할 때마다 잠깁니다.

이렇게 하면 다른 사람이 데이터를 얻을 때 비관적 잠금이 해제될 때까지 차단됩니다. 데이터를 얻으려는 스레드는 다시 잠금을 얻은 다음 데이터를 얻게 됩니다.

비관적 잠금은 트랜잭션을 차단하고 낙관적 잠금은 롤백 및 재시도를 수행하며 각각 장점과 단점이 있으며, 서로 다른 시나리오에 적응하는 차이만 있습니다. 예를 들어, 낙관적 잠금은 쓰기가 상대적으로 적은 상황, 즉 충돌이 거의 발생하지 않는 시나리오에 적합합니다. 이렇게 하면 잠금 비용을 절약하고 시스템의 전체 처리량을 늘릴 수 있습니다. 그러나 충돌이 자주 발생하면 상위 계층 애플리케이션이 계속 재시도하게 되어 실제로 성능이 저하되므로 이 시나리오에는 비관적 잠금이 더 적합합니다.

요약: 낙관적 잠금은 쓰기가 상대적으로 적고 충돌이 거의 발생하지 않는 시나리오에 적합하며, 쓰기가 많고 충돌이 많은 시나리오는 비관적 잠금에 적합합니다.

낙관적 잠금의 기초 --- CAS

낙관적 잠금을 구현하려면 CAS라는 개념을 이해해야 합니다.

CAS란 무엇인가요? 비교 및 교환(Compare-and-Swap), 즉 비교하고 대체하거나 비교하고 설정합니다.

비교: A 값을 읽고 B로 업데이트하기 전에 원래 값이 A인지 확인합니다(다른 스레드에 의해 수정되지 않았으므로 여기서 ABA 문제는 무시하세요).

교체: 그렇다면 A를 B로 업데이트하고 종료합니다. 그렇지 않은 경우 업데이트되지 않습니다.

위의 두 단계는 원자적 작업으로, CPU의 관점에서는 즉시 완료되는 것으로 이해할 수 있습니다.

CAS를 사용하면 낙관적 잠금을 구현할 수 있습니다.

public class OptimisticLockSample{
    
    public void test(){
        int data = 123; // 共享数据
        
        // 更新数据的线程会进行如下操作
        for (;;) {
            int oldData = data;
            int newData = doSomething(oldData);
            
            // 下面是模拟 CAS 更新操作,尝试更新 data 的值
            if (data == oldData) { // compare
                data = newData; // swap
                break; // finish
            } else {
                // 什么都不敢,循环重试
            }
        }   
    }
    
    /**
    * 
    * 很明显,test() 里面的代码根本不是原子性的,只是展示了下 CAS 的流程。
    * 因为真正的 CAS 利用了 CPU 指令。
    *  
    * */ 
    
}

CAS는 Java의 기본 메서드를 통해서도 구현됩니다.

public final class Unsafe {
    
    ...
    
    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object 
    var5);
    
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    
    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);  
    
    ...
}

위에 작성된 것은 낙관적 잠금을 간단하고 직관적으로 구현한 것입니다(정확히 말하면 낙관적 잠금 프로세스여야 합니다). 이는 여러 스레드가 동시에 읽을 수 있도록 합니다(잠금 작업이 전혀 없기 때문입니다). 데이터가 업데이트됩니다.

단 하나의 스레드만 데이터를 성공적으로 업데이트할 수 있으므로 다른 스레드는 롤백하고 다시 시도하게 됩니다. CAS는 CPU 명령을 사용하여 하드웨어 수준에서 원자성을 보장하여 잠금과 같은 효과를 얻습니다.

낙관적 잠금의 전체 프로세스에서 잠금 및 잠금 해제 작업이 없다는 것을 알 수 있으므로 낙관적 잠금 전략을 잠금 없는 프로그래밍이라고도 합니다. 즉, 낙관적 잠금은 실제로 "잠금"이 아니며 단지 반복하고 재시도하는 CAS 알고리즘일 뿐입니다.

관련 추천: "

java Development Tutorial

"

Spin lock

synchronized and Lock 인터페이스

Java에서 잠금을 구현하는 방법에는 두 가지가 있습니다. 하나는 동기화된 키워드를 사용하는 것이고, 다른 하나는 다음을 사용하는 것입니다. Lock 인터페이스의 구현 클래스입니다.

기사에서 좋은 비교를 봤습니다. 매우 생생합니다. 동기화된 키워드는 모든 운전 요구를 충족할 수 있는 자동 변속기와 같습니다.

하지만 드리프트나 다양한 고급 작업 등 좀 더 고급 작업을 하고 싶다면 Lock 인터페이스 구현 클래스인 매뉴얼 기어가 필요합니다.

그리고 각 Java 버전에서 다양한 최적화를 거쳐 동기화가 매우 효율적이 되었습니다. 단지 Lock 인터페이스의 구현 클래스만큼 사용하기가 편리하지 않을 뿐입니다.

동기화 잠금 업그레이드 프로세스가 최적화의 핵심입니다: 편향 잠금-> 경량 잠금-> 중량 잠금

class Test{
    private static final Object object = new Object(); 
    
    public void test(){
        synchronized(object) {
            // do something        
        }   
    }
    
}
synchronized 키워드를 사용하여 특정 코드 블록을 잠글 때는 처음에 객체를 잠급니다(즉, 위 코드의 객체)는 헤비급 잠금이 아니라 편향된 잠금입니다.

편향된 잠금은 문자 그대로 "획득하기 위해 첫 번째 스레드를 향해 편향되는" 잠금을 의미합니다. 스레드가 동기화된 코드 블록을 실행한 후에는 바이어스 잠금을 적극적으로 해제하지 않습니다. 두 번째로 동기화된 코드 블록에 도달하면 스레드는 이때 잠금을 보유하고 있는 스레드가 자신인지 확인하고(잠금을 보유하고 있는 스레드 ID는 객체 헤더에 저장되어 있음), 그렇다면 계속 실행됩니다. 보통. 이전에 해제된 적이 없기 때문에 여기에서 다시 잠글 필요가 없으며, 하나의 스레드가 처음부터 끝까지 잠금을 사용하고 있다면 편향된 잠금은 추가 오버헤드가 거의 없으며 성능이 매우 높다는 것은 분명합니다.

一旦有第二个线程加入锁竞争,偏向锁转换为轻量级锁(自旋锁)。锁竞争:如果多个线程轮流获取一个锁,但是每次获取的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程获取锁的时候,发现锁已经被占用,需要等待其释放,则说明发生了锁竞争。

在轻量级锁状态上继续锁竞争,没有抢到锁的线程进行自旋操作,即在一个循环中不停判断是否可以获取锁。获取锁的操作,就是通过 CAS 操作修改对象头里的锁标志位。先比较当前锁标志位是否为释放状态,如果是,将其设置为锁定状态,比较并设置是原子性操作,这个是 JVM 层面保证的。当前线程就算持有了锁,然后线程将当前锁的持有者信息改为自己。

假如我们获取到锁的线程操作时间很长,比如会进行复杂的计算,数据量很大的网络传输等;那么其它等待锁的线程就会进入长时间的自旋操作,这个过程是非常耗资源的。其实这时候相当于只有一个线程在有效地工作,其它的线程什么都干不了,在白白地消耗 CPU,这种现象叫做忙等。(busy-waiting)。所以如果多个线程使用独占锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized 就是轻量级锁,允许短时间的忙等现象。这是一种择中的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。

显然,忙等是有限度的(JVM 有一个计数器记录自旋次数,默认允许循环 10 次,可以通过虚拟机参数更改)。如果锁竞争情况严重,

达到某个最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是通过 CAS 修改锁标志位,但不修改持有锁的线程 ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是上面说的忙等,即不会自旋),等待释放锁的线程去唤醒。在 JDK1.6 之前, synchronized直接加重量级锁,很明显现在通过一系列的优化过后,性能明显得到了提升。

JVM 中,synchronized 锁只能按照偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有把这个称为锁膨胀的过程),不允许降级。

可重入锁(递归锁)

可重入锁的字面意思是"可以重新进入的锁",即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归函数里这个锁会阻塞自己么?

如果不会,那么这个锁就叫可重入锁(因为这个原因可重入锁也叫做递归锁)。

Java 中以 Reentrant 开头命名的锁都是可重入锁,而且 JDK 提供的所有现成 Lock 的实现类,包括 synchronized 关键字锁都是可重入的。

如果真的需要不可重入锁,那么就需要自己去实现了,获取去网上搜索一下,有很多,自己实现起来也很简单。

如果不是可重入锁,在递归函数中就会造成死锁,所以 Java 中的锁基本都是可重入锁,不可重入锁的意义不是很大,我暂时没有想到什么场景下会用到;

注意:有想到需要不可重入锁场景的小伙伴们可以留言一起探讨。

下图展示一下 Lock 的相关实现类:

Java의 다양한 잠금 목록

公平锁和非公平锁

如果多个线程申请一把公平锁,那么获得锁的线程释放锁的时候,先申请的先得到,很公平。如果是非公平锁,后申请的线程可能先获得锁,是随机获取还是其它方式,都是根据实现算法而定的。

对 ReentrantLock 类来说,通过构造函数可以指定该锁是否是公平锁,默认是非公平锁。因为在大多数情况下,非公平锁的吞吐量比公平锁的大,如果没有特殊要求,优先考虑使用非公平锁。

而对于 synchronized 锁而言,它只能是一种非公平锁,没有任何方式使其变成公平锁。这也是 ReentrantLock 相对于 synchronized 锁的一个优点,更加的灵活。

以下是 ReentrantLock 构造器代码:

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock 内部实现了 FairSync 和 NonfairSync 两个内部类来实现公平锁和非公平锁。

可中断锁

字面意思是"可以响应中断的锁"。

首先,我们需要理解的是什么是中断。 Java 中并没有提供任何可以直接中断线程的方法,只提供了中断机制。那么何为中断机制呢?

线程 A 向线程 B 发出"请你停止运行"的请求,就是调用 Thread.interrupt() 的方法(当然线程 B 本身也可以给自己发送中断请求,

即 Thread.currentThread().interrupt()),但线程 B 并不会立即停止运行,而是自行选择在合适的时间点以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java 的中断不能直接终止线程,只是设置了状态为响应中断的状态,需要被中断的线程自己决定怎么处理。这就像在读书的时候,老师在晚自习时叫学生自己复习功课,但学生是否复习功课,怎么复习功课则完全取决于学生自己。

回到锁的分析上来,如果线程 A 持有锁,线程 B 等待持获取该锁。由于线程 A 持有锁的时间过长,线程 B 不想继续等了,我们可以让线程 B 中断。

自己或者在别的线程里面中断 B,这种就是 可中段锁。

在 Java 中, synchronized 锁是不可中断锁,而 Lock 的实现类都是 可中断锁。从而可以看出 JDK 自己实现的 Lock 锁更加的灵活,这也就是有了 synchronized 锁后,为什么还要实现那么些 Lock 的实现类。

Lock 接口的相关定义:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    
    void unlock();
    Condition newCondition();
}

其中 lockInterruptibly 就是获取可中断锁。

共享锁

字面意思是多个线程可以共享一个锁。一般用共享锁都是在读数据的时候,比如我们可以允许 10 个线程同时读取一份共享数据,这时候我们可以设置一个有 10 个凭证的共享锁。

在 Java 中,也有具体的共享锁实现类,比如 Semaphore。 

互斥锁

字面意思是线程之间互相排斥的锁,也就是表明锁只能被一个线程拥有。

在 Java 中, ReentrantLock、synchronized 锁都是互斥锁。

读写锁

读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)。

在 Java 中, ReadWriteLock 接口只规定了两个方法,一个返回读锁,一个返回写锁。

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();
    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

文章前面讲过[乐观锁策略](#乐观锁的基础 --- CAS),所有线程可以随时读,仅在写之前判断值有没有被更改。

读写锁其实做的事情是一样的,但是策略稍有不同。很多情况下,线程知道自己读取数据后,是否是为了更改它。那么为何不在加锁的时候直接明确。

这一点呢?如果我读取值是为了更新它(SQL 的 for update 就是这个意思),那么加锁的时候直接加写锁,我持有写锁的时候,别的线程。

无论是读还是写都需要等待;如果读取数据仅仅是为了前端展示,那么加锁时就明确加一个读锁,其它线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器加 1)。

虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略。因为读写锁并没有在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁。乐观锁特指无锁编程。

JDK 内部提供了一个唯一一个 ReadWriteLock 接口实现类是 ReentrantReadWriteLock。通过名字可以看到该锁提供了读写锁,并且也是可重入锁。

总结

Java 中使用的各种锁基本都是悲观锁,那么 Java 中有乐观锁么?结果是肯定的,那就是 java.util.concurrent.atomic 下面的原子类都是通过乐观锁实现的。如下:

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;
}

通过上述源码可以发现,在一个循环里面不断 CAS,直到成功为止。

参数介绍

-XX:-UseBiasedLocking=false 关闭偏向锁
JDK1.6 
-XX:+UseSpinning 开启自旋锁
-XX:PreBlockSpin=10 设置自旋次数 
JDK1.7 之后 去掉此参数,由 JVM 控制

本文转自:https://blog.tommyyang.cn/2019/08/13/%E5%8F%B2%E4%B8%8A%E6%9C%80%E5%85%A8-Java-%E4%B8%AD%E5%90%84%E7%A7%8D%E9%94%81%E7%9A%84%E4%BB%8B%E7%BB%8D-2019/

위 내용은 Java의 다양한 잠금 목록의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

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