美文网首页
锁(一)

锁(一)

作者: 小小的coder | 来源:发表于2023-11-06 13:18 被阅读0次
    11.png

    乐观锁与悲观锁

    乐观锁和悲观锁是一个宏观上的分类,它并不是指特定的哪个锁。而是在并发情况下两种不同的策略。

    乐观锁
    就是很乐观,每次去使用数据的时候都认为不会有其他线程修改数据,所以不会上锁。在更新数据的时候,则会在更新之前先判断有没有别的线程更新了这个数据,如果有,则重新读取,再次尝试更新,直到更新成功,或者报错哦放弃更新。如果没有,则当前线程将自己修改的数据成功写入。

    悲观锁
    就是很悲观,认为每次去使用数据的时候一定有其他线程来修改数据,所以在获取数据的时候会先加锁,确保数据不被修改。这样其他线程拿数据的时候就会被挡住,直到锁释放。

    乐观锁在 Java 中是通过无锁编程实现,通常采用 CAS 算法,在 Java 中,synchronized 和 Lock 的实现类都是悲观锁。

    乐观锁基础--CAS
    CAS 全称 Compare-and-Swa,即 比较并替换。
    比较:读取到一个值 A,在将其更新为 B 之前,检查原来的值是否为 A(未被其他线程修改过)
    替换:如果是 A,则把 A 更新为 B,结束,否则不会更新。
    比较和替换都是原子操作,可以理解为瞬间完成,下面模仿写一个乐观锁的逻辑伪代码:

    public void test(){
    int data = 123; //数据
    //更新数据的线程会进行如下操作
    while(true){
    int oldData = data;
    int newData = doSomething(oldData);
    //模拟CAS操作
    if(data==oldData){ //比较,检查 data 有没有被改变,没有的话更新数据,否则一直循环判断比较
    data = newData;
    break;
    }
    }
    }

    Java 是通过 native 方法实现的 CAS。Android 中原子类就是使用 CAS 乐观锁,比如 AtomicInteger 等。

    乐观锁的特点是回滚重试,悲观锁的特点是阻塞事务,在写操作比较少的情况下,即冲突很少发生的场景中,使用乐观锁可以省去锁的开销,加大了系统的吞吐量。但是如果在冲突经常发生的情况下,乐观锁会不断进行重试,反而降低了性能,所以这时候悲观锁比较合适。

    所以总结:

    ● 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
    ● 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

    CAS 虽然高效,但是也存在三大问题:

    1. ABA 问题:即内存值原来是 A,后来被修改成 B,然后又被修改成 A,那么在 CAS 检查时会发现值没有变化,但实际上是变化了的。解决版本是在变量前面加版本号,每次更新时版本号加一,变化过程就变成了:
      A-B-A >> 1A-2B-3A
    2. 循环时间长开销大:CAS 如果长时间不成功,会导致其一直自旋,给 CUP 带来开销
    3. 只能保证一个共享变量的原子操作:对多个共享变量操作时,CAS 无法保证操作的原子性。

    synchronized(读:星隔来) 与 Lock interface
    Java 的两种加锁方式:一种是使用 synchronized 关键字,一种是实现 Lock 接口。

    synchronized :
    ● 修饰普通方法
    ● 修饰静态方法
    ● 修饰代码块
    synchronized 的锁升级过程

    class Test{
    private static final Object object = new Object();
    public void test(){
    synchronized(object) { // do something
    }
    }
    }

    当使用 synchronized 锁住某个代码块的时候,一开始锁对象(即上面 object)并不是重量级锁。而是偏向锁。

    偏向锁的字面意思就是 “偏向于第一个获取它的线程”的锁,线程执行完同步代码块之后,并不会主动释放偏向锁。

    当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己,如果是则正常往下执行。由于之前没有释放,这里就不需要再重新加锁,如果重头到尾都是一个线程在使用锁,很明显偏向锁几乎没有额外开销,性能极高。

    一旦第二个线程加入来锁竞争,偏向锁会转换为轻量级锁。

    锁竞争:如果多个线程轮流获取一个锁,但是每次获取的时候都很顺利,没有发生阻塞,就不存在锁竞争,只有当某个线程获取锁的时候,发现锁已经被占用,需要等待释放,则说明发生了锁竞争。

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

    当获取锁的线程操作时间很长时,比如进行复杂计算,那么其他等待锁的线程就会进入长时间的自旋操作,其实这时候相当于只有一个线程在工作,其他线程什么都做不了,这种现象称为 忙等。

    忙等 是有限度的,JVM 有一个计数器来记录自旋次数,默认允许循环 10 次。

    如果锁竞争严重,当某个线程的自旋次数达到最大时,会将轻量级锁升级为重量级锁(修改方法依然是通过CAS修改锁标志位,但不修改持有锁的线程 ID),当后续线程尝试获取锁时,会发现被占用的锁是重量级锁,则直接将自己挂起,等待释放锁的线程去唤醒。

    可重入锁(递归锁)
    可重入锁的意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。
    比如在一个递归函数里有加锁操作,那么递归函数会自己阻塞自己吗?如果不会,则这么锁就是可重入锁。

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

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

    如果没有特殊要求,优先考虑使用非公平锁。而对于 synchronized 锁而言,它只能是一种非公平锁,没有任何方式使其变成公平锁。

    可中断锁
    意思是可以响应中断的锁。
    Java 中没有提供任何可以直接中断线程的方法,只提供中断机制。

    当线程A 向线程B 发出停止运行请求时,就是调用 Thread.interrupt() 方法。但线程B 不会立即停止运行,而是自行选择在合适的时间点以自己的方式响应中断,也可以直接忽略此中断。
    也就是说,Java 不能直接中断线程,只是设置了状态为响应中断的状态,需要被中断的线程自己决定怎么处理。

    如果线程 A 持有锁,线程 B 等待持获取该锁。由于线程 A 持有锁的时间过长,线程 B 不想继续等了,我们可以让线程 B 中断自己或者在别的线程里面中断 B,这种就是 可中断锁。

    synchronized 锁是不可中断锁,而 Lock 的实现类都是 可中断锁。

    共享锁

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

    互斥锁

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

    相关文章

      网友评论

          本文标题:锁(一)

          本文链接:https://www.haomeiwen.com/subject/ixztwdtx.html