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

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


  • 首页

  • 归档

  • 搜索

每天一个 Linux 命令(11)—— diff 命令简介

发表于 2021-11-16

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

命令简介

diff 命令主要用于逐行比较两个指定的文件,显示文件之间的差异。也可以逐对比较两个目录中的相应文件。如果两个文件相同,diff 命令通常不会输出任何信息;如果两个文件不同,diff命令将会以不同的方式(取决于命令行选项)显示两个文件的不同点。

最新版本的 diff 命令还支持二进制文件比较。

命令格式

1
css复制代码diff[参数][文件1或目录1][文件2或目录2]

命令参数

参数 解释
-a,--text 把所有文件当作文本文件处理,逐行比较。
-b,--ignore-space-change 比较时忽略因空格数量的多少而引起的差别。
-B,--ignore-blank-lines 比较时忽略因空行而引起的差别。
-c,-C num,--context[=num] 首先输出两个文件的名字与修改时间等信息,其次输出第一个和第二个文件以起始行和终止行表示的数据行范围,最后输出不同的数据行及前后相邻的 num 行数据。num 的默认值为 3。
-d,--minimal 尽可能找出文件之间的微小差异。这个选项会影响 diff 命令的运行速度。
-e,--ed 针对两个文件的差别,输出一个 ed 脚本文件,利用其中提供的 ed 编辑命令,能够确保两个文件一致。
-E,--ignore-tab-expansion 比较时忽略因制表符展开而引起的差异。
-i,--ignore-case 比较时忽略大小写字母的差异。
-I regexp,--ignore-matching-lines=regexp 比较时忽略两个文件中均匹配指定模式的数据行。
-l,--paginate 把比较结果传递给 pr 命令,形成一个标记时间、命令行和页码标题的输出形式,写到标准输出。
-N,--new-file 把不存在的文件作为空文件处理。
-q,--brief 仅当文件不同时才输出文字说明信息(文件相同时不显示任何信息)。可用于比较二进制数据文件。
-r,--recursive 按目录方式逐队比较文件时,递归地处理两个目录中的任何子目录。
-s,--report-identical-files 如果两个文件相同,输出文字说明信息。如果两个文件不同,按常规处理,显示存在差异的数据行。
-S file,--starting-file=file 比较目录时,从指定的文件开始逐一比较。可用于恢复中断的比较。
-t,--expand-tabs 展开制表符,输出适当数量的空格。
-T,--initial-tab 在输出的数据行之前插入一个制表符。
-u,-U num,--unified[=num] 同 -c 选项,只是以起始行与输出行数替代数据行范围。
-w,--ignore-all-space 比较时忽略所有的空格字符。
-W num,--width=num 指定数据显示的最大行宽,默认的行宽为 130 列。
-x pattern,--exclude=pattern 按目录方式比较时,忽略基本文件名部分(不含目录)匹配指定模式的文件或子目录。
-X file,--exclude-from=file 按目录方式比较时,忽略基本文件名部分(不含目录)匹配指定文件中包含的任何模式的文件或子目录。
-y,--side-by-side 采用两列的形式显示文件之间的差异。
--from-file=file1 使用指定的文件作为第一个文件参数,与其他文件进行比较。注意,指定的文件可以是一个目录。
--to-file=file2 使用指定的文件作为第二个文件参数,与其他文件进行比较。注意,指定的文件可以是一个目录。
--ignore-file-name-case 比较文件名时,忽略大小写字母的差异。
--left-column 指定 -y 选项两列显示时,再指定此选项表示仅在左边第一列输出相同的数据行,第二列省略。
--speed-large-files 假定文件存在大量零散的微小差异,采用探索法加速处理这样的大型文件。
--strip-trailing-cr 删除输入数据行尾的回车字符。
--suppress-common-lines 禁止输出两个文件相同的数据行(默认的做法)。
--unidirectional-new-file 把不存在的第一个文件作为空文件处理。

应用实例

  1. 比较两个文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码$ cat file1.txt
1001
1002
1003

$ cat file2.txt
1001
1002
1003a
1004

$ diff file1.txt file2.txt
3c3,4
< 1003
---
> 1003a
> 1004

上面的 3c3,4 表示 file1.txt 和 file2.txt 文件在 3 行和第 4 行内容有所不同;

diff 的 normal 显示格式有三种提示:

  • a:add
  • c:change
  • d:delete

参考文档

  • diff命令
  • 《Linux 常用命令简明手册》—— 邢国庆编著

本文转载自: 掘金

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

Linux系统基本介绍!详细解析Linux系统中的进程,线程

发表于 2021-11-16

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

基本概念

  • 在Linux中,进程就是一个数据结构
  • 理解文件描述符,重定向,管道命令的底层工作原理,就可以从操作系统的角度理解Linux中的线程和进程是没有区别的

进程

