JAVA中常见的锁和锁的实现

一、乐观锁和悲观锁

1、乐观锁(Optimistic Locking)​​​​​​​--读多写少

乐观锁:假设多个线程之间的冲突很少发生,因此不加锁直接访问共享资源,然后在更新时检查是否发生冲突。

乐观锁的实现

(1)版本号(Versioning)

在数据实体中添加一个版本字段(如version),每次修改数据时,不仅更新业务字段,还递增版本号。当一个线程尝试更新数据时,它会先读取当前版本号,然后执行业务逻辑并计算新的数据状态,最后在更新数据时附带当前读取到的版本号。如果在提交更新时发现数据库中当前版本号与之前读取时不一致(表明有其他线程在此期间进行了更新),则更新操作失败,通常会抛出异常,如OptimisticLockingFailureException。在Java中,可以使用JPA或Hibernate等ORM框架提供的@Version注解来自动管理版本字段。

(2)CAS(Compare-And-Swap)

Java的java.util.concurrent.atomic包提供了诸如AtomicIntegerAtomicLongAtomicReference等原子类,它们利用了CPU级别的CAS指令来实现无锁的原子更新。在更新操作中,CAS会比较当前值与预期值,只有两者相等时才会更新值,否则不会修改。这种机制可以在不使用锁的情况下实现乐观并发控制,线程在更新时检查值是否被其他线程改变,如果没有改变则更新,否则可以采取重试或其他策略。

(3)StampedLock的乐观读模式

java.util.concurrent.locks.StampedLock提供了一种读写锁的变体,其中包含了乐观读(optimistic read)模式。在乐观读模式下,线程获取一个所谓的“乐观读取戳”(stamp),而不是实际的锁。在读取数据后,线程可以使用这个戳来验证数据在读取期间是否被其他线程写入,如果没有则认为读取有效,否则需要重新读取。

2、悲观锁(Pessimistic Locking)--写多读少或并发写

悲观锁默认加锁,假设会有并发冲突发生,因此在访问共享资源之前先获取锁。

悲观锁的实现

(1)synchronized关键字

Java中的synchronized关键字用于声明方法或代码块为同步的,它隐式地获取对象锁。当一个线程进入synchronized代码块或方法时,会锁定对象,其他试图访问同一对象锁的线程将被阻塞,直到持有锁的线程退出同步区域并释放锁。这种锁定机制假定并发环境下存在大量冲突,因此在访问数据前就进行锁定,确保数据操作的独占性。

(2)ReentrantLock

ReentrantLockjava.util.concurrent包提供的可重入、互斥锁实现。与synchronized相比,它提供了更精细的锁定控制,如公平锁与非公平锁的选择、可中断的锁请求、以及条件队列(Condition)支持。使用ReentrantLock时,线程必须显式地调用lock()方法获取锁和unlock()方法释放锁。在锁定期间,其他线程的锁请求将被阻塞,直到锁被释放,这符合悲观锁的策略。

(3)数据库事务中的悲观锁

在使用JDBC或ORM框架(如JPA、Hibernate)操作数据库时,可以通过设置事务隔离级别或使用特定的查询语句来实现悲观锁。例如,使用SELECT ... FOR UPDATE语句在查询时立即锁定所选行,阻止其他事务对该行的修改,直到当前事务结束。

二、公平锁和非公平锁​​​​​​

1、公平锁(fair Lock)--FIFO

公平锁:在锁的分配上遵循“先来后到”的原则。当锁可用时,优先分配给等待时间最长的线程,从而减少了线程饥饿的可能性。公平锁保证了所有线程按照申请锁的顺序获得锁,类似于队列中的FIFO(先进先出)规则。

公平锁的实现

(1)ReentrantLock 的公平模式:ReentrantLock 是 Java 中提供的一种可重入锁,通过 ReentrantLock(boolean fair) 构造函数可以创建公平锁。公平模式会按照先来后到的顺序分配锁,但由于公平锁需要维护一个有序队列,因此可能会带来一些性能损失。

(2)synchronized 关键字:synchronized 关键字所获得的锁默认是公平锁,即按照先后顺序获取锁。但是,synchronized 无法设置为非公平锁模式。

