开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

SpringMVC之第一个实战

发表于 2021-11-27

第一个SpringMVC项目

通过Spring的学习,我们都知道可以通过xml或者注解的方法进行配置,SpringMVC也可以通过这两种方式进行配置。一般使用注解版本,更加简洁方便,便于维护。

配置版

  1. 新建一个Module 我们命名为spring-02-hello 并且添加web4.0的支持。

image.png

image.png

image.png

  1. 导入SpringMVC的依赖
  2. 配置web.xml 主要是配置DispatcherServlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<!-- 注册DispatcherServlet -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

<!-- 加载springmvc的配置文件 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc-servlet.xml</param-value>
</init-param>


<!-- 配置启动级别 1为最高级别 -->
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

  1. 编写SpringMVC的配置文件,名称最好取名为springmvc-servlet.xml

这里的名称其实任意 最好按照官方的要求来写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- 处理映射器 -->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"></bean>

<!-- 处理器适配器 -->
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"></bean>

<!-- 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="InternalResourceViewResolver">
<!-- 前缀 -->
<property name="prefix" value="/WEB-INF/jsp/"></property>
<!-- 后缀 -->
<property name="suffix" value=".jsp"></property>
</bean>

</beans>

这里包含了SpringMVC的三大件,后续在注解版中不需要配置,但是需要清楚原理;

对应上一章节的图片来看,HandlerMapping就是处理映射器,用来将请求映射为HandlerExecution类型的对象;HandlerExecution就是映射后的对象;HandlerAdapter就是处理器适配器,用于处理映射过来的数据。

  1. 编写一个Controller类,需要返回一个ModelAndView,封装数据和视图;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码package com.my.controller;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HelloController implements Controller {

@Override
public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
ModelAndView modelAndView = new ModelAndView();
// /WEB-INF/jsp/hello.jsp
modelAndView.setViewName("hello");
modelAndView.addObject("msg","使用配置版本的SpringMVC");
return modelAndView;
}
}
  1. 这时候需要在springmvc-servlet.xml中进行注册bean
1
bash复制代码<bean id="/hello" class="com.my.controller.HelloController"></bean>
  1. 最后创建一个hello.jsp页面,用于接收封装的数据
1
2
3
4
5
6
7
8
9
10
11
javascript复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>

${msg}

</body>
</html>

  1. 项目结构:

image.png

  1. 配置tomcat进行测试
    image.png

image.png


注意点

  • 有时候会出现404,优先考虑jar包的依赖是否导入

image.png

  • 没有导入可以进行相应设置:
  1. 打开Project Structure - Artifacts
  2. 选择当前的Artifacts 在WEB-INF下新建lib目录
    image.png
  3. 将所有的jar包添加到lib中

image.png
4. 全选OK

image.png

  • 对于配置版的看法:
  1. 实际开发中,主要是注解版投入生产,在这里主要是让大家理解SpringMVC执行原理
  2. 对于映射器等几个名称不需要记住,知道即可以。

注解版

  1. 注解版比配置版更加简单,首先仍然是创建modules,这次命名为spring-03-hello-anno,并且添加web4.0依赖
  2. 由于maven的资源过滤问题,建议在pom.xml添加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
  1. 和配置版一样在web.xml中进行固定的配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc-servlet.xml</param-value>
</init-param>

<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

