乐观锁
常用代表:CAS
什么是乐观锁?永远处于乐观积极状态,因此乐观锁觉得并发操作期间是不会出问题的,操作数据 不加锁,只会在最后更新数据时检查数据有没有被修改,没有的话才更新(更新期间加锁,保证是原子性的)。
提到 CAS,这个也是高频考点。大白话介绍一下、便于更好理解乐观锁。
CAS 是一种乐观锁实现机制(比较并替换),主要是三部分:内存值+旧的预期值+要修改的值。每次修改数据先比较内存中值与预期值是否相同,不同就自旋,相同才修改。实现依靠unsafe(里面全是native修饰的本地方法,可以直接调用操作系统) + lock cmpxchg(底层依靠硬件指令)。
当然CAS的特性也导致了它的 缺点,ABA问题、自旋导致的资源浪费、只能保障一个变量的原子性。
悲观锁
顾名思义,永远处于悲观消极状态,因此悲观锁觉得并发操作每次都可能有问题,于是每次都会加锁。
所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会阻塞住直到拿到锁 Java 中的悲观锁就是 Synchronized、AQS 框架下的锁则是先尝试 cas 乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
乐观锁 VS 悲观锁
乐观锁适用于读多写少的场景,因为它是不加锁的,相较于悲观锁不用加锁、释放锁,节省了开销。但是若写的多,冲突严重,可能导致线程一直 while 自旋,浪费资源,反而降低了性能。此时在这种写多读少的场景使用悲观锁就更合适。
独占锁
[独享锁、排它锁]
独占锁锁一次只能被一个线程所持有。如果线程 T 对数据 A 加上排它锁后,则其他线程不能再对 A 加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK 中的 synchronized 和 JUC 中 Lock 的实现类就是互斥锁。
共享锁
共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
需要注意的是,对于Java ReentrantLock 而言,其是独享锁。但是对于 Lock 的另一个实现类 ReadWriteLock,其读锁是共享锁,其写锁是独占锁。读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。独占锁与共享锁也是通过 AQS 来实现的,通过实现不同的方法,来实现独享或者共享。
公平锁
常见代表:new ReentrantLock(true)、new ReentrantReadWriteLock(true)
公平锁 是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大。
非公平锁
常见代表:synchronized、new ReentrantLock(false)、new ReentrantReadWriteLock(false)
非公平锁 是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
synchronized 和 lock 对比更有助于理解公平锁和非公平锁。
- synchronized 自动释放锁,lock 手动释放(容易死锁) lock.unlock();synchronized 阻塞后,其他线程一直等待,lock有超时时间。
- lock 等待锁过程中可以用 interrupt 来中断等待,而 synchronized 只能等待锁的释放,不能响应中断。
- synchronized 使用 Object 对象本身的 wait 、 notify、notifyAll 调度机制,而 Lock 可以使用 Condition 进行线程之间的调度。