Java中锁的类型多种多样,有简单有复杂,适合各种不同的应用场景,接下来会分几章给大家详细介绍java中各种类型的锁。
一、悲观锁和乐观锁的说明
1、悲观锁(Pessimistic Lock):对于同一个数据的并发操作,想的很坏,很悲观,都认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。别的线程想拿数据就被挡住,直到悲观锁被释放,悲观锁中的共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
此外阻塞、唤醒以及引起的CPU状态切换等处理悲观锁的机制会产生额外的开销,还有增加产生死锁的机会,另外还会降低程序的并行性。
Java中synchronized关键字和Lock的实现类,以及数据库中的行锁、表锁、读锁(共享锁)和写锁(排他锁)都是悲观锁。
2、乐观锁(Optimistic Lock):很乐观,每次去拿数据的时候都认为别的线程不会修改。所以不会上锁,只有在想要更新数据时候,去检查在读取至更新这段时间别的线程有没有修改过这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入;如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
乐观锁在Java中是通过使用无锁编程来实现,所以不会产生任何锁和死锁,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
数据库实现乐观锁并不会使用数据库提供的锁机制。一般实现乐观锁的方式就是数据表字段增加版本号(version)或者是时间戳来实现,使用版本号是最常用的。
二、悲观锁和乐观锁的调用方式
1、悲观锁的调用方式
//悲观锁的调用方式
// synchronized
public synchronized void testMethod() {
// 操作同步资源
}
// Reentrantlock
private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用同一个锁
public void modifyPublicResources() {
lock.lock();
//操作同步资源
lock.unlock();
}
悲观锁通过显式的锁定再操作同步资源,但是如果存在嵌套锁的情况下,会出现死锁,例如:
public class DeadlockExample {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
System.out.println("method1 acquired lock1");
synchronized (lock2) {
System.out.println("method1 acquired lock2");
}
}
}
public void method2() {
synchronized (lock2) {
System.out.println("method2 acquired lock2");
synchronized (lock1) {
System.out.println("method2 acquired lock1");
}
}
}
}
在上面的代码中,method1()获取lock1锁后,又尝试获取lock2锁;method2()获取lock2锁后,又尝试获取lock1锁。如果两个线程分别调用这两个方法,且在相应的时刻互相等待对方释放锁,就会出现死锁。实际应用中解决这种问题的方法是尽量避免嵌套锁的使用,并且对锁的获取顺序进行规定。比如,在上面的代码中,可以规定获取锁的顺序为先获取lock1锁,再获取lock2锁,这样就可以避免死锁的出现。
2、乐观锁的调用方式
public class OptimisticLockDemo {
private String value;
private AtomicInteger version = new AtomicInteger(0);
public void update(String newValue) {
while (true) {
int currentVersion = version.get();
//业务处理
if (currentVersion == version.get()) {
value = newValue;
version.incrementAndGet();
break;
}
}
}
}
乐观锁采用无锁方式,所以不存在死锁的情况,因此在并发性能上会更加优越,适用于读多写少的场景
三、乐观锁的实现方式CAS
1、CAS简介
通过调用方式示例,我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?我们通过介绍乐观锁的主要实现方式 “CAS” 的技术原理来为大家解惑。
CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
CAS算法涉及到三个操作数:
-
需要读写的内存值 V。
-
进行比较的值 A。
-
要写入的新值 B。
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
2、CAS存在的三大问题
CAS虽然很高效,但是它也存在三大问题,这里也简单说一下:
1)、 ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。
2)、循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
3)、只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
四、悲观锁和乐观锁的应用场景
悲观锁阻塞线程,乐观锁回滚重试,他们各有优缺点,不分仲伯,适合不同的场景:
1、悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
2、乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
关于数据库实现悲观锁和乐观锁的示例可以参考文章: