likes
comments
collection
share

Lock和synchronized是什么?

作者站长头像
站长
· 阅读数 26

在Java中,Locksynchronized是用于管理多线程环境中对资源的访问,以保证线程安全的两种机制。本文将介绍一下他们的基本原理以及代码中的实际用法示例。

简单介绍一下Lock

当谈到Java中的Lock机制,特别是如ReentrantLock这样的具体实现,我们可以从几个不同的层面深入理解其原理和工作方式。Lock接口提供了比内置的synchronized关键字更复杂的锁定机制,允许更精细的控制和扩展的功能。

1. Lock接口基础

Lock接口定义了锁定操作必须遵守的基本协议,主要方法包括lock(), unlock(), tryLock(), 和 lockInterruptibly()。这些方法提供了显示的锁定和解锁功能,相比于synchronized的自动锁管理,开发者需要明确地指示何时获取锁和释放锁。

2. ReentrantLock作为Lock的实现

ReentrantLockLock接口的一个实现,提供了可重入的互斥锁功能。可重入意味着同一个线程可以多次获取同一个锁,而不会导致死锁。

锁的状态管理

  • 状态表示ReentrantLock使用一个state变量来表示锁的持有计数。当线程首次获得锁时,state被设置为1。如果同一个线程再次获取这个锁,state就会增加。每次释放锁时,state递减,直到0,这时锁被完全释放。
  • 所有者追踪ReentrantLock还追踪当前持有锁的线程,这允许它实现可重入性和检查锁的所有权。

锁获取

  • 公平性选择ReentrantLock可以配置为公平或非公平。在公平模式下,锁倾向于授予等待时间最长的线程,而在非公平模式下,锁可能会被新请求的线程抢占。
  • 锁机制:线程试图通过调用lock()获得锁。如果锁已被其他线程持有,则当前线程会被挂起(阻塞),直到锁可用。
  • 条件变量支持ReentrantLock允许创建一个或多个Condition实例,这些实例与锁绑定,提供类似Object监视器方法(waitnotifynotifyAll)的功能。

锁的释放

  • 释放锁:当线程完成其临界区内的操作后,必须调用unlock()来释放锁。这是与synchronized明显不同的地方,后者会在方法或代码块结束时自动释放锁。

3. 底层实现细节

ReentrantLock的实现依赖于底层的同步器框架,即AbstractQueuedSynchronizer(AQS)。AQS使用一个双向队列来管理等待锁的线程,这是实现锁定和同步机制的核心。

  • 节点和队列:AQS内部包含一个节点(Node)类和一个队列。每个节点包含一个线程引用,表示等待锁的线程,以及关于线程等待状态的信息。
  • 独占模式ReentrantLock在AQS的独占模式下工作,这意味着一次只有一个线程能持有锁。
  • CAS操作:AQS使用比较并交换(CAS)操作来管理其状态字段,这提供了非阻塞的状态管理,是实现高效同步的关键。

通过这些机制,ReentrantLock提供了一种比synchronized更灵活和功能丰富的锁机制,适用于更复杂的并发场景。

简单介绍一下synchronized

synchronized是Java中的一个基本同步机制,它内置于Java语言和JVM中。它主要用于控制多线程对共享资源的访问,防止出现数据不一致的情况。synchronized可以用于方法或者特定代码块,确保同一时间只有一个线程执行特定的代码段。

原理概述

synchronized关键字实现了对一个对象的监视器(monitor)的锁定和解锁。在Java中,每个对象都隐含关联一个监视器,这个监视器可以帮助实现对临界区的互斥访问。

  • 方法锁:当synchronized用于实例方法或静态方法时,锁定的是对象实例(this)或类的Class实例。
  • 块锁:当synchronized用于代码块时,需要指定一个锁对象。

锁的状态

JVM为每个对象和类维护一个锁(或监视器锁),这个锁有几个状态:

  • 无锁状态
  • 偏向锁:这是一种优化手段,假设一个锁主要被一个线程访问,将锁的所有权偏向这个线程,避免了真正的争夺。
  • 轻量级锁:当锁对象被不同的线程访问,但没有竞争出现时,使用轻量级锁来优化。
  • 重量级锁:当有真正的锁竞争时,锁会升级为重量级锁,此时会涉及到操作系统级的互斥原语。

工作原理

当一个线程尝试获取一个由synchronized保护的锁时,它需要检查锁的状态:

  • 如果锁是偏向锁且已偏向于调用线程,线程将直接进入同步块。
  • 如果锁是可用的(无锁状态),JVM会尝试通过CAS操作将锁的状态设置为当前线程所有,变为轻量级锁。
  • 如果锁已被其他线程持有,当前线程会根据锁的状态进行相应的处理:
    • 如果锁是轻量级锁,JVM可能会自旋,尝试获取锁几次。
    • 如果自旋失败,或锁已是重量级锁,线程将被阻塞直到锁被释放。

底层实现

在底层,synchronized关键字的实现依赖于JVM的内部机制,具体如下:

  • 对象头:在HotSpot JVM中,每个对象头包含两部分信息,标记字(mark word)和类型指针。标记字中存储了对象的锁状态信息、哈希码、偏向线程ID、年龄等信息。
  • 监视器(Monitor):JVM内部使用Monitor对象来支持synchronized,该对象包含两个基本结构:WaitSet和EntryList。被阻塞的线程会加入到EntryList,等待获取锁;等待对象的线程会加入WaitSet。

