**Java多线程技术一直是Java程序员必备的核心技能之一。在Java多线程编程中,为了保证数据的一致性和安全性,常常需要使用锁的机制来防止多个线程同时修改同一个共享资源。锁是实现并发访问控制的一种机制,多线程之间共同访问共享资源的时候,悲观锁是最常见的方式,不过在高并发场景中,全局锁所带来的并发性阻塞问题也是不可避免的。为了解决这种问题,人们又引出了乐观锁的概念。在本文中,我们将详细探讨Java多线程中的乐观锁和悲观锁。**
一、什么是悲观锁
悲观锁是多线程并发控制的一种机制。悲观锁一般都是基于数据库锁的实现方式实现的。当一个线程要对共享资源进行访问时,那么就会使用悲观锁定义好的机制获取该资源的锁。这个过程中,如果其他线程也想获取该锁,就需要等待当前线程释放锁之后才能获取该锁。当然,也可以通过设置超时时间等机制来避免死锁等问题。
在Java中,我们可以使用synchronized关键字实现悲观锁,synchronized可以在对象级别上使用,也可以在类级别上使用。当某个线程访问synchronized锁定的对象时,该对象的状态会被设置为被锁定状态,如果其他线程也想修改这个状态,就必须等待当前线程释放锁之后才能获取该锁。这种方式看似非常理想,但实际上只是针对小并发量的业务场景。
public class LockExample {private int count = 0;public synchronized void increment() {// some code herethis.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 herethis.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多线程中的锁机制。