优雅地接口调优之锁优化

继续进行接口调优,这一系列其实看起来干哇哇的,要是每个都详细举例子的话,篇幅就太长了。其实可以收藏起来,当项目上需要优化的时候,拿出来对照一下,看看哪一项比较契合自己的项目,再进行深入的研究。当实际需要并且手足无措的时候,这系列文章将是 你指路的明灯,欧耶。

好了,进入正文,本系列预计会有以下总结文章:

今天这篇,我们详细聊聊锁优化。

在接口调优的时候,锁优化也是很关键的性能优化策略之一 。写项目的时候但凡用到锁主要是对多线程并发控制,一个嘎嘎猛 的锁策略可以极大地提高系统的并发性能 。但是若是一个极度拉垮 的锁,则会严重影响性能甚至拖垮系统 。所以我们在使用锁的时候需要慎重考虑,合理优化。

以下是关于接口调优中锁优化的一些建议,也是本文主要要研究的内容。

  • 1.使用全局锁
  • 2.使用读写锁
  • 3.使用锁分离
  • 4.使用细粒度锁
  • 5.使用乐观锁
  • 6.注意锁超时机制
  • 7.使用无锁算法
  • 8.避免死锁
  • 9.锁精细化工具
  • 10.使用分布式锁
  • 11.充分利用并发集合

好的,接下来我们一个一个解释一下:

1.全局锁

不太考虑大并发和性能的时候可以使用全局锁 (Coarse-Grained Locks)。

全局锁的话,同一时刻只有一个线程可以持有该锁。当一个线程获得了全局锁,其他线程必须等待该线程释放锁后才能获得锁。这也就是为什么要求并发比较小的原因。使用起来比较简单,不用考虑太多的问题,缺点是有一定的性能瓶颈。

举例

假如有一个计数器,对同一数据进行加减操作,而且访问量也不大。这时候就可以加一个粗粒度的全局锁,既保证了数据的安全性,也更加的高效。

java 复制代码
class Counter {
    private int value = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        try {
            lock.lock();
            value++;
        } finally {
            lock.unlock();
        }
    }

    public void decrement() {
        try {
            lock.lock();
            value--;
        } finally {
            lock.unlock();
        }
    }

    public int getValue() {
        return value;
    }
}

increment 方法和decrement方法都使用了全局锁,也就是说整个方法在同一时刻只允许一个线程执行。

总结

  • 特点: 一个全局锁用于控制整个接口或系统的访问。
  • 优点: 实现简单,避免了复杂的锁管理。
  • 缺点: 并发度低,可能导致性能瓶颈,因为任何时候只能有一个线程能够执行接口中的任何一部分代码。
  • 适用场景: 当接口中的操作是互斥的且并发度较低时,全局锁是一种简单有效的选择。

2.读写锁

对于读写操作频繁的场景,可以考虑使用读写锁(ReentrantReadWriteLock),以允许多个线程同时读取,提高读操作的并发性能。

读写锁一般分为读锁和写锁,读锁之间共享,写锁之间互斥,读锁和写锁也互斥,也就是加了读锁的不能加写锁。

例子

先举个例子:假设有一个图书馆的书架,多个读者可以同时从书架上取阅图书,但只有一个图书管理员能够进行书籍的整理和更新,整理的时候,要求所有书本放回书架,也就是说需要加写锁的时候,要求没有读锁存在。

java 复制代码
class Library {
    private final Map<String, String> bookShelf = new HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    public String borrowBook(String title) {
        try {
            readLock.lock();
            return bookShelf.get(title);
        } finally {
            readLock.unlock();
        }
    }

    public void returnBook(String title, String newStatus) {
        try {
            writeLock.lock();
            // Only the librarian (one thread) can update the book status
            bookShelf.put(title, newStatus);
        } finally {
            writeLock.unlock();
        }
    }
}

在这个例子中,ReentrantReadWriteLock被用作读写锁的实现。readLock用于读取操作,而writeLock用于写入操作。读者可以同时读取图书,而只有一个线程(图书管理员)能够更新图书的状态,当有多个管理员需要更新时就需要等待锁的释放。这种设计在读操作频繁且互相不影响的情况下,可以提高并发性能。

