Lombok @Locked指南


在本文中,我们学习了如何使用Lombok的@Locked注解。

Lombok 引入了该注释以更好地支持虚拟线程。它代表了ReentrantLock对象的替代。我们看到如何使用@Lock.Read和@Lock.Write注释来指定读写锁,而不是使用通用锁。最后,我们重点介绍了 @Locked和@Synchronized注释之间的几个区别。

Project Lombok是一个 Java 库,它提供了各种注释,我们可以使用它们来生成标准方法和功能,从而减少样板代码。例如,我们可以使用 Lombok 生成 getter 和 setter、构造函数,甚至在代码中引入设计模式,例如Builder 模式。

在本教程中,我们将学习如何使用Lombok 版本 1.18.32 中引入的@Locked注释。

为什么要使用@Locked注解?
首先,让我们了解@Locked注释的必要性。

Java 21引入了虚拟线程,以简化并发应用程序的实现、维护和调试。它们与标准线程的区别在于,它们由 JVM 而不是操作系统管理。因此,它们的分配不需要系统调用,也不依赖于操作系统的上下文切换。

但是,我们应该意识到虚拟线程可能带来的潜在性能问题。例如,当阻塞操作在同步块或方法内执行时,它仍然会阻塞操作系统的线程。这种情况称为锁定。另一方面,如果阻塞操作在同步块或方法之外,则不会引起任何问题。

此外,固定可能会对应用程序的性能产生负面影响,尤其是当阻塞操作被频繁调用且持续时间较长时。值得注意的是,不频繁且持续时间较短的阻塞操作不会引起此类问题。

解决固定问题的一种方法是用ReentrantLock替换synchronized。这就是新的 Lombok 注释发挥作用的地方。

理解@Locked注解
简单来说,@Locked注解是作为ReentrantLock的一个变体而创建的,其主要目的是为虚拟线程提供更好的支持。

此外,我们只能在静态或实例方法上使用此注解。它将方法中提供的整个代码包装到获取锁的块中。此外,我们可以指定要用于锁定的ReentrantLock类型的字段。结果,Lombok 对该特定字段执行锁定。方法执行完成后,它将解锁。

现在,让我们看看@Locked注释的实际作用。

依赖设置
让我们在pom.xml中添加lombok依赖项:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.34</version>
    <scope>provided</scope>
</dependency>

值得注意的是,我们需要 1.18.32 或更高版本才能使用此注释。

使用
让我们使用increment()和get()方法创建Counter类:

public class Counter {
    private int counter = 0;
    private ReentrantLock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();
        try {
            counter++;
        } finally {
            lock.unlock();
        }
    }
    public int get() {
        lock.lock();
        try {
            return counter;
        } finally {
            lock.unlock();
        }
    }
}

由于counter++不是原子操作,因此我们需要包含锁定,以确保共享对象的原子更新在多线程环境中对其他线程可见。否则,我们将得到不正确的结果。

这里,我们使用ReentrantLock来锁定increment()和get()方法,确保一次只有一个线程可以调用该方法。

让我们替换increment()和get()方法中的锁 ,并改用@Locked注释:

@Locked
public void increment() {
    counter++;
}
@Locked
public int get() {
    return counter;
}

我们将代码行数减少到每个方法只有一行。现在,让我们了解一下底层发生了什么。Lombok 创建一个名为$LOCK或$lock的ReentrantLock类型的新字段,具体取决于我们是在静态方法还是实例方法上使用锁定。

然后,它将方法中提供的代码包装到获取ReentrantLock的块中。最后,当我们退出该方法时,它会释放锁。

此外,使用@Locked注释注释的多个方法将共享同一个锁。如果我们需要不同的锁,我们可以创建一个ReentrantLock实例变量并将其名称作为参数传递给@Locked注释。

让我们测试这些方法以确保它们正常工作:

@Test
void givenCounter_whenIncrementCalledMultipleTimes_thenReturnCorrectResult() throws InterruptedException {
    Counter counter = new Counter();
    Thread.Builder builder = Thread.ofVirtual().name("worker-", 0);
    Runnable task = counter::increment;
    Thread t1 = builder.start(task);
    t1.join();
    Thread t2 = builder.start(task);
    t2.join();
    assertEquals(2, counter.get());
}

 @Locked.Read和@Locked.Write注释
我们可以使用@Locked.Read和@Locked.Write注释代替ReentrantReadWriteLock。

顾名思义,用@Locked.Read lock注释的方法对读锁起作用,而用@Locked.Write lock注释的方法对写锁起作用。

让我们修改increment()和get()方法中提供的代码 并使用@Locked.Write和@Locked.Read注释:

@Locked.Write
public void increment() {
    counter++;
}
@Locked.Read
public int get() {
    return counter;
}

提醒一下,对读写操作使用单独的锁可以提高性能,特别是在操作繁重时。

值得注意的是, Lombok 创建的ReentrantReadWriteLock字段的名称 与为@Locked注释生成的字段名称相同。因此,如果我们想在同一个类中使用这两个注释,我们需要为其中一个锁指定一个自定义名称。

@Locked和@Synchronized之间的区别
除了@Locked注解外,Lombok还提供了类似的@Synchronized注解,这两个注解的目的都是保证线程安全,下面我们来看一下它们的区别。

@Locked注释是ReentrantLock的替代品,而@Synchonized注释则取代了synchronized修饰符。就像关键字一样,我们只能在静态或实例方法上使用它。但是,synchronized锁定this ,而注释锁定 Lombok 创建的特定字段。

此外,使用虚拟线程时建议使用@Locked注解,而在相同情况下使用@Synchronized可能会导致性能问题。