IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Java 源码 - 经验总结:各种锁在工作中使用场景和细节 -> 正文阅读

[Java知识库]Java 源码 - 经验总结:各种锁在工作中使用场景和细节

不啰嗦,我们直接开始!

引导语

有的面试官喜欢让同学在说完锁的原理之后,让你重写一个新的锁,要求现场在白板上写出大概的思路和代码逻辑,这种面试题目,蛮难的,我个人觉得其侧重点主要是两个部分:

  1. 考察一下你对锁原理的理解是如何来的,如果你对源码没有解读过的话,只是看看网上的文章,或者背面试题,也是能够说出大概的原理,但你很难现场写出一个锁的实现代码,除非你真的看过源码,或者有和锁相关的项目经验;
  2. 我们不需要创造,我们只需要模仿 Java 锁中现有的 API 进行重写即可。

如果你看过源码,这道题真的很简单,你可以挑选一个你熟悉的锁进行模仿。

在锁章节中我们之前说的都是排它锁,这小节我们以共享锁作为案列,自定义一个共享锁。

1、需求

一般自定义锁的时候,我们都是根据需求来进行定义的,不可能凭空定义出锁来,说到共享锁,大家可能会想到很多场景,比如说对于共享资源的读锁可以是共享的,比如对于数据库链接的共享访问,比如对于 Socket 服务端的链接数是可以共享的,场景有很多,我们选择共享访问数据库链接这个场景来定义一个锁。

2、详细设计

假定(以下设想都为假定)我们的数据库是单机 mysql,只能承受 10 个链接,创建数据库链接时,我们是通过最原始 JDBC 的方式,我们用一个接口把用 JDBC 创建链接的过程进行了封装,这个接口我们命名为:创建链接接口。

共享访问数据库链接的整体要求如下:所有请求加在一起的 mysql 链接数,最大不能超过 10(包含 10),一旦超过 10,直接报错。

在这个背景下,我们进行了下图的设计:

图片描述

这个设计最最关键的地方,就是我们通过能否获得锁,来决定是否可以得到 mysql 链接,如果能获得锁,那么就能得到链接,否则直接报错。

接着我们一起来看下落地的代码:

2.1、定义锁?

首先我们需要定义一个锁出来,定义时需要有两个元素:

  1. 锁的定义:同步器 Sync;
  2. 锁对外提供的加锁和解锁的方法。

共享锁的代码实现如下:

// 共享不公平锁
public class ShareLock implements Serializable{
	// 同步器
  private final Sync sync;
  // 用于确保不能超过最大值
  private final int maxCount;
 
  /**
   * 初始化时给同步器 sync 赋值
   * count 代表可以获得共享锁的最大值
   */
  public ShareLock(int count) {
    this.sync = new Sync(count);
    maxCount = count;
  }
 
  /**
   * 获得锁
   * @return true 表示成功获得锁,false 表示失败
   */
  public boolean lock(){
    return sync.acquireByShared(1);
  }
 
  /**
   * 释放锁
   * @return true 表示成功释放锁,false 表示失败
   */
  public boolean unLock(){
    return sync.releaseShared(1);
  }
}  

从上述代码中可以看出,加锁和释放锁的实现,都依靠同步器 Sync 的底层实现。

唯一需要注意的是,锁需要规定好 API 的规范,主要是两方面:

  1. API 需要什么,就是锁在初始化的时候,你需要传哪些参数给我,在 ShareLock 初始化时,需要传最大可共享锁的数目;
  2. 需要定义自身的能力,即定义每个方法的入参和出参。在 ShareLock 的实现中,加锁和释放锁的入参都没有,是方法里面写死的 1,表示每次方法执行,只能加锁一次或释放锁一次,出参是布尔值,true 表示加锁或释放锁成功,false 表示失败,底层使用的都是 Sync 非公平锁。

以上这种思考方式是有方法论的,就是我们在思考一个问题时,可以从两个方面出发:API 是什么?API 有什么能力?

2.2、定义同步器 Sync

Sync 直接继承 AQS ,代码如下:

class Sync extends AbstractQueuedSynchronizer {
 
  // 表示最多有 count 个共享锁可以获得
  public Sync(int count) {
    setState(count);
  }
 
  // 获得 i 个锁
  public boolean acquireByShared(int i) {
    // 自旋保证 CAS 一定可以成功
    for(;;){
      if(i<=0){
        return false;
      }
      int state = getState();
      // 如果没有锁可以获得,直接返回 false
      if(state <=0 ){
        return false;
      }
      int expectState = state - i;
      // 如果要得到的锁不够了,直接返回 false
      if(expectState < 0 ){
        return false;
      }
      // CAS 尝试得到锁,CAS 成功获得锁,失败继续 for 循环
      if(compareAndSetState(state,expectState)){
        return true;
      }
    }
  }
 
