掘金 后端 ( ) • 2024-04-21 00:22

在Java中,锁(Locks)是用来控制多线程对共享资源的访问的机制,确保在同一时间内只有一个线程可以访问特定的资源或执行特定的代码段。锁主要用于实现线程的同步。在Java中,有两种类型的锁被广泛讨论:可重入锁(Reentrant Locks)和非可重入锁(Non-reentrant Locks)。这两种锁的主要区别在于它们对已经持有锁的线程再次请求锁的处理方式。

可重入锁的基本概念

可重入锁(ReentrantLock)允许同一个线程多次获取同一把锁。这种设计使得一个线程可以进入任何一个它已经拥有的锁同步着的代码块。

ReentrantLock 类的源码解析

ReentrantLockjava.util.concurrent.locks包中的一个类,它实现了Lock接口。ReentrantLock类使用了一个内部类Sync来管理锁的状态,SyncAbstractQueuedSynchronizer (AQS)的子类。AQS是实现同步组件的框架。

Sync 类

Sync内部类在ReentrantLock中有两种主要形态:

  • NonfairSync (非公平锁)
  • FairSync (公平锁)

两者的主要区别在于锁获取的公平性。非公平锁可能会抢占正在等待的线程,而公平锁则按等待的顺序来获取锁。

abstract static class Sync extends AbstractQueuedSynchronizer {
    abstract void lock();

    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }
}

锁的获取和释放

  • 锁的获取:当调用lock()方法时,NonfairSyncFairSync将尝试通过nonfairTryAcquirefairTryAcquire方法来设置状态。如果当前状态为0(未被锁定),状态将被设置为1,并将锁的所有者设置为当前线程。
  • 锁的释放:当调用unlock()方法时,tryRelease方法会被调用,减少持有的状态。如果减后的状态为0,表示锁已经完全释放,可以被其他线程获取。

可重入性的实现

可重入性是通过检查锁的当前所有者是否为正在执行的线程来实现的。如果一个线程已经持有了锁,它可以直接再次获取锁,状态值将增加,而不是从0变到1。

小结

通过这样的设计,ReentrantLock在提供锁的基本功能(互斥)的同时,还支持重入,使得编程模型更加灵活。此外,ReentrantLock提供的条件变量(Condition)支持使得Java的多线程编程更加强大。相比之下,非重入锁在递归调用时容易导致死锁,因此在实际使用中其应用场景更受限。

非重入锁的基本概念

非重入锁,顾名思义,是一次性锁。它要求锁的获取和释放严格一一对应,同一个线程如果已经持有锁,则不能再次获取,直到它释放了锁。如果尝试再次获取,将会导致锁的请求阻塞或者异常。

实现非重入锁的一个简单例子

下面是一个使用Java实现的非重入锁的简单示例,我们用最基本的同步机制来展示非重入锁的工作原理:

public class SimpleNonReentrantLock {
    private boolean isLocked = false;
    private Thread lockingThread = null;

    public synchronized void lock() throws InterruptedException {
        while (isLocked && lockingThread != Thread.currentThread()) {
            wait();
        }
        isLocked = true;
        lockingThread = Thread.currentThread();
    }

    public synchronized void unlock() {
        if (Thread.currentThread() != this.lockingThread) {
            throw new IllegalMonitorStateException("Calling thread has not locked this lock");
        }
        isLocked = false;
        lockingThread = null;
        notify();
    }
}

源码解析

  1. 锁的状态管理

    • isLocked: 一个布尔标志,表示锁的状态(是否被锁定)。
    • lockingThread: 记录获取了锁的线程,这是为了确保只有锁的持有者可以释放它。
  2. lock 方法

    • 使用while循环来检查锁是否已经被其他线程占用。
    • 如果isLocked为真且锁的持有者不是当前线程,则当前线程通过wait()调用等待。
    • 当锁被释放(即isLocked变为假)或当前线程是锁的持有者时,退出循环,设置isLocked为真,并记录锁的持有者为当前线程。
  3. unlock 方法

    • 首先检查调用unlock()的线程是否为锁的持有者。
    • 如果不是,抛出IllegalMonitorStateException异常。
    • 如果是,将isLocked标志设置为假,释放锁,并通过notify()唤醒可能在等待这个锁的线程。

非重入锁的特性

  • 简单性:非重入锁的逻辑比可重入锁简单,因为它不需要处理一个线程多次获取锁的情况。
  • 防止递归:非重入锁通过防止同一线程多次获得锁来简化死锁的风险管理,但这也意味着它不能在递归调用中使用,否则会导致线程死锁。

适用场景

由于其特性,非重入锁比较适合那些简单的同步需求场景,其中锁只需要简单的互斥而不涉及复杂的递归调用逻辑。对于需要递归调用同步方法的场景,非重入锁可能不是一个好的选择。

总之,非重入锁提供了一种更为直接和简化的线程同步机制,但它的使用需要小心,以避免因不恰当的使用导致死锁或其他同步问题。

可重入锁的场景案例

场景描述

假设我们正在开发一个在线商店的Spring Boot应用,其中包括一个订单处理系统。系统需求指定,订单处理过程必须是同步的,以避免同一时间对同一产品的过度销售(库存问题)。我们将使用ReentrantLock来确保订单处理过程的线程安全。

代码示例