synchronized的使用和实现在JVM层面是高度优化的,包括锁升级和自旋锁等机制,使得其在不同情况下提供不同级别的性能优化。通过这些复杂的策略,JVM尽可能地减少同步的开销,同时保持线程安全。

使用Lock的代码示例

在Spring Boot项目中,使用Lock的案例通常涉及到复杂的业务逻辑或需要更细粒度控制的并发场景。下面,我将提供一个使用ReentrantLock的实际示例,这是一个常见的可重入锁,用于保证多线程环境下数据的一致性和线程的安全。

案例背景

假设我们有一个在线电商平台,需要在高并发环境下处理库存更新。为了避免多个用户同时下单时导致的库存超卖问题,我们可以使用ReentrantLock来确保在更新库存数量时的线程安全。

步骤和代码示例

  1. 依赖配置

首先,确保你的Spring Boot项目已经设置了适当的依赖。通常,Spring Boot Starter已包含了你需要的大部分依赖。

  1. 服务层实现

我们在服务层实现库存更新的逻辑,使用ReentrantLock来同步访问临界区代码:

import org.springframework.stereotype.Service;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Service
public class InventoryService {

    private final Lock lock = new ReentrantLock();

    public void updateInventory(String productId, int quantity) {
        lock.lock();
        try {
            // 模拟获取当前库存,实际中应从数据库或缓存中获取
            int currentInventory = getCurrentInventory(productId);
            
            // 检查库存是否足够
            if (currentInventory >= quantity) {
                // 执行库存更新操作,实际中应写入数据库
                int newInventory = currentInventory - quantity;
                saveInventory(productId, newInventory);
            } else {
                throw new IllegalStateException("Not enough inventory for product: " + productId);
            }
        } finally {
            lock.unlock();
        }
    }

    private int getCurrentInventory(String productId) {
        // 这里只是一个示例,实际应从数据库获取
        return 100; // 假设始终返回100件库存
    }

    private void saveInventory(String productId, int newInventory) {
        // 这里只是一个示例,实际应更新数据库
        System.out.println("Inventory updated for product " + productId + ": " + newInventory);
    }
}
  1. 调用服务

在你的控制器或其他业务逻辑中调用这个服务方法:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/inventory")
public class InventoryController {

    @Autowired
    private InventoryService inventoryService;

    @PostMapping("/update")
    public String updateInventory(@RequestParam String productId, @RequestParam int quantity) {
        try {
            inventoryService.updateInventory(productId, quantity);
            return "Inventory updated successfully!";
        } catch (IllegalStateException e) {
            return e.getMessage();
        }
    }
}

解释

在这个例子中,ReentrantLock被用来保护库存更新的代码段。当多个线程尝试执行updateInventory方法时,ReentrantLock确保一次只有一个线程可以执行库存减少的操作。这样可以防止多线程同时操作同一数据导致的数据不一致问题,如库存的超卖现象。

通过这种方式,你可以在Spring Boot应用中安全地处理复杂的并发数据修改操作,而不仅限于库存管理,同样的策略也可以应用于订单处理、财务事务等需要高度并发控制的业务场景。

使用sychronized的代码示例

在Spring Boot项目中,使用synchronized关键字的场景通常比较简单,适用于保护临界区以避免多线程同时访问同一资源导致的问题。下面是一个具体的例子,展示如何在Spring Boot中使用synchronized来同步访问和修改一个简单的计数器。

案例背景

假设我们的Spring Boot应用需要跟踪某个特定操作(比如用户的登录尝试)的次数。为了确保计数器的正确性,在多线程环境下(例如多个用户同时登录),我们需要使用synchronized来防止并发访问导致的计数错误。

步骤和代码示例

  1. 创建一个服务类

首先,创建一个服务类来封装计数逻辑,使用synchronized关键字确保线程安全。

import org.springframework.stereotype.Service;

@Service
public class CounterService {

    private int loginAttemptCounter = 0;

    public synchronized void incrementLoginAttempt() {
        loginAttemptCounter++;
    }

    public synchronized int getLoginAttemptCount() {
        return loginAttemptCounter;
    }
}

在这个服务类中,我们定义了两个方法:incrementLoginAttemptgetLoginAttemptCount。这两个方法都被标记为synchronized,这意味着同时只有一个线程能够执行这些方法中的任何一个。这保证了loginAttemptCounter这个共享变量的访问和修改是线程安全的。

  1. 在控制器中使用服务

接下来,我们在一个REST控制器中使用这个服务。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoginController {

    @Autowired
    private CounterService counterService;

    @PostMapping("/login")
    public String login() {
        // 假设这里有一些登录逻辑
        counterService.incrementLoginAttempt();
        return "Login Attempt Recorded";
    }

    @GetMapping("/login/attempts")
    public String getAttempts() {
        int attempts = counterService.getLoginAttemptCount();
        return "Total Login Attempts: " + attempts;
    }
}

代码解释

在这个简单的示例中,每当用户尝试登录时,login方法就会被调用,并通过调用CounterService中的incrementLoginAttempt方法来增加计数器。getAttempts方法可以返回到目前为止的登录尝试次数。

使用synchronized确保了无论何时调用incrementLoginAttemptgetLoginAttemptCount,对loginAttemptCounter的读写操作都是互斥的,从而避免了并发错误。

使用场景

这个使用synchronized的例子虽然简单,但是非常适合处理小规模的并发控制。对于复杂的并发场景或者高性能需求,可能需要考虑使用更高级的并发控制机制,如java.util.concurrent包中的锁机制或其他并发工具。

转载自:https://juejin.cn/post/7371649020123824128
评论
请登录