计算机结构

  • 计算机结构:
    • 内存空间:
      • 上半部表示用户空间
      • 下半部表示内存空间
    • 进程
    • 磁盘
    • 输入输出设备
  • 用户空间: 装着用户进程需要使用的资源
  • 内核空间: 存放内核进程需要加载的系统资源,这些资源一般是不允许用户访问的. 但是有的用户进程会共享一些内核空间的资源,比如一些动态链接库等
  • 编译好的可执行程序只是一个文件. 不是进程,可执行文件必须要载入内存,包装成一个进程才能运行
  • 进程: 是要依靠操作系统创建的,每个进程都有固有属性
    • 进程号PID
    • 进程状态
    • 打开的文件
      • 进程创建好后,读入程序,程序才会被系统执行
  • 对于操作系统,进程就是一个数据结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c复制代码struct task_struct {
// 进程状态
long state;
// 虚拟内存结构体
struct mm_struct **mm;
// 进程号
pid_t pid;
// 指向父结构的指针
struct task_struct _ _rcu *parent;
// 子进程列表
struct list_head children;
// 存放文件系统信息的指针
struct fs_struct *fs;
// 一个包含进程打开的文件指针的数组
struct files_struct *files;
}
  • task_struct是Linux对于一个进程的描述,称为进程描述符
  • **mm: 指向的是进程的虚拟内存,即载入资源和可执行文件的位置
  • *files: 指向一个存放所有该进程打开的文件指针的数组

文件描述符

  • *files:
    • 文件指针数组
    • 通常情况下,一个进程会从files[0] 读取输入,将输出写入files[1], 将错误信息写入files[2]
  • 文件描述符:
    • 每个进程被创建时 ,file的前三位被填入默认值,分别指向标准输入流,标准输出流,标准错误流
    • 文件描述符就是指这个数组的索引
    • 程序的文件描述符默认情况下0是输入 ,1是输出 ,2是错误
  • 默认情况下,计算机的输入流是键盘,输出流是显示器,错误流也是显示器.进程通过系统调用让内核进程访问硬件资源.在Linux中,一切皆文件,设备也是文件,可以进行读和写. 比如当前进程如果需要调用其余资源,则需要进行系统调用,让内核将文件打开,这个文件会被放到files的第4个位置
  • 输入重定向:
    • 程序读取数据会从files[0] 中读取
    • 只要将files[0] 指向一个文件,而不是键盘
    • 程序就会从这个文件中读取数据,而不是键盘
1
bash复制代码$ command < file.txt
  • 输出重定向:
    • 程序会将数据写入到files[1] 中输出
    • 只要将files[1] 指向一个文件,而不是显示器
    • 程序就会将数据写入到这个文件中,而不会写入到显示器
1
bash复制代码$ command > file.txt
  • 错误重定向:
    • 程序会将数据写入到files[2] 中输出
    • 只要将files[2] 指向一个文件,而不是显示器
    • 程序就会将数据写入到这个文件中,而不会写入到显示器
1
bash复制代码$ command > file.txt
  • 管道符: 将一个进程的输出流和另一个进程的输入流接起一条 [管道], 数据就在这条管道中进行传递
1
bash复制代码$ cmd1 | cmd2 | cmd3
  • Linux中一切皆文件:
    • 不管是设备,进程 ,socket套接字还是真正的文件,全部都可以读写,统一装进一个简单的files数组
    • 进程通过简单的文件描述符访问相应的资源. 具体细节交于操作系统,能够实现有效解耦,优美高效

线程

  • 多线程和多进程都是并发,都可以提高处理器的利用效率
  • Linux中线程和进程基本没有区别: 从Linux内核角度来说,线程和进程没有区别对待
    • 系统调用函数pthread() 可以新建一个线程. 系统调用fork() 可以新建一个子进程
    • 无论是线程还是进程,都是用task_struct结构表示的,唯一的区别就是共享的数据区域不同
      • 线程基本上和进程没有区别,只是线程的某些数据区域和父进程是共享的,子进程是拷贝数据,而不是共享
    • 多线程程序要利用锁机制,避免多个线程同时同时向同一区域写入数据,否则会造成数据错乱
  • 问题: 既然进程和线程基本没有区别,并且多进程数据不共享,就不会存在数据错乱的问题,为什么多线程的使用比多进程要更加普遍呢?
    • 因为在现实场景中,数据共享的并发更加普遍
  • 只有Linux系统将线程看作是共享数据的进程,不做特殊对待. 其余操作系统是对线程和进程区别对待的,线程和进程各自持有特殊的数据结构
  • Linux中新建线程和进程的效率都是很高的:
    • 对于新建进程时内存区域拷贝的问题
    • Linux采用copy-on-write策略优化
    • 即并不真正复制父进程的内存区域空间,而是等到写操作时才去复制
    • 所以,在Linux中新建线程和进程都是很迅速的

本文转载自: 掘金

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

JVM垃圾收集器简介 前言 1内存回收简介

发表于 2021-11-16

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

前言

在前面的文章中我们讲了java内存中的分区,下面我们就来讲一下JVM的垃圾收集原理。
image.png

感兴趣的朋友可以去看看:JAVA的内存简介

1.内存回收简介

Java 的⾃动内存管理主要是针对对象内存的回收和对象内存的分配。而对象都是存储在堆中的,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)同时,Java ⾃动内存管理
最核⼼的功能是堆内存中对象的分配与回收。Java 堆是垃圾收集器管理的主要区域,因此我们也称它为GC堆(Garbage Collected Heap)。