  // 释放 i 个锁
  @Override
  protected boolean tryReleaseShared(int arg) {
    for(;;){
      if(arg<=0){
        return false;
      }
      int state = getState();
      int expectState = state + arg;
      // 超过了 int 的最大值,或者 expectState 超过了我们的最大预期
      if(expectState < 0 || expectState > maxCount){
        log.error("state 超过预期,当前 state is {},计算出的 state is {}",state
        ,expectState);
        return false;
      }
      if(compareAndSetState(state, expectState)){
        return true;
      }
    }
  }
}

整个代码比较清晰,我们需要注意的是:

  1. 边界的判断,比如入参是否非法,释放锁时,会不会出现预期的 state 非法等边界问题,对于此类问题我们都需要加以判断,体现出思维的严谨性;
  2. 加锁和释放锁,需要用 for 自旋 + CAS 的形式,来保证当并发加锁或释放锁时,可以重试成功。写 for 自旋时,我们需要注意在适当的时机要 return,不要造成死循环,CAS 的方法 AQS 已经提供了,不要自己写,我们自己写的 CAS 方法是无法保证原子性的。

2.3、通过能否获得锁来决定能否得到链接

锁定义好了,我们需要把锁和获取 Mysql 链接结合起来,我们写了一个 Mysql 链接的工具类,叫做 MysqlConnection,其主要负责两大功能:

  1. 通过 JDBC 建立和 Mysql 的链接;
  2. 结合锁,来防止请求过大时,Mysql 的总链接数不能超过 10 个。

首先我们看下 MysqlConnection 初始化的代码:

public class MysqlConnection {
  private final ShareLock lock;
  
  // maxConnectionSize 表示最大链接数
  public MysqlConnection(int maxConnectionSize) {
    lock = new ShareLock(maxConnectionSize);
  }
}

我们可以看到,在初始化时,需要制定最大的链接数是多少,然后把这个数值传递给锁,因为最大的链接数就是 ShareLock 锁的 state 值。

接着为了完成 1,我们写了一个 private 的方法:

// 得到一个 mysql 链接,底层实现省略
private Connection getConnection(){}

然后我们实现 2,代码如下:

// 对外获取 mysql 链接的接口
// 这里不用try finally 的结构,获得锁实现底层不会有异常
// 即使出现未知异常,也无需释放锁
public Connection getLimitConnection() {
  if (lock.lock()) {
    return getConnection();
  }
  return null;
}
 
// 对外释放 mysql 链接的接口
public boolean releaseLimitConnection() {
  return lock.unLock();
}

逻辑也比较简单,加锁时,如果获得了锁,就能返回 Mysql 的链接,释放锁时,在链接关闭成功之后,调用 releaseLimitConnection 方法即可,此方法会把锁的 state 状态加一,表示链接被释放了。

以上步骤,针对 Mysql 链接限制的场景锁就完成了。

3、测试

锁写好了,接着我们来测试一下,我们写了一个测试的 demo,代码如下:

public static void main(String[] args) {
  log.info("模仿开始获得 mysql 链接");
  MysqlConnection mysqlConnection = new MysqlConnection(10);
  log.info("初始化 Mysql 链接最大只能获取 10 个");
  for(int i =0 ;i<12;i++){
    if(null != mysqlConnection.getLimitConnection()){
      log.info("获得第{}个数据库链接成功",i+1);
    }else {
      log.info("获得第{}个数据库链接失败:数据库连接池已满",i+1);
    }
  }
  log.info("模仿开始释放 mysql 链接");
  for(int i =0 ;i<12;i++){
    if(mysqlConnection.releaseLimitConnection()){
      log.info("释放第{}个数据库链接成功",i+1);
    }else {
      log.info("释放第{}个数据库链接失败",i+1);
    }
  }
  log.info("模仿结束");
}

以上代码逻辑如下:

  1. 获得 Mysql 链接逻辑:for 循环获取链接,1~10 都可以获得链接,11~12 获取不到链接,因为链接被用完了;
  2. 释放锁逻辑:for 循环释放链接,1~10 都可以释放成功,11~12 释放失败。

我们看下运行结果,如下图:

图片描述

从运行的结果,可以看出,我们实现的 ShareLock 锁已经完成了 Mysql 链接共享的场景了。

4、总结?

同学们阅读到这里不知道有没有两点感受:

  1. 重写锁真的很简单,最关键的是要和场景完美贴合,能满足业务场景的锁才是好锁;
  2. 锁其实只是来满足业务场景的,本质都是 AQS,所以只要 AQS 学会了,在了解清楚场景的情况下,重写锁都不难的。

锁章节最核心的就是 AQS 源码解析的两章,只要我们把 AQS 弄懂了,其余锁的实现,只要稍微看下源码实现,几乎马上就能知道其底层实现的原理,大多数都是通过操作 state 来完成不同的场景需求,所以还是建议大家多看 AQS 源码,多 debug AQS 源码,只要 AQS 弄清楚了,锁都很简单。

不啰嗦,文章结束,建议三连!

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-10-08 11:39:38  更:2021-10-08 11:40:30 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 19:21:46-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码