/ 和 /* 的区别:

  • < url-pattern > / </ url-pattern >
  • 只匹配所有的请求 不匹配.jsp等
  • < url-pattern > /* </ url-pattern >
  • 如果设置成这样,那么xx.jsp会再次进入DispatcherServlet进行匹配然后就变成了xx.jsp.jsp形成死循环

  1. 和配置版一样配置springmvc-servlet.xml
  • 这里和配置版有区别
  • context:component-scan : 自动扫描的包,指定包下的注解生效,由IOC容器统一管理。这里熟悉Spring的应该都了解
  • mvc:default-servlet-handler :让Spring MVC不处理静态资源
  • mvc:annotation-driven : 支持mvc注解驱动,自动帮我们注入了处理映射器和处理适配器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">

<context:component-scan base-package="com.my.controller"></context:component-scan>


<mvc:annotation-driven></mvc:annotation-driven>
<mvc:default-servlet-handler></mvc:default-servlet-handler>


<bean id="InternalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
</beans>
  1. 创建Controller : com/my/controller/SpringAnnoController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码package com.my.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class SpringAnnoController {

@RequestMapping("/hello")
public String sayHello(Model model){
model.addAttribute("msg","使用注解配置SpringMVC");
return "hello";
}
}
  • 这里我们无需再实现Controller接口,直接通过Controller注解完成。
  • @RequestMapping代表着映射路径,这里的路径就是 xxx/项目名/hello 无需再设置视图。
  • 方法中声明的model是为了把数据带到视图中去。
  • 这个sayHello方法的返回值适配上web.xml中的前缀后缀最后的跳转到 /WEB-INF/jsp/hello.jsp
  1. 最后在/WEB-INF/jsp/创建视图层hello.jsp
1
2
3
4
5
6
7
8
9
10
11
javascript复制代码<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>

${msg}

</body>
</html>
  1. 最后配置tomcat启动项目即可

image.png

  1. 项目结构

image.png


小结

实战结合上一章的内容,我们可以很明确的了解SpringMVC基于三大件:处理映射器、处理适配器以及视图解析器。 在注解版中我们已经看不到处理映射器和适配器了,但是我们需要了解SpringMVC的执行原理,以便于更好地学习。在实际开发中,通常我们只需要手动配置视图解析器即可。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

字符串匹配算法

发表于 2021-11-27

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战

字符存储匹配算法是各种编程语言都会提供的字符匹配函数的底层依赖,它可以分为单模式匹配和多模式匹配算法。

单模式匹配:BF算法和RK算法,RK算法是BF算法的改进,它巧妙借助了哈希算法,提升了匹配的效率。

BF

  1. BF算法是Brute Force的缩写,中文译作暴力匹配算法,也叫朴素匹配算法。
  2. 两个概念:主串和模式串

如在字符串A中查找字符串B,则字符串A就是主串,字符串B就是模式串

将主串长度记为n,模式串的长度记作m。因为是在主串中查找模式串,所以n>m
3. BF算法的思想可概括为:在主串中,检查起始位置分别是0,1,2……n-m且长度为m的n-m+1个子串,看有没有更模式串匹配的。
4. 极端情况下,如主串是“aaaaa…aaaaa”,模式串是“aaaab”。每次都比对m个字符,要比对n-m+1次,所以最坏的时间复杂度是O(m*n)。
5. 虽然BF算法时间复杂度很高,但在实际开发中使用的非常常见。

原因1:实际软件开发中,大部分情况下,模式串和主串的长度都不会太长。每次模式串与主串中的子串匹配时,当中途不能遇到匹配的字符的时候,就可以停止,不需要全部对比一次。所以理论上最坏情况时间复杂度是O(m*n),但这更多的是统计意义上的,大部分情况中,这个算法执行的很高效。

原因2:朴素字符串匹配算法思想简单,代码实现也非常简单,简单就意味着不容易出错。工程中,在满足性能要求的前提下,简单是首选,也是常说的KISS(keep it Simple and Stupid)设计原则。

RK

  1. RK算法的全称是Rabin-Karp算法,是两位发明人的名字拼接。是BF算法的升级版
  2. BF算法的问题在于每次检查主串与子串是否匹配,需要依次对比每个字符,所以BF算法的时间复杂就比较高。但引入哈希算法,时间复杂度立即就会降低。
  3. RK算法的思路:

通过哈希算法对主串中的n-m+1个子串分别求哈希值,

然后逐个于模式串的哈希值比较大小,如果相等就说明有对应的模式串。
4. 通过哈希算法计算字符的哈希值时,需要遍历子串中的每个字符,这只提供了模式串与子串比较的效率,但整体的效率并没有提高。
5. 为了提高哈希算法计算子串哈希值的效率,可以通过哈希算法的设计来解决。

假设要匹配的字符串的字符集中只包含k个字符,这就可以用一个k进制数来表示一个子串,这个k进制数转化成十进制,作为子串的哈希值。

  1. 这种哈希算法有个特点,在主串中,相邻两个子串的哈希值的计算公式有一定关系。
  2. RK算法的时间复杂度:
1. 整个RK算法包含两个部分,计算子串哈希值和模式串哈希值与子串哈希值之间的比较。
2. 第一部分,只需要扫描一遍主串就能计算出所有子串的哈希值了,复杂度是O(n)。
3. 模式串哈希值与每个子串哈希值之间的比较时间复杂度是O(1),总共需要比较n-m+1个子串的哈希值,所有,这部分的时间复杂度也是O(n)。所以RK算法整体时间复杂度就是`O(n)`。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

从JDK中学习设计模式——策略模式

发表于 2021-11-27

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

概述

策略模式(Strategy Pattern)是通过一些独立的类来封装一系列算法,并且它们之间可以相互转换。每一个算法称为一种策略或政策,因此又称为政策模式(Policy Pattern),是一种对象行为型模式。

策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,为了保证这些策略在使用时具有一致性,一般会提供一个抽象的策略类来做规则的定义,而每种算法则对应于一个具体策略类,使用算法的环境类是针对抽象策略类进行编程的。

结构

策略模式UML.png

  • Context(环境类):是使用算法的角色,维持了抽象策略类的引用。
  • Strategy(抽象策略类):声明了所有具体策略类的公共方法,一般是抽象类或接口,但也可以是具体类。
  • ConcreteStrategy(具体策略类):实现了在抽象策略类中声明的算法。

优点

  1. 将策略的定义与使用分开,有利于关注点的分离。
  2. 避免了使用多重条件判断,有利于系统的维护。
  3. 新增策略不必修改环境类,符合开闭原则。
  4. 策略模式的环境类通过聚合使用策略,减少了继承的使用。
  5. 抽象策略类提供了算法复用的机制,避免了重复的代码。

缺点

  1. 当策略过多时,会造成类的膨胀。
  2. 所有的策略类都需要对外暴露,不符合迪米特法则。
  3. 在客户端中无法组合使用多个策略类。

应用场景

  1. 系统需要动态地在几种算法中选择一种。
  2. 系统需要屏蔽算法的具体规则。
  3. 多个类只有在算法或行为上稍有不同的场景,通过策略模式可以避免使用难以维护的多重条件选择语句。

JDK 中的应用

在 JDK 中 java.util.Comparator 就使用了策略模式。

java.util.Comparator 就是抽象策略类:

1
2
3
4
java复制代码@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}

java.util.Arrays 是环境类:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class Arrays {    
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

红黑树效率如此之高,MySQL为何不采用? ①-二叉树、红黑

发表于 2021-11-27

①-二叉树、红黑树、Hash、B+树

索引:索引是帮忙MySQL高效获取的排好序的数据结构

索引数据结构:

  • 二叉树
  • 红黑树
  • Hash表
  • B-Tree

1.二叉查找树(Binary Search Trees)

左节点比父节点要小,右节点比父节点要大。他的高度决定的查找效率。

平衡二叉查找树:

非正常的倾斜二叉查找树:

如果某一列数据遇到像‘倾斜二叉查找树’,那么这个二叉树索引,其实就蜕化成了“链表”,查询此列数据还是全表扫描的方式,就失去了加索引的意义。

树在插入的时候非常有可能导致倾斜,不同的插入顺序会导致树的高度不一样,而树的高度直接影响了树的查找效率。不平衡的二叉查找树自然查找效率更低。

2.红黑树(Red-Black Trees)

本质还是一个二叉树,但是他叫做二叉平衡树。解决二叉树一边倒的可能性。平衡树在插入和删除的时候,会通过旋转操作将树的左右节点达到平衡。

Java中的HashMap和TreeSet,Java8中HashMap的实现因为用红黑树代替链表(链表长度>8)时。

红黑树规则定义:

1.任何一个节点都有颜色,红色或黑色

2.根节点是黑色的

3.父子节点之间不能出现两个连续的红节点

4.任何一个根节点,遍历到他的子孙节点,所经过的黑色节点数必须相同

5.空节点被认为是黑色的

因为以上规则的限制,保证了红黑树的自平衡。红黑树从根到叶子节点的最长路径不会超过最短路径的2倍。

3.Hash索引

hash是一种key-value形式的数据结构。实现一般是数组+链表的结构,通过hash函数计算出key在数组中的位置,然后如果出现hash冲突就通过链表来解决(拉链法)。当然还有其他的解决hash冲突的方法。hash这种数据结构是很常用的,比如我们系统使用HashMap来构建热点数据缓存,存取效率很好。

hash结构存数据首先通过计算key的hash值来确定其在数组中的位置,如果有冲突就在该数组位置建一个链表。这样很明显有几个问题:

即使是具有相同特征的key计算出来的位置可能相隔很远,连续查询效率低下。即不支持范围查询。

hash索引存储的事计算得到的hash值和行指针,而不存储具体的行值,所以通过hash索引查询数据需要进行两次查询(首先查询行的位置,然后找到具体的数据)

hash索引查询数据的前提就是计算hash值,也就是要求key为一个能准确指向一条数据的key,所以对于like等一类的匹配查询是不支持的。

所以我们可以知道的是hash索引适用于快速选取某一行的数据。范围查找的这种搜索,Hash无法进行满足。

4.B+ 树

而在MySQL索引中虽然也使用了树结构,但是并不是使用的二叉树。因为在数据库中数据最终都是存放在磁盘上的,而如果树的节点过多的话,那么在节点之间转移会花费较多的时间。在MySQL的实现中选择将更多内容放在同一个节点,对同一个节点的操作转入在内存中完成,减少在外存中节点之间转移的次数,以达到提高效率的目的。这就是B+Tree,在B+Tree的实现中一个三层的树结构就基本上可以满足我们几乎所有的需求了。

3.红黑树效率如此之高,MySQL为何不采用

在实际场景应用当中,MySQL表数据,一般情况下都是比较庞大、海量的。如果使用红黑树,树的高度会特别高,红黑树虽说查询效率很高。但是在海量数据的情况下,树的高度并不可控。如果我们要查询的数据,正好在树的叶子节点。那查询会非常慢。故而MySQL并没有采用红黑树来组织索引。

最后推荐一个可视化的数据结构网站:

www.cs.usfca.edu/~galles/vis…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「Linux 奏章 21」备份与恢复

发表于 2021-11-27

「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」。

实体机无法做快照,如果系统出现异常或者数据损坏,后果严重,要重做系统,还会造成数据丢失。所以我们可以使用备份和恢复技术。

Linux 备份和恢复有两种方式:

  • tar 打包:把需要的文件(或者分区)用 tar 打包就行,下次需要恢复时,再解压覆盖即可。
  • ⭐使用指令 dump 与 resotre

若 Linux 上没有 dump 与 restore 指令,需要先安装:

  • yum -y install dump
  • yum -y install resotre
  1. dump 备份

dump 支持分卷与增量备份(所谓增量备份是指备份上次备份后修改/增加过的文件,也称差异备份)

1.1 基本语法

⭐dump [-cu] [-123456789] [-f <备份后文件名>] [-T <日期>] <分区>

⭐dump [-wW]

参数说明:

  • -c:创建新的归档文件,并将由一个或多个文件参数所指定的内容写入归档文件的开头
  • -0123456789:备份层级,0 为最完整备份(备份所有文件);若指定 0 以上的层级,则备份至上一次备份以来修改或新增的文件,到 9 后再次轮替
  • -f <备份后文件名>:指定备份后的文件名
  • -j:调用 bzlib 库压缩备份文件,也就是将备份后的文件压缩成 bz2 格式,让文件更小
  • -T <日期>:指定开始备份的时间与日期
  • -u:备份完毕后,在 /etc/dumpdares 中记录备份的文件系统、层级、日期与时间等
  • -t:指定文件名,若该文件已存在备份文件,则列出名称
  • -W:显示需要备份的文件及其最后一次备份的层级、时间、日期
  • -w:与 -W 类似,但仅显示需要备份的文件

1.2 应用案例

  • ① 将 /boot 分区所有内容备份到 /opt/book.bak0.bz2 文件,备份层级为 “0”
    • dump -0uj -f /opt/boot.bak0.bz2 /boot
  • ② 修改文件,然后增量备份 /boot 分区
    • dump -1uj -f /opt/boot.bak1.bz2 /boot
  • ③ 显示最后一次备份的层级、时间…
    • dump -W
  • ④ 查看备份时间文件
    • cat /etc/dumpdates
  • 注:增量备份仅支持分区,如果备份文件/目录,不再支持增量备份,即只能使用 0 级别备份!
    • 先 dump -0j -f /opt/etc.bak0.ba2 /etc/ : dump 备份 /etc 整个目录
    • 后 dump -1j -f /opt/etc.bak1.ba2 /etc/ : dump 增量备份 /etc 目录会出错

⭐dump + crontab: 无人值守备份!

  1. restore 恢复

⭐restore 指令用来恢复已备份的文件,可以从 dump 生成的备份文件中恢复成原文件。

2.1 基本语法

  • ⭐ restore [模式选项] [选项]
  • 四个模式选项(一次命令中只能指定一个参数)
    • -C : 使用对比模式,将备份的文件与已存在的文件相互对比
      • restore -C -f /opt/boot.bak1.bz2 : 将 boot.bak1.bz2 备份文件与最新已存在的文件比较
    • -i : 使用交互模式,在进行还原操作时,restore 指令将依序询问用户
    • -r : 进行还原模式(操作)
      • restore -r -f /opt/boot.bak0.bz2 : 恢复到第1次完全备份状态
      • restore -r -f /opt/boot.bak1.bz2 : 恢复到第2次增量备份状态
    • -t : 查看模式,看备份文件有哪些
      • restore -t -f /opt/boot.bak0.bz : 查看 /opt/boot.bak0.bz2 有什么文件
  • 选项
    • -f <备份文件> : 从指定的文件中读取备份数据,进行还原操作

注:有几个增量备份文件就恢复几个,按顺序恢复即可。

希望本文对你有所帮助🧠

欢迎在评论区留下你的看法🌊,我们一起讨论与分享🔥

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

异常 - 断言 - 日志

发表于 2021-11-26

这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战

异常

Java异常体系图:

image.png

  • Error和Exception下面的RunTimeException及其子类属于不需要强制捕获的异常。
  • 其他 exception 以下的其他的异常及其子类都需要强制捕获异常。

image.png

  • 异常体可以往下传递。

image.png

  • 不过这种用法平时并不怎么用。

一些使用异常中的建议:

  • 尽量使用JDK已定义异常!
  • 自定义异常应该从RuntimeException派生。
  • 先定义BaseException,再其派生子类。
  • 自定义异常应该提供多个构造方法。

断言

我们开发中看某些框架的代码的时候会看到assert xxx这种写法,这就是java的断言机制。

JVM默认关闭断言指令:

  • 给Java虚拟机传递-ea参数启用断言
  • 只能在开发和测试阶段启用断言

日志

日志是为了替代System.out.println(),可以定义格式,重定向到文件等 日志可以存档,便于追踪问题 日志记录可以按级别分类,便于打开或关闭某些级别 可以根据配置文件调整日志,无需修改代码。

JDK的log

jdk 内置logging(7个日志级别,默认为infoSEVERE WARNING INFO (默认级别) CONFIG FINE FINER FINEST)

image.png

代码中设置level,可以指定输出固定的那个内容。

缺点:

  • JVM启动时读取配置文件并完成初始化
  • JVM启动后无法修改配置
  • 需要在JVM启动时传递参数 -Djava.util.logging.config.file=config-file-name

Commons Log

是由Apache创建的一个日志模块,一般的用法如下:

image.png

我们开发中用的最多的还是下面Log4J。

Log4J

目前最流行的日志方式

image.png

组件化日志系统

  • appender 用来把日志输出不同的目的地
  • filter 用来过滤日志
  • layout 格式化输出格式
  • console 等等 输出到的地方

我们不需要关心这个,只需要通过配置文件来配置

通过Commons Logging也可以实现log4j

使用Log4j:

通过Commons Logging实现日志,不需要修改代码即可使用Log4j 、

使用Log4j只需要把log4j2.xml和相关jar放入classpath 、

如果要更换Log4j,只需要移除log4j2.xml和相关jar 只有扩展Log4j时,才需要引用Log4j的接口

参考文档: logging.apache.org/log4j/

使用配置xml

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Laravel 存取数组类型到 PgSql 带来的思考 硬核

发表于 2021-11-26

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」

需求描述

今天需要一个问题,推荐算法同学在PgSql数据库创建了一个 integer[] 类型的字段,需要我们业务同学写入数据。

调研了好久发现 Laravel 没有提供底层支持,需要自己实现。

遇到的坑

  1. 我使用的 NaviCat 作为数据库的可视化管理工具,发现在我的可视化工具上根本没有 integer[] 这个类型,显示的是 varchar 类型,这里困扰了我半天。
  2. PHP 获得 pgsql 中数组类型的数据得到是这种类型 {1,2,3} ,就是字符串!所以我的NaviCat 显示 varchar 类型也是合理的。

实现需求

可以只看核心代码部分,代码段中写明了注释。

简单来说就是 {} 和 [] 的互相转化,基于字符串拼接的方式实现。

因为不管是pgsql中存储的还是我通过PHP读取到的类型都是字符串,并不是数组类型,这是个核心问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
php复制代码public static function updateJoinHouseGroupId($userid, $houseGroupId)
{
$model = self::query()
->selectRaw('"joinHouseGroupId"')
->where('userid', $userid)
->first();

if ($model) {
$model->houseGroupId = $houseGroupId;
$joinHouseGroupId = $model->joinHouseGroupId;

//核心代码begin---------------------------
if ($joinHouseGroupId) {
//已经有的要重新拼接
$joinHouseGroupIdStr = substr($joinHouseGroupId, 1, strlen($joinHouseGroupId) - 2);
//查询专场是否在有效期内
$joinHouseGroupIds = explode(',', $joinHouseGroupIdStr);
$joinNotExpireHouseGroupIds = HouseGroupInfo::getNotExpireIds($joinHouseGroupIds);
if ($joinNotExpireHouseGroupIds) {
$joinHouseGroupId = "{" . implode(',', $joinNotExpireHouseGroupIds) . "," . $houseGroupId . "}";
} else {
$joinHouseGroupId = "{" . $houseGroupId . "}";
}
} else {
$joinHouseGroupId = "{" . $houseGroupId . "}";
}
//核心代码end----------------------------------

self::query()
->where('userid', $userid)
->update([
'joinHouseGroupId' => $joinHouseGroupId,
]);
}
}

进一步思考

上述代码勉强实现了需求,但是不够优雅,我期望的形式是,自定义一个类型,比如 pgArray

当我在model中做如下设置时,能自动实现 gpsql中存取的数组转成PHP可以直接处理的数组。

1
2
3
ini复制代码protected $casts = [
'xxxx' => 'pgArray',
];

以最简单的int型数组举例,PHP能处理的数组是这样的:[1,2,3]

而PgSql中存储的数组是这样的:{1,2,3}

要实现在存取时自动转换

大家有什么好想法欢迎在评论区讨论

硬核文章推荐

PHP转Go 2021年年中总结

如何第一时间收到接口报错?不用测试妹子再质疑你是不是接口挂了。

Git使用实战:多人协同开发,紧急修复线上bug的Git操作指南。

性能优化反思:不要在for循环中操作DB

性能优化反思:不要在for循环中操作DB 进阶版

最后

👍🏻:觉得有收获请点个赞鼓励一下!

🌟:收藏文章,方便回看哦!

💬:评论交流,互相进步!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Kylin】初识

发表于 2021-11-26

这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战

一、概述

Apache Kylin, 一种 MOLAP 的数据分析引擎。

最早脱胎于 eBay 中国研发中心, 并贡献给 Apache 基金会, 目前 Apache Kylin 的核心开发团队已经自立门户, 创建了 Kyligence (Kylin Intelligence) 公司。值得一提的是, Apache Kylin 是第一个由中国人主导的 Apache 顶级项目。eBay 使用的传统数据仓库和商业智能平台遇到瓶颈, Hadoop 平台虽然可以批量处理大规模数据, 但无法提供高效的数据交互分析。于是, Kylin 被 eBay 孵化了。

2021-05-0112-35-36.png

Kylin 提供多维数据分析(MOLAP)的秒级响应。Kylin 的特点:

  • 数据源和模型: 主要支持 Hive、Kafka
  • 构建引擎: 早期支持 MapReduce 计算引擎, 新版本支持 Spark、Flink 计算引擎。除了全量构建外, 对基于时间的分区特性, 支持增量构建
  • 存储引擎: 构建好的 Cube 以 Key-Value 的形式存储在 HBase 中, 通过优化 Rowkey 加速查询。每一种维度的排列组合计算结果被保存为一个物化视图,叫 Cuboid
  • 优化算法: Cube 本身就是用空间换时间, 也会根据算法, 剪枝优化掉一些多余的 Cuboid , 寻求平衡
  • 访问接口: 支持标准 SQL 接口, 可以对接 Zeppelin、Tableau 等 BI 工具。SQL 通过查询引擎, 可以被路由到对应的 Cuboid 上。

(1)应用场景

Kylin 典型的应用场景如下:

  • 巨大的数据量, 单个数据源表千亿行数据级别, 且单个数据源达百 TB 级别
  • 巨大的查询压力(查询的高并发)
  • 查询的快速响应
  • 下游较灵活的查询方式, 需支持带有复杂条件的 SQL 查询

Kylin 的核心思想是预计算, 将数据按照指定的维度和指标, 预先计算出所有可能的查询结果, 利用空间换时间来加速模式固定的 OLAP 查询。

(2)基本术语

数据仓库是一种信息系统的资料储存理论, 强调的是利用某些特殊的资料储存方式, 让所包含的资料特别有利于分析和处理, 从而产生有价值的资讯, 并可依此做出决策。

利用数据仓库的方式存放的资料, 具有一旦存入, 便不会随时间发生变动的特性, 此外, 存入的资料必定包含时间属性, 通常一个数据仓库中会含有大量的历史性资料, 并且它可利用特定的分析方式, 从其中发掘出特定的资讯。

OLTP:联机事务处理,传统的关系型数据库的应用。

OLAP ( Online Analytical Process ), 联机分析处理, 以多维度的方式分析数据。它是呈现集成性决策信息的方法, 多用于数据仓库 或 商务智能。其主要的功能在于方便大规模数据分析及统计计算, 可对决策提供参考和支持。与之相区别的是联机交易处理( OLTP ), 联机交易处理, 侧重于基本的、日常的事务处理, 主要是数据的增删改查。

OLAP 的概念, 在实际应用中存在广义和狭义两种不同的理解方式。广义上的理解与字面上的意思相同, 泛指一切不会对数据进行更新的分析处理。但更多的情况下 OLAP 被理解为其狭义上的含义, 即与多维分析相关, 基于立方体( Cube )计算而进行的分析。

OLAP 有多种实现方法, 根据存储数据的方式不同可以分为 ROLAP、MOLAP、HOLAP:

  1. ROLAP(Relational OLAP), 细节数据、聚合后的数据都保存在类关系型的数据库中。Hive、SparkSQL 等属于 ROLAP。
  2. MOLAP(Multidimensional OLAP), 事先将汇总数据计算好, 存放在自己特定的多维数据库中, 用户的 OLAP 操作可以直接映射到多维数据库的访问, 不通过 SQL 访问, 其实质是空间换时间。Apache Kylin 本质上是 MOLAP。
  3. HOLAP(Hybrid OLAP), 表示基于混合数据组织的 OLAP 实现(Hybrid OLAP)。如低层是关系型的, 高层是多维矩阵型的。这种方式具有更好的灵活性。

1)事实表和维度表

事实表(Fact Table) :是指存储有事实记录的表, 如系统日志、销售记录、传感器数值等;

事实表的记录是动态增长的, 所以它的体积通常远大于维度表。

维度表(Dimension Table):维表, 也称为查找表(Lookup Table), 是与事实表相对应的一种表;

它保存了维度的属性值, 可以跟事实表做关联; 相当于将事实表上经常重复的属性抽取、规范出来用一张表进行管理。

常见的维度表有: 日期表(存储与日期对应的周、月、季度等属性)、地区表(包含国家、省/州、城市等属性)等。
维度表的变化通常不会太大。

使用维度表有许多好处:

  • 缩小了事实表的大小
  • 便于维度的管理和维护, 增加、删除和修改维度的属性, 不必对事实表的大量记录进行改动
  • 维度表可以为多个事实表重用

2)维度和度量

维度:指审视数据的角度, 它通常是数据记录的一个属性, 例如时间、地点等。

度量:被聚合的统计值, 也就是聚合运算的结果。通常是一个数值, 如总销售额、不同的用户数等。

分析人员往往要结合若干个维度来审查度量值, 以便在其中找到变化规律。

在一个 SQL 查询中, Group By 的属性通常就是维度, 而所计算的值则是度量。

1
2
3
sql复制代码SELECT part_dt, lstg_site_id, sum(price) as total_selled, count(distinct seller_id) as sellers
FROM kylin_sales
GROUP BY part_dt, lstg_site_id;

以上查询中, part_dt、lstg_site_id 是维度, sum(price)、count(distinct seller_id) 是度量。

3)星型模型和雪花模型

星型模型(Star Schema):是数据仓库维度建模中常用的数据模型之一。

它的特点是一张事实表, 以及一到多个维度表, 事实表与维度表通过主外键相关联, 维度表之间没有关联, 就像许多小星星围绕在一颗恒星周围, 所以名为星型模型。

雪花模型(SnowFlake Schema):就是将星型模型中的某些维表抽取成更细粒度的维表, 然后让维表之间也进行关联, 这种形状酷似雪花的的模型称为雪花模型。

低版本的 Kylin 只支持星型模型, 从 2.0 开始支持雪花模型。

4)Cube 和 Cuboid

Cube 即多维立方体, 也叫数据立方体。

这是由三个维度(维度数可以超过 3 个,上图仅为了方便画图表达)构成的一个 OLAP 立方体, 立方体中包含了满足条件的 cell(子立方块)值, 这些 cell 里面包含了要分析的数据, 称之为度量值。

  • 立方体: 由维度构建出来的多维空间, 包含了所有要分析的基础数据, 所有的聚合数据操作都在立方体上进行
  • 维度: 观察数据的角度。一般是一组离散的值。对于 N 个维度来说, 所有可能的组合有 2 的 N 次方个
  • 度量: 即聚合计算的结果, 一般是连续的值
  • Cuboid: 特指 Kylin 中在某一种维度组合下所计算的数据
  • 事实表中的一个字段, 要么是维度, 要么是度量(可以被聚合)
  • 给定一个数据模型, 可以对其上的所有维度进行组合。对于 N 个维度来说, 所有可能的组合有 2 的 N 次方个
  • Cube (或称 Data Cube), 即数据立方体, 是一种常用于数据分析与索引技术, 它可以对原始数据建立多维度索引, 大大加快查询效率。数据立方体只是多维模型的一个形象的说法。
  • Cuboid 特指 Kylin 中在某一种维度组合下所计算的数据。

组合示意图,如下:
2021-05-0117-35-58.png

二、Kylin 技术架构

Apache Kylin 系统可以分为: 在线查询 和 离线构建两部分。

在线查询模式主要处于上半部分, 离线构建处于下半部分。

2021-05-0117-36-27.png

Kylin 技术架构如下:

  • 数据源主要是 Hadoop Hive, 数据以关系表的形式输入, 保存着待分析的数据。根据元数据的定义, 构建引擎从数据源抽取数据, 并构建 Cube
  • Kylin 可以使用 MapReduce 或 Spark 作为构建引擎。构建后的 Cube 保存在右侧的存储引擎中, 一般选用 HBase 作为存储
  • 完成了离线构建后, 用户可以从查询系统发送 SQL 进行查询分析
  • Kylin 提供了各种 Rest API、JDBC/ODBC 接口。无论从哪个接口进入, SQL 最终都会来到 Rest 服务层, 再转交给查询引擎进行处理
  • SQL 语句是基于数据源的关系模型书写的, 而不是 Cube:
+ `Kylin` 在设计时, 刻意对查询用户屏蔽了 `Cube` 的概念
+ 只需要理解关系模型就可以使用 `Kylin`, 没有额外的学习门槛, 传统的 `SQL` 应用也很容易迁移
+ 查询引擎解析 `SQL`, 生成基于关系表的逻辑执行计划, 然后将其转换为基于 `Cube` 的物理执行计划, 最后查询预计算生成的 `Cube` 并产生结果, 整个过程不会访问原始数据源

组件的功能:

  • REST Server: 提供 Restful 接口, 例如创建、构建、刷新、合并等 Cube 相关操作, Kylin 的 Projects、Tables 等元数据管理, 用户访问权限控制, SQL 的查询等。
  • Query Engine: 使用开源的 Apache Calcite 框架来实现 SQL 解析, 可以理解为 SQL 引擎层
  • Routing: 负责将解析 SQL 生成的执行计划转换成 Cube 缓存的查询,这部分查询是可以在秒级甚至毫秒级完成
  • Metadata: Kylin 中有大量的元数据信息, 包括 Cube 的定义、星型模型的定义、Job 和 执行 Job 的输出信息、模型的维度信息等等, Kylin 的元数据和 Cube 都存储在 HBase 中, 存储的格式是 json 字符串
  • Cube Build Engine: 所有模块的基础, 它主要负责 Kylin 预计算中创建 Cube, 创建的过程是首先通过 Hive 读取原始数据, 然后通过一些 MapReduce 或 Spark 计算生成 Htable, 最后将数据 load 到 HBase 表中。

工作原理

Apache Kylin 的工作原理是对数据模型做 Cube 预计算, 并利用计算的结果加速查询。

具体工作过程如下:

  • 指定数据模型, 定义维度和度量
  • 预计算 Cube, 计算所有 Cuboid 并保存为物化视图(存储到 HBase 中)
  • 执行查询时, 读取 Cuboid, 计算并产生查询结果

高效 OLAP 分析:

  • Kylin 的查询过程不会扫描原始记录, 而是通过预计算预先完成表的关联、聚合等复杂运算
  • 利用预计算的结果来执行查询, 相比非预计算的查询技术, 其速度一般要快一到两个数量级, 在超大的数据集上优势更明显
  • 数据集达到千亿乃至万亿级别时, Kylin 的速度可以超越其他非预计算技术1000倍以上

Kylin 生态

如图:
2021-05-0810-25-15.png

  • Apache Kylin 核心 : Kylin 的 OALP 引擎由元数据引擎、查询引擎、任务引擎、存储引擎组成。另外, 它还有一个 REST 服务器对外提供查询请求的服务
  • 可扩展性: 提供插件机制支持额外的特性和功能
  • 与其他系统的整合: 可整合任务调度器, ETL 工具、监控及告警系统
  • 驱动包(Drivers): 提供 ODBC、JDBC 驱动支持与其他工具(如 Tableau )的整合

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【死磕Java并发】-----JUC之读写锁:Reent

发表于 2021-11-26

「这是我参与11月更文挑战的第 16 天,活动详情查看:2021最后一次更文挑战


重入锁ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较少。然而读服务不存在数据竞争问题,如果一个线程在读时禁止其他线程读势必会导致性能降低。所以就提供了读写锁。

读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞。

读写锁的主要特性:

  1. 公平性:支持公平性和非公平性。
  2. 重入性:支持重入。读写锁最多支持65535个递归写入锁和65535个递归读取锁。
  3. 锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁

读写锁ReentrantReadWriteLock实现接口ReadWriteLock,该接口维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。

1
2
3
4
csharp复制代码public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}

ReadWriteLock定义了两个方法。readLock()返回用于读操作的锁,writeLock()返回用于写操作的锁。ReentrantReadWriteLock定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
arduino复制代码/** 内部类  读锁 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 内部类 写锁 */
private final ReentrantReadWriteLock.WriteLock writerLock;

