首页 > 编程语言 >Java中的各种锁

Java中的各种锁

时间:2023-09-02 22:06:11浏览次数:39  
标签:各种 Java synchronized CAS 线程 自旋 操作

img

悲观锁、乐观锁、公平锁、非公平锁、独享锁、共享锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁

面试必备:深入了解Java中乐观锁和悲观锁的秘密 (qq.com)

通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现! (qq.com)

Java锁最全详解:乐观锁/悲观锁+公平锁/非公平锁+独享锁/共享锁 (qq.com)

Java中加锁方式有两种,一种是synchronized关键字,另一种是用Lock接口的实现类。

synchronized关键字是自动档,可以满足一切日常驾驶需求。但是如果你想要玩漂移或者各种骚操作,就需要手动档了——各种Lock的实现类。

图片

ReentrantLock、ReadLock、WriteLock 是Lock接口最重要的三个实现类。对应了“可重入锁”、“读锁”和“写锁”。

ReadWriteLock是一个工厂接口,而ReentrantReadWriteLock是ReadWriteLock的实现类,它包含两个静态内部类ReadLock和WriteLock。这两个静态内部类又分别实现了Lock接口。

悲观锁&乐观锁

锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁,而是在并发情况下的两种不同策略。

乐观锁假设多个线程之间很少会发生冲突,因此在读取时不会加锁,而在更新数据时会检查是否有其他线程修改了数据。如果没有冲突就执行更新操作;

悲观锁假设多个线程之间经常会发生冲突,因此在读取数据时会加锁,防止其他线程修改数据,直到操作完成后才释放锁。

乐观锁的实现方式

乐观锁的实现方式常见的有版本号机制CAS(比较并交换)机制

  • 版本号机制:在数据库表中添加一个版本号字段,每次更新操作时都会将版本号+1。当线程要更新数据时,会先读取数据的版本号,然后进行更新操作。在提交更新时,若刚才读取到的version值等于当前数据库中的version值才会更新;否则重试更新操作,直到更新成功。
  • CAS:Compare And Swap(比较与交换)。就是用一个预期值和要更新的变量进行比较,两值相等才会更新。CAS是一个原子操作,底层依赖于一条CPU对的原子指令。CAS包含三个操作数:内存地址V,旧的预期值A和新的值B。CAS操作首先读取内存地址V中的值,如果该值等于旧的预期值A,那么将内存地址V中的值更新为新的值B;否则,不进行任何操作。CAS操作失败会重试。

Java中的Atomic类就是基于CAS机制实现的乐观锁,比如AtomicIntegerAtomicLong等(java.util.concurrent.atomic包里面的原子类都是利用乐观锁实现的。)。

悲观锁的实现方式

就是在读取数据时直接加锁,防止其他线程修改数据。常见的悲观锁实现方式包括:

  • synchronized关键字:synchronized关键字是Java中最基本的锁机制,他可以用来修饰方法或者代码块,保证同一时间只有一个线程可以执行被锁定的代码

  • ReentrantLock类:ReentrantLock是Java中高级的锁机制,他提供了更灵活的锁定方式,可以实现公平锁和非公平锁,支持可重入特性,同时还可以配合条件变量等功能进行更复杂的线程同步操作。

乐观锁与悲观锁的选择

乐观锁适用于并发写比较少的场景,因为乐观锁不会阻塞读操作,适合读多写少的场景。

悲观锁适合并发写比较多的场景,因为悲观锁可以有效地阻塞其他线程的读和写操作,保证数据的一致性。但是,悲观锁有可能会引起线程竞争、降低性能。

乐观锁存在的问题

CAS虽然是高效的原子操作,但是存在三大问题:

ABA问题:CAS在比较和替换时只考虑了值是否相等,而没有考虑到值的版本信息。如果一个值在操作过程中被修改了两次,从A->B->A,那么CAS会认为值没有变化,从而进行操作。

ABA问题的解决思路是在变量前追加上版本号或者时间戳。JDK1.5以后的AtomicStampedReference类就是用来解决ABA问题的,其中的CompareAndSet()方法就是首先检查当前饮用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志设置为给定的更新值。

自旋时间过长:CAS算法在失败时会一直自旋,等待共享变量可用,如果共享变量一直不可用,就会出现自旋时间过长的问题,浪费CPU资源

如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。

只能保证单个变量的原子性:CAS只能保证单个变量的原子性

如果需要多个变量的原子操作,就需要使用锁等其他方式进行保护。

从JDK1.5开始提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

公平锁 VS 非公平锁

图片

就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。

java jdk并发包中的ReentrantLock可以指定构造函数的boolean类型来创建公平锁,比如:公平锁可以使用new ReentrantLock(true)实现。

图片

非公平锁上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程,缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

java jdk并发包中的ReentrantLock的构造函数的默认就是采用非公平锁的实现,使用new ReentrantLock(false)来实现,与上面的公平锁相反的声明方式。

独享锁 VS 共享锁

图片

是指该锁一次只能被一个线程所持有,比如:刚刚谈到的ReentrantLock就是独享锁。

图片

是指该锁可被多个线程所持有,Lock的另一个实现类ReadWriteLock,其读锁就是共享锁,其写锁却是独享锁。

ReadWriteLock的读锁(共享锁)可保证并发读非常高效,但读写,写读 ,写写的过程是互斥的。

