在并发编程中,锁是线程同步的关键。通过锁的机制,多个线程可以协调访问共享资源,保证数据的一致性。根据不同的需求,Java 提供了多种锁机制,每种锁的特点和适用场景各异。本文将深入分析 Java 锁的七大分类及其特点,帮助你全面理解锁的使用,并在项目中更好地应用。
1. 偏向锁/轻量级锁/重量级锁
这三种锁特指 synchronized
锁的状态,它们通过 JVM 内部的对象头(Mark Word)来控制锁的状态。这些锁的主要作用是根据线程竞争的程度来动态调整锁的类型,确保线程的执行效率。
1.1 偏向锁
- 定义:偏向锁是最轻量级的锁,适用于线程没有竞争的场景。当某个线程获得偏向锁后,其他线程无法获得该锁,直到偏向锁被释放。
- 特点:性能最优,不会进行加锁操作,仅仅在第一次获取时进行标记。
- 适用场景:适合线程访问共享资源时没有竞争的场景。
1.2 轻量级锁
- 定义:当偏向锁存在竞争时,它会升级为轻量级锁。轻量级锁通过 CAS 操作来避免线程阻塞。
- 特点:通过自旋(CAS)尝试获取锁,避免线程阻塞,性能较高。
- 适用场景:适合多线程之间竞争较少的场景。
1.3 重量级锁
- 定义:当轻量级锁无法满足需求时,锁会升级为重量级锁。重量级锁通过操作系统的同步机制来实现,线程获取不到锁时会被阻塞。
- 特点:性能较差,因为涉及到线程的上下文切换和阻塞。
- 适用场景:适合竞争激烈且锁持有时间较长的场景。
锁的升级路径:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
2. 可重入锁/非可重入锁
锁的可重入性是指线程在持有锁的情况下,是否能够再次获取相同的锁。
2.1 可重入锁
- 定义:可重入锁允许同一个线程多次获得同一把锁。
- 典型实现:
ReentrantLock
就是一个典型的可重入锁,它允许线程在持有锁的情况下再次获得该锁。 - 适用场景:适合在递归调用或者需要多次获取同一锁的场景。
2.2 非可重入锁
- 定义:非可重入锁要求线程在释放锁后才能再次获得锁。
- 典型实现:
Mutex
锁通常是非可重入锁。 - 适用场景:适合对同一资源的访问需要严格控制的场景。
3. 共享锁/独占锁
锁的共享性决定了它是否允许多个线程同时获得锁。
3.1 共享锁
- 定义:共享锁允许多个线程同时获得锁。
- 典型实现:读写锁中的读锁是共享锁,可以多个线程同时获取并读取数据。
- 适用场景:适合读操作较多的场景,减少线程等待时间。
3.2 独占锁
- 定义:独占锁保证在同一时刻,只有一个线程可以获得锁。
- 典型实现:写锁是独占锁,只有一个线程能够写入数据。
- 适用场景:适合对数据进行修改时使用。
4. 公平锁/非公平锁
公平性锁是指线程能否按照请求的顺序公平地获得锁。
4.1 公平锁
- 定义:公平锁保证线程按照申请锁的顺序依次获得锁,类似于“先来先得”。
- 典型实现:
ReentrantLock
在构造时指定true
可实现公平锁。 - 适用场景:适合对线程公平性要求较高的场景。
4.2 非公平锁
- 定义:非公平锁不保证线程获取锁的顺序,可能会出现线程插队现象。
- 典型实现:
ReentrantLock
默认实现为非公平锁。 - 适用场景:适合对公平性要求不高且希望提高性能的场景。
5. 悲观锁/乐观锁
悲观锁和乐观锁的区别在于对资源争用的假设:悲观锁认为资源会发生冲突,而乐观锁认为资源不会发生冲突。
5.1 悲观锁
- 定义:悲观锁假设线程间会发生资源争用,在访问共享资源前先加锁。
- 典型实现:
synchronized
和ReentrantLock
是悲观锁的常见实现。 - 适用场景:适合多线程写操作较多、临界区代码复杂的场景。
5.2 乐观锁
- 定义:乐观锁假设线程间不会发生冲突,不加锁,访问数据时判断是否发生了冲突,若冲突则重试。
- 典型实现:CAS(Compare-And-Swap)是乐观锁的实现方式,
AtomicInteger
类就是一个典型的乐观锁实现。 - 适用场景:适合读操作多、写操作少的场景。
6. 自旋锁/非自旋锁
自旋锁和非自旋锁的区别在于获取锁失败时是否立即放弃。
6.1 自旋锁
- 定义:自旋锁是一种忙等待的锁,当线程无法获取锁时,线程会在原地等待一段时间,然后继续尝试获取锁。
- 典型实现:
ReentrantLock
的tryLock
方法就是一种自旋锁的实现。 - 适用场景:适合锁竞争较少、线程等待时间短的场景。
6.2 非自旋锁
- 定义:非自旋锁是在无法获取锁时,线程直接放弃或进入阻塞状态,等待唤醒。
- 典型实现:
synchronized
锁就是一种非自旋锁。 - 适用场景:适合锁竞争较激烈的场景。
7. 可中断锁/不可中断锁
可中断锁是指线程在获取锁的过程中可以响应中断,而不可中断锁则在获取锁时无法中断。
7.1 可中断锁
- 定义:可中断锁在获取锁时可以响应中断。
- 典型实现:
ReentrantLock
提供了lockInterruptibly
方法,允许线程在等待获取锁时响应中断。 - 适用场景:适合需要中断机制的任务,避免线程长时间阻塞。
7.2 不可中断锁
- 定义:不可中断锁一旦开始获取,就无法响应中断,直到获取锁成功或出现异常。
- 典型实现:
synchronized
锁是不可中断的锁。 - 适用场景:适合不要求中断的任务,保证锁的稳定性。
总结
在多线程编程中,锁的选择对程序的性能和稳定性至关重要。根据不同的应用场景和需求,选择合适的锁类型,可以有效提高并发程序的效率。本文介绍了 Java 锁的七大分类,包括偏向锁、轻量级锁、可重入锁、共享锁、悲观锁、乐观锁、自旋锁和可中断锁,了解它们的特点和应用场景,能够帮助你更好地设计并发系统,避免常见的性能瓶颈和同步问题。