需要注意的点

但是使用读写锁时有需要注意的几个点:

  • 选择合适的场景: 读写锁适用于读操作远远多于写操作的场景,就像上边的读者取书,很频繁,但是管理员更新书的次数很少。若是这个写操作频率很高,因为写锁是个互斥锁,又进入了频繁等待加锁解锁的情况,这时候读写锁的性能优势可能不够明显。
  • 精确控制读写锁的使用范围: 在使用读写锁时,要确保读取共享资源的代码块尽可能小,以便更多的线程可以同时读取,且不影响写锁,因为读锁持有的时候,写锁是无法被持有的。写入资源的代码块应该尽量减小写锁的持有时间,以减少对读取操作的影响。
  • 避免死锁: 读写锁与普通的互斥锁不同,可以允许多个线程同时持有读锁,但是只能有一个线程持有写锁。在设计时要确保不会因为持有读锁而导致死锁的情况。比如一个线程拿了读锁然后又去获取写锁。又或者没注意重入性的时候,拿了写锁,又跑去拿写锁。
  • 注意锁的升级与降级: 读写锁支持锁的升级(从读锁升级为写锁)和降级(从写锁降级为读锁)。在使用时要注意避免死锁和确保升级和降级的正确性。
  • 考虑使用 ReentrantReadWriteLock: 在 Java 中,ReentrantReadWriteLock 提供了读写锁的实现。使用它可以更方便地管理锁的升级和降级,嘎嘎好用,但也需要注意谨慎使用。

3.分段锁

分段锁既是将不同的资源或数据分离到不同的锁中,避免不同资源之间的锁竞争。这样可以降低锁的竞争程度,提高并发度。

例子

先举个例子,从例子中解释

java 复制代码
import java.util.concurrent.locks.ReentrantLock;

public class LockStripingExample {
    private final int segments = 16; // 假设将数据分为 16 段
    private final ReentrantLock[] locks = new ReentrantLock[segments];

    public LockStripingExample() {
        for (int i = 0; i < segments; i++) {
            locks[i] = new ReentrantLock();
        }
    }

    public void segmentedLockMethod(int key) {
        int segment = key % segments;
        locks[segment].lock();
        try {
            // 在特定段下的操作
        } finally {
            locks[segment].unlock();
        }
    }
}

这个例子是将数据分为多个段,每个段使用独立的锁 。这样若是操作数据段1的时候加了锁,其他操作数据段1的被block了,但是数据段2等其他数据段是可以正常操作的。明白了吧,就是把一块豆腐切成好几块,交给不同的厨师去处理,最后回来装一起。效率是高了,但是又额外引入了咋切分,咋组合的问题。

值得注意的是,数据之间若是有关联,那么锁分段可能并不是很好的选择,比如,各个小块豆腐之间还有一些关联,这个要注意那个的形状,那个要注意这个做多久了,那么无疑是增加了巨大的难度和复杂度。

总结

  • 特点: 将数据结构划分为多个段,每个段使用独立的锁,从而提高并发度。
  • 优点:某一时刻只有部分数据被锁定,可以提高并发度,同时相对于全局锁,复杂度较低。
  • 缺点: 需要谨慎选择分段策略,不同段之间的关联可能会引入额外的复杂性。
  • 适用场景: 在具有大量独立数据的场景中,锁分段是一种有效的提高并发度的方法。

4.细粒度锁

对于一些独立的资源或数据,可以考虑使用细粒度锁,将锁的范围缩小到最小需要的范围,以减小锁的竞争。

就是在同一个代码块中根据具体的业务添加不同的锁,锁不同的逻辑,增加并发度。

例子

java 复制代码
public class FineGrainedLockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void method1() {
        synchronized (lock1) {
            // lock1 下的操作
        }
    }

    public void method2() {
        synchronized (lock2) {
            // lock2 下的操作
        }
    }
}

method1method2 分别使用不同的锁,以提高并发度。这是一种粒度细锁的简单实现。也就是说将不同的步骤放到不同的锁里,或者说是将不同的对象放到不同的锁里