final Sync sync;

/** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
this(false);
}

/** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}

/** 返回用于写入操作的锁 */
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
/** 返回用于读取操作的锁 */
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }

stract static class Sync extends AbstractQueuedSynchronizer {
/**
* 省略其余源代码
*/
}
public static class WriteLock implements Lock, java.io.Serializable{
/**
* 省略其余源代码
*/
}

public static class ReadLock implements Lock, java.io.Serializable {
/**
* 省略其余源代码
*/
}

ReentrantReadWriteLock与ReentrantLock一样,其锁主体依然是Sync,它的读锁、写锁都是依靠Sync来实现的。所以ReentrantReadWriteLock实际上只有一个锁,只是在获取读取锁和写入锁的方式上不一样而已,它的读写锁其实就是两个类:ReadLock、writeLock,这两个类都是lock实现。

在ReentrantLock中使用一个int类型的state来表示同步状态,该值表示锁被一个线程重复获取的次数。但是读写锁ReentrantReadWriteLock内部维护着两个一对锁,需要用一个变量维护多种状态。所以读写锁采用“按位切割使用”的方式来维护这个变量,将其切分为两部分,高16为表示读,低16为表示写。分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过为运算。假如当前同步状态为S,那么写状态等于 S & 0x0000FFFF(将高16位全部抹去),读状态等于S >>> 16(无符号补0右移16位)。代码如下:

