目录
乐观锁与悲观锁
乐观锁
乐观锁的冲突检测
悲观锁
读锁与写锁
重量级锁与轻量级锁
重量级锁
轻量级锁
自旋锁
公平锁与非公平锁
可重入锁与不可重入锁
乐观锁与悲观锁
乐观锁
在乐观锁中,假设数据并不会发生冲突,在正式提交数据时会对数据进行冲突检测,如果发生了冲突则会返回错误信息,将决定权交给用户
乐观锁的冲突检测
冲突检测是乐观锁的一个重要功能.
实现这个功能的方式主要是:引入一个版本号,每次线程中的工作内存进行操作后,版本号会相应的+1,只有在提交数据工作内存中的版本号大于主存中的版本号才能提交成功.
示例:
在乐观锁下两个线程分别进行+1操作
首先线程1先被调度,将n进行+1操作后版本号也+1.
线程1中的数据要拷贝到主存中,首先要看两者的版本号.线程1中的版本号大于主存中的版本号,可以写入数据.
此时,线程2也进行+1操作,但线程2中的工作内存n还没有更新.
当线程2想要写入的时候,会发现自己的版本号没有大于主存中的版本号.
写入失败,主存中的数据就不会更新.
悲观锁
总是假设为最坏的情况,每次去拿数据的时候都会认为别人去修改.所以每次去拿数据的时候都会上锁,这样别人再想拿数据的时候就会阻塞.
对于synchronized来说,初始会使用"乐观锁",发现竞争比较激烈的时候才会自动转换到"悲观锁"
读锁与写锁
在多线程的环境下,数据的读取线程之间是不会互斥的,但读取线程与写入线程之间会互斥.
如果两种情景下,都使用同一把锁.就会产生极大的性能损耗.
为了解决这种情况,就有了读锁与写锁(readers-writer lock)
对于读操作与写操作
- 线程之间的读操作,并不会有线程安全的问题,可以之间并发读取.
- 线程之间的写操作,就会产生线程安全的问题.
- 线程之间的读操作与写操作,会产生线程安全的问题.
读写锁就可以把这些情况都区分开,降低性能的消耗.
在java标准库中,提供了ReentrantReadWriteLock类,其中实现了读锁写.
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
readLock.lock();
readLock.unlock();
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
writeLock.lock();
writeLock.unlock();
其中:(互斥就是线程会进入阻塞状态)
- 读锁与读锁之间不互斥
- 写锁与写锁之间互斥
- 读锁与写锁之间互斥
读写锁适用于频繁读,但不频繁写的场景下.
重量级锁与轻量级锁
锁的核心特性为:"原子性".
这种特性的来源,能追溯到CPU这种硬件提供的.
- CPU 提供了 "原子操作指令".
- 操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
- JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.
重量级锁
加锁机制依赖操作系统(内核态)提供mutex.
就会发生大量的内核态与用户态之间的转换即线程的调度
轻量级锁
加锁机制尽量不依赖于操作系统的mutex,而是在用户态中使用代码实现.如果无法实现,再去调用mutex.
可以减少内核态与用户态之间的转换
synchronized初始为轻量级锁,在竞争激烈的时候才转换为重量级锁
自旋锁
线程在抢锁失败后会进入阻塞状态(放弃CPU,暂时停止运行).需要经过较久的时间才会被重新调度.
通常情况下,抢夺的锁很快就会被释放出来.就没必要进入阻塞(放弃CPU).
自旋锁就可以解决这种情况.
自旋锁在竞争锁失败后,会立即尝试获取锁,循环直到成功获取到锁.一旦锁被释放,很快就能够获取到.
自旋锁虽然能在锁被释放的第一时间获取到锁,但如果锁被占用的时间较长就会持续的消耗CPU的资源.
公平锁与非公平锁
- 公平锁:遵守先来后到的顺序,先尝试获取锁的线程首先能占用到
- 非公平锁:不遵守先来后到的顺序,随机调度
在操作系统中,锁的调度都是随机的.如果不加以任何限制,那么其就是一个非公平锁.
而实现公平锁,需要一个额外的数据结构来存储线程尝试获取锁的顺序.
synchronized是一个非公平锁
可重入锁与不可重入锁
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁.加锁后仍然可以进行递归操作重新获取锁而不会阻塞自己的锁就为可重入锁.(因为这个原因可重入锁也叫做递归锁)
synchronized是可重入锁