锁的由来,及乐观锁悲观锁的应用实现优化

242 阅读3分钟

本篇文章我们就来聊一聊什么是锁,为什么需要锁,究竟如何加锁,当然我说的也可能有不足之处,欢迎大家积极在评论区探讨。

1 锁的由来

1.1 并发

并发其实很简单,并发就是多线程并发访问,就是有N多个线程都来获取相同的资源,

1.2 串行化

我们说的线程安全也就是多线程并发访问资源时,保证其有序性,那么如何来保证有序性呢?

1.3 锁

锁就是保证并发有序性的一种手段。及加锁可以实现线程安全。

2 那么锁该怎么加呢?

加锁时候其实就需要去考虑场景了

2.1 乐观锁 读多写少

乐观锁本质是不加锁,我们认为遇到并发写的可能性会很低,每次拿到的数据我们认为是不会被修改的,所以是不会阻塞的,但是为了保证数据的一致性,我们在并发写的时候需要再次确认一下,拿到的是不是最新的数据。如果不是,再去取一份,然后再次验证,直到验证通过后,才可以写入。

2.2 被观锁 读少写多

悲观锁就是认为写多,读少,每次读写数据时都会上锁,让其他线程想要获取资源的线程进入阻塞状态,直到当前获取到资源的线程释放资源,下一个线程才能获取到资源。

3 锁的实现

3.1 乐观锁

Java中常用的乐观锁就是CAS算法+自旋(需要考虑ABA问题,CPU性能)。

适应性自旋锁(是JVM对自旋的一种优化,会选择一个最佳时间进行自旋,而不是一直保持自旋消耗CPU资源)

实现:

// 乐观锁
public class AtomicInteger extends Number implements Serializable {  
​
    private volatile int value;  
​
    public final int get() {  
        return value;  
    }  
​
    public final int getAndIncrement() {  
        for (;;) {  
            int current = get();  
            int next = current + 1;  
            if (compareAndSet(current, next))  
                return current;  
        }  
    }  
​
    public final boolean compareAndSet(int expect, int update) {  
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
    }  
}    

JDK1.5开始atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

也有一些是基于版本号解决ABA问题,版本号只增不减。

3.2 悲观锁

对于Java来说,synchronized关键字和Lock的实现类都是悲观锁。

RetreenLock的原理是基于AQS,AQS是先尝试使用CAS自旋,如果获取不到就会转化为悲观锁。

悲观锁往往是需要通过阻塞线程实现的,想要更为深刻的理解悲观锁,我们就需要知道线程的各个状态。

线程的状态,如下图所示:

image.png

  • 线程A 访问资源,发现此时没有锁

  • 线程A 获取到资源,锁住资源,不允许其他线程操作

  • 线程B、C...访问资源,发现资源被锁,然后B、C...进入阻塞状态

  • 当线程A操作完成,释放资源的锁

  • 此时B、C会进入就绪状态进行竞争,能力强的得到锁(这里就引入公平锁【按先后顺序获取锁】、非公平锁的概念【强势的获取到锁】)在锁竞争时,就可能出现死锁的情况。

    锁竞争又会带出优化锁的知识点【

    • 降低锁的粒度
    • 减少持有锁的时间
    • 锁分离(读写锁分离)
    • 锁升级(对加锁的一种优化操作)】
  • 线程B 获取到锁,线程状态重新恢复到运行态,未获取到锁的继续恢复阻塞,等待时机

  • ...