1
2
3
4
5
6
7
java复制代码static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

写锁

写锁就是一个支持可重入的排他锁。

写锁的获取

写锁的获取最终会调用tryAcquire(int arg),该方法在内部类Sync中实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//当前锁个数
int c = getState();
//写锁
int w = exclusiveCount(c);
if (c != 0) {
//c != 0 && w == 0 表示存在读锁
//当前线程不是已经获取写锁的线程
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//超出最大范围
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
//是否需要阻塞
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//设置获取锁的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}

该方法和ReentrantLock的tryAcquire(int arg)大致一样,在判断重入时增加了一项条件:读锁是否存在。因为要确保写锁的操作对读锁是可见的,如果在存在读锁的情况下允许获取写锁,那么那些已经获取读锁的其他线程可能就无法感知当前写线程的操作。因此只有等读锁完全释放后,写锁才能够被当前线程所获取,一旦写锁获取了,所有其他读、写线程均会被阻塞。

写锁的释放

获取了写锁用完了则需要释放,WriteLock提供了unlock()方法释放写锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public void unlock() {
sync.release(1);
}

public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

写锁的释放最终还是会调用AQS的模板方法release(int arg)方法,该方法首先调用tryRelease(int arg)方法尝试释放锁,tryRelease(int arg)方法为读写锁内部类Sync中定义了,如下:

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码protected final boolean tryRelease(int releases) {
//释放的线程不为锁的持有者
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
//若写锁的新线程数为0,则将锁的持有者设置为null
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}

写锁释放锁的整个过程和独占锁ReentrantLock相似,每次释放均是减少写状态,当写状态为0时表示 写锁已经完全释放了,从而等待的其他线程可以继续访问读写锁,获取同步状态,同时此次写线程的修改对后续的线程可见。

读锁

读锁为一个可重入的共享锁,它能够被多个线程同时持有,在没有其他写线程访问时,读锁总是或获取成功。

读锁的获取

读锁的获取可以通过ReadLock的lock()方法:

1
2
3
csharp复制代码public void lock() {
sync.acquireShared(1);
}

Sync的acquireShared(int arg)定义在AQS中:

1
2
3
4
arduino复制代码public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}