java的堆中主要分为年轻代和老年代。

年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。他们默认大小比例为8:1。年轻代使用复制-清除算法和并行收集器进行垃圾回收,对年轻代的垃圾回收称为初级回收(minor GC)

而老年代中一般都是存活了很久的对象或者是比较大的对象(大对象创建时会直接放到老年代),在老年代发生的GC成为 Full GC 。Full GC 不会像 Minor GC那么频繁Full GC采用 标记-清除算法 收集垃圾的时候会产生许多内存碎片(不连续的存储空间),一般 Full GC 之前都会先进行Minor GC,目的时把新生代中的大对象或过期对象移动到老年代。

对象都会⾸先在 Eden 区域分配,在⼀次新⽣代垃圾回收后,如果对象还存活,则会进⼊ s0 或者 s1,紧接着进行GC,GC之后会将仍然存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认为 15 岁)的对象会被移动到年老代中,如果年龄没有达到,则会将它放到另一个Survivor区。不管怎样,都会保证两块Survivor区的其中一块是空的,当其中一块填满或者GC时间到,则会触发GC进行垃圾回收。大家可以通过图片了解一下这个过程。

image.png

本文转载自: 掘金

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

Mysql 温故知新系列「字符串函数(concat, con

发表于 2021-11-16

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

字符串函数

mysql 不仅提供了辅助聚合记录的操作,同时也提供了对字符串的拼接、截取、替换等常规的字符操作

concat

concat() 接收一个或多个字符串参数,最后会将他们拼接在一起,变成一个字符串在返回、但是,倘若其中任意一个参数为 null 则整个字符串都会返回为 null

image.png

image.png

在示例中,我们,如果需要在多个字符之间插入分隔符,则需要将分隔符当作一个普通的字符传参,mysql 中为我们提供了一个强化版的字符拼接函数: concat_ws

concat_ws

concat_ws(seperator,string1,string2, ... ),接收多个参数,第一个是分隔符,必填,从第二个开始,就是我们需要拼接的字符串,这个函数会自动的在多个参数之间插入指定的分隔符

image.png

这里换一张表演示 concat_ws 对空字符数据的处理

image.png

如图,如果连接的字符中存在有空数据,concat_ws 并不会将整个结果作为 null 返回,仅仅是忽略掉那个空的字符

length

以字节为单位,获取字符串的长度,在 mysql 中,不同的字符编码,会占据不同的不同个数的字节长度,比如我们最习惯使用的 utf8 需要占据 3 个字节,以及在 mysql8 往后中出现的最多的 utf8mb4 占据了 4 个字节

我们可以使用 show character set; 来查看当前 mysql 支持的字符集以及他们的需要的字节数

image.png

题归正传,在 mysql 中,utf8 编码下的中文字符,会占据 3 个字节

image.png

char_length

与 length 相比,char_length 最大的区别就是按字符进行统计,即他不关心字符编码,单纯的统计字符的个数

image.png

举个例子,我们做了一个自愿付费的站点,需要吸引人点击但又不想被人直接吧资源爬去了,需要对文章的内容做定长截取,这个实现现在有两种方案,①在后台做;②直接在数据库处理好,后台代码都不需要动,配合上 mybaits 的 xml 形式的 sql,改了直接部署,简单极了

原创文章,未经允许,禁止转载

– by 安逸的咸鱼

本文转载自: 掘金

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

Go 与结构体

发表于 2021-11-16

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

结构体

结构体是将零个或多个任意类型的命名变量组合在一起的聚合数据类型。每个变量都叫做结构体的成员。

1
2
3
4
5
go复制代码type Employee struct {
ID int
Name string
age int
}

Employee就是一个结构体。

定义结构体时要注意

1.如果一个成员变量的首字母大写,则它是可导出的。

2.成员变量的顺序对于结构体的同一性很重要,顺序不同表示不同的结构体。

3.结构体中的成员变量类型不能是结构体本身,但可以是该结构体的指针类型

1
2
3
4
5
6
go复制代码type Node struct {
value int
next *Node
//这样是非法的
//next Node
}

4.结构体的零值由结构体成员的零值构成。

结构体变量的声明

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码type Point struct {
X int
Y int
}

// 方式一
p1 := Point{1, 2} // X=1 Y=2
// 方式二
p2 := Point{X: 3, Y: 4} // X=3 Y=4

// 如果某个成员变量没有指定,那么该成员变量的值为该类型零值
p3 := Point{Y:6} // X=0 Y=6

方式一和方式二不可以混用。并且不能通过方式一绕过不可导出变量无法在其他包中使用的规则。

1
2
3
4
5
6
7
8
9
go复制代码// p.go
package p
type T struct { a, b int }

// q.go
package q
import "p"
var _ = p.T{1, 2} // 编译错误
var _ = p.T{a: 1, b: 2} // 编译错误

如果在函数中需要修改结构体变量的内容时,需要传递指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码type Point struct {
X, Y int
}

func update(p Point, x int) { // 函数接收到的是实惨的一个副本
p.X = x
}

func update2(p *Point, x int) {
p.X = 2
}

func main() {
p := Point{}
update(p, 1)
fmt.Println(p) // {0 0}
update2(&p, 2)
fmt.Println(p) // {2 2}
}

结构体的比较

必须所有的成员变量是可比较的,那这个结构体是可变比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码type Point struct {
X, Y int
}

type Book struct {
name string
chapters []string
}

func main() {
p1 := Point{X: 1, Y: 2}
p2 := Point{X: 1, Y: 2}
p3 := Point{X: 1, Y: 3}
fmt.Println(p1 == p2, p2 == p3) // true false

b1 := Book{name: "abc", chapters: []string{"1", "2"}}
b2 := Book{name: "abc", chapters: []string{"1", "2"}}
fmt.Print(b1 == b2) // 编译错误,无法比较,因为chapters是slice类型 不能比较
}

结构体嵌套和匿名成员

结构体嵌套的目的是为了复用
Go允许我们定义不带名称的结构体成员(即匿名成员),这样可以快捷访问到匿名成员里的成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码type Point struct {
X, Y int
}

type Circle struct {
Point
Radius int
}

type Wheel struct {
Circle
Spokes int
}

func main() {
var w Wheel
w.X = 8 // 等价于w.Circle.Point.X
w.Y = 8
w.Radius = 5 // 等价于w.Circle.Radius
w.Spokes = 20

fmt.Printf("%#v", w) // main.Wheel{Circle:main.Circle{Point:main.Point{X:8, Y:8}, Radius:5}, Spokes:20}
}

外围的结构体Wheel不仅获得匿名成员Circle的内部成员变量,也会获得Circle的方法

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
go复制代码type Point struct {
X, Y int
}

func (p Point) Print() {
fmt.Printf("%#v", p)
}

type Circle struct {
Point
Radius int
}

func (c Circle) Print() {
fmt.Printf("%#v", c)
}


type Wheel struct {
Circle
Spokes int
}

// func (w Wheel) Print() {
// fmt.Printf("%#v", w)
// }

func main() {
var w Wheel
w.X = 8
w.Y = 8
w.Radius = 5
w.Spokes = 20

fmt.Printf("%#v\n", w) // main.Wheel{Circle:main.Circle{Point:main.Point{X:8, Y:8}, Radius:5}, Spokes:20}
w.Print() // main.Circle{Point:main.Point{X:8, Y:8}, Radius:5}
}

如果在Wheel上再加一个匿名命名成员Band, 它也有Print方法

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
go复制代码type Band struct {
Name string
}

func (b Band) Print() {
fmt.Printf("%#v", b)
}

type Wheel struct {
Circle
Band
Spokes int
}


func main() {
var w Wheel
w.X = 8
w.Y = 8
w.Radius = 5
w.Spokes = 20

fmt.Printf("%#v\n", w) // main.Wheel{Circle:main.Circle{Point:main.Point{X:8, Y:8}, Radius:5}, Band:main.Band{Name:""}, Spokes:20}
w.Print() // 编译报错
w.Circle.Print() // main.Circle{Point:main.Point{X:8, Y:8}, Radius:5}
w.Band.Print() // main.Band{Name:""}
}

如果外围结构体Wheel有Print方法,会直接调用该方法;如果没有,但其内部的多个匿名成员都有该方法,需要显示指定调用哪个匿名结构体的Print方法

本文转载自: 掘金

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

Spring AOP(一)核心概念 Spring AOP(一

发表于 2021-11-16

Spring AOP(一)核心概念

spring的两大核心IOC和AOP,对于IOC在项目中会经常使用,但是对于AOP虽然每个人都用到过,但是不一定都会有自己动手来写AOP的实践,在近期的项目中有使用AOP的场景,刚好借此机会将AOP部分的知识记录下来巩固加深。特意写这篇文章来记录下AOP使用的一些感受。

核心概念

在学习AOP之前需要先知道什么是AOP,对于AOP的概念就不多余啰嗦了,网上资料很多这里就着重讲一下自己对于AOP的理解,AOP即面向切面编程,那么应该怎么理解面向切面编程呢。

举个列子我们在公司中会有很多业务部门,各个业务部门处理自己的业务,我们可以将业务部门理解为纵向的多条线,那在公司中也有一些部门他们不属于业务部门,但是他们的职能是管理一些人力等其他工作,我们一般叫做职能部,职能部的一些工作就像一把切刀一样,横向的插入所有业务部,来完成职能部的工作。这里的职能部我们就可以理解为面向切面编程的切面。
图片1.png
回到程序中来讲,面向切面编程就是当我们在项目中遇到一些可以为其他业务模块共同服务的功能时,这些功能又不属于我们自己的业务功能,我们可以将这部分代码抽离出来来为这些业务代码服务,这样我们就可以再不改动业务代码的前提下,进行新功能的开发后期如果不需要这样的功能或者需要修改的时候,我们也可以再不改动业务代码的前提下进行代码改动减少风险。最常见的场景就是在一个现有项目中要追加一些跟踪日志,那么AOP会是很好的一个实现方式。我们可以通过声明一个日志切面。然后只要在切面中指定需要在那些地方进行日志处理,以及处理的逻辑,那么我们就可以实现不修改当前代码的前提下,添加日志跟踪的需求,讲了这么多我们接下来看一下AOP中几个核心概念。
图片2.png

AOP关键名词

切面 aspect: 一般声明我们的AOP的功能代码,即声明该部分是一个AOP来对业务代码进行切割,进行AOP的业务逻辑。切面中一般都会包含我们的通知advice和切点pointcut。

通知 advice: advice就是指我们在AOP中要进行处理的逻辑。一般有before,after/afterReturning,around,和afterThrowing分别对应前置,后置,围绕和异常。这里讲一下after和afterReturning的区别和执行顺序。

spring5.2.7RELEASE之前的执行顺序

1
2
3
4
5
java复制代码graph LR 
A[Around]-->B[Before]
B-->C[Around]
C-->D[After]
D-->E[AfterReturning]

spring5.2.7RELEASE之后的执行顺序

1
2
3
4
5
java复制代码graph LR 
A[Around]-->B[Before]
B-->C[AfterReturning]
C-->D[After]
D-->E[Around]

为什么会出现这种情况是因为spring调整了通知的Order执行顺序详细可以参考链接。
image.png
切点 pointcut: 声明切面作用的地方,一般都指定项目中特定包/类的方法(常用的都是指service层的一些方法),在spring中声明pointcut的方法是不需要写逻辑代码的,主要用来声明pointcut的作用范围。

1
2
3
4
5
6
7
8
9
10
11
java复制代码/**
* 只声明一个切点,不做任何逻辑处理
* <p>
* execution(): 表达式主体。
* 一个*号:表示返回类型,*号表示所有的类型。
* 包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service.impl包、子孙包下所有类的方法。
* 第二个*号:表示类名,*号表示所有的类。
* *(…):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
*/
@Pointcut("execution(* com.troyqu.annotation.aop.aspectj.execcontroller..*.*(..))")
public void execPointCut() {}

连接点 jointpoint: 被拦截到的点,也就是AOP中命中了AOP声明规则的对象或者方法,一般项目中都对应到我们被切面命中的方法。通过JointPoint对象可以拿到被命中方法的一些关键参数。

1
2
3
4
5
java复制代码@Before("execPointCut()")
public void beforePointCut(JoinPoint joinPoint) {
logger.info("come in execPointCut before {}", joinPoint.getSignature());
logger.info("come in execPointCut before");
}

目标对象 target object: 代理的目标对象,spring aop通过代理的方式来实现,目标对象就是我们需要被AOP逻辑处理的类,对应上图中的用户服务/资源服务/订单服务,大多数项目中都是就是我们的UserService这些类。

织入 weave: 指把切面应用到目标对象上,生成代理对象的过程。

代理 AOP Proxy: 一般编码时我们自己感受不到,spring都帮我们封装好了,常见的方式有(jdk动态代理或者CGlib)。

通知类型

  • 前置(Before):在目标方法被调用之前调用的处理逻辑;
  • 后置(After):在目标方法执行完成之后调用的通知;
  • 返回(After-returning):在目标方法成功return语句之后调用的通知,也就是说After-returning会在After之前触发;
  • 异常(After-throwing):在目标方法执行抛出异常时调用的通知;
  • 环绕(Around):在被通知的方法调用之前和调用之后执行的通知;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码    @Before("execPointCut()")
public void beforePointCut(JoinPoint joinPoint) {
logger.info("come in execPointCut before {}", joinPoint.getSignature());
logger.info("come in execPointCut before");
}

@After("execPointCut()")
public void afterPointCut() {
logger.info("come in execPointCut after");
}

@AfterReturning("execPointCut()")
public void afterRetPointCut() {
logger.info("come in execPointCut AfterReturning");
}
}

本文转载自: 掘金

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

jvm——垃圾回收算法

发表于 2021-11-16

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

前言

jvm——初入垃圾回收 - 掘金 (juejin.cn)

以上这篇文章中有说过如何判断对象是否为垃圾,但是回收的话还需要一些回收的算法。

常见的回收算法有以下三种:

1、标记清除算法

2、标记整理算法

3、标记复制算法

一般不会单独采用以上算法,而是使用分代垃圾回收机制(此篇不讲)

标记清除算法

标记清除算法是最早出现的也是最基础的垃圾收集算法,该算法于1960年由Lisp之父John McCarthy提出。

该算法可以分为两个阶段:1、标记 2、清除

标记

image.png

通过上图我们认为与 GC ROOT 没有关联的对象就标记为垃圾。

清除

image.png

把刚才标记的对象内存释放出来,然后放入空闲内存列表中,下次我们需要创建对象的时候就从空闲内存列表中进行使用。

小结

该算法是最基础的收集算法,后续的收集算法很多都是以该算法为基础,并对其算法的缺点进行改进而得到的。主要有以下两个缺点:

1、执行的效率极不稳定,当堆中有大量需要回收的对象这时候就需要做大量的标记和清除操作,标记和清除的执行效率随数量的增长而降低。
2、内存空间碎片化问题,标记、清除操作完之后会产生大量的不连续内存碎片,从而导致当程序中需要分配较大对象的时候无法找到足够的连续内存而不得不提前触发再一次垃圾收集动作。

标记整理算法

标记整理算法的主要分为两个阶段:1、标记 2、整理

标记

该阶段和标记清除算法的第一个阶段是一样的。
image.png

整理

阶段主要是解决标记清除算法出现的内存空间碎片化的问题,在清除垃圾的时候把内存整理好。
image.png

小结

标记整理算法在整理的过程中需要牵扯到对象的内存移动,必然这个效率肯定会比较低。

标记复制算法

标记复制算法简称为复制算法,为了解决标记清除算法面对大量对象需要回收时执行效率低的问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

标记

标记清除算法一样,与 GC ROOT 没有关联的对象就标记为垃圾。
image.png

复制回收

image.png

把不需要回收的对象复制到另一部分空闲的内存中,那么之前的那一块内存就都是垃圾,把之前整块都清空,然后交换位置,下次进行回收的时候进行同样的操作。
image.png

一般不会采用其中一种进行垃圾回收,而是采用分代回收机制: jvm——分代垃圾回收机制 - 掘金 (juejin.cn)

本文转载自: 掘金

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

JAVA HashMap(学习笔记) HashMap

发表于 2021-11-16

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

HashMap

  1. 数据结构

数组 + 链表 + 红黑树(jdk>=1.8)

  1. 重要的变量

1.8版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码    /**
* 默认初始容量,2的4次方 = 16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 最大容量2的30次方, 并且容量必须是2的幂
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认加载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 链表转红黑树的阈值
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 红黑树变回链表的阈值
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
*链表转红黑树时hash表最小容量阈值,达不到优先扩容。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
  1. HashMap PUT

4.1 步骤

  1. 判断当前table也就是数组是否为空,为空则进行初始化,扩容也就是调用resize方法
  2. 根据计算出来的hash值 i = ((n - 1) & hash索引,判断当前table[ i ] 是不是为空,为空直接插入
  3. 接下来就是桶内操作了(一条列表、一颗红黑树或称为桶),定义一个节点e 作为工具人,暂存找到相同hash的节点。
  4. 获取当前数组table[i] 的节点 p,首先判断是否等于putKey的hash,相等将p节点赋予e节点
  5. 首节点hash 与 key 的hash 不相等,然后判断其是不是红黑树节点,是就进去遍历树节点,有就返回给e节点,没就先创建新节点返回传给 e 节点。
  6. 不是树节点,那就只能是链表结构了,遍历链表,寻找节点,也是有就返回给e节点,没就先创建新节点返回传给 e 节点,这里有一个判断,其链表长度是否到了可以转为树的判断,够长了就转红黑树。
  7. 随后,判断e节点工具人是否为空,不为空赋值操作,覆盖掉记录。
  8. 判断容量是否需要扩容。

4.2 hash值的计算

1
2
3
4
5
6
7
java复制代码//计算hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
//不是空值,用它的hasecode ^(异或运算符) 它的高16位
//两个二进制数值如果在同一位上相同,则结果中该位为0,否则为1, 这里不是很理解,只知道是减少hash碰撞
}

4.2 (n - 1) & hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码 对于(n - 1) & hash 的简单理解
&运算符 两个二进制数值如果在同一位上都是1,则结果中该位为1,否则为0

(n - 1) & hash 通过hash获取索引位置 这个与%(求余)的计算是一样的,只不过这个位运算符效率更高,
为什么不用 n & hash 呢
假如 n = 16,那么此时 n 的二进制为
0000 0000 0000 0000 0000 0000 0000 1000
hash的二进制值为 随便写一个
0000 0000 0000 0000 1000 1000 0110 1010
那么&的结果,就只有两种结果,不是0,就是16,key值就只能放到0或者16的位置上了,很快满出。

而,15的话,就提供了多种可能0-15,也防止了数组越界,妙呀。
0000 0000 0000 0000 0000 0000 0000 0111 15的二进制

0000 0000 0000 0000 1000 1000 0110 1010 hash的二进制
hash是不确定的,那么就有多种组合方式了。

4.2 看看put的代码

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
57
58
59
java复制代码//put代码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
//如果table 为空 ,或者table的长度是 0 的话,对他进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/** 对于(n - 1) & hash 的简单理解
* & 两个二进制数值如果在同一位上都是1,则结果中该位为1,否则为0,
* (n - 1) & hash 通过hash获取索引位置 这个与%(求余)的计算是一样的
* 只不过这个位运算符效率更高,
* 为什么不用 n & hash 呢
* 假如 n = 16,那么此时 n 的二进制为
* 0000 0000 0000 0000 0000 0000 0000 1000
* hash的二进制值为 随便写一个
* 0000 0000 0000 0000 1000 1000 0110 1010
* 那么&的结果,不是0,就是16,key值就只能放到0或者16的位置上了,很快满出。
* 而,15的话,就提供了多种可能0-15,也防止了数组越界,妙呀。
* 0000 0000 0000 0000 0000 0000 0000 0111 15的二进制
*/
if ((p = tab[i = (n - 1) & hash]) == null) //当前位置为空的话,就直接赋值了。
tab[i] = newNode(hash, key, value, null);
else { //此时 p 为数组中已存在的Node node
Node<K,V> e; K k; //这个node e节点只是充当一个工具人,暂存找到的节点,后面赋值更改value值
// 这里判断node的值是否与put进来的key相等了?
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //相等就先将这个p节点 赋值给 e 节点咯。
else if (p instanceof TreeNode) //这里判断该p节点是不是树节点。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //上面都不是,就只能往链表里面遍历一次,看看存不存在该key
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //没找到
p.next = newNode(hash, key, value, null); //直接新节点,put进去,尾插法
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); //这里考虑链表需不需要变为红黑树
break;
}
if (e.hash == hash && // 这里表示找到了,终止循环,走下面换value
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; // 下一个
}
}
if (e != null) { // existing mapping for key ,已经存在过的key,将他value从新替换
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //记录修改次数
if (++size > threshold)
resize();//长度不够了,扩容
afterNodeInsertion(evict);
return null;
}
  1. HashMap 扩容