转账的时候,若是锁了转账的方法,那就毫无并发性能可言,但是若是锁的账户,那么能达到一样的效果,却极大地提高了性能。

总结

  • 特点: 将接口中的不同部分分别加锁,以提高并发度。
  • 优点: 提高了并发度,多个线程可以同时访问接口的不同部分。
  • 缺点: 实现和维护更为复杂,可能引入死锁等问题。
  • 适用场景: 当接口中的操作可以分解成相对独立的部分,且需要提高并发度时,粒度细锁是一种可行的选择。

5.乐观锁

在一些情况下,可以考虑使用乐观锁机制。乐观锁的常用实现方式包括版本号、CAS 算法等。

如何使用乐观锁

使用乐观锁进行接口调优时,通常需要遵循以下基本步骤:

  1. 选择合适的数据结构: 想要使用乐观锁,最起码有一个版本号时间戳的数据结构。确保我们的数据结构支持记录版本信息。
  2. 读取数据: 在执行更新之前,首先读取要修改的数据。同时,获取数据的版本号或时间戳。
  3. 执行业务逻辑: 在读取数据后,执行相应的业务逻辑。在此期间,其他线程可能也在尝试修改相同的数据。
  4. 比较版本号: 在准备提交更新之前,再次读取数据,获取最新的版本号或时间戳。比较两个版本号或时间戳,检查是否有其他线程修改了数据。如果版本号相同,表示没有冲突,可以继续。
  5. 更新数据: 如果版本号匹配,表示当前线程读取的数据在执行期间没有被修改,可以安全地进行更新操作。更新数据时,同时更新版本号或时间戳,确保其唯一性和递增。
  6. 处理冲突: 如果在比较版本号时发现冲突,需要执行相应的冲突解决策略。通常,这包括回滚操作或者重试整个操作。

简单说就是先读取,再修改,准备保存之前,再读取一下,然后比较版本号,一致就保存,不一致就执行其他策略

但是有极端情况 ,在第二次读取之后,数据被修改了,这种情况被称为"读取-修改-写入"之间的竞态条件。乐观锁是不能100%避免这种情况的。

例子

简单举个例子,假设有一个实体类 Product,其中包含一个版本号字段:

java 复制代码
@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private double price;

    @Version
    private int version; // 版本号字段

    // 省略构造函数和其他属性的 getter 和 setter 方法
}

更新方法,使用乐观锁来确保并发更新的安全性:

java 复制代码
@Service
public class ProductService {

    private final EntityManager entityManager;

    public ProductService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Transactional
    public String updateProduct(Long productId, String newName, double newPrice) {
        try {
            // 1. 读取数据并设置乐观锁
            Product product = entityManager.find(Product.class, productId);
            entityManager.lock(product, LockModeType.OPTIMISTIC);

            // 2. 执行业务逻辑
            product.setName(newName);
            product.setPrice(newPrice);

            // 3. 更新数据(版本号会在更新时自动递增)
            entityManager.persist(product);

            return "Update successful";
        } catch (OptimisticLockingFailureException e) {
            // 4. 处理乐观锁冲突
            return "Update failed due to conflict, please retry";
        }
    }
}

上边的例子中@Version 注解用于标记 version 字段,该字段在每次更新时会自动递增。在 updateProduct 方法中,通过 entityManager.lock 方法使用乐观锁,当版本号冲突时,会抛出 OptimisticLockingFailureException 异常,然后可以根据需要进行冲突处理。

需要注意的点