tryAcqurireShared(int arg)尝试获取读同步状态,该方法主要用于获取共享式同步状态,获取成功返回 >= 0的返回结果,否则返回 < 0 的返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
ini复制代码protected final int tryAcquireShared(int unused) {
//当前线程
Thread current = Thread.currentThread();
int c = getState();
//exclusiveCount(c)计算写锁
//如果存在写锁,且锁的持有者不是当前线程,直接返回-1
//存在锁降级问题,后续阐述
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//读锁
int r = sharedCount(c);

/*
* readerShouldBlock():读锁是否需要等待(公平锁原则)
* r < MAX_COUNT:持有线程小于最大数(65535)
* compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
*/
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
/*
* holdCount部分后面讲解
*/
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}

读锁获取的过程相对于独占锁而言会稍微复杂下,整个过程如下:

  1. 因为存在锁降级情况,如果存在写锁且锁的持有者不是当前线程则直接返回失败,否则继续
  2. 依据公平性原则,判断读锁是否需要阻塞,读锁持有线程数小于最大值(65535),且设置锁状态成功,执行以下代码(对于HoldCounter下面再阐述),并返回1。如果不满足改条件,执行fullTryAcquireShared()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
ini复制代码final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
//锁降级
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
}
//读锁需要阻塞
else if (readerShouldBlock()) {
//列头为当前线程
if (firstReader == current) {
}
//HoldCounter后面讲解
else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
//读锁超出最大范围
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//CAS设置读锁成功
if (compareAndSetState(c, c + SHARED_UNIT)) {
//如果是第1次获取“读取锁”,则更新firstReader和firstReaderHoldCount
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
}
//如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程,则将firstReaderHoldCount+1
else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
//更新线程的获取“读取锁”的共享计数
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}

