Java多线程锁技术漫谈:乐观锁VS悲观锁

Java多线程技术一直是Java程序员必备的核心技能之一。在Java多线程编程中,为了保证数据的一致性和安全性,常常需要使用锁的机制来防止多个线程同时修改同一个共享资源。锁是实现并发访问控制的一种机制,多线程之间共同访问共享资源的时候,悲观锁是最常见的方式,不过在高并发场景中,全局锁所带来的并发性阻塞问题也是不可避免的。为了解决这种问题,人们又引出了乐观锁的概念。在本文中,我们将详细探讨Java多线程中的乐观锁和悲观锁。

一、什么是悲观锁

悲观锁是多线程并发控制的一种机制。悲观锁一般都是基于数据库锁的实现方式实现的。当一个线程要对共享资源进行访问时,那么就会使用悲观锁定义好的机制获取该资源的锁。这个过程中,如果其他线程也想获取该锁,就需要等待当前线程释放锁之后才能获取该锁。当然,也可以通过设置超时时间等机制来避免死锁等问题。

在Java中,我们可以使用synchronized关键字实现悲观锁,synchronized可以在对象级别上使用,也可以在类级别上使用。当某个线程访问synchronized锁定的对象时,该对象的状态会被设置为被锁定状态,如果其他线程也想修改这个状态,就必须等待当前线程释放锁之后才能获取该锁。这种方式看似非常理想,但实际上只是针对小并发量的业务场景。

public class LockExample {
    
    
    private int count = 0;

    public synchronized void increment() {
    
    
        // some code here
        this.count++;
    }
}

在上面的代码中,使用了synchronized关键字来实现悲观锁。在increment()方法中,使用了this关键字来对整个方法进行加锁,确保在一个线程执行到该方法期间,不会有其他线程同时访问这个方法。

悲观锁的缺点是,由于每个线程在访问资源之前都需要获取锁,因此当并发性非常高的时候,会导致大量的线程在等待锁,从而让整个应用程序的性能急剧下降。
在高并发量的业务场景中,使用悲观锁就显得不太合适了,它很容易造成死锁等问题,从而导致应用程序的性能下降。

二、什么是乐观锁

如果说悲观锁是一种防守性的锁,那么乐观锁就是一种进攻性的锁,它尝试着最大程度地避免锁定资源。乐观锁更多地体现了一种“乐观”的思想,它认为多个线程访问相同的资源的概率并不是那么大,所以在访问共享资源的时候,它并不对该资源进行特别的锁定操作,而是直接针对该资源执行读取和修改操作,并在修改操作完成之后进行版本校验。如果读取到的版本号与当前版本号一致,表示操作成功,程序继续运行,否则表示已经有其他线程对该资源进行了修改,则需要进行Retry操作,重新读取版本号,然后再进行更新操作。

在Java当中,乐观锁的实现方式主要有两种,一种是CAS,即Compare And Swap,另一种是版本号机制(例如AtomicInteger类)。

使用Compare-And-Swap算法

CAS是一种通过让CPU底层内存操作指令实现原子操作的机制。CAS机制操作的原理是将当前内存中的值与CAS指令中的值进行比较,如果一致,则将当前内存中的值更新为新的值,如果不一致,则重新执行该操作。
Compare-And-Swap(CAS)算法是一种基于乐观锁的算法,能够实现非常高的并发性。基本思路是,在我们修改共享资源的时候,先读取这个资源当前的状态,然后对这个状态进行比对,如果状态没有发生改变,就更新这个资源的状态,否则忽略这次修改操作,并等待下一次机会再去修改。

下面是一个使用CAS算法来实现乐观锁的示例:

public class CASExample {
    
    
    private volatile int value;

    public void increment() {
    
    
        while (true) {
    
    
            // 使用CAS算法进行操作
            int current = this.value;
            int next = current + 1;
            if (compareAndSwap(current, next)) {
    
    
                break;
            }
        }
    }

    // 比较并替换方法
    private synchronized boolean compareAndSwap(int current, int next) {
    
    
        if (this.value == current) {
    
    
            this.value = next;
            return true;
        }
        return false;
    }
}

在该例子中,increment()方法不断循环获取共享资源的值,然后判断是否需要更新资源。当需要更新资源的时候,它会调用compareAndSwap()方法来进行比较并替换操作。如果当前的值与我们读取的值相同,则将新的值替换掉旧的值。

需要注意的是,在Compare-And-Swap算法中,只有当共享资源足够热门且争用激烈的时候,CAS算法才能发挥真正的作用,否则,因为CAS算法需要额外的操作来获取当前资源的状态,因此它的性能可能比传统的悲观锁机制还要低。