同时在使用乐观锁进行接口调优时,有一些需要注意的关键点,以确保正确性和性能的平衡。

  1. 冲突检测: 在使用乐观锁时,最终存储修改的时候需要检测是否存在冲突。可以通过比较版本号或时间戳等来判断数据是否已经被其他线程修改了。如果检测到冲突,也就是版本号或时间戳不一致了,需要执行相应的冲突解决策略,例如回滚操作、重试等。

  2. 原子性操作: 乐观锁通常基于原子性的比较-更新操作 ,例如MySQL的UPDATE ... WHERE语句,它可以在同一数据库事务内执行读取、比较和更新操作。确保在检测冲突和更新数据之间不会发生其他线程的干扰。使用原子性操作时,要确保底层的比较和更新是原子的,以避免竞态条件。

  3. 版本号管理: 如果使用版本号进行乐观锁,要确保版本号的管理是正确的。每次更新数据时,版本号都应该递增或更新。注意版本号的溢出问题,确保版本号的范围足够大,不会因为溢出而导致错误的比较结果。

  4. 性能影响: 乐观锁通常在无冲突的情况下能够提供更好的性能,但在存在冲突时可能需要进行重试操作,这可能导致性能下降。比如,100个线程操作一个数据,一个搁那操作呢,其余的全部一直在重试。那还不如一个锁来得实在。真正使用的时候,要进行性能测试,确保乐观锁的性能优势能够超过冲突检测和重试的开销。

  5. 事务边界: 乐观锁通常适用于较短的事务,长时间的事务可能增加发生冲突的可能性,因此需要谨慎使用乐观锁。考虑事务的边界,尽量将乐观锁用于事务较短的情况,以减少冲突的可能性。

  6. 冲突解决策略: 当检测到冲突时,需要有合理的冲突解决策略。这可能包括回滚事务、重试操作、提供冲突信息给用户等。冲突解决策略应该根据具体应用场景来制定,并确保在解决冲突的同时维护数据的一致性

6.锁超时机制

锁一定要设置超时时间的,不然线程嘎了,从此天各一方。所以在获取锁时需要设置超时机制,从而避免死锁情况的发生。当超过一定时间无法获取锁时,可以释放资源或进行其他处理。

常见的超时机制

  • 释放资源: 如果在超时时间内无法获取锁,可以尝试释放已经获取的资源,以避免资源泄漏。释放资源的具体操作取决于业务场景,可能涉及到数据库连接、文件句柄等资源的释放。比如,一群人在商店里排队买东西,结账只能一个一个来,有人等不及了,就将拿到的商品一一放回原位,然后离开。
  • 记录日志: 记录有关无法获取锁的信息,以便后续的排查和监控。包括时间、线程信息、资源等。使用合适的日志级别,确保信息足够详细,但不应导致日志过度增长。
  • 抛出异常: 可以选择抛出特定的异常来表示获取锁超时的情况。这样的异常可以在调用方进行捕获和处选择合适的异常类型,以便区分不同的故障情况。当多个用户抢购同一限量商品时,没抢到的用户则可以接收到已售罄的报错信息。
  • 重试机制: 在无法获取锁的情况下,可以选择进行一定次数的重试,以增加获取锁成功的机会。设置重试间隔,是为了避免过于频繁地尝试获取锁,以减轻系统负担。这个机制其实在分布式锁里很常用,没能获取到锁的时候,会等待一定时间,继续尝试获取。

需要注意的是

  1. 超时时间选择: 超时时间的选择需要谨慎,过短可能导致频繁的获取锁失败,过长可能导致线程长时间等待。这需要根据系统性能和业务需求进行调整。
  2. 对资源释放的风险评估:释放资源时需要评估可能引发的风险。例如,如果释放了数据库连接,可能导致其他操作的失败或数据不一致。
  3. 避免死锁: 超时机制可以一定程度上避免死锁情况,但要注意可能引入的新问题。例如,释放资源后可能导致其他线程进入临界区,需要确保这种情况下不会引发新的死锁。
  4. 日志记录的精度: 确保记录的日志信息足够精确,包括了线程信息、时间戳、锁标识等。这有助于后续的问题追踪和诊断。
  5. 异常处理: 在抛出异常时,确保调用方能够处理异常情况,避免出现未捕获的异常而导致系统不稳定。

设置超时机制是一种常见的避免死锁的手段

7.无锁算法

在一些场景中,可以考虑使用无锁算法,如CAS 操作,以提高并发性能。

无锁并不是真的放任自由,而是通过各种手段实现了,类似加锁了的线程安全。

举个例子

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class LockFreeAlgorithmExample {
    private AtomicInteger counter = new AtomicInteger(0);

    public void lockFreeMethod() {
        int oldValue, newValue;
        do {
            oldValue = counter.get();
            newValue = oldValue + 1;
        } while (!counter.compareAndSet(oldValue, newValue));
        // 无锁下的操作
    }
}