fullTryAcquireShared(Thread current)会根据“是否需要阻塞等待”,“读取锁的共享计数是否超过限制”等等进行处理。如果不需要阻塞等待,并且锁的共享计数没有超过限制,则通过CAS尝试获取锁,并返回1

读锁的释放

与写锁相同,读锁也提供了unlock()释放读锁:

1
2
3
csharp复制代码public void unlock() {
sync.releaseShared(1);
}

unlcok()方法内部使用Sync的releaseShared(int arg)方法,该方法定义在AQS中:

1
2
3
4
5
6
7
arduino复制代码public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

调用tryReleaseShared(int arg)尝试释放读锁,该方法定义在读写锁的Sync内部类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ini复制代码protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//如果想要释放锁的线程为第一个获取锁的线程
if (firstReader == current) {
//仅获取了一次,则需要将firstReader 设置null,否则 firstReaderHoldCount - 1
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
}
//获取rh对象,并更新“当前线程获取锁的信息”
else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
//CAS更新同步状态
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}

HoldCounter

在读锁获取锁和释放锁的过程中,我们一直都可以看到一个变量rh (HoldCounter ),该变量在读锁中扮演着非常重要的作用。

我们了解读锁的内在机制其实就是一个共享锁,为了更好理解HoldCounter ,我们暂且认为它不是一个锁的概率,而相当于一个计数器。一次共享锁的操作就相当于在该计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。所以HoldCounter的作用就是当前线程持有共享锁的数量,这个数量必须要与线程绑定在一起,否则操作其他线程锁就会抛出异常。我们先看HoldCounter的定义:

