乐观锁 && 悲观锁
悲观锁
:共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程。
public void performSynchronisedTask() {
synchronized (this) {
// 需要同步的操作
}
}
private Lock lock = new ReentrantLock();
lock.lock();
try {
// 需要同步的操作
} finally {
lock.unlock();
}
高并发场景下,会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销,并且还可能存在死锁问题。
上下文切换:
- 线程 A 正在执行计算任务,比如在做数学运算。它执行了一段时间后,操作系统的调度器决定它的时间片(CPU 执行时间)到了,需要把 CPU 分配给其他线程。
- 此时,操作系统会进行上下文切换:保存线程 A 的状态(当前执行到哪一步,寄存器里的值,堆栈指针等),然后将 CPU 的控制权交给线程 B。
- 线程 B 被切换过来执行,假设线程 B 正在处理文件 I/O 操作,读取硬盘文件。操作系统让线程 B 开始执行它的任务,直到线程 B 等待文件系统响应,此时发生了 I/O 阻塞。
- 当线程 B 因 I/O 操作阻塞时,操作系统无法让它继续执行,因此会再次进行上下文切换:保存线程 B 的状态,将 CPU 控制权交还给线程 A。
- 线程 A 再次执行:操作系统恢复线程 A 的状态,使其从刚才停止的地方继续执行数学运算。
这个过程反复进行,每次切换 CPU 需要先保存当前线程的状态,再恢复另一个线程的状态。虽然从外部看,线程 A 和 B 好像在并行执行,但其实它们是通过上下文切换轮流执行的。
乐观锁
:乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
CAS算法:
Compare And Swap(比较与交换),思路:用一个预期值
和要更新的变量值
进行比较,两值相等才会进行更新,用于实现乐观锁
。
CAS涉及到三个操作数:
- V:要更新的变量值(Var)
- E:预期值(Expected)
- N:拟写入的新值(New)
当且仅当V等于E时,CAS通过原子
(原子性)方式用新值N来更新V的值。如果不相等,说明已经有其他线程更新了V,则当前线程放弃更新。
自旋锁机制
:由于CAS操作可能会因为并发冲突而失败,因此通常与while
循环搭配使用,在失败后不断重试,直到操作成功。(弊端:如果一直不成功,会给CPU带来非常大的执行开销)
CAS算法存在问题:ABA问题
ABA问题:线程读取到某个变量的值为A,并准备更新它时,另一个线程可能已经将这个变量的值从A修改为B,随后又改回A。由于CAS只检查当前值是否为A,便认为值没有变化,并成功执行了更新操作,但实际上在此过程中发生了变化,这可能导致数据一致性问题。
解决:
- 添加版本号或时间戳,Java的
AtomicStampedReference
- 锁机制(Synchronized-同步)
- GC托管对象
如何使用Synchronized?
- 修饰实例方法(锁当前对象实例)
synchronized void method() {
//业务代码
}
- 修饰静态方法(锁当前类)
静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
synchronized static void method() {
//业务代码
}
静态
synchronized
方法和非静态synchronized
方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态synchronized
方法,而线程 B 需要调用这个实例对象所属类的静态synchronized
方法,是允许的,不会发生互斥现象,因为访问静态synchronized
方法占用的锁是当前类的锁,而访问非静态synchronized
方法占用的锁是当前实例对象锁。
- 修饰代码块(锁指定对象 / 类)
synchronized(object)
表示进入同步代码库前要获得 给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 给定 Class 的锁
synchronized(this) {
//业务代码
}
尽量不要使用
synchronized(String a)
因为 JVM 中,字符串常量池具有缓存功能。
Synchronized底层原理
synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取而代之的是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。
不过两者的本质都是对对象监视器 monitor
的获取。
锁升级:锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
ReentrantLock
ReentrantLock
实现了 Lock
接口,是一个可重入且独占式的锁,和 synchronized
关键字类似。不过,ReentrantLock
更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
ReentrantLock
里面有一个内部类 Sync
,Sync
继承 AQS(AbstractQueuedSynchronizer
),添加锁和释放锁的大部分操作实际上都是在 Sync
中实现的。Sync
有公平锁 FairSync
和非公平锁 NonfairSync
两个子类。
公平锁 : 按顺序来,先申请,先拿锁。锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
非公平锁:随机或按照其他顺序。锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
可重入锁(递归锁):线程可以再次获取自己的内部锁。比如一个线程获取了某个对象的锁,此时这个对象还没有释放锁,当其再想获取这个对象的锁时,还是可以获取的,如果是不可重入锁,就会造成死锁。
Lock实现类、synchronized关键字锁都是可重入的。
可中断锁:获取锁的过程中可以被中断,不需要等到获取锁之后才能进行其他逻辑处理。
ReentrantLock
不可中断锁:获取锁的过程中不可被中断,一旦线程申请了锁,就只能等到拿到锁之后才能进行其他的逻辑处理。
Synchroniezd