这个例子类似于自旋锁,lockFreeMethod 方法使用了 AtomicInteger 类,通过原子性的 compareAndSet 操作来实现无锁算法。这是个基于 CAS(Compare and Swap)的简单无锁示例。

总结

  • 特点: 完全避免使用锁,采用无锁算法实现并发操作。
  • 优点: 避免了锁带来的性能开销,具有更好的可伸缩性。
  • 缺点: 实现较为复杂,需要考虑原子性操作和并发控制。
  • 适用场景: 在高并发、对性能要求极高的场景中,无锁算法可能是一种选择。

8.避免死锁

死锁是指两个或多个线程相互等待对方释放锁资源,从而导致程序无法继续执行的情况。

注意锁的获取顺序,避免循环等待的情况,以防止死锁的发生。可以使用工具检测死锁,并及时解决。

避免死锁的常见策略

  1. 锁的顺序: 确保所有线程以相同的顺序获取锁。如果所有线程都按照相同的顺序获取锁,就可以避免循环等待的情况,减少死锁的可能性。
  2. 锁的粒度: 谨慎选择锁的粒度,避免持有过多锁。锁的粒度过大可能导致竞争激烈,而过小可能增加死锁的风险。使用细粒度锁,尽量减小锁的持有时间。
  3. 使用超时机制: 在获取锁的过程中,使用超时机制,即尝试获取锁的操作在一定时间内没有成功则放弃,防止因为等待时间过长而引起死锁。
  4. 定时重试: 如果在一定时间内无法获取锁,可以选择定时重试,而不是无限等待。这样可以在一段时间后释放已经获取的锁,重新尝试获取所需的锁。
  5. 事务管理: 对于涉及多个资源的操作,使用事务管理来确保所有资源的一致性。在某些情况下,数据库事务可以自动处理死锁。
  6. 避免嵌套锁: 尽量避免在持有一个锁的情况下去申请另一个锁。如果确实需要多个锁,确保它们按照相同的顺序获取,以减少死锁的可能性。
  7. 死锁检测: 在一些系统中,可以启用死锁检测机制,系统会周期性地检查死锁的存在,并尝试解除死锁。
  8. 合理的并发控制策略: 使用合理的并发控制策略,例如乐观锁、悲观锁等,以减小锁的冲突概率。
  9. 使用线程池: 使用线程池可以控制并发线程数量,减小线程的数量可以降低死锁的发生概率。
  10. 仔细设计并发结构: 在设计并发结构时,仔细考虑各个线程之间的交互关系,以及它们对共享资源的访问方式。合理的设计可以降低死锁的风险。

9.锁精细化工具

使用专业的锁精细化工具,例如分布式锁框架、缓存锁工具等,来简化锁的管理和提供更高级别的锁控制。