1
2
3
4
arduino复制代码static final class HoldCounter {
int count = 0;
final long tid = getThreadId(Thread.currentThread());
}

HoldCounter 定义非常简单,就是一个计数器count 和线程 id tid 两个变量。按照这个意思我们看到HoldCounter 是需要和某给线程进行绑定了,我们知道如果要将一个对象和线程绑定仅仅有tid是不够的,而且从上面的代码我们可以看到HoldCounter 仅仅只是记录了tid,根本起不到绑定线程的作用。那么怎么实现呢?答案是ThreadLocal,定义如下:

1
2
3
4
5
6
csharp复制代码static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}

通过上面代码HoldCounter就可以与线程进行绑定了。故而,HoldCounter应该就是绑定线程上的一个计数器,而ThradLocalHoldCounter则是线程绑定的ThreadLocal。从上面我们可以看到ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程Id,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是这样HoldCounter绑定线程id而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已。

看到这里我们明白了HoldCounter作用了,我们在看一个获取读锁的代码段:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}

这段代码涉及了几个变量:firstReader 、firstReaderHoldCount、cachedHoldCounter 。我们先理清楚这几个变量:

1
2
3
java复制代码private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
private transient HoldCounter cachedHoldCounter;

firstReader 看名字就明白了为第一个获取读锁的线程,firstReaderHoldCount为第一个获取读锁的重入数,cachedHoldCounter为HoldCounter的缓存。