使用版本号机制AtomicInteger类

版本号机制,也称为时间戳机制,在执行Redis等缓存操作时比较常见。其核心思想是在每个需要被控制的资源对象中增加一个版本号字段,在每次执行修改操作的时候,都需要对该版本号进行更新。如果修改成功,则版本号 +1,否则不做任何操作。这样,在执行读取该资源的操作时,只需要比对版本号即可,如果版本号一致,则表示可以进行后续操作,否则表示已经有其他线程对该资源进行了修改,需要进行Retry操作。
AtomicInteger类是线程安全的,可以轻松的实现乐观锁。在下面的代码中,我们使用AtomicInteger来实现一个计数器,使用incrementAndGet()方法来实现自增操作。

public class AtomicIntegerExample {
    
    
    private AtomicInteger count = new AtomicInteger();

    public void increment() {
    
    
        // some code here
        this.count.incrementAndGet();
    }
}

三、悲观锁和乐观锁的比较

在实际开发中,悲观锁和乐观锁都有各自的优缺点,开发人员需要根据具体的业务场景灵活选择。下面,我们将对悲观锁和乐观锁进行详细比较。

3.1 实现难度

悲观锁的实现相对来说还是比较简单的,只需要在代码中引入synchronized关键字等机制即可。而乐观锁的实现难度就要大一些了。在使用CAS机制时,需要使用一些较为底层的技术,整个实现过程比较繁琐,需要小心处理其边界条件的问题,尤其是针对高并发场景的时候。

3.2 性能表现

在高并发场景下,乐观锁的性能表现往往明显优于悲观锁。这是由于悲观锁对共享资源进行频繁的锁定和解锁操作,很容易引起线程阻塞,从而导致系统的性能下降。因此,在高并发场景下,采用乐观锁能够更好地提高程序的并发性能。

3.3 实现复杂度

悲观锁实现本质上就是一个加锁/解锁的过程。而乐观锁则需要涉及到版本控制等方面。因此,实现复杂度方面,悲观锁要明显低于乐观锁。

3.4 数据冲突的处理

悲观锁能够很好地处理数据冲突问题,因为它始终锁定共享资源,排除其他线程的干扰,从而有效避免数据冲突问题。而乐观锁则需要对版本号进行控制,如果版本号不一致,则需要对该资源进行Retry操作,如果Retry次数过多,那么就可能会引发重试超时等问题,从而影响系统的正常运行。

四、总结

从上述比较的内容中我们可以看出,悲观锁和乐观锁各自有一些优缺点,在实际开发中,开发人员需要根据具体的业务需求、访问频率等因素进行灵活选择。悲观锁在并发性能较低的情况下,可以保证数据的一致性,但是因为每个线程在访问资源时都需要获取锁,因此它的性能可能不是很好。相比之下,乐观锁能够利用CAS算法等技术来保证资源的一致性,从而实现更高的并发性能。在高并发场景中,乐观锁能够更好地提高程序的并发性能,不过需要注意对版本号进行控制,避免重试次数过多的情况发生。无论哪种锁机制,都需要注意保证数据的一致性和安全性,避免发生脏读、幻读、乱序写等不安全的情况。

附加代码解析

完整的代码如下:

import java.util.concurrent.atomic.AtomicInteger;

public class LockExample {
    
    
    private int count1 = 0;
    private AtomicInteger count2 = new AtomicInteger();
    private volatile int count3;

    public synchronized void increment1() {
    
    
        this.count1++;
    }

    public void increment2() {
    
    
        this.count2.incrementAndGet();
    }

    public void increment3() {
    
    
        while (true) {
    
    
            int current = this.count3;
            int next = current + 1;
            if (compareAndSwap(current, next)) {
    
    
                break;
            }
        }
    }

    private synchronized boolean compareAndSwap(int current, int next) {
    
    
        if (this.count3 == current) {
    
    
            this.count3 = next;
            return true;
        }
        return false;
    }
}

在该代码中,我们定义了一个LockExample类,包含了三个属性count1、count2、count3,分别用于演示悲观锁、AtomicInteger乐观锁和CAS算法的乐观锁。这些属性都实现了一个increment()方法,用于对属性进行自增操作。这里,我们使用了三种不同的锁机制,分别是使用synchronized关键字的悲观锁、使用AtomicInteger类的乐观锁,以及使用CAS算法的乐观锁。通过上面的代码,我们可以更好的了解Java多线程中的锁机制。

猜你喜欢

转载自blog.csdn.net/weixin_40986713/article/details/130972883
今日推荐