名称 简述 特点 适用场景
ReentrantLock java.util.concurrent.locks.ReentrantLock 是 Java 标准库提供的可重入锁 。相比于传统的 synchronized 关键字,ReentrantLock 提供了更多的灵活性,包括可中断锁、超时获取锁、公平性等特性。 可重入锁,提供更灵活的锁定和解锁操作,支持可中断锁、超时锁、公平性。 适用于需要灵活控制锁的情况,例如需要中断等待锁的线程、需要设置锁超时时间或者需要公平竞争的场景。
ReadWriteLock java.util.concurrent.locks.ReadWriteLock 接口定义了读写锁,其中包括读锁和写锁。读写锁允许多个线程同时读取共享资源,但只允许一个线程写入。ReentrantReadWriteLock 是其标准库实现。 分为读锁和写锁,读锁可以被多个线程共享,写锁是独占的。适用于读多写少的场景。 适用于读操作远远多于写操作的情况,可以提高并发性能。
StampedLock java.util.concurrent.locks.StampedLock 是在Java 8引入的一种锁机制,它提供了乐观锁和悲观锁的支持。StampedLock 的乐观读锁比 ReentrantReadWriteLock 更为轻量级。 提供了乐观读锁和悲观读锁,适用于读多写少的场景,悲观读锁使用类似ReentrantReadWriteLock,乐观读锁不阻塞写入操作,但需要手动检查读取的数据是否被写入。 适用于读操作频繁、写操作较少且写入时对读操作没有影响的场景。
Atomic 类 java.util.concurrent.atomic 包中的原子类,例如 AtomicInteger 提供了一系列的原子操作,适用于对单个变量进行原子操作的场景。 适用于简单的计数、状态标记等场景,不需要复杂的锁机制。
ConcurrentHashMap java.util.concurrent.ConcurrentHashMap 是一个线程安全的哈希表实现,通过分段锁(Segment)来提高并发性能,允许多个线程同时读取和部分并发写入。 基于分段锁实现,允许多个线程同时读取,部分并发写入。 适用于读操作频繁、写操作相对较少的情况,提供高并发读取和写入的能力。
LockSupport java.util.concurrent.locks.LockSupport 提供了线程阻塞和唤醒的工具类,可以用于自定义的锁实现。 提供线程阻塞和唤醒的工具,不是用于实现锁,而是辅助工具。 通常用于自定义锁的实现,不是作为直接的锁使用。
AQS(AbstractQueuedSynchronizer) java.util.concurrent.locks.AbstractQueuedSynchronizer 是一个提供了同步框架的抽象基类,可以用于自定义锁的实现。ReentrantLock 和 CountDownLatch 等锁类都是基于 AQS 实现的。 提供了同步框架的抽象基类,用于自定义锁的实现,ReentrantLock 和 CountDownLatch 等锁类都是基于 AQS 实现的。 适用于需要实现自定义锁的情况,能够灵活控制同步状态。
分布式锁 对于分布式系统,使用分布式锁工具如 ZooKeeper 或 Redis 等,可以实现在多个节点之间的协同工作,保证分布式环境下的数据一致性和并发控制。 用于分布式系统,可以跨多个节点实现协同工作,保证分布式环境下的数据一致性和并发控制。 适用于需要在分布式系统中进行锁控制的情况,确保多个节点之间的数据一致性。

在选择锁精细化工具时,需要综合考虑具体的业务需求、性能要求和系统设计。每个工具都有其特定的优势和限制,合适的选择将有助于提高系统的并发性能和可维护性。

10.分布式锁

这个就用的比较多了,对于分布式系统,合理选择分布式锁机制,可以避免因分布式环境导致的并发问题。

分布式锁的目标是在分布式环境下确保多个节点之间的同步,以避免资源冲突和数据不一致

之前有写过一篇分布式锁的文章,可以凑着一起看看分布式锁原理

操作和注意事项:

  1. 选择适当的分布式锁实现: 不同的分布式系统可能有不同的分布式锁实现,例如基于 ZooKeeper 的分布式锁、基于 Redis 的分布式锁等。根据实际情况选择合适的实现。

  2. 获取锁: 在获取分布式锁时,需要注意锁的获取是否具备原子性,确保在竞争条件下只有一个线程能够成功获取锁。

  3. 设置锁超时时间: 设置合理的锁超时时间,避免因为获取锁失败而一直阻塞。超时时间的选择需要考虑业务逻辑的特性和系统性能。

  4. 锁的释放: 确保在使用完锁之后及时释放,避免因为锁未及时释放而导致其他节点无法获取锁。

  5. 避免死锁: 在分布式环境下,由于网络分区、节点故障等原因,可能出现分布式锁的死锁情况。因此,需要在设计和使用分布式锁时考虑避免死锁的策略。

  6. 容错机制: 分布式系统中可能存在网络抖动、节点故障等问题,需要考虑引入容错机制,确保在一些异常情况下系统能够继续运行。

  7. 考虑锁粒度: 在设计分布式锁时,需要考虑锁的粒度。锁的粒度过大可能导致性能问题,而过小可能引起过多的锁竞争。

  8. 使用版本号或唯一标识: 在实现分布式锁时,可以使用版本号或者唯一标识来确保锁的唯一性。这有助于避免因为误操作或节点故障而导致锁无法正常释放。若是多个操作使用同一把锁,还存在错误释放锁的问题。

  9. 考虑锁的重入性: 在设计分布式锁时,需要考虑锁是否支持重入。即同一个线程能否多次获取同一个分布式锁。

  10. 测试和模拟故障场景: 在使用分布式锁之前,进行充分的测试和模拟故障场景,确保在各种异常情况下系统行为正常。

  11. 了解底层实现机制: 了解所选分布式锁的底层实现机制,理解其性能、可靠性和适用场景。

