并发 - 锁机制

本文详细阐述了乐观锁的工作原理、版本号机制和CAS算法,讨论了ABA问题的解决方案,以及死锁的概念和避免策略。此外,介绍了偏向锁、轻量级锁以及锁优化技术,如自旋锁、锁粗化和锁消除,以提升并发性能。
摘要由CSDN通过智能技术生成

一、乐观锁

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源是否被其它线程修改了。一般使用版本号机制或 CAS 算法,在 Java 中java.util.concurrent.atomic包下面的原子变量类(比如AtomicInteger、LongAdder)就是使用了乐观锁的一种实现方式 CAS 实现的。

  • 版本号机制
    一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

  • CAS算法:compare and swap(比较与交换)
    不使用锁的情况下实现多线程之间的变量同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS是一个原子操作,底层依赖于一条CPU原子指令cmpxchg,CAS算法涉及到三个操作数:

    • 要更新的值 V
    • 预期值 A
    • 拟写入的新值 B

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

乐观锁的缺点
1 ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,并不能说明它没有被其他线程修改过,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

解决:atomic包中提供了AtomicStampedReference来解决,其内部有数据应用和版本号,主要思路是添加一个版本号,原来路径A->B->A就变成了1A->2B->3A。

2 循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。

3 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里(共享变量)来进行 CAS 操作.

二、死锁

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,这两个线程就会互相等待而进入死锁状态。
在这里插入图片描述

public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                  System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

死锁必须具备以下四个条件:

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

如何避免线程死锁?
只要破坏产生死锁的四个条件中的其中一个就可以了。

  • 破坏互斥条件
    这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的)。

  • 破坏请求与保持条件
    一次性申请所有的资源。

  • 破坏不剥夺条件
    占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

  • 破坏循环等待条件
    按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

三、偏向锁

在没有多线程竞争的情况下,尽量减少不必要的轻量级锁的执行。轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里,存储锁偏向的线程ID。

偏向锁获取过程:

  • (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
  • (2)如果为可偏向状态,则测试线程ID是否指向当前线程。
  • (3)如果并未指向当前线程,通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID。
  • (4)如果CAS获取偏向锁失败,则表示有竞争(至少有过其他线程曾经获得过偏向锁,因为线程不会主动去释放偏向锁)。当到达全局安全点(safepoint)时,会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态(标志位为“01”),然后重新偏向新的线程;如果线程仍然活着,撤销偏向锁后升级到轻量级锁状态(标志位为“00”),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
  • (5)执行同步代码。
    在这里插入图片描述

四、轻量级锁:

加锁

  1. 在代码进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),JVM先在当前线程的栈桢中创建用于存储锁记录(Lock Record)的空间,并将对象头中的Mark Word复制到锁记录中。官方称为Displaced Mark Word。如果为偏向锁状态,会在当前偏向锁的线程栈中创建锁记录。
  2. 复制成功后,虚拟机将使用CAS尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word(对象的地址存储)。
  3. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
    在这里插入图片描述
  4. 如果更新失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,是就说明当前线程已经拥有了这个对象的锁,可直接进入同步块继续执行。否则说明多个线程竞争锁,若当前只有一个等待线程,则可通过自旋稍微等待一下。 但是当自旋超过一定的次数,或者等待线程过多时,轻量级锁膨胀为重量级锁,锁标志的状态值变为10。

解锁:
通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word,如果成功则线程成功释放锁,如果CAS失败则说明存在其他线程竞争此时锁已经膨胀为重量级锁,此时释放锁并且唤醒被阻塞的线程。
在这里插入图片描述
注:当高平发时,就关闭这个锁升级过程,直接重量级锁,省去锁转换开销。

总结:

  • 一个对象刚开始实例化的时候,没有任何线程来访问。它是可偏向的,所以当第一个线程来访问它的时候,它会偏向这个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
  • 一旦有第二个线程访问这个对象,表明在这个对象上已经存在竞争了。检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程。如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁;如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
  • 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

五、锁优化

5.1 自旋锁、适应性自旋锁
Java的线程是映射到操作系统原生线程之上的,阻塞或唤醒一个线程需要从用户态到内核态之间的来回转换。

自旋锁是指在一个线程获取锁失败,并不会立即阻塞线程,不放弃CPU时间片,而是通过一段循环,进行尝试获取锁状态,自旋次数并不是固定写死的,可以通过之前这把锁的获得情况来动态调整,如果之前有成功获取这把锁的线程,那么JVM会认为这把锁是能够被获取的,此时会自适应的增加一些自旋次数,如果之前没有一个线程成功获取这把锁,JVM为了避免无意义的循环带来的资源浪费,会选择减少自旋次数,或者说不去自旋,而直接阻塞。

5.2 锁粗化
将多次连续在一起的加锁、解锁操作合并到一起,即将多个连续的锁扩展成一个范围更大的锁

public String getString(String s1,String s2){
 	StringBuffer sb=new StringBuffer();
  	sb.append(s1);
  	sb.append(s2);
 	return sb.toString();
}

StringBuffer类的append方法是synchronized关键字修饰的,那么每次循环体执行都要进行加锁与解锁操作,这样无疑会带来很大的性能损失,因此JVM会将当前加在append方法上的锁的范围进行粗化,粗化到第一个append方法之前到第二个append方法之后。

5.3 锁销除
虚拟机即时编译器在运行时,若检查到共享数据不可能存在竞争,则进行消除的操作。

  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Quartz中,数据库集群是一种高可用性的解决方案。它允许多个Quartz实例共享同一个数据库,并且能够自动协调任务的执行,从而保证任务的可靠性和稳定性。 但是,在多个Quartz实例同时操作同一个数据库时,必须确保它们之间的数据一致性。这就需要使用锁机制来保证数据的正确性和安全性。 Quartz提供了两种类型的锁:悲观锁和乐观锁。它们分别采用不同的方式来保证数据的一致性。 1. 悲观锁 悲观锁是一种悲观的认为并发环境下会出现冲突的锁机制。它在操作数据时,会先加锁,然后再进行操作,操作完成后再释放锁。 在Quartz中,悲观锁是通过数据库中的行级锁来实现的。当一个Quartz实例要对某个任务进行操作时,它会先获取该任务的行级锁,然后再进行操作。其他实例在此期间无法获取该任务的锁,从而保证了数据的一致性。 2. 乐观锁 乐观锁是一种乐观的认为并发环境下不会出现冲突的锁机制。它在操作数据时,不会加锁,而是通过版本号等方式来判断数据是否发生了变化。 在Quartz中,乐观锁是通过版本号来实现的。每个任务都有一个版本号,当一个Quartz实例要对某个任务进行操作时,它会先获取该任务的版本号,然后进行操作。如果在此期间该任务的版本号发生了变化,则说明其他实例已经对该任务进行了操作,当前实例的操作会失败,需要重新获取版本号并重试。这样可以保证数据的一致性。 总的来说,悲观锁适用于高并发、数据冲突严重的场景,但是会带来较大的性能开销;而乐观锁适用于并发量较小、数据冲突不严重的场景,性能开销较小。在Quartz中,可以根据具体的业务需求选择合适的锁机制来保证数据的正确性和安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值