这样设计的原因是:就是尽最大的解放并发读的操作,因为读占据了更大的访问请求,我只会在涉及少部分写的操作的时候,才考虑独享锁,从而提升并发的效率。

自旋锁

有一种锁叫自旋锁。所谓自旋,说白了就是一个 while(true) 无限循环。

刚刚的乐观锁就有类似的无限循环操作,那么它是自旋锁吗?

不是。尽管自旋与 while(true) 的操作是一样的,但还是应该将这两个术语分开。“自旋”这两个字,特指自旋锁的自旋。

synchronized锁升级:偏向锁 → 轻量级锁 → 重量级锁

前面提到,synchronized关键字就像是汽车的自动档,现在详细讲这个过程。一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡一样。那么自旋锁在哪里呢?这里的轻量级锁就是一种自旋锁

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

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

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销

显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。

一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。

可重入锁(递归锁)

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

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

可中断锁

可中断锁,字面意思是“可以响应中断的锁”。

这里的关键是理解什么是中断。Java并没有提供任何直接中断某线程的方法,只提供了中断机制。何谓“中断机制”?线程A向线程B发出“请你停止运行”的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java的中断不能直接终止线程,而是需要被中断的线程自己决定怎么处理。

这好比是父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己

在Java中,synchronized就是不可中断锁,而Lock的实现类都是可中断锁,可以简单看下Lock接口。

/* Lock接口 */
public interface Lock {

    void lock(); // 拿不到锁就一直等,拿到马上返回。

    void lockInterruptibly() throws InterruptedException; // 拿不到锁就一直等,如果等待时收到中断请求,则需要处理InterruptedException。

    boolean tryLock(); // 无论拿不拿得到锁,都马上返回。拿到返回true,拿不到返回false。

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 同上,可以自定义等待的时间。

    void unlock();

    Condition newCondition();
}

标签:各种,Java,synchronized,CAS,线程,自旋,操作
From: https://blog.51cto.com/u_15872442/7334875

相关文章

  • Java 乘等赋值运算
    下面这个题目是在一公司发过来的,如果你对Java的赋值运算比较了解的话,会很快知道答案的。  这个运算符在Java里面叫做乘等或者乘和赋值操作符,它把左操作数和右操作数相乘赋值给左操作数。例如下面的:density*=invertedRatio; 其实等于的就是 density=density*invertedR......
  • JavaFX+SpringBoot桌面项目并打包成exe可执行文件
    1.创建标准Maven工程2.引入依赖<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation=&quo......
  • 无涯教程-JavaScript - LOGINV函数
    LOGINV函数替代Excel2010中的LOGNORM.INV函数。描述该函数返回x的对数正态累积分布函数的逆函数,其中ln(x)的分布通常带有参数mean和standard_dev。如果p=LOGNORMDIST(x,...),则LOGINV(p,...)=x使用对数正态分布来分析对数转换的数据。语法LOGINV(probability,mean,s......
  • 无涯教程-JavaScript - HYPGEOMDIST函数
    HYPGEOMDIST函数替代Excel2010中的HYPGEOM.DIST函数。描述该函数返回超几何分布。HYPGEOMDIST返回给定样本数量,给定样本数量,总体成功率和总体数量的概率。将HYPGEOMDIST用于具有有限总体的问题,其中每个观察输出都是成功或失败,并且给定大小的每个子集的选择可能性均等。......
  • 学习JavaScript的路径
    学习JavaScript的路径可以按照以下步骤进行:了解基本概念:首先学习JavaScript的基本概念,包括变量、数据类型、运算符、数组、对象、循环和条件语句等。可以通过阅读相关的教材、在线课程或者参考W3Schools和MDN文档等来学习。学习控制DOM元素:学习如何使用JavaScript控制DOM元素,包......
  • 代码扫描提示:java: Compilation failed: internal java compiler error
    检查Idea中编译的版本和项目的是否一致 ......
  • 无涯教程-JavaScript - GAMMAINV函数
    GAMMAINV函数取代了Excel2010中的GAMMA.INV函数。描述该函数返回伽马累积分布的倒数。如果p=GAMMADIST(x,...),则GAMMAINV(p,...)=x您可以使用此函数来研究变量的分布可能偏斜的变量。语法GAMMAINV(probability,alpha,beta)争论Argument描述Required/OptionalP......
  • Java 服务器cup占用率过高 以及 内存泄漏排查方法
    cup占用率过高常见能够引起CPU100%异常的情况都有哪些?Java 内存不够或者溢出导致GCoverheadlimitexceeded。代码中互相竞争导致的死锁。特别耗费计算资源的操作,比如正则匹配,Java中的正则匹配默认有回溯问题,复杂的正则匹配引起的CPU异常。死循环引起的CPU高度密集计算。针对第1......
  • 前缀树(Trie)的java实现
    前缀树prefixtree,又叫做trie。关键Feature如下:树形结构根节点为空结点包含Node[]nexts;//size26intisEnd;//有多少个字符串以当前字符结尾intpass;//多少个字符串经过了当前字符常用操作insertdeletesearch//字符串在前缀树中出现的次数prefi......
  • 无涯教程-JavaScript - GAMMADIST函数
    GAMMADIST函数取代了Excel2010中的GAMMA.DIST函数。描述该函数返回伽马分布。您可以使用此功能来研究可能具有偏斜分布的变量。伽马分布通常用于排队分析。语法GAMMADIST(x,alpha,beta,cumulative)争论Argument描述Required/OptionalXThevalueatwhichyouwantt......