11.充分利用并发集合

Java中提供了诸如ConcurrentHashMapConcurrentLinkedQueue等并发集合,可以在不加锁的情况下实现高效的并发操作。

总结一下

名称 特点 适用场景
ConcurrentHashMap 使用分段锁(Segment)来提高并发性能,允许多个线程同时读取,部分并发写入。适用于读操作频繁、写操作相对较少的场景,提供高并发读写的能力。 适用于需要高并发读取和写入的键值对存储,例如缓存、计数器等。
ConcurrentSkipListMap 和 ConcurrentSkipListSet 基于跳表数据结构,提供有序的键值对存储。支持并发读写,插入、删除和查找的时间复杂度都是 O(log n)。适用于需要有序存储和高并发操作的场景。 适用于需要高并发有序存储的场景,例如实现有序映射或集合。
ConcurrentLinkedQueue 和 ConcurrentLinkedDeque 基于非阻塞算法的无锁队列,提供高效的并发队列操作。适用于多生产者、多消费者的场景,支持先进先出(FIFO)的队列操作。 适用于实现生产者-消费者模型,多线程之间进行数据交换和协同工作的场景。
CopyOnWriteArrayList 和 CopyOnWriteArraySet 通过在修改时创建副本,实现了读写分离,从而避免了读操作和写操作之间的冲突。适用于读多写少的场景。 适用于读操作频繁,写操作相对较少的场景,例如读取频繁的列表或集合。
LinkedBlockingQueue 和 LinkedBlockingDeque 基于链表的阻塞队列,支持有界和无界两种形式。在队列为空时,消费者会被阻塞等待;在队列已满时,生产者会被阻塞等待。 适用于实现生产者-消费者模型,对队列长度有限制的场景。
ArrayBlockingQueue 基于数组的阻塞队列,具有固定容量。支持有界队列,当队列满时生产者被阻塞,当队列空时消费者被阻塞。 适用于实现生产者-消费者模型,且对队列长度有明确限制的场景。
ConcurrentLinkedQueue 基于链表的非阻塞队列,适用于高并发的情况。支持多线程同时插入和移除操作,不需要加锁。 适用于多线程生产者-消费者模型或者其他需要高并发队列操作的场景。

这些并发集合类都是 Java 针对多线程并发环境设计的,并提供了线程安全的操作。选择适当的集合类需要根据具体的业务需求和并发访问模式,以提高程序的性能和可维护性。

所以锁的优化,主要考虑业务与并发性的要求。在加了锁之后,进行相关的性能测试是很有必要的,再根据测试的结果继续进行优化,以达到对应的要求。

至此,完结撒花。

相关推荐
小臭希1 分钟前
Java——琐碎知识点一
java·开发语言
学c真好玩12 分钟前
Django创建的应用目录详细解释以及如何操作数据库自动创建表
后端·python·django
Asthenia041212 分钟前
GenericObjectPool——重用你的对象
后端
Piper蛋窝23 分钟前
Go 1.18 相比 Go 1.17 有哪些值得注意的改动?
后端
excel37 分钟前
招幕技术人员
前端·javascript·后端
盖世英雄酱581361 小时前
什么是MCP
后端·程序员
淋一遍下雨天1 小时前
Spark Streaming核心编程总结(四)
java·开发语言·数据库
琢磨先生David1 小时前
重构数字信任基石:Java 24 网络安全特性的全维度革新与未来防御体系构建
java·web安全·密码学
程序修理员2 小时前
技能点总结
java
Jennifer33K2 小时前
报错_NoSuchMethodException: cn.mvc.entity.User.<init>()
java