在多线程并发编程场景中,锁作为一种至关重要的同步工具,承担着协调多个线程对共享资源访问秩序的任务。其核心作用在于确保在特定时间段内,仅有一个线程能够对资源进行访问或修改操作,从而有效地保护数据的完整性和一致性。锁作为一种底层的安全构件,有力地防止了竞态条件和数据不一致性的问题,尤其在涉及多线程或多进程共享数据的复杂场景中显得尤为关键。
而了解锁的分类,能帮助我们何种业务场景下使用选择哪种锁。
计划于所得获取与释放方式进行分类,Java中的锁可以分为:显式锁和隐式锁。
Java中的隐式锁(也称为内置锁或自动锁)是通过使用synchronized关键字实现的一种线程同步机制。当一个线程进入被synchronized修饰的方法或代码块时,它会自动获得对象级别的锁,退出该方法或代码块时则会自动释放这把锁。
synchronized
在Java中,隐式锁的实现机制主要包括以下两种类型:
互斥锁(Mutex): 虽然Java标准库并未直接暴露操作系统的互斥锁提供使用,但在Java虚拟机对synchronized关键字处理的底层实现中,当锁竞争激烈且必须升级为重量级锁时,会利用操作系统的互斥量机制来确保在同一时刻仅允许一个线程持有锁,从而实现严格的线程互斥控制。
内部锁(Intrinsic Lock)或监视器锁(Monitor Lock): Java语言为每个对象内建了一个监视器锁,这是一个更高级别的抽象。我们可以通过使用synchronized关键字即可便捷地管理和操作这些锁。当一个线程访问被synchronized修饰的方法或代码块时,会自动获取相应对象的监视器锁,并在执行完毕后自动释放,这一过程对用户透明,故被称为隐式锁。每个Java对象均与一个监视器锁关联,以此来协调对该对象状态访问的并发控制。
优点:
缺点:
java.util.concurrent.locks.Lock
适用场景: 隐式锁适用于相对简单的多线程同步需求,尤其是在只需要保护某个对象状态完整性,且无需过多关注锁策略灵活性的场合。对于要求更高并发性和更复杂锁管理逻辑的应用场景,显示锁通常是一个更好的选择。
显式锁是由java.util.concurrent.locks.Lock接口及其诸多实现类提供的同步机制,相较于通过synchronized关键字实现的隐式锁机制,显式锁赋予开发者更为精细和灵活的控制能力,使其能够在多线程环境中精准掌控同步动作。显式锁的核心作用在于确保在任何时刻仅有一个线程能够访问关键代码段或共享数据,从而有效防止数据不一致性问题和竞态条件。
相较于隐式锁,显式锁提供了更为多样化的锁操作选项,包括但不限于支持线程在等待锁时可被中断、根据先后顺序分配锁资源的公平锁与非公平锁机制,以及能够设定锁获取等待时间的定时锁功能。这些特性共同增强了显式锁在面对复杂并发场景时的适应性和可调控性,使之成为解决高度定制化同步需求的理想工具。
日常开发中,常见的显式锁分类有如下几种:
Lock
lock()
unlock()
按照线程对资源的访问权限来分类,可以将锁分为:独占锁(Exclusive Lock)和共享锁(Shared Lock)。
独占锁(Exclusive Lock),又称排他锁或写锁,是一种同步机制,它确保在任一时刻,最多只有一个线程可以获得锁并对受保护的资源进行访问或修改。一旦线程获得了独占锁,其他所有试图获取同一锁的线程将被阻塞,直到拥有锁的线程释放锁为止。独占锁主要用于保护那些在并发环境下会被多个线程修改的共享资源,确保在修改期间不会有其他线程干扰,从而维护数据的一致性和完整性。
对于独占锁就像图书馆里的某本书,这本书只有唯一的一本。当一个读者想要借阅这本书时,他会去图书管理员那里登记并拿到一个“借书凭证”(相当于独占锁)。此时,这本书就被锁定了,其他读者无法借阅这本书,直至第一个读者归还书本并交回“借书凭证”。这就像是线程获得了独占锁,只有拥有锁的线程可以修改或操作资源(书本),其他线程必须等待锁的释放才能执行相应的操作。
而独占锁的实现方式,主要有如下两种:
ReentrantLock
共享锁(Shared Lock)也称为读锁(Read Lock),是一种多线程或多进程并发控制的同步机制,它允许多个线程同时读取共享资源,但不允许任何线程修改资源。在数据库系统和并发编程中广泛使用,确保在并发读取场景下数据的一致性。
共享锁就像图书馆里有一套多人阅读的杂志合订本,这套合订本可以被多个读者同时翻阅,但是任何人都不能带走或在上面做标记。当一个读者要阅读时,他会向图书管理员申请“阅读凭证”(相当于共享锁)。如果有多个读者想阅读,图书管理员会给他们每人一份阅读凭证,这样大家都可以坐在阅览室里一起阅读这套合订本,但是都不能单独占有或改变它。在并发编程中,多个线程可以同时获取共享锁进行读取操作,但都不能修改数据,这就像是多个线程同时持有共享锁读取资源,但不允许在此期间进行写操作。
实现共享锁的关键机制是读写锁(ReadWriteLock),这是一种特殊类型的共享锁机制,它巧妙地将对共享资源的访问权限划分为了读取权限和写入权限两类。在读写锁的控制下,多个线程可以同时进行对共享数据的读取操作,形成并发读取,而对数据的写入操作则采取独占式处理,确保同一时间段内仅有一个线程执行写入操作。在写入操作正在进行时,无论是其他的读取操作还是写入操作都会被暂时阻塞,直至写操作结束。
读写锁包含两种锁模式:读锁(ReadLock) 和 写锁(WriteLock)。当多个线程需要访问同一份共享数据时,只要这些线程都是进行读取操作,则都能成功获取并持有读锁,从而实现并行读取。然而,一旦有线程尝试进行写入操作,那么不论是其他正在执行读取的线程还是准备进行写入的线程,都无法继续获取读锁或写锁,直至当前写操作全部完成并释放写锁。这样,读写锁有效地平衡了读取密集型任务的并发性和写入操作的原子性要求。
如以下使用共享锁示例:
public class SharedResource { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); private int data; public void modifyData(int newData) { // 获取写锁(独占锁),在同一时刻只有一个线程可以获取写锁 writeLock.lock(); System.out.println(Thread.currentThread().getName() + " Modify Data"); try { // 修改数据 this.data = newData; // 数据修改相关操作... } finally { // 无论如何都要确保解锁 writeLock.unlock(); } } public int readData() { // 获取读锁(共享锁),允许多个线程同时获取读锁进行读取操作 readLock.lock(); System.out.println(Thread.currentThread().getName() + " Read Data"); try { // 读取数据,此时其他读取线程也可以同时读取,但不允许写入 return this.data; }finally { // 释放读锁 readLock.unlock(); } } public static void main(String[] args) { SharedResource resource = new SharedResource(); Thread reader1 = new Thread(() -> System.out.println("Reader 1 reads: " + resource.readData()), "Reader1"); Thread reader2 = new Thread(() -> System.out.println("Reader 2 reads: " + resource.readData()), "Reader1"); Thread writer = new Thread(() -> resource.modifyData(42), "Writer1"); reader1.start(); reader2.start(); writer.start(); // 等待所有线程执行完成 try { reader1.join(); reader2.join(); writer.join(); } catch (InterruptedException e) { e.printStackTrace(); } }}
public class SharedResource {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private int data;
public void modifyData(int newData) {
// 获取写锁(独占锁),在同一时刻只有一个线程可以获取写锁
writeLock.lock();
System.out.println(Thread.currentThread().getName() + " Modify Data");
try {
// 修改数据
this.data = newData;
// 数据修改相关操作...
} finally {
// 无论如何都要确保解锁
writeLock.unlock();
}
public int readData() {
// 获取读锁(共享锁),允许多个线程同时获取读锁进行读取操作
readLock.lock();
System.out.println(Thread.currentThread().getName() + " Read Data");
// 读取数据,此时其他读取线程也可以同时读取,但不允许写入
return this.data;
}finally {
// 释放读锁
readLock.unlock();
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Thread reader1 = new Thread(() -> System.out.println("Reader 1 reads: " + resource.readData()), "Reader1");
Thread reader2 = new Thread(() -> System.out.println("Reader 2 reads: " + resource.readData()), "Reader1");
Thread writer = new Thread(() -> resource.modifyData(42), "Writer1");
reader1.start();
reader2.start();
writer.start();
// 等待所有线程执行完成
reader1.join();
reader2.join();
writer.join();
} catch (InterruptedException e) {
e.printStackTrace();
打印结果:
在这个示例中,使用了 ReentrantReadWriteLock 来控制对 data 的读写操作。readData() 方法使用读锁,允许多个线程同时读取数据,而 modifyData() 方法使用写锁,确保同一时间只有一个线程可以修改数据。这样就可以在并发场景下既保证数据读取的并发性,又避免了数据因并发写入而造成的不一致性问题。
ReentrantReadWriteLock
data
readData()
modifyData()
按照锁的占有权是否可以重入,可以把锁分为:可重入锁以及不可重入锁。
可重入锁(Reentrant Lock)作为一种线程同步机制,具备独特的重入特性,即当线程已经获取了锁后,它可以再次请求并成功获得同一把锁,从而避免了在递归调用或嵌套同步块中产生的死锁风险。这意味着在执行锁保护的代码区域时,即便调用了其他同样被该锁保护的方法或代码片段,持有锁的线程也能顺利完成操作。
在多线程环境下,可重入锁扮演着至关重要的角色,它严格限制了同一时间只能有一个线程访问特定的临界区,有效防止了并发访问引发的数据不一致和竞态条件问题。此外,通过允许线程在持有锁的状态下重新获取该锁,可重入锁巧妙地解决了同类锁之间由于互相等待而形成的潜在死锁状况,从而提升了多线程同步的安全性和可靠性。
可重入锁主要可以通过以下三种方式实现:
java.util.concurrent.locks.ReentrantLock
以下为可重入锁使用示例:
public class ReentrantLockExample { private final Lock lock = new ReentrantLock(); // 假设这是一个需要同步访问的共享资源 private int sharedResource; public void increment() { // 获取锁 lock.lock(); try { // 在锁保护下执行操作 sharedResource++; // 这里假设有个内部方法也需要同步访问sharedResource doSomeOtherWork(); } finally { // 无论发生什么情况,最后都要释放锁 lock.unlock(); } } // 可重入的内部方法 private void doSomeOtherWork() { // 因为当前线程已经持有锁,所以可以再次获取 lock.lock(); try { // 执行依赖于sharedResource的操作 sharedResource -= 1; System.out.println("Inner method executed with sharedResource: " + sharedResource); } finally { // 释放锁 lock.unlock(); } } public static void main(String[] args) { ReentrantLockExample example = new ReentrantLockExample(); Thread thread1 = new Thread(example::increment); Thread thread2 = new Thread(example::increment); thread1.start(); thread2.start(); // 等待两个线程执行完毕 try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } // 输出最终的sharedResource值 System.out.println("Final sharedResource value: " + example.sharedResource); }}
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
// 假设这是一个需要同步访问的共享资源
private int sharedResource;
public void increment() {
// 获取锁
lock.lock();
// 在锁保护下执行操作
sharedResource++;
// 这里假设有个内部方法也需要同步访问sharedResource
doSomeOtherWork();
// 无论发生什么情况,最后都要释放锁
lock.unlock();
// 可重入的内部方法
private void doSomeOtherWork() {
// 因为当前线程已经持有锁,所以可以再次获取
// 执行依赖于sharedResource的操作
sharedResource -= 1;
System.out.println("Inner method executed with sharedResource: " + sharedResource);
// 释放锁
ReentrantLockExample example = new ReentrantLockExample();
Thread thread1 = new Thread(example::increment);
Thread thread2 = new Thread(example::increment);
thread1.start();
thread2.start();
// 等待两个线程执行完毕
thread1.join();
thread2.join();
// 输出最终的sharedResource值
System.out.println("Final sharedResource value: " + example.sharedResource);
示例中,increment()方法和内部的doSomeOtherWork()方法都需要在获取锁的情况下执行。由于ReentrantLock是可重入的,所以在increment()方法内部调用doSomeOtherWork()时,线程仍然可以成功获取锁,并继续执行操作。当所有操作完成时,通过finally块确保了锁的释放。这样可以避免死锁,并确保在多线程环境下对共享资源的访问是线程安全的。
increment()
doSomeOtherWork()
finally
不可重入锁(Non-reentrant Lock)是一种线程同步机制,它的核心特征在于禁止同一个线程在已经持有锁的前提下再度获取相同的锁。若一个线程已取得不可重入锁,在其执行路径中遇到需要再次获取该锁的场景时,该线程将会被迫等待,直至原先获取的锁被释放,其他线程才有可能获取并执行相关临界区代码。
此类锁机制同样服务于多线程环境下的资源共享保护,旨在确保同一时间内仅有单一线程能够访问临界资源,从而有效规避数据不一致性和竞态条件等问题。相较于可重入锁,不可重入锁在递归调用或涉及锁嵌套的复杂同步场景下表现出局限性,因其可能导致线程阻塞和潜在的死锁风险,降低了线程同步的灵活性和安全性。在实际开发中,除非有特殊的需求或场景约束,否则更建议采用可重入锁以实现更为稳健高效的线程同步控制。
在Java标准库中并没有直接提供名为“不可重入锁”的内置锁,通常我们会通过对比ReentrantLock(可重入锁)来理解不可重入锁的概念。理论上,任何不具备可重入特性的锁都可以认为是不可重入锁。但在实际应用中,Java的synchronized关键字修饰的方法或代码块在早期版本中曾经存在过类似不可重入的行为,但在目前Java的所有版本中,synchronized关键字所实现的锁实际上是可重入的。
按照获取锁的公平性,也即请求顺序,将锁分为公平锁盒非公平锁。
公平锁是一种线程调度策略,在多线程环境下,当多个线程尝试获取锁时,锁的分配遵循“先请求先服务”(First-Come, First-Served, FCFS)原则,即按照线程请求锁的顺序来分配锁资源。这意味着等待时间最长的线程将优先获得锁。公平锁可以有效避免某个线程长期得不到锁而导致的饥饿现象,所有线程都有平等获取锁的机会。它确保了线程的调度更加有序,减少了不公平竞争导致的不确定性。
公平锁的实现,可以通过java.util.concurrent.locks.ReentrantLock的构造函数传入true参数,可以创建一个公平的ReentrantLock实例。
true
ReentrantLock fairLock = new ReentrantLock(true); //创建一个公平锁
以下使用公平锁示例:
public class FairLockExample { private final ReentrantLock fairLock = new ReentrantLock(true); // 使用true参数创建公平锁 public void criticalSection() { fairLock.lock(); // 获取公平锁 try { // 在此区域内的代码是临界区,同一时间只有一个线程可以执行 System.out.println(Thread.currentThread().getName() + " entered the critical section at " + LocalDateTime.now()); // 模拟耗时操作 Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { fairLock.unlock(); // 释放公平锁 } } public static void main(String[] args) { final FairLockExample example = new FairLockExample(); Runnable task = () -> { example.criticalSection(); }; // 创建并启动多个线程 for (int i = 0; i < 10; i++) { Thread t = new Thread(task); t.start(); } }}
public class FairLockExample {
private final ReentrantLock fairLock = new ReentrantLock(true); // 使用true参数创建公平锁
public void criticalSection() {
fairLock.lock(); // 获取公平锁
// 在此区域内的代码是临界区,同一时间只有一个线程可以执行
System.out.println(Thread.currentThread().getName() + " entered the critical section at " + LocalDateTime.now());
// 模拟耗时操作
Thread.sleep(1000);
Thread.currentThread().interrupt();
fairLock.unlock(); // 释放公平锁
final FairLockExample example = new FairLockExample();
Runnable task = () -> {
example.criticalSection();
};
// 创建并启动多个线程
for (int i = 0; i < 10; i++) {
Thread t = new Thread(task);
t.start();
在这个示例中,我们创建一个公平锁,我们创建了多个线程,每个线程都在执行criticalSection方法,该方法内部的代码块受到公平锁的保护,因此在任何时候只有一个线程能在临界区内执行。当多个线程尝试获取锁时,它们会按照请求锁的顺序来获取锁,确保线程调度的公平性。
criticalSection
非公平锁是一种线程调度策略,在多线程环境下,当多个线程尝试获取锁时,锁的分配不遵循“先请求先服务”(First-Come, First-Served, FCFS)原则,而是允许任何等待锁的线程在锁被释放时尝试获取,即使其他线程已经在等待队列中等待更长时间。非公平锁在某些场景下可以提高系统的并发性能,因为它允许刚释放锁的线程或者其他新到达的线程立刻获取锁,而不是强制排队等待。
实现方式也同公平锁,也是通过java.util.concurrent.locks.ReentrantLock的构造函数,但是我们要传入false参数,可以创建一个非公平的ReentrantLock实例。
false
ReentrantLock fairLock = new ReentrantLock(false); //创建一个非公平锁
我们常说或者常用的悲观锁以及乐观锁就是以对共享资源的访问方式来区分的。
悲观锁(Pessimistic Lock)是一种并发控制策略,它假设在并发环境下,多个线程对共享资源的访问极有可能发生冲突,因此在访问资源之前,先尝试获取并锁定资源,直到该线程完成对资源的访问并释放锁,其他线程才能继续访问。悲观锁的主要作用是在多线程环境中防止数据被并发修改,确保数据的一致性和完整性。当一个线程获取了悲观锁后,其他线程必须等到锁释放后才能访问相应资源,从而避免了数据竞态条件和脏读等问题。悲观锁适合写操作较多且读操作较少的并发场景。
而悲观锁的实现可以通过synchronized关键字实现的对象锁或类锁。或者通过java.util.concurrent.locks.Lock接口的实现类,如ReentrantLock。
悲观锁虽然在并发场景下数据的一致性和完整性。但是他却有一些缺点,例如:
以下我们使用显式锁ReentrantLock实现一个悲观锁的示例:
import java.util.concurrent.locks.ReentrantLock;public class Account { private final ReentrantLock lock = new ReentrantLock(); private double balance; public void deposit(double amount) { lock.lock(); try { // 持有锁进行存款操作 balance += amount; // 更新账户余额的其他逻辑... } finally { lock.unlock(); // 保证锁一定会被释放 } } public void withdraw(double amount) { lock.lock(); try { // 持有锁进行取款操作 if (balance >= amount) { balance -= amount; // 更新账户余额的其他逻辑... } } finally { lock.unlock(); } }}
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private final ReentrantLock lock = new ReentrantLock();
private double balance;
public void deposit(double amount) {
// 持有锁进行存款操作
balance += amount;
// 更新账户余额的其他逻辑...
lock.unlock(); // 保证锁一定会被释放
public void withdraw(double amount) {
// 持有锁进行取款操作
if (balance >= amount) {
balance -= amount;
乐观锁并不是Java本身提供的某种内置锁机制,而是指一种并发控制策略,它基于乐观假设:即在并发访问环境下,认为数据竞争不太可能发生,所以在读取数据时并不会立即加锁。乐观锁适用于读多写少的场景或者并发较少的场景。
Java中的乐观锁通过CAS(Compare and Swap / Compare and Set)算法实现,而数据库层面我们常使用版本号或者时间戳等进行控制。
CAS(Compare and Swap / Compare and Set)
CAS(Compare and Swap / Compare and Set): Java提供了java.util.concurrent.atomic包中的原子类,如AtomicInteger、AtomicLong等,它们通过CAS操作来实现乐观锁。CAS操作是一个原子指令,它只会修改数据,当且仅当该数据的当前值等于预期值时才进行修改。例如,AtomicInteger中的compareAndSet方法就是在乐观锁思想下实现的一种无锁化更新操作。
java.util.concurrent.atomic
AtomicInteger
AtomicLong
compareAndSet
import java.util.concurrent.atomic.AtomicInteger;AtomicInteger counter = new AtomicInteger(0);// 乐观锁更新示例public void incrementCounter() { while (true) { int expected = counter.get(); int updated = expected + 1; if (counter.compareAndSet(expected, updated)) { // 更新成功,退出循环 break; } // 更新失败,意味着有其他线程在此期间改变了值,继续尝试 }}
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger counter = new AtomicInteger(0);
// 乐观锁更新示例
public void incrementCounter() {
while (true) {
int expected = counter.get();
int updated = expected + 1;
if (counter.compareAndSet(expected, updated)) {
// 更新成功,退出循环
break;
// 更新失败,意味着有其他线程在此期间改变了值,继续尝试
在Java中,JVM为了解决多线程环境下的同步问题,对锁机制进行了优化,将其分为偏向锁、轻量级锁和重量级锁三种状态。
偏向锁是一种Java虚拟机(JVM)在多线程环境下优化同步性能的锁机制,它适用于大多数时间只有一个线程访问同步代码块的场景。当一个线程访问同步代码块时,JVM会把锁偏向于这个线程,后续该线程在进入和退出同步代码块时,无需再做任何同步操作,从而大大降低了获取锁和释放锁的开销。偏向锁是Java内存模型中锁的三种状态之一,位于轻量级锁和重量级锁之前。
优点: 对于没有或很少发生锁竞争的场景,偏向锁可以显著减少锁的获取和释放所带来的性能损耗。
额外存储空间:偏向锁会在对象头中存储一个偏向线程ID等相关信息,这部分额外的空间开销虽然较小,但在大规模并发场景下,累积起来也可能成为可观的成本。
锁升级开销:当一个偏向锁的对象被其他线程访问时,需要进行撤销(revoke)操作,将偏向锁升级为轻量级锁,甚至在更高竞争情况下升级为重量级锁。这个升级过程涉及到CAS操作以及可能的线程挂起和唤醒,会带来一定的性能开销。
适用场景有限:偏向锁最适合于绝大部分时间只有一个线程访问对象的场景,这样的情况下,偏向锁的开销可以降到最低,有利于提高程序性能。但如果并发程度较高,或者线程切换频繁,偏向锁就可能不如轻量级锁或重量级锁高效。
轻量级锁是一种在Java虚拟机(JVM)中实现的同步机制,主要用于提高多线程环境下锁的性能。它不像传统的重量级锁那样,每次获取或释放锁都需要操作系统级别的互斥操作,而是尽量在用户态完成锁的获取与释放,避免了频繁的线程阻塞和唤醒带来的开销。轻量级锁的作用主要是减少线程上下文切换的开销,通过自旋(spin-wait)的方式让线程在一段时间内等待锁的释放,而不是立即挂起线程,这样在锁竞争不是很激烈的情况下,能够快速获得锁,提高程序的响应速度和并发性能。
在Java中,轻量级锁主要作为JVM锁状态的一种,它介于偏向锁和重量级锁之间。当JVM发现偏向锁不再适用(即锁的竞争不再局限于单个线程)时,会将锁升级为轻量级锁。
轻量级锁适用于同步代码块执行速度快、线程持有锁的时间较短且锁竞争不激烈的场景,如短期内只有一个或少数几个线程竞争同一线程资源的情况。
在Java中,轻量级锁的具体实现体现在java.util.concurrent.locks包中的Lock接口的一个具体实现:java.util.concurrent.locks.ReentrantLock,它支持可配置为公平或非公平模式的轻量级锁机制,当使用默认构造函数时,默认是非公平锁(类似于轻量级锁的非公平性质)。不过,JVM的内置synchronized关键字在JDK 1.6之后引入了锁升级机制,也包含了偏向锁和轻量级锁的优化。
java.util.concurrent.locks
重量级锁是指在多线程编程中,为了保护共享资源而采取的一种较为传统的互斥同步机制,通常涉及到操作系统的互斥量(Mutex)或者监视器锁(Monitor)。在Java中,通过synchronized关键字实现的锁机制在默认情况下就是重量级锁。确保任何时刻只有一个线程能够访问被锁定的资源或代码块,防止数据竞争和不一致。保证了线程间的协同工作,确保在并发环境下执行的线程按照预定的顺序或条件进行操作。
在Java中,重量级锁主要指的是由synchronized关键字实现的锁,它在JVM内部由Monitor实现,属于内建的锁机制。另外,java.util.concurrent.locks包下的ReentrantLock等类也可实现重量级锁,这些锁可以根据需要调整为公平锁或非公平锁。
重量级锁适用于:
在Java中,偏向锁、轻量级锁和重量级锁之间的转换是Java虚拟机(JVM)为了优化多线程同步性能而设计的一种动态调整机制。转换条件如下:
偏向锁到轻量级锁的转换: 当有第二个线程尝试获取已经被偏向的锁时,偏向锁就会失效并升级为轻量级锁。这是因为偏向锁假定的是只有一个线程反复获取锁,如果有新的线程参与竞争,就需要进行锁的升级以保证线程间的互斥。
轻量级锁到重量级锁的转换: 当轻量级锁尝试获取失败(CAS操作失败),即出现了锁竞争时,JVM会认为当前锁的持有者无法很快释放锁,因此为了避免后续线程无休止地自旋等待,会将轻量级锁升级为重量级锁。这个转换过程通常发生在自旋尝试获取锁达到一定次数(自旋次数是可配置的)或者系统处于高负载状态时。
偏向锁到重量级锁的转换: 如果当前线程不是偏向锁指向的线程,那么首先会撤销偏向锁(解除偏向状态),然后升级为轻量级锁,之后再根据轻量级锁的规则判断是否需要进一步升级为重量级锁。
锁状态的转换是为了在不同的并发环境下,既能保证数据的正确性,又能尽可能地提高系统性能。JVM会根据实际情况自动调整锁的状态,无需我们手动干预。
分段锁(Segmented Lock 或 Partitions Lock)是一种将数据或资源划分为多个段(segments),并对每个段分配单独锁的锁机制。这样做的目的是将锁的粒度细化,以便在高并发场景下提高系统的并发性能和可扩展性,特别是针对大型数据结构如哈希表时非常有效。通过减少锁的粒度,可以使得在多线程环境下,不同线程可以同时访问不同段的数据,减小了锁争抢,提高了系统的并行处理能力。在大规模数据结构中,如果只有一个全局锁,可能会因为热点区域引发大量的锁竞争,分段锁则能有效地分散锁的压力。
Java中,分段锁在实现上可以基于哈希表的分段锁,例如Java中的ConcurrentHashMap,将整个哈希表分割为多个段(Segment),每个段有自己的锁,这样多个线程可以同时对不同段进行操作。例外也可以基于数组或链表的分段锁,根据数据索引将数据分布到不同的段,每段对应一个独立的锁。
ConcurrentHashMap
分段锁可以提高并发性能,减少锁竞争,增加系统的并行处理能力。其优点:
分段锁也有一些缺点:
分段锁适用于大数据结构的并发访问,如高并发环境下对哈希表的操作。以及分布式系统中,某些分布式缓存或数据库系统也采用类似的分片锁策略来提高并发性能。
自旋锁(Spin Lock)是一种简单的锁机制,用于多线程环境中的同步控制,它的工作原理是当一个线程试图获取已经被另一个线程持有的锁时,该线程不会立即进入睡眠状态(阻塞),而是不断地循环检查锁是否已经被释放,直到获取到锁为止。这种“循环等待”的行为被称为“自旋”。自旋锁主要用于保证同一时刻只有一个线程访问临界区资源,防止数据竞争。相比传统阻塞式锁,自旋锁在持有锁的线程很快释放锁的情况下,可以减少线程的上下文切换开销。
我们使用AtomicInteger实现一个简单的自旋锁:
import java.util.concurrent.atomic.AtomicInteger;class SimpleSpinLock { private AtomicInteger locked = new AtomicInteger(0); public void lock() { while (locked.getAndSet(1) == 1) { // 自旋等待 } // 已经获取锁,执行临界区代码 } public void unlock() { locked.set(0); }}
class SimpleSpinLock {
private AtomicInteger locked = new AtomicInteger(0);
public void lock() {
while (locked.getAndSet(1) == 1) {
// 自旋等待
// 已经获取锁,执行临界区代码
public void unlock() {
locked.set(0);
自旋锁优点:
自旋锁缺点:
说到各种锁,就会想到死锁问题,对于死锁有兴趣的可以参考这篇文章: 这里就不过多赘述。
本文介绍了多种Java中的锁机制,包括可重入锁(Reentrant Lock)、公平锁、非公平锁、悲观锁、乐观锁、偏向锁、轻量级锁、重量级锁、分段锁以及自旋锁。这些锁各有优缺点和适用场景,如可重入锁支持递归锁定,悲观锁确保数据一致性但可能引起性能开销,乐观锁在读多写少场景下表现优异,偏向锁和轻量级锁用于优化单线程重复访问,重量级锁提供严格的互斥性,分段锁通过减小锁粒度提高并发性能,而自旋锁则在短时间内获取锁的场景中能减少线程上下文切换。根据不同的并发需求和性能考量,开发者可以选择合适的锁机制。
本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等
原文链接:https://www.cnblogs.com/coderacademy/p/18093724
本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728