首先,我们创建一个Spring Boot应用并配置一个简单的订单处理服务。我们将使用ReentrantLock确保处理订单的方法是线程安全的。

1. Maven依赖配置(pom.xml)

确保您的pom.xml文件中包括Spring Boot依赖,如下所示:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

2. 订单处理服务(OrderService.java)

package com.example.demo.service;

import org.springframework.stereotype.Service;

import java.util.concurrent.locks.ReentrantLock;

@Service
public class OrderService {

    private final ReentrantLock lock = new ReentrantLock();

    public void processOrder(String productId, int quantity) {
        lock.lock();  // Block until the lock is available
        try {
            // Simulate order processing
            System.out.println("Processing order for product " + productId + ", quantity " + quantity);
            Thread.sleep(1000); // This sleep simulates processing time
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("Failed to process order for product " + productId);
        } finally {
            lock.unlock();  // Always ensure locks are released
        }
    }
}

3. REST Controller(OrderController.java)

package com.example.demo.controller;

import com.example.demo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    @GetMapping("/order/{productId}/{quantity}")
    public String placeOrder(@PathVariable String productId, @PathVariable int quantity) {
        orderService.processOrder(productId, quantity);
        return "Order placed for product " + productId + ", quantity " + quantity;
    }
}

4. 主应用类(DemoApplication.java)

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

运行和测试

在您的IDE或命令行中运行上述Spring Boot应用。您可以使用任何HTTP客户端(如curl或Postman)来测试订单处理API。例如:

curl http://localhost:8080/order/1234/1

此命令应触发订单处理服务,由于加了锁,即使多个请求同时到达,每个请求也会被依次安全处理。

小结

通过以上示例,我们可以看到在Spring Boot中使用ReentrantLock来同步处理特定的方法是非常直接的。ReentrantLock提供的显式锁定机制比synchronized关键字更灵活,特别是在需要可配置锁行为(如公平性)或其他高级功能时。这种方式适用于需要高度控制并发访问的复杂应用场景。

非重入锁的场景案例

场景描述

假设我们在开发一个在线电影票务系统,用户可以在线选座并购买电影票。为了简化问题,我们假设在选择座位后,用户必须支付,系统需要保证在支付过程中,座位选择状态不会被同一用户的其他请求影响(例如,打开多个浏览器标签页尝试并行支付)。我们将使用非重入锁来实现这一需求。

代码示例

下面的代码示例将演示如何在Spring Boot应用中实现并使用一个简单的非重入锁。

1. Maven依赖配置(pom.xml)

确保您的pom.xml文件中包括Spring Boot依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

2. 非重入锁实现(SimpleNonReentrantLock.java)

package com.example.demo.lock;

public class SimpleNonReentrantLock {
    private boolean isLocked = false;

    public synchronized void lock() {
        if (isLocked) {
            throw new IllegalStateException("Lock is already acquired and is not reentrant.");
        }
        isLocked = true;
    }

    public synchronized void unlock() {
        isLocked = false;
        notify();
    }
}

3. 支付服务(PaymentService.java)

package com.example.demo.service;

import com.example.demo.lock.SimpleNonReentrantLock;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    private final SimpleNonReentrantLock lock = new SimpleNonReentrantLock();

    public void processPayment(String seatId) {
        lock.lock();
        try {
            // Simulate payment processing
            System.out.println("Processing payment for seat " + seatId);
            Thread.sleep(1000); // This sleep simulates processing time
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("Payment processing was interrupted for seat " + seatId);
        } finally {
            lock.unlock();
        }
    }
}

4. REST Controller(PaymentController.java)

package com.example.demo.controller;

import com.example.demo.service.PaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PaymentController {

    @Autowired
    private PaymentService paymentService;

    @GetMapping("/pay/{seatId}")
    public String makePayment(@PathVariable String seatId) {
        paymentService.processPayment(seatId);
        return "Payment initiated for seat " + seatId;
    }
}

5. 主应用类(DemoApplication.java)

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

运行和测试

运行您的Spring Boot应用,并通过以下命令测试支付API:

curl http://localhost:8080/pay/1A

如果尝试从相同或不同的终端多次快速执行该命令,将因锁的非重入性而失败,后续尝试将会抛出异常。

小结

通过上述示例,我们演示了如何在Spring Boot中使用自定义的非重入锁来控制对特定资源的访问,这种锁是通过抛出异常来防止同一线程的多次入锁,适用于简单的同步需求场景。这个例子简化了非重入锁的实现并显示了其在实际应用中的一种可能用途。

总结

在Java中,锁是用于控制多线程对共享资源访问的关键工具,可分为可重入锁和非可重入锁两种类型。以下是关于这两种锁类型的重点总结:

可重入锁 (Reentrant Lock)

  • 定义: 允许同一个线程多次获得同一把锁,从而避免死锁,增加灵活性。
  • 实现: 通过java.util.concurrent.locks.ReentrantLock,支持公平与非公平两种策略。
  • 使用场景: 适用于锁的持有者需要多次进入锁保护的代码块的情况,如递归调用。

非可重入锁 (Non-reentrant Lock)

  • 定义: 一旦一个线程获取了锁,即使是同一个线程也不能再次获取此锁,直到它被释放。
  • 实现: 通过自定义简单锁机制实现,不在Java标准库中明确提供。
  • 使用场景: 适用于需要简单锁定逻辑的场景,确保操作的原子性,避免复杂的递归调用问题。