# Java 并发 - Java 中所有的锁
Java 中往往按照是否含有某一特性来定义锁,通过特性将锁进行分组归类。
# 乐观锁 VS 悲观锁
# 概念
悲观锁:在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java 中,synchronized 关键字和 Lock 的实现类都是悲观锁。
乐观锁:认为自己在使用数据时不会有别的线程修改数据,所以不会加锁,只有在更新数据的时候回去判断之前有没有其它线程更新了这个数据。如果没有被更新,则将自己家的数据写入,如果已经被更新,则根据不同的实现方式去执行不同的操作(例如报错或者自动重试)。乐观锁是 Java 中通过无锁编程来实现的,最常采用的是 CAS 算法,Java 原子类中的递增操作就是通过 CAS 自旋实现的。
# 使用场景
- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
//------------------------- 悲观锁的调用方式 ------------------------- | |
// synchronized | |
public synchronized void testMethod() { | |
// 操作同步资源 | |
} | |
// ReentrantLock | |
private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁 | |
public void modifyPublicResources() { | |
lock.lock(); | |
// 操作同步资源 | |
lock.unlock(); | |
} | |
//------------------------- 乐观锁的调用方式 ------------------------- | |
private AtomicInteger atomicInteger = new AtomicInteger(); // 需要保证多个线程使用的是同一个 AtomicInteger | |
atomicInteger.incrementAndGet(); // 执行自增 1 |
# 自旋锁 VS 适应性自旋锁
阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃 CPU 的执行时间,看看持有锁的线程是否很快就会释放锁。
而为了让当前线程 “稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是 10 次,可以使用 - XX:PreBlockSpin 来更改)没有成功获得锁,就应当挂起线程。
自旋锁的实现原理同样也是 CAS,AtomicInteger 中调用 unsafe 进行自增操作的源码中的 do-while 循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
# 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
这四种锁是指锁的状态,专门针对 synchronized 的。在介绍这四种锁状态之前还需要介绍一些额外的知识。
总结而言: 偏向锁通过对比 Mark Word 解决加锁问题,避免执行 CAS 操作。而轻量级锁是通过用 CAS 操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。
# 公平锁 VS 非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
# 可重入锁 VS 非可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提是锁对象得时同一个对象或者 class),不会因为之前已经获取过还没有释放而阻塞。Java 中 ReentrantLock 和 synchronized 都是可重入锁,可重入锁的一个优点时可一定程度避免死锁。
public class Widget { | |
public synchronized void doSomething() { | |
System.out.println("方法1执行..."); | |
doOthers(); | |
} | |
public synchronized void doOthers() { | |
System.out.println("方法2执行..."); | |
} | |
} |
# 独享锁(排他锁)VS 共享锁
# 概念
独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程 T 对数据 A 加上排它锁后,则其他线程不能对 A 加任何类型的锁。获得排他锁的线程即能读数据又能修改数据。JDK 中的 synchronized 和 JUC 中的 Lock 的实现类就是互斥锁(排他锁)。
共享锁是指该锁可被多个线程所持有。如果线程 T 对线程 A 加上共享锁后,则其他线程只能对 A 再加共享锁,不能加排他锁。获得共享锁的线程只能读数据,不能修改数据。
** 独享锁和共享锁也是通过 AQS 来实现的。** 通过实现不同方法,来实现独享或者共享。