//公平锁
ReentrantLock fairLock = new ReentrantLock(true);

2、非公平锁(Nonfair Lock)--性能较高

非公平锁:锁的分配不考虑线程的等待时间,即使新到达的线程也可能“插队”获得锁,优先级高于已经在队列中等待的线程。非公平锁在尝试获取锁时,首先会尝试通过CAS(Compare-And-Swap)操作直接获取锁,如果失败(即锁已被其他线程持有),才会加入等待队列。这种直接尝试获取锁的方式可能导致已等待的线程长时间得不到锁,形成“饥饿”现象,但在许多并发场景下,尤其是锁竞争不激烈或锁持有时间较短的情况下,非公平锁的性能优势更为明显。

非公平锁的实现

// 默认构造函数创建非公平锁
ReentrantLock nonfairLock = new ReentrantLock(); 

 

三、互斥锁/读写锁

1、互斥锁(Mutex Lock)--同步保障

互斥锁:确保任何时候只有一个线程访问被保护的资源,无论该线程是进行读操作还是写操作。互斥锁提供了最强的同步保障,防止了数据竞争。synchronized 关键字和 ReentrantLock 在未指定为读写锁时即为互斥锁。

互斥锁的实现

(1)synchronized关键字

synchronized关键字可以直接作用于方法或代码块,提供一种内置的互斥锁机制。当一个线程进入synchronized区域时,它会获得对象锁,其他试图进入该区域的线程将被阻塞,直到持有锁的线程退出同步块或方法并释放锁。

public class Example {
    public synchronized void synchronizedMethod() {
        // 受互斥锁保护的代码
    }

    public void synchronizedBlock() {
        synchronized (this) {
            // 受互斥锁保护的代码
        }
    }
}

 (2)ReentrantLock

ReentrantLock类提供了一个可重入的互斥锁,它比synchronized关键字更灵活,支持公平锁和非公平锁的选择、可中断的锁请求以及更复杂的条件队列(Condition)功能。使用ReentrantLock需要显式地调用lock()unlock()方法来获取和释放锁。

import java.util.concurrent.locks.ReentrantLock;

class Example {
    private final ReentrantLock lock = new ReentrantLock();

    public void lockedMethod() {
        lock.lock();
        try {
            // 受互斥锁保护的代码
        } finally {
            lock.unlock();
        }
    }
}

​​​​​​​2、读写锁(Read-Write Lock)--多线程读/单线程写

读写锁:它分为读锁(共享锁)和写锁(独占锁)。读锁允许多个读线程并发访问,而写锁在同一时间只允许一个写线程访问。读写锁提高了在读多写少场景下的并发性能,因为它允许并发读,但会限制并发写和读写之间的并行,读写锁确保了读读不互斥、读写互斥、写写互斥的特性。

读写锁的实现

import java.util.concurrent.locks.ReentrantReadWriteLock;

// 创建读写锁实例
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

// 获取读锁和写锁
ReentrantReadWriteLock.ReadLock readerLock = rwl.readLock();
ReentrantReadWriteLock.WriteLock writerLock = rwl.writeLock();

// 使用读锁进行读操作
readerLock.lock();
try {
    // 读取共享资源的代码
} finally {
    readerLock.unlock();
}

// 使用写锁进行写操作
writerLock.lock();
try {
    // 更新共享资源的代码
} finally {
    writerLock.unlock();
}

四、独占锁/共享锁(参考读写锁)

独占锁(也称写锁)一次只能由一个线程持有,其他试图获取该锁的线程将被阻塞。独占锁确保任何时候只有一个线程访问被保护的资源,适用于需要对资源进行排他性写入或修改的场景。ReentrantLock在没有指定为读写锁时默认表现为独占锁。

共享锁(也称读锁)允许多个线程同时持有,通常用于读取操作。当一个线程持有共享锁时,其他线程也可以同时获取该锁进行读取,但任何写操作都会被阻塞,直到所有共享锁都被释放。ReentrantReadWriteLock 的读锁就是一个典型的共享锁。

五、可重入锁/不可重入锁

1、可重入锁(Reentrant Lock)--不会死锁