理清楚上面所有的变量了,HoldCounter也明白了,我们就来给上面那段代码标明注释,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码//如果获取读锁的线程为第一次获取读锁的线程,则firstReaderHoldCount重入数 + 1
else if (firstReader == current) {
firstReaderHoldCount++;
} else {
//非firstReader计数
if (rh == null)
rh = cachedHoldCounter;
//rh == null 或者 rh.tid != current.getId(),需要获取rh
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
//加入到readHolds中
else if (rh.count == 0)
readHolds.set(rh);
//计数+1
rh.count++;
cachedHoldCounter = rh; // cache for release
}

这里解释下为何要引入firstRead、firstReaderHoldCount。这是为了一个效率问题,firstReader是不会放入到readHolds中的,如果读锁仅有一个的情况下就会避免查找readHolds。

锁降级

上开篇是LZ就阐述了读写锁有一个特性就是锁降级,锁降级就意味着写锁是可以降级为读锁的,但是需要遵循先获取写锁、获取读锁在释放写锁的次序。注意如果当前线程先获取写锁,然后释放写锁,再获取读锁这个过程不能称之为锁降级,锁降级一定要遵循那个次序。

在获取读锁的方法tryAcquireShared(int unused)中,有一段代码就是来判读锁降级的:

1
2
3
4
5
6
7
8
9
scss复制代码int c = getState();
//exclusiveCount(c)计算写锁
//如果存在写锁,且锁的持有者不是当前线程,直接返回-1
//存在锁降级问题,后续阐述
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//读锁
int r = sharedCount(c);

锁降级中读锁的获取释放为必要?肯定是必要的。试想,假如当前线程A不获取读锁而是直接释放了写锁,这个时候另外一个线程B获取了写锁,那么这个线程B对数据的修改是不会对当前线程A可见的。如果获取了读锁,则线程B在获取写锁过程中判断如果有读锁还没有释放则会被阻塞,只有当前线程A释放读锁后,线程B才会获取写锁成功。

推荐阅读

因为里面很多地方涉及到了AQS部分,推荐阅读如下部分:

  1. 【死磕Java并发】—–J.U.C之AQS:AQS简介
  2. 【死磕Java并发】—–J.U.C之AQS:CLH同步队列
  3. 【死磕Java并发】—–J.U.C之AQS:同步状态的获取与释放
  4. 【死磕Java并发】—–J.U.C之AQS:阻塞和唤醒线程

参考资料

  1. Doug Lea:《Java并发编程实战》
  2. 方腾飞:《Java并发编程的艺术》
  3. 【Java并发编程实战】—–“J.U.C”:ReentrantReadWriteLock
  4. Java多线程(十)之ReentrantReadWriteLock深入分析

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【重构技巧】Java->更符合心智模型的DSL重构

发表于 2021-11-26

这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战

DSL

Domain-specific language: 一种专注于某一领域,仅针对部分表达方式的计算机编程语言。

它有几个特点:

  • 方法链 Method Chaining
  • 功能序列 Functional Sequence
  • 嵌套函数 Nested Functions 嵌套函数
  • Lambda表达式/闭包 Lambda Expressions/Closures

概念有点抽象,先看代码吧

假设你想发一些邮件,你需要一个类能够方便的设置收信人、发信人、标题、内容。

一个传统的java api(具体业务代码都省略了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class Mailer {
public void from(String fromAddress) {
}

public void to(String toAddress) {
}

public void subject(String theSubject) {
}

public void message(String body) {
}

public void send() {
}
}

测试要这样写:

1
2
3
4
5
6
7
8
java复制代码public static void main(String[] args) {
Mailer mailer = new Mailer();
mailer.from("build@example.com");
mailer.to("example@example.com");
mailer.subject("build notification");
mailer.message("some details about build status");
mailer.send();
}

我们可以做些重构,使这个api更流畅,更像DSL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码package dsl.example;

public class Mailer {
public Mailer from(String fromAddress) {
return this;
}

public Mailer to(String toAddress) {
return this;
}

public Mailer subject(String theSubject) {
return this;
}

public Mailer message(String body) {
return this;
}

public void send() {
}
}

测试:

1
java复制代码

这样看起来好多了,但是如果能消除new就更好了。因为用户的兴趣在于发送邮件,而不是在创建对象。

1
2
3
4
5
6
7
8
java复制代码public static void main(String[] args) {
new Mailer()
.from("build@example.com")
.to("example@example.com")
.subject("build notification")
.message("some details about build status")
.send();
}

测试:

1
2
3
4
5
6
7
8
java复制代码public static void main(String[] args) {
Mailer.mail()
.from("build@example.com")
.to("example@example.com")
.subject("build notification")
.message("some details about build status")
.send();
}

可以做一下静态导入

1
2
3
4
5
6
7
8
java复制代码public static void main(String[] args) {
import static dsl.example.Mailer.mail;mail()
.from("build@example.com")
.to("example@example.com")
.subject("build notification")
.message("some details about build status")
.send();
}

这样,一个DSL的语句就完成了。一般来说,使用Java编写的DSL不会造就一门业务用户可以上手的语言,而会是一种业务用户也会觉得易读的语言,同时,从程序员的角度,它也会是一种阅读和编写都很直接的语言。

小结

创建DSL最好的方法是,首先将所需的API原型化,然后在基础语言的约束下将它实现。DSL的实现将会牵涉到连续不断的测试来肯定我们的开发确实瞄准了正确的方向。该“原型-测试”方法正是测试驱动开发模式(TDD-Test-Driven Development)所提倡的。

其实JDK8提供的很多api已经有很多内部DSL的语义,比如Stream流的find、count等操作都是一种DSL的语义表达,本文只是简单的说明了如何构造DSL,有机会计划找一个实际的业务代码用DSL的方式重构,敬请期待。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…161162163…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%