5.1 Jdk7-扩容死锁

jdk7链表采用头插法
而扩容的方式都是大同小异,都是通过新建一个大一点的数组(通常是2的指数幂,也必须是这样),随后将节点按照位置再安放好。

JDK7 HashMap 扩容在多线程场景下,会形成一个链表环的情况,也就意味着这是一个死循环,况且本来HashMap也不支持多线程的场景

JDK7 扩容代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;//第一行
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);//第二行
e.next = newTable[i];//第三行
newTable[i] = e;//第四行
e = next;//第五行
}
}
}
  • 第一行:记录oldhash表中e.next
  • 第二行:rehash计算出数组的位置(hash表中桶的位置)
  • 第三行:e要插入链表的头部,所以要先将e.next指向new hash表中的第一个元素
  • 第四行:将e放入到new hash表的头部
  • 第五行:转移e节点,继续循环下去

5.2 JDK8 扩容

Java8 HashMap扩容跳过了Jdk7扩容的坑,对源码进行了优化, 采用高低位拆分转移方式,避免了链表环的产生

JDK8 扩容代码

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
java复制代码//扩容的代码
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // oldTab 就是旧的数组table
int oldCap = (oldTab == null) ? 0 : oldTab.length; //旧数组长度
int oldThr = threshold; //旧的扩容阈值=长度*扩容因子(0.75)
int newCap, newThr = 0; //新的长度,新的阈值
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) { //MAXIMUM_CAPACITY = 1 << 30; 2的30次方
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && //oldCap << 1 旧长度的2倍。
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold 阈值扩大两倍,他也只能是2的倍数了。
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr; //oldCap = 0,oldThr > 0 时,将阈值设置为table长度。
else { //oldCap = 0,oldThr = 0 一开始,初始化
// zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //新长度的一个空数组
table = newTab;
if (oldTab != null) { // 下面就是搬节点的过程了
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null) // 只有一个节点,直接拿过去,不用考虑会形成链表环
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode) // 树节点有自己的处理方式。了解红黑树再说
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null; // 低位头节点,尾节点
Node<K,V> hiHead = null, hiTail = null; // 高位位头节点,尾节点
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) { //old 旧数组长度
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) { // 区分好高低位 的节点,直接转移到新数组上面
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

—————————————————分界线————————

以及用于并发的ConcurrentHashMap

jdk7 用大hash表 套小hash 表 ,基于ReentranLock实现分段锁
在这里插入图片描述

jdk8 采用 synchronized + cas 保证线程安全
在这里插入图片描述

本文转载自: 掘金

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

分数Rank排名SQL

发表于 2021-11-16

描述

分数表中,如两个分数相同,则两个分数的rank相同,将分数按rank值顺序排列;

建表、插入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码-- 1、建表
CREATE TABLE `test_score` (
`id` varchar(40) NOT NULL COMMENT '主键',
`score` float(10,2) DEFAULT NULL COMMENT '分数',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2、插入数据
INSERT INTO `test_score` (`id`, `score`) VALUES ('111', 82.50);
INSERT INTO `test_score` (`id`, `score`) VALUES ('222', 34.50);
INSERT INTO `test_score` (`id`, `score`) VALUES ('333', 60.00);
INSERT INTO `test_score` (`id`, `score`) VALUES ('444', 60.00);
INSERT INTO `test_score` (`id`, `score`) VALUES ('555', 59.00);
INSERT INTO `test_score` (`id`, `score`) VALUES ('666', 90.00);
INSERT INTO `test_score` (`id`, `score`) VALUES ('777', 90.00);

image.png

功能SQL

  • 方式一
1
2
3
4
5
6
7
8
sql复制代码SELECT
a.id,
a.score AS score,
( SELECT count( DISTINCT b.score ) FROM test_score b WHERE b.score >= a.score) AS Rank
FROM
test_score a
ORDER BY
a.score DESC;
  • 方式二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sql复制代码SELECT
t.id,
t.score,
tb.rank
FROM
`test_score` t,
(
SELECT
ta.score,
( @rowNum := @rowNum + 1 ) AS rank
FROM
( SELECT DISTINCT score FROM `test_score` ORDER BY score DESC ) ta,
( SELECT @rowNum := 0 ) setrownum
) tb
WHERE
t.score = tb.score
ORDER BY
tb.rank ASC,
t.id ASC

处理结果

image.png

说明

  • :=表示赋值,可作用于set、update、select;
  • @标识符表示用户变量字段,为了区分系统变量、字段、用户自定义变量,需要在用户变量前,增加@标识符,否则在上述sql中会被理解为一个字段。

本文转载自: 掘金

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

CAS(compare and swap)算法原理详解

发表于 2021-11-16

CAS简介

CAS 的意思是 compare and swap,比较并交换。cas的引入是为了解决java锁机制带来的性能问题。锁机制存在以下问题:

(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。

(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

解决线程安全问题volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。
独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

CAS原理流程图

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

未命名文件(1).png
比如一个很简单的操作,把变量 A = 2 加 1,结果为 3.

则先读取 A 的当前值 E 为 2,在内存计算结果 V 为 3,比较之前读出来的 A 的当前值 2 和 最新值,如果最新值为 2 ,表示这个值没有被别人改过,则放心的把最终的值更新为 3.

ABA 问题

有一种情况是,在你更新结果之前,其他有个线程在中途把 A 更新成了 5 ,又更新回了 2。但是在当前线程看起来,没有被改过。

JAVA CAS实现原理

JAVA中的CAS是通过调用JNI(JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言)的代码实现的。

以JAVA中的AtomicInteger的compareAndSet为例:

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
java复制代码public class AtomicInteger extends Number implements java.io.Serializable {

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
try {
// 计算变量 value 在类对象中的偏移
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

public final boolean compareAndSet(int expect, int update) {
/*
* compareAndSet 实际上只是一个壳子,主要的逻辑封装在 Unsafe 的
* compareAndSwapInt 方法中
*/
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

// ......
}

public final class Unsafe {
// compareAndSwapInt 是 native 类型的方法,继续往下看
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
// ......
}

compareAndSet实际上只是一个壳子,主要的逻辑封装在Unsafe的compareAndSwapInt方法中
对该方法进行分析:

参数名称 含义
o 需要修改的对象
offset 需要修改的字段到对象头的偏移量(通过偏移量,可以快速定位修改的是哪个字段)
expected 期望值
x 要设置的值

C++源码

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
57
58
59
60
61
62
63
ini复制代码// unsafe.cpp
/*
* 这个看起来好像不像一个函数,不过不用担心,不是重点。UNSAFE_ENTRY 和 UNSAFE_END 都是宏,
* 在预编译期间会被替换成真正的代码。下面的 jboolean、jlong 和 jint 等是一些类型定义(typedef):
*
* jni.h
* typedef unsigned char jboolean;
* typedef unsigned short jchar;
* typedef short jshort;
* typedef float jfloat;
* typedef double jdouble;
*
* jni_md.h
* typedef int jint;
* #ifdef _LP64 // 64-bit
* typedef long jlong;
* #else
* typedef long long jlong;
* #endif
* typedef signed char jbyte;
*/
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
// 根据偏移量,计算 value 的地址。这里的 offset 就是 AtomaicInteger 中的 valueOffset
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
// 调用 Atomic 中的函数 cmpxchg,该函数声明于 Atomic.hpp 中
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

// atomic.cpp
unsigned Atomic::cmpxchg(unsigned int exchange_value,
volatile unsigned int* dest, unsigned int compare_value) {
assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
/*
* 根据操作系统类型调用不同平台下的重载函数,这个在预编译期间编译器会决定调用哪个平台下的重载
* 函数。相关的预编译逻辑如下:
*
* atomic.inline.hpp:
* #include "runtime/atomic.hpp"
*
* // Linux
* #ifdef TARGET_OS_ARCH_linux_x86
* # include "atomic_linux_x86.inline.hpp"
* #endif
*
* // 省略部分代码
*
* // Windows
* #ifdef TARGET_OS_ARCH_windows_x86
* # include "atomic_windows_x86.inline.hpp"
* #endif
*
* // BSD
* #ifdef TARGET_OS_ARCH_bsd_x86
* # include "atomic_bsd_x86.inline.hpp"
* #endif
*
* 接下来分析 atomic_windows_x86.inline.hpp 中的 cmpxchg 函数实现
*/
return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest,
(jint)compare_value);
}

先想办法拿到变量value在内存中的地址。
通过Atomic::cmpxchg实现比较替换,其中参数x是即将更新的值,参数e是原内存的值。

concurrent包的实现

由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:

A线程写volatile变量,随后B线程读这个volatile变量。
A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

首先,声明共享变量为volatile;
然后,使用CAS的原子条件更新来实现线程之间的同步;
同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:

aHR0cDovL2RsLml0ZXllLmNvbS91cGxvYWQvYXR0YWNobWVudC8wMDgzLzI1ODQvYjdiMjQ3MmYtNmI5My0zZjg1LTllNDQtMjlhOWZmNzc0YzhlLnBuZw.png

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

本文转载自: 掘金

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

1…320321322…956

开发者博客

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