可重入锁:允许同一个线程在已经持有该锁的情况下,再次对其加锁而不被阻塞。例如,如果一个线程持有一个可重入锁进入某个方法,在该方法内部又调用了另一个也需要同样锁的方法,那么由于锁的可重入性,使得线程在递归调用或嵌套调用时,不会因再次请求相同的锁而导致死锁。Java 的 ReentrantLock 和 synchronized 关键字实现的锁都是可重入的。

可重入锁的实现

(1)ReentrantLock

ReentrantLock是Java并发包提供的一个可重入、互斥的锁实现。它支持公平锁和非公平锁模式,并提供了比synchronized关键字更丰富的功能,如可中断锁请求、条件变量等。ReentrantLock内部维护一个线程持有计数器,当线程首次获得锁时,计数器加1;当线程释放锁时,计数器减1。只有当计数器为0时,锁才真正被释放,其他线程才能获得锁。因此,一个线程可以多次获得同一个ReentrantLock实例,而不会相互阻塞。

(2)synchronized

Java语言内置的关键字synchronized修饰的方法或代码块也是可重入的。当一个线程进入synchronized方法或代码块时,会在当前线程的栈帧中记录锁信息。后续递归或嵌套的synchronized代码块如果使用的是同一锁对象,那么由于锁识别到是由当前线程持有,所以允许再次进入,不会造成自我阻塞。

2、不可重入锁(Non-Reentrant Lock)--可能会死锁不建议使用

不可重入锁:一个线程一旦获得了锁,就不能再重新获得该锁,即使是在同一方法或者不同层次的嵌套调用中。如果尝试重入,将会导致死锁。Java标准库中没有直接提供不可重入锁,但在特定场景下,开发者可能需要自行实现或使用第三方库提供的不可重入锁。

六、分段锁(Segment Locking)

分段锁:一种将锁分成多个段的锁机制,每个段都有自己的锁。这样,不同的线程可以同时访问不同段的共享资源,从而提高并发性能。Java中的 ConcurrentHashMap 就是使用分段锁来实现高并发访问的。

分段锁的实现

(1)在Java 7及更早版本的ConcurrentHashMap中,分段锁的具体实现如下:

Segment数组:CHM内部维护了一个Segment数组,每个Segment代表哈希表的一部分,相当于一个独立的、线程安全的小型哈希表。每个Segment都继承自ReentrantLock,即每个分段都是一个可重入锁。

哈希桶分布:插入到CHM中的键值对会被分散到各个Segment中。通过散列函数和Segment数量对键的哈希值进行二次哈希,确定键值对应该落入哪个Segment

并发访问:当多个线程同时访问CHM时,只要它们操作的是不同的Segment,就能实现并发执行,互不干扰。如果多个线程访问同一个Segment,则需要竞争对应的Segment锁,遵循锁的互斥原则,确保数据安全性。

操作方法:CHM的所有操作(如put()get()remove()等)都通过定位到对应的Segment,并在其上进行相应操作。操作过程中,首先尝试获取对应的Segment锁,成功后再进行数据操作,操作完成后释放锁。

(2)Java 8对ConcurrentHashMap的实现进行了重大革新,不再使用分段锁,而是采用了以下设计:

Node结构:使用Node节点代替原有的Entry,每个节点包含键值对和指向下一个节点的引用。节点之间通过链表或树形结构连接,形成链表桶或红黑树桶。

CAS操作:对Node的更新操作(如插入、删除、替换等)大量使用了CAS(Compare-And-Swap)原子操作,以实现无锁化更新。这减少了对锁的依赖,提升了并发性能。

细粒度锁:虽然不再有明确的分段锁概念,但Java 8的CHM在某些操作(如扩容、树化等复杂操作)时仍会使用锁。不过,这些锁通常是针对单个桶(即一个链表或红黑树的头节点)进行的,具有比分段锁更细的粒度,进一步减少了锁竞争。

扩容机制:扩容不再是原来的“rehash all”,而是采用了一种更高效的方式,只迁移部分桶的数据,且迁移过程中允许其他线程并发访问和更新。

七、自旋锁

