目录
synchronized和 ReentrantLock的区别
面试复盘
Java 中的锁 大全
悲观锁
专业解释
适合写操作多的场景
先加锁可以保证写操作时数据正确
显式的锁定之后再操作同步资源
自我理解
悲观锁认为自己使用数据的时候一定有其他线程来修改数据
因此在获取数据的时候会选择先加锁
确保数据不会被别的线程修改
synchronized 和 Lock 的实现类都是悲观锁
乐观锁
专业解释
适合读操作多的场景
不加锁的特点能够使其读操作的性能大幅提升
乐观锁则能直接去操作同步资源
是一种无锁算法
自我理解
乐观锁认为自己在操作数据的时候不会有别的线程修改数据,所以不会加锁,所以他只会在自己操作数据的时候检查是否有其他线程修改更新的这个数据。
如果乐观锁去操作数据,这个数据没有更新的话。当前线程会直接将修改成功的数据写入,如果数据已经被其他线程更新了。要通过不同的实现方式进行不同操作。乐观锁在Java中是通过使用无锁编程来实现的,常用的是CAS算法。
Java原子类中的递增操作就是用CAS 自旋完成的
悲观锁的调用
import java.util.concurrent.locks.ReentrantLock;
public class OptiPessLockDemo {
// 悲观锁的调用方式
public synchronized void m1(){
// 加锁后的业务逻辑...
}
// 保证多个线程使用的是同一个lock对象的前提下
ReentrantLock lock=new ReentrantLock();
public void m2(){
lock.lock();
try {
// 操作同步资源
}finally {
lock.unlock();
}
}
// 两个都是悲观锁
}
假设在任何时候都可能发生冲突,因此,线程必须显式地获取锁以确保数据一致性和线程安全,直到它执行完毕并释放锁。
对于 synchronized
,在方法级别加锁时,锁是针对该对象的,保证同一时刻只有一个线程能够执行该方法。因此,当一个线程在执行 m1()
方法时,其他线程不能同时执行同一个对象上的 m1()
方法。
synchronized
关键字的锁粒度是方法级别的,锁住的是整个方法。在方法执行期间,其他线程不能进入这个方法。synchronized
实现的是隐式加锁和解锁,不需要显式地调用lock()
和unlock()
。ReentrantLock
是 Java 中的一个显式锁,它提供了比synchronized
更细粒度的锁控制和更多功能。ReentrantLock
通过lock.lock()
显式地加锁,调用lock.unlock()
来释放锁。通常,这两个操作会放在try...finally
代码块中,以确保即使出现异常,锁也能得到释放。
乐观锁的调用
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
public class OptiPessLockDemo2 {
// 乐观锁的调用方式
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
public static void main(String[] args) {
int oldValue = atomicInteger.get();
int newValue = oldValue + 1;
// 如果值没有被修改,原子性操作成功
return atomicInteger.compareAndSet(oldValue, newValue);
}
}
这种方式在判断和更新之间,确保了只有一个线程能够成功更新值,其他线程则会重试或失败,从而保证了乐观锁的行为。
总之,乐观锁的核心是希望在操作时不加锁,直到最后验证冲突发生与否。如果有冲突,则可以通过重试、回滚等方式处理。
synchronized
和 ReentrantLock
的区别
synchronized
和 ReentrantLock
都用于实现 悲观锁(Pessimistic Locking),即在多线程环境中对共享资源进行加锁,以保证线程安全。尽管两者实现的目标相同,它们在使用方式、灵活性、性能等方面有一些不同。下面我们来详细分析它们的 相同点 和 区别。
相同点
- 线程安全:
-
- 两者都能确保在多线程环境下,访问共享资源时,只有一个线程能够持有锁,防止多个线程同时修改共享数据,避免数据不一致。
- 互斥性:
-
- 两者都保证了同一时刻只有一个线程能够执行被保护的代码块或方法。其他线程必须等待当前线程释放锁后才能执行。
- 支持重入性:
-
synchronized
和ReentrantLock
都是可重入的(Reentrant),即同一线程可以多次获取同一个锁,而不会发生死锁。
- 作用范围:
-
- 两者都可以应用于同步方法或同步代码块,保护共享资源。
区别
特性 |
|
|
实现方式 | 隐式加锁,Java 编译器在编译时自动处理。 | 显式加锁,需要手动调用 和 。 |
锁粒度 | 锁定整个方法或代码块,无法精确控制。 | 可以精确控制锁定的范围,允许更灵活的锁定操作。 |
性能 | 相对较低,特别是在高并发环境下,由于 JVM 的锁优化不足,可能导致性能瓶颈。 | 在高并发时, 性能优于 ,尤其在锁竞争激烈时。 |
中断支持 | 不支持中断,线程获取锁时无法响应中断。 | 支持中断,可以使用 来在等待锁时响应中断。 |
公平性 | 非公平锁,线程不一定按照请求的顺序获取锁。 | 可以选择公平锁或非公平锁。使用构造函数 来创建公平锁。 |
死锁避免 | 需要小心死锁问题, 无法避免死锁。 | 通过 提供的 方法和超时机制可以更灵活地避免死锁。 |
锁释放机制 | 锁由 JVM 自动管理,方法执行完后自动释放。 | 必须手动调用 释放锁,通常与 语句配合使用。 |
可重入性 | 支持,可同一线程多次获取同一锁。 | 支持,可同一线程多次获取同一锁。 |
性能监控 | 无法直接获取锁的状态。 | 可以通过 获取当前线程持有锁的次数,进行监控。 |
锁升级 | 不支持锁的升级(无法从轻量级锁升级为重量级锁)。 | 可以通过锁的竞争情况动态升级为不同的锁类型(如偏向锁、轻量锁、重量锁)。 |
详细对比
- 锁的获取与释放:
-
synchronized
是 隐式加锁,锁的获取和释放是自动完成的。线程在执行被synchronized
修饰的代码时,会自动获取该对象的锁,方法或代码块执行完后自动释放锁。ReentrantLock
是 显式加锁,必须手动调用lock()
获取锁,必须手动调用unlock()
释放锁。通常会配合try...finally
语句使用,以保证在执行完业务逻辑后无论是否发生异常都能够释放锁。
- 中断响应:
-
synchronized
在获取锁时无法响应中断。如果线程在等待锁的过程中被中断,它会继续等待,直到获取到锁。ReentrantLock
提供了lockInterruptibly()
方法,它允许在等待锁的过程中响应中断。线程在等待锁时,如果被中断,能够及时退出等待。
- 公平性:
-
synchronized
是 非公平锁,即任何线程在请求锁时,获取锁的顺序并不一定按照请求的顺序。可能先被调用的线程后获得锁,后调用的线程反而先获取到锁。ReentrantLock
可以通过构造函数指定是否为 公平锁。如果设置为公平锁,锁会按照请求顺序分配给线程,先请求的线程会先获得锁。公平锁相对会增加一些性能开销,因此通常默认是非公平锁。
- 性能:
-
- 在低并发环境下,
synchronized
的性能和ReentrantLock
差不多,但在高并发环境下,ReentrantLock
会因为其灵活性和优化而表现得更好。 synchronized
锁的实现较为简单,但在高并发下可能存在 锁竞争 的问题,导致性能瓶颈。ReentrantLock
在 锁竞争 情况下提供了更多的优化方式,如通过自旋锁、CAS 等机制来减少线程的阻塞,从而提高性能。
- 在低并发环境下,
- 死锁避免:
-
synchronized
并没有提供直接的 API 来避免死锁。需要开发者自己通过代码设计来避免死锁问题。ReentrantLock
提供了tryLock()
方法,可以尝试获取锁,如果获取失败,可以选择放弃或重试。这种机制可以帮助开发者避免死锁或减少锁的等待时间。
- 锁的升级和监控:
-
synchronized
锁的状态无法直接监控,只能通过 JVM 的内部调试工具进行查看。ReentrantLock
提供了诸如getHoldCount()
等方法来获取当前线程持有锁的次数,此外,还可以通过getQueueLength()
来获取等待锁的线程数量,便于性能监控和分析。
总结
synchronized
:适用于简单的同步需求,易于使用,代码简洁,但不够灵活,性能在高并发场景下可能有所下降。ReentrantLock
:功能更强大,提供了更多灵活性,如公平锁、中断响应、尝试加锁等,适用于复杂的多线程应用。但需要手动管理锁的获取与释放,使用上稍微复杂一些。
如果你的应用需求比较简单,且对性能要求不高,使用 synchronized
就足够了。如果你需要更多的控制、灵活性和对高并发场景的优化,ReentrantLock
会是更好的选择。