自旋锁:让等待锁的线程在原地循环(自旋)一段时间,而不是立即挂起,直到锁被释放。自旋期间,线程持续检查锁是否可用,如果在很短的时间内锁被释放,那么线程就可以立即获得锁并继续执行,避免了线程上下文切换的开销。自旋锁适用于锁持有时间非常短的场景。Java中虽没有直接提供自旋锁,但 ReentrantLock 可以通过设置参数启用自旋等待,另外,java.util.concurrent.atomic 包中的原子类(如AtomicInteger)在一定程度上实现了自旋锁的效果。

自旋锁的实现

(1)使用AtomicInteger等原子类

原子类如AtomicIntegerAtomicBoolean等提供了原子性的CAS(Compare-And-Swap)操作,可以用来实现自旋锁。

import java.util.concurrent.atomic.AtomicInteger;


//state被初始化为0,表示锁未被持有。当线程尝试获取锁时,通过compareAndSet方法尝试将状态从0改为1。
//如果当前状态已经是1(即锁已被其他线程持有),该方法返回false,线程将继续循环(自旋)尝试。
//一旦状态为0且成功设置为1,线程就获得了锁。解锁时,只需将状态重置为0。
public class SpinLock {
    private AtomicInteger state = new AtomicInteger(0);

    public void lock() {
        while (!state.compareAndSet(0, 1)) {
            // do nothing or yield to reduce CPU usage
        }
    }

    public void unlock() {
        state.set(0);
    }
}

(2)使用locks下的park/unpark

java.util.concurrent.locks.LockSupport类提供了低级别的线程阻塞和唤醒支持,可以用来实现自旋锁。这种方法结合了自旋和线程阻塞,当自旋一定次数后仍未获得锁,线程会进入阻塞状态,以减少不必要的CPU消耗。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

//owner字段表示当前持有锁的线程,count表示锁的重入次数。线程在尝试获取锁时,如果发现锁未被持有或自己已经持有锁(重入),则直接更新状态并返回。
//否则,线程会进行一定次数的自旋,若仍未能获得锁,则调用LockSupport.park使当前线程进入阻塞状态。
//解锁时,递减重入计数,当计数为0时,将owner设为null,并通过LockSupport.unpark唤醒等待的线程。
public class SpinLockWithPark {
    private Thread owner = null;
    private int count = 0;

    public void lock() {
        Thread currentThread = Thread.currentThread();
        while (true) {
            if (owner == null) {
                // 初次获取锁
                if (Thread.currentThread().equals(owner)) {
                    count++;
                    return;
                }
            } else if (currentThread.equals(owner)) {
                // 同一线程重入
                count++;
                return;
            } else {
                // 自旋一定次数后,尝试park
                for (int i = 0; i < SPIN_LIMIT; i++) {
                    if (owner == null) {
                        break;
                    }
                }
                LockSupport.park(this);
            }
        }
    }

    public void unlock() {
        if (Thread.currentThread().equals(owner)) {
            count--;
            if (count == 0) {
                owner = null;
                LockSupport.unpark(Thread.currentThread());
            }
        }
    }
}

八、锁升级

 偏向锁/轻量级锁/重量级锁

这是Java虚拟机(JVM)对s ynchronized 关键字底层实现的三种锁升级状态,属于锁的优化技术

偏向锁(Biased Locking:假设大多数情况下,锁会被同一线程多次获得,因此引入偏向锁,使锁偏向于第一个获得它的线程。后续该线程再次请求锁时,无需进行任何同步操作即可直接获得,大大降低了锁的开销。

轻量级锁(Lightweight Locking:当偏向锁失败(即有其他线程竞争锁)时,JVM会尝试升级为轻量级锁。轻量级锁依赖CAS操作尝试快速获取锁,避免了重量级锁带来的系统调用和线程上下文切换的开销。

重量级锁(Heavyweight Locking:当轻量级锁也失败(即CAS操作持续失败,意味着存在多个线程竞争)时,锁升级为重量级锁。此时,JVM会为锁分配内核对象(如Monitor),涉及线程上下文切换和操作系统层面的同步操作,开销较大,但能保证严格的互斥性和可见性。

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值