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

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


  • 首页

  • 归档

  • 搜索

【golang】 select

发表于 2021-11-22

Go 语言中的 select 也能够让 Goroutine 同时等待多个 Channel 可读或者可写,在多个文件或者 Channel 状态改变之前,select 会一直阻塞当前线程或者 Goroutine。

image.png

select 是与 switch 相似的控制结构,与 switch 不同的是,select 中虽然也有多个 case,但是这些 case 中的表达式必须都是 Channel 的收发操作。

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}

上述控制结构会等待 c <- x 或者 <-quit 两个表达式中任意一个返回。无论哪一个表达式返回都会立刻执行 case 中的代码,当 select 中的两个 case 同时被触发时,会随机执行其中的一个。

现象

非阻塞的收发

通常情况下,select 语句会阻塞当前 Goroutine 并等待多个 Channel 中的一个达到可以收发的状态。但是如果 select 控制结构中包含 default 语句,那么这个 select 语句在执行时会遇到以下两种情况:

  1. 当存在可以收发的 Channel 时,直接处理该 Channel 对应的 case;
  2. 当不存在可以收发的 Channel 时,执行 default 中的语句;
1
2
3
4
5
6
7
8
9
10
go复制代码func main() {
ch := make(chan int)
select {
case i := <-ch:
println(i)

default:
println("default")
}
}
1
2
go复制代码$ go run main.go
default

非阻塞的 Channel 发送和接收操作是很有必要的,在很多场景下不希望 Channel 操作阻塞当前 Goroutine,只是想看看 Channel 的可读或者可写状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码errCh := make(chan error, len(tasks))
wg := sync.WaitGroup{}
wg.Add(len(tasks))
for i := range tasks {
go func() {
defer wg.Done()
if err := tasks[i].Run(); err != nil {
errCh <- err
}
}()
}
wg.Wait()

select {
case err := <-errCh:
return err
default:
return nil
}

随机执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码func main() {
ch := make(chan int)
go func() {
for range time.Tick(1 * time.Second) {
ch <- 0
}
}()

for {
select {
case <-ch:
println("case1")
case <-ch:
println("case2")
}
}
}
1
2
3
4
5
go复制代码$ go run main.go
case1
case2
case1
...

select 在遇到多个 <-ch 同时满足可读或者可写条件时会随机选择一个 case 执行其中的代码。

数据结构

select 在 Go 语言的源代码中不存在对应的结构体,但是可以使用 runtime.scase 结构体表示 select 控制结构中的 case。

1
2
3
4
go复制代码type scase struct {
c *hchan // chan
elem unsafe.Pointer // data element
}

因为非默认的 case 中都与 Channel 的发送和接收有关,所以 runtime.scase 结构体中也包含一个 runtime.hchan 类型的字段存储 case 中使用的 Channel。

实现原理

select 语句在编译期间会被转换成 OSELECT 节点。每个 OSELECT 节点都会持有一组 OCASE 节点,如果 OCASE 的执行条件是空,那就意味着这是一个 default 节点。

image.png

上图展示的就是 select 语句在编译期间的结构,每一个 OCASE 既包含执行条件也包含满足条件后执行的代码。

编译器在中间代码生成期间会根据 select 中 case 的不同对控制语句进行优化,这一过程都发生在 cmd/compile/internal/gc.walkselectcases 函数中,在这里会分四种情况介绍处理的过程和结果:

  1. select 不存在任何的 case;
  2. select 只存在一个 case;
  3. select 存在两个 case,其中一个 case 是 default;
  4. select 存在多个 case;

直接阻塞

当 select 结构中不包含任何 case。

1
2
3
4
5
6
7
8
go复制代码func walkselectcases(cases *Nodes) []*Node {
n := cases.Len()

if n == 0 {
return []*Node{mkcall("block", nil, nil)}
}
...
}

将类似 select {} 的语句转换成调用 runtime.block 函数。

1
2
3
go复制代码func block() {
gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1)
}

runtime.block 的实现非常简单,它会调用 runtime.gopark 让出当前 Goroutine 对处理器的使用权并传入等待原因 waitReasonSelectNoCases。

简单总结一下,空的 select 语句会直接阻塞当前 Goroutine,导致 Goroutine 进入无法被唤醒的永久休眠状态。

单一管道

当 select 条件只包含一个 case,那么编译器会将 select 改写成 if 条件语句。下面对比了改写前后的代码。

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码// 改写前
select {
case v, ok <-ch: // case ch <- v
...
}

// 改写后
if ch == nil {
block()
}
v, ok := <-ch // case ch <- v
...

cmd/compile/internal/gc.walkselectcases 在处理单操作 select 语句时,会根据 Channel 的收发情况生成不同的语句。当 case 中的 Channel 是空指针时,会直接挂起当前 Goroutine 并陷入永久休眠。

非阻塞操作

当 select 中仅包含两个 case,并且其中一个是 default 时,Go 语言的编译器就会认为这是一次非阻塞的收发操作。cmd/compile/internal/gc.walkselectcases 会对这种情况单独处理。不过在正式优化之前,该函数会将 case 中的所有 Channel 都转换成指向 Channel 的地址。

发送

首先是 Channel 的发送过程,当 case 中表达式的类型是 OSEND 时,编译器会使用条件语句和 runtime.selectnbsend 函数改写代码。

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码select {
case ch <- i:
...
default:
...
}

if selectnbsend(ch, i) {
...
} else {
...
}

这段代码中最重要的就是 runtime.selectnbsend,提供了向 Channel 非阻塞地发送数据的能力。向 Channel 发送数据的 runtime.chansend 函数包含一个 block 参数,该参数会决定这一次的发送是不是阻塞的。

1
2
3
go复制代码func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc())
}

由于向 runtime.chansend 函数传入了非阻塞,所以不存在接收方或者缓冲区空间不足时,当前 Goroutine 都不会阻塞而是会直接返回。

接收

由于从 Channel 中接收数据可能会返回一个或者两个值,所以接收数据的情况会比发送稍显复杂,不过改写的套路是差不多的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码// 改写前
select {
case v <- ch: // case v, ok <- ch:
......
default:
......
}

// 改写后
if selectnbrecv(&v, ch) { // if selectnbrecv2(&v, &ok, ch) {
...
} else {
...
}

返回值数量不同会导致使用函数的不同,两个用于非阻塞接收消息的函数 runtime.selectnbrecv 和 runtime.selectnbrecv2 只是对 runtime.chanrecv 返回值的处理稍有不同。

1
2
3
4
5
6
7
8
9
go复制代码func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
selected, _ = chanrecv(c, elem, false)
return
}

func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {
selected, *received = chanrecv(c, elem, false)
return
}

因为接收方不需要,所以 runtime.selectnbrecv 会直接忽略返回的布尔值,而 runtime.selectnbrecv2 会将布尔值回传给调用方。与 runtime.chansend 一样,runtime.chanrecv 也提供了一个 block 参数用于控制这次接收是否阻塞。

常见流程

在默认的情况下,编译器会使用如下的流程处理 select 语句:

  1. 将所有的 case 转换成包含 Channel 以及类型等信息的 runtime.scase 结构体;
  2. 调用运行时函数 runtime.selectgo 从多个准备就绪的 Channel 中选择一个可执行的 runtime.scase 结构体;
  3. 通过 for 循环生成一组 if 语句,在语句中判断自己是不是被选中的 case;
    一个包含三个 case 的正常 select 语句其实会被展开成如下所示的逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码selv := [3]scase{}
order := [6]uint16
for i, cas := range cases {
c := scase{}
c.kind = ...
c.elem = ...
c.c = ...
}
chosen, revcOK := selectgo(selv, order, 3)
if chosen == 0 {
...
break
}
if chosen == 1 {
...
break
}
if chosen == 2 {
...
break
}

展开后的代码片段中最重要的就是用于选择待执行 case 的运行时函数 runtime.selectgo。

  1. 执行一些必要的初始化操作并确定 case 的处理顺序;
  2. 在循环中根据 case 的类型做出不同的处理;

初始化

runtime.selectgo 函数首先会执行必要的初始化操作并决定处理 case 的两个顺序 — 轮询顺序 pollOrder 和加锁顺序 lockOrder。

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
go复制代码func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))

ncases := nsends + nrecvs
scases := cas1[:ncases:ncases]
pollorder := order1[:ncases:ncases]
lockorder := order1[ncases:][:ncases:ncases]

norder := 0
for i := range scases {
cas := &scases[i]
}

for i := 1; i < ncases; i++ {
j := fastrandn(uint32(i + 1))
pollorder[norder] = pollorder[j]
pollorder[j] = uint16(i)
norder++
}
pollorder = pollorder[:norder]
lockorder = lockorder[:norder]

// 根据 Channel 的地址排序确定加锁顺序
...
sellock(scases, lockorder)
...
}

轮询顺序 pollOrder 和加锁顺序 lockOrder 分别是通过以下的方式确认的:

  • 轮询顺序:通过 runtime.fastrandn 函数引入随机性;
  • 加锁顺序:按照 Channel 的地址排序后确定加锁顺序;
    随机的轮询顺序可以避免 Channel 的饥饿问题,保证公平性;而根据 Channel 的地址顺序确定加锁顺序能够避免死锁的发生。这段代码最后调用的 runtime.sellock 会按照之前生成的加锁顺序锁定 select 语句中包含所有的 Channel。

循环

为 select 语句锁定了所有 Channel 之后就会进入 runtime.selectgo 函数的主循环,它会分三个阶段查找或者等待某个 Channel 准备就绪:

  1. 查找是否已经存在准备就绪的 Channel,即可以执行收发操作;
  2. 将当前 Goroutine 加入 Channel 对应的收发队列上并等待其他 Goroutine 的唤醒;
  3. 当前 Goroutine 被唤醒之后找到满足条件的 Channel 并进行处理;
    runtime.selectgo 函数会根据不同情况通过 goto 语句跳转到函数内部的不同标签执行相应的逻辑,其中包括:
  • bufrecv:可以从缓冲区读取数据;
  • bufsend:可以向缓冲区写入数据;
  • recv:可以从休眠的发送方获取数据;
  • send:可以向休眠的接收方发送数据;
  • rclose:可以从关闭的 Channel 读取 EOF;
  • sclose:向关闭的 Channel 发送数据;
  • retc:结束调用并返回;
    循环执行的第一个阶段,查找已经准备就绪的 Channel。循环会遍历所有的 case 并找到需要被唤起的 runtime.sudog 结构,在这个阶段,会根据 case 的四种类型分别处理:
  1. 当 case 不包含 Channel 时;

    • 这种 case 会被跳过;
  2. 当 case 会从 Channel 中接收数据时;

    • 如果当前 Channel 的 sendq 上有等待的 Goroutine,就会跳到 recv 标签并从缓冲区读取数据后将等待 Goroutine 中的数据放入到缓冲区中相同的位置;
    • 如果当前 Channel 的缓冲区不为空,就会跳到 bufrecv 标签处从缓冲区获取数据;
    • 如果当前 Channel 已经被关闭,就会跳到 rclose 做一些清除的收尾工作;
  3. 当 case 会向 Channel 发送数据时;

    • 如果当前 Channel 已经被关,闭就会直接跳到 sclose 标签,触发 panic 尝试中止程序;
    • 如果当前 Channel 的 recvq 上有等待的 Goroutine,就会跳到 send 标签向 Channel 发送数据;
    • 如果当前 Channel 的缓冲区存在空闲位置,就会将待发送的数据存入缓冲区;
  4. 当 select 语句中包含 default 时;

    • 表示前面的所有 case 都没有被执行,这里会解锁所有 Channel 并返回,意味着当前 select 结构中的收发都是非阻塞的;
      image.png

      第一阶段的主要职责是查找所有 case 中是否有可以立刻被处理的 Channel。无论是在等待的 Goroutine 上还是缓冲区中,只要存在数据满足条件就会立刻处理,如果不能立刻找到活跃的 Channel 就会进入循环的下一阶段,按照需要将当前 Goroutine 加入到 Channel 的 sendq 或者 recvq 队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
...
gp = getg()
nextp = &gp.waiting
for _, casei := range lockorder {
casi = int(casei)
cas = &scases[casi]
c = cas.c
sg := acquireSudog()
sg.g = gp
sg.c = c

if casi < nsends {
c.sendq.enqueue(sg)
} else {
c.recvq.enqueue(sg)
}
}

gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)
...
}

除了将当前 Goroutine 对应的 runtime.sudog 结构体加入队列之外,这些结构体都会被串成链表附着在 Goroutine 上。在入队之后会调用 runtime.gopark 挂起当前 Goroutine 等待调度器的唤醒。
image.png

等到 select 中的一些 Channel 准备就绪之后,当前 Goroutine 就会被调度器唤醒。这时会继续执行 runtime.selectgo 函数的第三部分,从 runtime.sudog 中读取数据。

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
go复制代码func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
...
sg = (*sudog)(gp.param)
gp.param = nil

casi = -1
cas = nil
sglist = gp.waiting
for _, casei := range lockorder {
k = &scases[casei]
if sg == sglist {
casi = int(casei)
cas = k
} else {
c = k.c
if int(casei) < nsends {
c.sendq.dequeueSudoG(sglist)
} else {
c.recvq.dequeueSudoG(sglist)
}
}
sgnext = sglist.waitlink
sglist.waitlink = nil
releaseSudog(sglist)
sglist = sgnext
}

c = cas.c
goto retc
...
}

第三次遍历全部 case 时,会先获取当前 Goroutine 接收到的参数 sudog 结构,会依次对比所有 case 对应的 sudog 结构找到被唤醒的 case,获取该 case 对应的索引并返回。

由于当前的 select 结构找到了一个 case 执行,那么剩下 case 中没有被用到的 sudog 就会被忽略并且释放掉。为了不影响 Channel 的正常使用,还是需要将这些废弃的 sudog 从 Channel 中出队。

当在循环中发现缓冲区中有元素或者缓冲区未满时就会通过 goto 关键字跳转到 bufrecv 和 bufsend 两个代码段,这两段代码的执行过程都很简单,它们只是向 Channel 中发送数据或者从缓冲区中获取新数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码bufrecv:
recvOK = true
qp = chanbuf(c, c.recvx)
if cas.elem != nil {
typedmemmove(c.elemtype, cas.elem, qp)
}
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
selunlock(scases, lockorder)
goto retc

bufsend:
typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
selunlock(scases, lockorder)
goto retc

这里在缓冲区进行的操作和直接调用 runtime.chansend 和 runtime.chanrecv 差不多,上述两个过程在执行结束之后都会直接跳到 retc 字段。

两个直接收发 Channel 的情况会调用运行时函数 runtime.send 和 runtime.recv,这两个函数会与处于休眠状态的 Goroutine 打交道。

1
2
3
4
5
6
7
8
go复制代码recv:
recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
recvOK = true
goto retc

send:
send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
goto retc

不过如果向关闭的 Channel 发送数据或者从关闭的 Channel 中接收数据,情况就稍微有一点复杂了:

  • 从一个关闭 Channel 中接收数据会直接清除 Channel 中的相关内容;
  • 向一个关闭的 Channel 发送数据就会直接 panic 造成程序崩溃;
1
2
3
4
5
6
7
8
9
10
11
go复制代码rclose:
selunlock(scases, lockorder)
recvOK = false
if cas.elem != nil {
typedmemclr(c.elemtype, cas.elem)
}
goto retc

sclose:
selunlock(scases, lockorder)
panic(plainError("send on closed channel"))

总体来看,select 语句中的 Channel 收发操作和直接操作 Channel 没有太多出入,只是由于 select 多出了 default 关键字所以会支持非阻塞的收发。

小节

首先在编译期间,Go 语言会对 select 语句进行优化,它会根据 select 中 case 的不同选择不同的优化路径:

  1. 空的 select 语句会被转换成调用 runtime.block 直接挂起当前 Goroutine;
  2. 如果 select 语句中只包含一个 case,编译器会将其转换成 if ch == nil { block }; n; 表达式;
    • 首先判断操作的 Channel 是不是空的;
    • 然后执行 case 结构中的内容;
  3. 如果 select 语句中只包含两个 case 并且其中一个是 default,那么会使用 runtime.selectnbrecv 和 runtime.selectnbsend 非阻塞地执行收发操作;
  4. 在默认情况下会通过 runtime.selectgo 获取执行 case 的索引,并通过多个 if 语句执行对应 case 中的代码;
    在编译器已经对 select 语句进行优化之后,Go 语言会在运行时执行编译期间展开的 runtime.selectgo 函数,该函数会按照以下的流程执行:
  5. 随机生成一个遍历的轮询顺序 pollOrder 并根据 Channel 地址生成锁定顺序 lockOrder;
  6. 根据 pollOrder 遍历所有的 case 查看是否有可以立刻处理的 Channel;
    1. 如果存在,直接获取 case 对应的索引并返回;
    2. 如果不存在,创建 runtime.sudog 结构体,将当前 Goroutine 加入到所有相关 Channel 的收发队列,并调用 runtime.gopark 挂起当前 Goroutine 等待调度器的唤醒;
  7. 当调度器唤醒当前 Goroutine 时,会再次按照 lockOrder 遍历所有的 case,从中查找需要被处理的 runtime.sudog 对应的索引;
    select 关键字是 Go 语言特有的控制结构,它的实现原理比较复杂,需要编译器和运行时函数的通力合作。

参阅

  • Go 语言设计与实现

本文转载自: 掘金

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

打造一个好用的测试容器 背景 需要明确的 Dockerfil

发表于 2021-11-22

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

背景

目前团队中的硬件资源总是挪来挪去,我在上面部署的测试虚机总是会被清理,重新搭建又很麻烦,要安装一堆的测试工具。为了彻底解决这个问题,我计划将打造一个测试容器,在需要的时候直接 docker run 就行了,几秒搞定测试机器。

需要明确的

  1. 测试机容器需要能够上网,本次通过在 host 配置代理,容器内部通过 host 的代理访问公网。
  2. 测试机使用 docker bridge 网络,所以需要 host 能够访问测试环境。
  3. 源镜像已经安装 python3、修改好镜像源(这些操作也可以在 Dockerfile 中做)。

Dockerfile

提前将需要用到的文件(jdk-8u311-linux-x64.tar.gz、Shanghai)拷贝到 Dockerfile 路径下。

Shanghai 的来源:cp /etc/localtime Shanghai

Shanghai 的用处:为了让容器和宿主机时区信息一致。

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
bash复制代码FROM py3:v1
MAINTAINER phyger

ADD Shanghai /etc/localtime

ENV http_proxy=http://172.17.0.1:8888
ENV https_proxy=http://172.17.0.1:8888

RUN yum install openssh-server -y && \
yum provides sshd && \
yum install git -y && \
yum install passwd -y && \
yum install wget -y && \
mkdir /usr/java && cd /usr/java && \
sed -i "s/#PermitRootLogin.*/PermitRootLogin yes/g" /etc/ssh/sshd_config && \
ssh-keygen -t rsa -P "" -f /etc/ssh/ssh_host_rsa_key && \
ssh-keygen -t ecdsa -P "" -f /etc/ssh/ssh_host_ecdsa_key && \
ssh-keygen -t dsa -P "" -f /etc/ssh/ssh_host_dsa_key && \
ssh-keygen -t ed25519 -P "" -f /etc/ssh/ssh_host_ed25519_key && \
echo "root:admin" | chpasswd

ADD jdk-8u311-linux-x64.tar.gz /usr/java/

RUN cd /usr/java/ && cp -pr jdk1.8.0_311 default

ENV JAVA_HOME=/usr/java/jdk1.8.0_311
ENV PATH=$JAVA_HOME/bin:$PATH
ENV CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib

EXPOSE 22

CMD ["-D"]

ENTRYPOINT ["/usr/sbin/sshd"]

为了让我们的容器具备 ssh 功能,我们的 ENTRYPOINT 使用 sshd 启动。同时我们在 Dockerfile 中安装了很多测试容器需要的软件和 JDK、生成了秘钥、打开了 root 用户登录、设置了环境变量。

构建镜像

docker build -t py3:v2 .

镜像构建

构建结果

ssh 登录测试

启动容器:

docker run -d --name py3 py3:v2

修改登录密码:

1
2
3
4
5
6
sql复制代码[root@x86build lifei]# docker exec -it bf40806f8f6e bash
[root@bf40806f8f6e /]# passwd
Changing password for user root.
New password:
Retype new password:
passwd: all authentication tokens updated successfully.

修改完成退出容器!

查看容器地址:

查看容器地址

登录:

1
2
3
4
csharp复制代码[root@x86build lifei]# ssh root@172.17.0.10
root@172.17.0.10's password:
Last login: Wed Nov 10 10:02:15 2021 from 172.17.0.1
[root@bf40806f8f6e /]#

登录成功!

接下来我们就可以将测试机接入 Jenkins 进行测试工作了。

本文转载自: 掘金

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

Spring IOC容器初始化 -- doCreateBea

发表于 2021-11-22

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

1.前言

这个方法的主要作用就是创建bean,不过前提是bean的预创建处理,需要已经完成。
bean的预创建处理,我在前面 Spring IOC容器初始化 – createBean()方法详解 中已经分析过了,大家感兴趣的可以自己看看。

2.doCreateBean() 源码

ps: 因为这个方法比较长,这里我就分段讲吧。

1. 在讲这段之前,建议大家先看看 BeanWrapper详解

图片.png

图片.png

  • 这块代码主要作用就是,给这个bean 创建出对应的 BeanWrapper 对象。
    1. 如果它是单例的,说明这个对象只会被创建一次,所以我们从 factoryBeanInstanceCache (在org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#getTypeForFactoryBean(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, boolean),判断当它是单列的时候调用: org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#getSingletonFactoryBeanForTypeCheck , 这里进行的 put 操作) 中取出时,直接可以移除。
    2. 如果instanceWrapper 为null,则调用 createBeanInstance(beanName, mbd, args)(后面细讲),创建出 BeanWrapper 对象。
    3. 拿到该 BeanWrapper,封装的 bean 实例,和bean的 class 对象。当 beanType 不是 NullBean(空的bean实现)类型时,修改它beanDefinition 中的 resolvedTargetType

2. 这部分是紧接,第一部分之后的 源码如下:

图片.png

  • postProcessed 标记字段,表示是否执行了 MergedBeanDefinitionPostProcessor 中的 postProcessMergedBeanDefinition 方法
  • applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);

图片.png

  • 通过源码我们可以看到,它会获取 所有的的 BeanPostProcessor ,如果该 beanPostProcessor 是 MergedBeanDefinitionPostProcessor 类型的 则会执行它的 postProcessMergedBeanDefinition 方法,执行完后,把 postProcessed 置为true。
  • MergedBeanDefinitionPostProcessor:属性合并的后置的后置处理器,通常在 postProcessMergedBeanDefinition 中,进行注解扫描,属性注入。

3. 急切地缓存单例能够解决循环引用,即使在像Beanfactoryaware这样的生命周期接口触发时。

图片.png

  • 当 创建的bean是单例的 && 允许循环引用 && 当前bean正在创建中,返回true
  • 当返回结果是 true 时,进入if.

3.1 getEarlyBeanReference(beanName, mbd, bean)

图片.png

  • 判断当前的bean 定义是否是合成的 && 返回是否已注册任何InstantiationAwareBeanPostProcessors
  • earlySingletonExposure : 是否需要提前曝光。
  • 取出所有的 BeanPostProcessors 进行遍历,当它是 SmartInstantiationAwareBeanPostProcessor 类型时,执行它的 getEarlyBeanReference 方法 。获取bean的引用对象,并返回。

3.2 default Object getEarlyBeanReference(Object bean, String beanName)(org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#getEarlyBeanReference)

主要作用:获取用于早期访问指定bean的引用,通常用于解析循环引用。
图片.png

  • 生成该 bean 的key

图片.png

  • 如果该beanname 为null 则直接返回 该beanClass
  • 当该 beanName 不为null 判断如果 beanClass 是 FactoryBean 的子类,则在 beanName前面拼上 “&” 前缀,返回beanName。

图片.png

  • 把生成的key 和 bean 放到这个map 中去 。
  • earlyProxyReferences : 记录提前曝光的 bean.
  • 返回生成的代理对象,或它本身。ps:wrapIfNecessary 这个方法后续细讲

3.3 protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory)

主要作用: 如果需要的话,添加给定的单例工厂来构建指定的单例。,用于单例的紧急注册,例如能够解析循环引用。
图片.png

  • 首先判断 一级缓存中有没有,若一级缓存没有
  • 则把它放到三级缓存中去,把它从二级缓存中移除
  • 把它添加到已注册单例的set集合中去,按注册顺序包含bean名称

4. 这部分的主要作用就是初始化 bean 实例

图片.png
ps: 这里面的 populateBean 和 initialzeBean 都是比较重要的,后面的文章中我会详细讲讲

  • populateBean: Spring容器会对bean进行填充,将各个属性值注入,注入相关的依赖Bean
  • initialzeBean: 在这个方法主要是完成bean 的初始化过程。调用 后置处理器 BeanPostProcessor 里面的postProcessBeforeInitialization方法, 调用 initialzingBean,调用实现的 afterPropertiesSet(),调用 init-mothod,调用相应的init方法,调用 后置处理器 BeanPostProcessor 里面的调用实现的postProcessAfterInitialization方法

5. 处理需要提前曝光的对象

图片.png

  • earlySingletonExposure:是否提前曝光,如果需要提前曝光则进入。
  • 调用 getSingleton 获取该beanName 下注册的单实例对象。(ps:getSingleton详解:4.3)。这里只有当检测到会有循环依赖的时候,earlySingletonReference 才会不为null。
  • exposedObject == bean : 则说明 exposedObject在前面的初始化方法中没有被改变。则把 earlySingletonReference 赋给 exposedObject。
  • 否则 当它不需要注入原始bean 且 已为给定名称注册了依赖bean。
    • allowRawInjectionDespiteWrapping:在循环引用的情况下,是否需要注入一个原始bean实例,即使注入的bean最终被封装。
      图片.png
    • hasDependentBean(beanName):是否已为给定名称注册了依赖bean。
      图片.png
    • dependentBeans: 获取依赖于指定bean的所有bean的名称
      图片.png
    • 创建一个长度和dependentBeans相同的 set集合,遍历 dependentBeans ,调用removeSingletonIfCreatedForTypeCheckOnly(dependentBean) : 删除给定bean名的单例实例(如果有的话),但仅当它没有用于类型检查之外的其他目的时。
      图片.png
    • 当存在没有删除的 dependentBean 则把它加入到 set集合中,若集合不为null 抛出异常。

6. 将bean注册为一次性的。

图片.png

  • registerDisposableBeanIfNecessary(beanName, bean, mbd):将给定的bean添加到此工厂的一次性bean列表中,注册其DisposableBean接口和/或给定的 Destroy 方法,在工厂关闭时调用(如果适用)
    图片.png
+ 获取可访问的上下文
+ 判断当它不是原型模式,且需要在关闭时销毁
![图片.png](https://gitee.com/songjianzaina/juejin_p8/raw/master/img/b0958a7a0e9fd30143cbbc553fda8aee1b8fc1ea23c65fbbc2913afc6044a225)
    - 当它是单例模式的时候:注册一个 DisposableBean 实现来执行给定bean的所有销毁工作:DestructionAwareBeanPostProcessors, DisposableBean接口,自定义销毁方法。
    - 当它是其他模式的时候,交由自定义的scope进行销毁逻辑
  • 返回 exposedObject

本文转载自: 掘金

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

一文带你分析基于XMl配置Mybatis初始化源码解析 My

发表于 2021-11-22

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

Mybatis基于XML配置初始化源码解析

Mybatis初始化源码解析

image.png

Mybatis通过Resource.getResourceAsStream()方法加载MybatisXML配置文件转换为字节输入流,然后通过SqlSessionFactoryBuilder()方法中的build()方法进行解析配置文件。

image.png

image.png

Mybatis初始化的时候会将Mybatis的配置信息全部加载到内存中,使用org.apache.ibatis.session.configuation实例来进行维护

  • 首先对Configuation对象进行解答

Configuration对象的结构和xml配置⽂件的对象⼏乎相同。
回顾⼀下xml中的配置标签有哪些:

1
2
3
4
5
6
diff复制代码- properties (属性)
- settings (设置)
- typeAliases (类型别名)
- typeHandlers (类型处理器)
- objectFactory (对象⼯⼚)
- mappers (映射器)

Configuration也有对应的对象属性来封装它们也就是说,初始化配置⽂件信息的本质就是创建Configuration对象,将解析的xml数据封装到Configuration内部属性中。

image.png

image.png

通过上图的ParseConfiguration方法传入Mybatis配置文件的顶层标签,进行解析

方法 作用
propertiesElement(root.evalNode(“properties”)) 解析<properties/>标签
Propertiessettings=settingsAsProperties(root.evalNode(“settings”)); 解析〈settings/>标签
loadCustomVfs(settings) 加载⾃定义的VFS实现类
typeAliasesElement(root.evalNode(“typeAliases”)); 解析<plugins />标签
objectFactoryElement(root.evalNode(“objectFactory”)) 解析<objectFactory />标签
objectWrapperFactoryElement(root.evalNode(“objectWrapperFactory”)) 解析<objectWrapperFactory />标签
reflectorFactoryElement(root.evalNode(“reflectorFactory”)) 解析<reflectorFactory />标签
settingsElement(settings) 赋值<settings />⾄Configuration属性
environmentsElement(root.evalNode(“environments”)) 解析〈environments />标签
databaseldProviderElement(root.evalNode(“databaseldProvider”)) 解析<databaseIdProvider />标签
typeHandlerElement(root.evalNode(“typeHandlers”)) 解析<typeHandlers />标签
mapperElement(root.evalNode(“mappers”)) 解析<mappers />标签

通过ParseConfiguration方法对XML配置文件解析拿到所有的解析后的信息。

  • MappedStatemen

MappedStatement与Mapper配置⽂件中的⼀个select/update/insert/delete节点相对应。mapper中配置的标签都被封装到了此对象中,主要⽤途是描述⼀条SQL语句。

初始化过程

回顾刚开始介绍的加载配置⽂件的过程中,会对mybatis-config.xm l中的各个标签都进⾏解析,其中有mappers标签⽤来引⼊mapper.xml⽂件或者配置mapper接⼝的⽬录。

1
2
xml复制代码<selectid="getUser"resultType="user">
select * from user where id=#{id}</select>

这样的⼀个select标签会在初始化配置⽂件时被解析封装成⼀个MappedStatement对象,然后存储在Configuration对象的mappedStatements属性中,mappedStatements是⼀个HashMap,存储时key=全限定类名+⽅法名,value =对应的MappedStatement对象。

在configuration中对应的属性为

1
java复制代码Map<String, MappedStatement> mappedStatements =new StrictMap<MappedStatement>("Mapped Statements collection")
  • 在XMLConfigBuilder中的处理
1
2
3
4
5
6
7
8
java复制代码private void parseConfiguration(XNode root) { try {
//省略其他标签的处理
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration.
Cause:" + e, e);
}
}

到此对xml配置⽂件的解析就结束了,回到步骤2.中调⽤的重载build⽅法

1
2
3
4
5
java复制代码// 5.调⽤的重载⽅法
public SqlSessionFactory build(Configuration config) {
//创建了 DefaultSqlSessionFactory 对象,传⼊ Configuration 对象。
return new DefaultSqlSessionFactory(config);
}

到此Mybatis基于XMl配置的初始化源码就分析完成,Mybatis初始化就是把XMl文件解析成一块一块的元素。然后把特定的元素传给build方法中,进行SQL语句的执行。

本文转载自: 掘金

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

CentOS 7 上 Nginx 的安装、配置和使用

发表于 2021-11-22

前不久,一个项目开发完成要上线,作为项目主开发的我也要负责一下部署。

项目是前后端分离的微服务架构:Spring Cloud 、Vue+ElementUI。在测试环境是直接 jar 包部署各微服务模块,前端直接是 npm run dev 启动。

部署到正式环境,前端要将 build 之后的项目放在 nginx 中启动,并配置 https,由于我之前也没有配过 nginx,所以踩了很多坑,仅写这篇博客当做笔记。

简介

Nginx是一款开源的、跨平台的高性能web服务器,它有着高性能,稳定性高,配置简单,模块结构化,资源消耗低的优点。同时支持反向代理、负载均衡、缓存的功能。其采用多进程+epoll(IO多路复用)模型,也对互联网高并发连接业务有着很好的支持。

CentOS 安装 Nginx

A. 依赖环境安装

要安装 nginx,要先安装 nginx 的依赖环境:gcc-c++、openssl、pcre、zlib

1. 安装 gcc-c++ 编译器和 openssl

1
2
r复制代码yum install gcc-c++ 
yum install -y openssl openssl-devel

2. 安装 pcre 包

1
复制代码yum install -y pcre pcre-devel

3. 再安装 zlib 包

1
复制代码yum install -y zlib zlib-devel

B. Nginx 安装

依赖环境安装完成后,我们开始安装 nginx

1. 在 /usr/local/ 目录下创建 java 文件夹

当然这个文件夹只是我用来放 nginx 安装包的

1
2
3
bash复制代码cd /usr/local
mkdir java
cd java/

2. 使用 wget 命令直接下载 nginx 安装包,也可以直接上传下载好的压缩包

1
arduino复制代码wget https://nginx.org/download/nginx-1.14.0.tar.gz

3. 解压并进入解压好的目录

1
2
bash复制代码tar -zxvf nginx-1.14.0.tar.gz 
cd nginx-1.14.0/

4. 使用 nginx 默认配置

1
bash复制代码./configure

5. 编译安装

1
2
go复制代码make
make install

如果没有报错,那么你的 /usr/local/目录下会多出一个 nginx/ 目录(默认安装目录)

image.png

进入这个目录看一下(我这里是因为我启动过了,多了一些文件)

image.png

此时,说明 nginx 已经安装成功,可以启动了。

6. 进入 /usr/local/nginx/sbin 目录执行启动命令

1
bash复制代码./nginx

7. 查看是否启动成功

1
perl复制代码ps -ef | grep nginx

image.png

网页上访问自己的 ip 地址端口为 80 ,会出现下图欢迎页,至此,nginx 安装完成。

image.png

C. 安装过程中的错误处理

上面安装过程的 第 5 步可能会出现如下错误

错误一

src/os/unix/ngx_user.c:26:7: error: ‘struct crypt_data’ has no member named ‘current_salt’

image.png
这个错误一般是服务器系统版本高或者 nginx 版本高。

解决办法:

在nginx安装文件夹下输入

1
bash复制代码vim src/os/unix/ngx_user.c

image.png
注释掉红框中的代码然后保存退出重新 make 即可。

错误二

cast between incompatible function types from ‘size_t (*)(ngx_http_script_engine_t )’ {aka ‘long unsigned int ()(struct )’} to ‘void ()(ngx_http_script_engine_t )’ {aka ‘void ()(struct *)’} [-Werror=cast-function-type]

image.png

解决办法:

打开 vim objs/Makefile 把 -Werrori删掉 (-Werror,它要求GCC将所有的警告当成错误进行处理)

配置 nginx

上面启动的 nginx,我们是用默认的配置文件启动的,真实项目使用的时候,我们肯定要去修改 nginx 的默认配置的。

A. 配置解析

使用如下命令查看上文中安装的 nginx 的默认配置文件:

1
bash复制代码vim /usr/local/nginx/nginx-1.14.0/conf/nginx.conf

image.png

这里只截取部分。下面我将列出 nginx 配置文件中的主要配置块的含义:

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
ini复制代码...              #全局块

events { #events块
...
}

http #http块
{
... #http全局块
server #server块
{
... #server全局块
location [PATTERN] #location块
{
...
}
location [PATTERN]
{
...
}
}
server
{
...
}
... #http全局块
}

1、全局块:配置影响nginx全局的指令。一般有运行nginx服务器的用户组,nginx进程pid存放路径,日志存放路径,配置文件引入,允许生成worker process数等。

2、events块:配置影响nginx服务器或与用户的网络连接。有每个进程的最大连接数,选取哪种事件驱动模型处理连接请求,是否允许同时接受多个网路连接,开启多个网络连接序列化等。

3、http块:可以嵌套多个server,配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置。如文件引入,mime-type定义,日志自定义,是否使用sendfile传输文件,连接超时时间,单连接请求数等。

4、server块:配置虚拟主机的相关参数,一个http中可以有多个server。

5、location块:配置请求的路由,以及各种页面的处理情况。

我们一般主要配置的就是 http 块下的server 块。

B. 配置实战

1. 部署 npm build 之后的前端资源

将 build 出来的 dist 文件夹放在 /usr/local/nginx/html/ 路径下(也可以是其他路径,不固定)

然后,修改 nginx.config 配置文件:

1
2
3
4
5
bash复制代码    location / {
root /usr/local/nginx/html/dist;
index index.html index.htm;
try_files $uri $uri/ /index.html; # 配置使用路由
}

这三行的意思是如果第一个存在,直接返回;不存在的话读取第二个,如果存在,读取返回;如果还是不存在,就会fall back到 try_files 的最后一个选项 /index.html,发起一个内部 “子请求”,也就是相当于 nginx 发起一个 HTTP 请求到 http://localhost:80/index.html。
保存修改之后,重新启动 nginx:

1
2
bash复制代码cd /usr/local/nginx/sbin
./nginx -s reload

此时,前端已经部署好了,接下来我们来配置后端各个微服务的部署

2. jar 包部署各个微服务,并配置 nginx

首先,假设 jar 包也部署在这台有 nginx 服务的服务器上,我们需要先启动各个微服务

1
bash复制代码nohup java -jar xxx.jar > xxx.log & tail -f xxx.log     # 这里的命令不再解释

然后开始修改 nginx.config 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码    location ^~ /erowplatform {
proxy_pass https://localhost:8088/erowplatform;
}

location ^~ /gcgk {
proxy_pass https://localhost:8088/gcgk;
}

location ^~ /auth {
proxy_pass https://localhost:8088/auth;
}


location ^~ /code {
proxy_pass https://locahost:8088/code;
}

location ^~ /ytb {
proxy_pass https://localhost:8088/ytb;
}

接下来我们来配置 https。

3. 配置 https

假设,你已经有了要配置 https 所需要的证书,至于证书的获取方式,本文不再叙述。

针对此项目,后端用 jar 包启动,我们需要先在代码中配置 https:

将你的证书放在网关模块下的 resources 下:xxx.pfx

然后修改 bootstrap.yml 文件:

1
2
3
4
5
6
7
java复制代码server:
port: 8080
ssl:
key-store: classpath:xxx.pfx
key-store-password: 123456
key-store-type: PKCS12
enabled: true #开启HTTPS

此时访问后端接口就要使用 https 了。

接下来我们来配置 nginx.conf,

先将 nginx 使用的两个证书文件(xxx.pem、xxx.key)放在某个路径下。

修改 server 块,正确指向证书所在路径:

1
2
3
4
5
6
7
8
9
10
ini复制代码    ssl on;
#ssl证书的pem文件路径
ssl_certificate /etc/nginx/conf.d/3997458__xxx.cn.pem;
#ssl证书的key文件路径
ssl_certificate_key /etc/nginx/conf.d/3997458__xxx.cn.key;

ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;

然后保存修改重启 nginx。

下面贴出完整的 myconfig.conf

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
java复制代码server {
#监听8080端口
listen 8080;
#对应的域名
server_name yyt.xxx.cn;
ssl on;
#ssl证书的pem文件路径
ssl_certificate /etc/nginx/conf.d/3997458__xxx.cn.pem;
#ssl证书的key文件路径
ssl_certificate_key /etc/nginx/conf.d/3997458__xxx.cn.key;

ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;
client_max_body_size 500M;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

location / {
root /etc/nginx/html/dist;
index index.html index.htm;
try_files $uri $uri/ @router; # 配置使用路由
}

location ^~ /erowplatform {
proxy_pass https://localhost:8088/erowplatform;
}

location ^~ /gcgk {
proxy_pass https://localhost:8088/gcgk;
}

location ^~ /auth {
proxy_pass https://localhost:8088/auth;
}


location ^~ /code {
proxy_pass https://localhost:8088/code;
}

location ^~ /ytb {
proxy_pass https://localhost:8088/yyt;
}


# 路由配置信息
location @router {
rewrite ^.*$ /index.html last;
}
}

域名是映射到服务器地址的。

为什么是 myconfig.conf 呐?

是因为我将配置文件单独放了,只需要原来的 nginx.conf 指向 myconfig.conf 即可。

1
2
bash复制代码        include /etc/nginx/conf.d/*.conf;
#include /etc/nginx/sites-enabled/*;

myconfig.conf 放在 /etc/nginx/conf.d/ 下。

至此,配置完成。

补充

https 默认访问的是 443 端口

假如访问 https://yyt.xxx.cn 地址,实际上相当于访问 https://yyt.xxx.cn:443 地址。

所以可以将上面 myconfig.conf 文件中的监听端口改为 443。不加端口号默认访问。

http 默认访问的 80 端口

假如访问 http://yyt.xxx.cn 地址,实际上相当于访问 http://yyt.xxx.cn:80 地址。

由此,我们可以这样配置让访问 http 的用户重定向到 https:

myconfig.conf 中新增一个 server

1
2
3
4
perl复制代码server { 
listen 80; server_name localhost;
rewrite ^(.*) https://$server_name$1 permanent;
}

本文转载自: 掘金

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

Java多线程详解!线程的中断方法interrupt()和终

发表于 2021-11-22

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

线程中断interrupt

  • interrupt() 的作用是中断当前线程
    • 当前线程可以中断本身运行的线程,如果其余线程调用本线程的interrupt() 方法时,会通过checkAccess() 检查权限,会抛出SecurityException异常
    • 如果本线程因为调用线程的wait() 方法 ,wait(long) 或者wait(long, int), 或者是调用了线程的join(), join(long),join(long, int),sleep(long),sleep(long, int) 进入阻塞状态时,调用interrupt() 方法,那么该线程的 [中断状态] 就会被清除并且会收到一个InterruptedException异常
    • 如果线程阻塞在一个Selector选择器中,那么通过interrupt() 中断时,线程的中断标记会被设置为true, 并且会立即从选择操作中返回
  • 通过interrupt() 中断线程时,中断标记会被设置为true, 中断一个 [已经终止的线程] 不会产生任何操作

终止线程

阻塞状态

  • 一般情况下,通过中断interrupt方式终止处于 [阻塞状态] 的线程
    • 当前线程通过调用sleep(),wait(),join() 等方法进入阻塞状态,若此时调用线程的interrupt() 方法可以将线程的中断标记设置为true
    • 由于处于阻塞状态,中断标记会被清除,同时产生一个InterruptedException异常
    • 将InterruptedException异常放在适当的位置就能终止线程
1
2
3
4
5
6
7
8
9
10
java复制代码@Override
public void run() {
try {
while (true) {
// 运行线程
}
} catch (InterruptedException e) {
// 捕获到产生的InterruptedException异常,退出while(true)循环,线程终止
}
}
  • 在while(true) 中不断执行任务,当线程处于阻塞状态时,调用线程的interrupt() 产生InterruptedException异常,中断的异常在while(true) 之外捕获,这样就可以退出while循环,终止异常
  • 对InterruptedException的捕获一般放在while(true) 循环体外,这样就可以在产生异常时退出while(true) 循环.如果对InterruptedException的捕获放置在while(true) 循环体之内,就需要添加额外的退出处理

运行状态

  • 一般情况下,通过 [标记] 终止处于 [运行状态] 的线程
  • 标记包括: 中断标记和额外添加标记

中断标记

1
2
3
4
5
6
java复制代码@Override
public void run() {
while (!isInterrupted) {
// 运行线程
}
}
  • isInterrupted() 方法用于判断线程的中断标记是否为true:
    • 当线程处于运行状态,并且需要终止时,可以调用线程的interrupt() 方法,将线程的中断标记设置为true. 此时isInterrupted() 会返回true, 此时就会退出while循环
  • interrupt() 并不会终止处于 [运行状态] 的线程,只是将线程的中断标记设置为true

额外添加标记

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码private volatile boolean flag = true;

protected void stopTask() {
flag = false;
}

@Override
public void run() {
while (flag) {
// 运行任务
}
}
  • 线程中有一个flag标记,默认值为true
  • 可以通过stopTask() 方法来设置flag标记,当需要终止该方法时,调用该线程的stopTask() 方法就可以让线程退出循环
  • 使用volatile定义flag类型: 保证flag的可见性.即其余线程通过调用stopTask() 方法修改了flag的值之后,本线程可以看到修改后flag的值

综合状态线程

  • 综合状态线程: 处于阻塞状态和运行状态的线程
  • 综合状态线程的通用终止方式:
1
2
3
4
5
6
7
8
9
10
11
java复制代码@Override
public void run() {
try {
// isInterrupted保证只要中断标记为true就终止线程
while (!isInterrupted()) {
// 运行线程
}
} catch (InterruptedException e) {
// InterruptedException保证只要当InterruptedException异常产生时就终止线程
}
}

interrupted()和isInterrupted()异同

  • 相同点: interrupted() 和isInterrupted() 都能够用于检测对象的 [中断标记]
  • 不同点:
    • interrupted() 除了返回 [中断标记] 之外,还会清除 [中断标记], 即将 [中断标记] 设置为false
    • isInterrupted() 仅仅返回 [中断标记]

本文转载自: 掘金

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

面试官超级喜欢问的JVM 前言 总结

发表于 2021-11-22

前言

随着阿巴阿巴在面试中愈战愈勇,这几天又约上面试了,这次面试官让她谈谈对JVM的理解。

回家等通知

面试官: 你对JVM的内存模型了解吗?能否讲讲里面的细节呢?

阿巴阿巴: JAVA虚拟机在执行JAVA程序的过程中,会把所有它管理的内存划分为若干个不同的数据区域,这些区域都有着各自的用途。

根据《JAVA 虚拟机规范SE 7 版》的规定,JAVA虚拟机所管理的内存将包括以下几个运行时的数据区域:堆、方法区、虚拟机栈、本地方法栈、程序计数器。

面试官: 那你能给我大概的介绍下这几区域吗?

阿巴阿巴: 方法区主要存放的是虚拟机加载的类信息、常量、静态变量,堆区域主要是存放对象的,虚拟机栈是用来存放方法运行时产生的栈帧的,本地方法栈则是用来存放本地方法(native)方法运行时产生的栈帧的,程序计数器是用于存放下一条指令所在单元的地址的地方。

面试官: 不错,那你知道什么时候栈内存会发生溢出嘛?

阿巴阿巴: 嗯,,,如果线程的栈深度大于虚拟机运行的最大深度,将抛出StackOverflowError异常,常常出现于递归的方法调用。

面试官: 那还有其他情况会出现栈内存溢出嘛?

阿巴阿巴: 好像没有其他情况了……

面试官: 堆的分代可以细讲一下嘛?

阿巴阿巴: 堆的话主要进行了一个分代,分成新生代、老年代、持久代。

阿巴阿巴: 新生代主要存一些朝生夕死的对象,老年代存的是比较稳定的对象或者是大对象,持久代用于存放用于存放静态文件,如今Java类、方法等。

面试官: 好的、那今天的面试就到这里吧,你先回去等通知哈。😈

阿巴阿巴: 好的。

当场拿offer

阿巴阿巴: 你对JVM的内存模型了解吗?能否讲讲里面的细节呢?

阿巴阿巴: JAVA虚拟机在执行JAVA程序的过程中,会把所有它管理的内存划分为若干个不同的数据区域,这些区域都有着各自的用途。

根据《JAVA 虚拟机规范SE 7 版》的规定,JAVA虚拟机所管理的内存将包括以下几个运行时的数据区域:堆、方法区、虚拟机栈、本地方法栈、程序计数器。

面试官: 那你能给我大概的介绍下这几区域吗?

阿巴阿巴: 方法区主要存放的是虚拟机加载的类信息、常量、静态变量,堆区域主要是存放对象的,虚拟机栈是用来存放方法运行时产生的栈帧的,本地方法栈则是用来存放本地方法(native)方法运行时产生的栈帧的,程序计数器是用于存放下一条指令所在单元的地址的地方。

面试官: 不错,那你知道什么时候栈内存会发生溢出嘛?

阿巴阿巴: 有2种情况,第一种情况:如果线程的栈深度大于虚拟机运行的最大深度,将抛出StackOverflowError异常,常常出现于递归的方法调用。还有一种情况:如果不停的创建线程,这样每个线程都会占用一定的空间,最后导致栈空间不足,报OOM异常。JVM的这几个区域里,堆、方法区、本地方法栈、虚拟机栈都有可能会出现OOM的一个异常。而程序计数器是唯一一个不会产生OOM的区域。

面试官: 可以啊,那再讲讲堆的分布呗!

阿巴阿巴: 堆主要分为新生代、老年代和持久代,这个持久代在JDK1.8版本的时候已经划出了堆内存了,使用元空间来替代,元空间已经不在JVM中,使用的是本地内存。

阿巴阿巴: 新生代主要存一些朝生夕死的对象,老年代存的是比较稳定的对象或者是大对象,持久代用于存放用于存放静态文件,如今Java类、方法等。

阿巴阿巴: 新生代的分区。

阿巴阿巴: 新生代分成Eden区和survivor区,其中survivor区又分为(s0和s1)俩个区域,它们的比例如图所示为8 : 1 : 1。新对象优先会在Eden区进行分配,如果是大对象那就直接进入老年代,当Eden区发生GC时,幸存的对象就会转入Survivor区,当在Survivor区中的对象的年龄达到设定的阈值时(默认是15),达到后就会被转入到老年代区域中。

面试官: 哪些区域是线程共享的,哪些区域是线程私有的呢?

阿巴阿巴: 方法区和堆区是线程共享的,虚拟机栈、本地方法栈、程序计数器这三个区域是线程私有的。

面试官: 既然堆事共享的,那么新生成对象时,堆里面的内存空间就有可能被多线程进行竞争,JVM在这一块是如何保证线程安全的呢?

阿巴阿巴: 这个嘛,JVM采用了2种方式来处理,1使用的是指针碰撞,即CAS进行内存空间的竞争,而且JVM还会维护一个空闲列表,来快速找到空闲的内存。同时JVM还会采用TLAB,Thread Local Allocation Buffer,即线程本地分配缓存区的方式,将Eden区的部分内存让线程私有,当需要创建新对象的时候使用这部分线程私有的空间来分配给对象,这样可以减少竞争带来的额外开销。

面试官: 那对象都是分配到堆里的嘛?

阿巴阿巴: 绝大多数对象都是分配在堆里面的,但是也有特殊情况,可以将对象在栈上分配,如果想要将对象的分配到栈上,那么就需要进行逃逸分析。

阿巴阿巴: 逃逸分析的基本行为就是分析对象的作用域,当一个对象在方法中被定义后,如果这个对象被引用到方法的外部,或者能被其他线程给访问到,那么称这个对象逃逸了。

阿巴阿巴: 如果确定了一个对象不会逃逸出方法之外,我们就可以将这个对象的内存分配到栈上,那么这个对象的内存空间就可以随着栈帧的的生命周期而变化,一起创建一起消亡,这样可以减少堆的负担,提高GC的性能,从而提高程序的运行效率。

阿巴阿巴: 但是逃逸分析默认是不打开的,因为逃逸分析需要对对象的作用域进行大量的计算,从而来判断对象是否会逃逸,而这种计算又会对性能造成影响,所以栈上分配实现起来比较复杂,一般不使用。用户可以使用参数 -XX : +DoEsapeAnalysis来手动开启逃逸分析。

面试官: 你对内空间配担保这块熟悉吗?

面试官: 空间分配担保一般出现在Minor GC的时候,Minor GC后会将符合条件的对象转移到老年代,如果老年代最大可用的连续空间大于新生代的所有对象的所占空间总和,那么这次Minor GC将会被认为是安全的,如果小于,那么JVM则会查看Handle配置HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。

面试官: 有了解过GC算法和垃圾回收器吗?

阿巴阿巴: 有的,GC算法主要分为下面这几类(滴滴滴。。。这时候面试官的电话响了)
面试官: 要不今天先这样,面试通过了,明天过来二面吧~

阿巴阿巴: 好哒。

总结

分享了一波JVM相关的高频面试题,了解JVM的一个模型机内部的一些基本机制,包括对象内存的分配,线程私有和共享相关的问题。

小巴由此开启了JVM相关的面试之旅,于是她回家天天恶补关于JVM的知识。

❤️/ 感谢支持 /

以上便是本次分享的全部内容,希望对你有所帮助^_^

喜欢的话别忘了 分享、点赞、收藏 三连哦~

欢迎关注公众号 程序员巴士,来自字节、虾皮、招银的三端兄弟,分享编程经验、技术干货与职业规划,助你少走弯路进大厂。

本文转载自: 掘金

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

FutureTask源码 - get方法解析

发表于 2021-11-22

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

💡 get方法

get方法是获取结果,如果当前任务仍然在执行中,那么将阻塞到获取结果

当状态为≤COMPLETING,说明线程当前正在运行中没有被取消,那么执行awaitDone方法阻塞等待.

report方法是收尾工作,返回结果.

1
2
3
4
5
6
7
ini复制代码public V get() throws InterruptedException, ExecutionException {
int s = state;
//执行状态是COMPLETING时执行awaitDone将线程加入等待队列中并挂起线程
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}

💡 awaitDone方法

该方法是实现阻塞的关键方法

1.查看入参是否设置了超时时间timed = true.来是否设置超时时间

2.自旋for(;;)

1.先判断当前线程是否中断,抛出异常,移除在等待队列中的等待节点

2.判断当前状态,如果大于COMPLETING,说明任务已经结束.线程置空并返回结果

3.如果还在执行state==COMPLETING,说明*当前任务已经执行结束,但是任务执行线程还没来得及给outcome赋值.*挂起线程,让其他任务执行线程优先执行.

4.等待节点为空,说明任务尚未执行,那么初始化一个等待节点.

5.如果没有入队列!queued,那么放到队列的头结点(由于是else if,因此创建等待节点时不会入队列)

5.如果设置了超时时间,计算当前还有多少时间超时t,如果超时t<0,删除对应节点并返回当前状态.阻塞t时间

6.阻塞等待直到被其他线程唤醒.(当任务线程执行结束,就会唤醒等待线程finishCompletion)

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
ini复制代码private int awaitDone(Boolean timed, long nanos)
throws InterruptedException {
//如果设置了超时时间timed=true,那么deadline就是超时时间,超过就超时了
final long deadline = timed ? System.nanoTime() + nanos : 0L;
// 用作线程的等待节点
WaitNode q = null;
Boolean queued = false;
// 自旋
for (;;) {
//如果当前线程被中断,删除等待队列中的节点,并抛出异常
if (Thread.interrupted()) {
// 移除等待队列中的等待节点
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
//如果执行状态已经完成或者发生异常,直接返回结果
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
//如果执行状态是正在执行,说明任务已经完成.那么现在需要给其他正在执行的任务让路,挂起线程.
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
//第一次进入循环,创建等待节点
else if (q == null)
q = new WaitNode();
//将节点加入到等待队列中,waiters相当于头阶段,不断将头结点更新为新节点
else if (!queued)
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
else if (timed) {
//如果设置了超时时间,在进行下次循环前查看是否已经超时,如果超时删除该节点进行返回
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
//挂起当前节点,阻塞
LockSupport.parkNanos(this, nanos);
} else
LockSupport.park(this);
}
}

触发流程:

1.第一轮for循环,执行逻辑q == null,新建等待节点q,循环结束

2.第二轮for循环,执行!q,入队,循环结束.

3.第三轮for循环,进行阻塞等待或者阻塞特定时间,直到阻塞被其他线程唤醒.

4.唤醒后第四轮for循环,根据前三个条件进入对应的逻辑中

💡 finishCompletion

该方法主要用于唤醒线程.当任务结束或者异常时,会调用该方法

被唤醒的线程就会从awaitDown方法中的LockSupport的park或者parkNanos方法处唤醒,然后继续执行awaitDown方法

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
ini复制代码private void finishCompletion() {
// 遍历等待节点
for (WaitNode q; (q = waiters) != null;) {
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
for (;;) {
Thread t = q.thread;
if (t != null) {
q.thread = null;
// 唤醒等待线程
LockSupport.unpark(t);
}
WaitNode next = q.next;
if (next == null)
break;
q.next = null; // unlink to help gc
q = next;
}
break;
}
}

done();

callable = null; // to reduce footprint
}

本文转载自: 掘金

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

Spring集成Mybatis和Spring事务

发表于 2021-11-22

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

Spring集成Mybatis

集成思路

Spring能集成很多的框架,是Spring的优势,通过集成其他框架试开发更简单方便,集成使用的SPring的IOC功能

要使用Mybatis步骤

要使用mybatis就要创建mybatis框架里的某些对象,使用这些对象就能使用mybatis的功能了,到底需要哪些呢,我们可以把这些对象交给Spring来管理,需要使用在容器里拿就可以了

  • 需要有dao的代理对象
  • 需要SQLSessionFactory,创建sqlSessionFactory对象,才能使用openSession()得到SqlSession对象
  • 数据源DataSource对象

整合mybatis

生成数据库表

1
2
3
4
5
6
sql复制代码CREATE TABLE `user` (
`id` int(20) NOT NULL,
`name` varchar(30) DEFAULT NULL,
`pwd` varchar(30) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

加入依赖xml

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
xml复制代码<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--spring依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.12</version>
</dependency>
<!--spring事务-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.12</version>
</dependency>
<!--springjdbc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.12</version>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<!--Mybatis-spring依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
<!--mysql连接数据库依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<!--druid依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
</dependencies>

实体类

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
typescript复制代码public class User {
private int id; //id
private String name; //姓名
private String pwd; //密码

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getPwd() {
return pwd;
}

public void setPwd(String pwd) {
this.pwd = pwd;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + ''' +
", pwd='" + pwd + ''' +
'}';
}
}

UserMapper.java

1
2
3
4
5
java复制代码@Mapper
public interface UserMapper {
public int InsertUser(User user);
public User finfdById(Integer id);
}

UserMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--绑定mapping接口-->

<mapper namespace="org.study.Mapper.UserMapper">

<insert id="InsertUser" >
INSERT INTO user
( id, name,pwd)
VALUES(#{id}, #{name}, #{pwd});
</insert>
<select id="finfdById" resultType="user">
select * from user where id= #{id}
</select>
</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserMapper userMapper;

@Override
public void insertUser(User user) {
userMapper.InsertUser(user);
}
@Override
public User findById(Integer id) {
User user = userMapper.finfdById(id);
return user;
}
}
1
2
3
4
5
6
7
java复制代码@Service
public interface UserService {

public void insertUser(User user);

public User findById(Integer id);
}

Mybatis配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
<typeAliases>
<package name="org.study.domain"/>
</typeAliases>

<mappers>
<package name="org.study.Mapper"/>
</mappers>
</configuration>

spring配置文件

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
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"
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">
<!--声明数据源-->

<context:annotation-config />
<context:component-scan base-package="org.study"/>
<bean id="MyDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_test?useSSL=false&amp;useUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=UTC"></property>
<property name="name" value="root"></property>
<property name="password" value="root"></property>
</bean>
<!--声明SqlSessionFactoryBean 在这个类的内部创建 SqlSessionFactory-->
<bean id="factory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--指定数据源-->
<property name="dataSource" ref="MyDataSource"></property>
<!--指定mybatis主配置文件-->
<property name="configLocation" value="classpath:mybatsi-config.xml"></property>
</bean>

<!--声明MapperScannerConfigurer-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--指定SqlSessionFactoryBean-->
<property name="sqlSessionFactoryBeanName" value="factory"></property>
<!--知道包名-->
<property name="basePackage" value="org.study.Mapper"></property>
</bean>


</beans>

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码@Test
public void shouldAnswerWithTrue()
{
String config ="application.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
UserService service = (UserServiceImpl) applicationContext.getBean("userServiceImpl");
User user1 = new User();
user1.setId(13);
user1.setName("liming");
user1.setPwd("12345");
service.insertUser(user1);
User user = service.findById(13);
System.out.println(user);

}

输出结果
image.png

Spring事务

概述

事务是一些列SQL语句的集合,是多条SQL要么都成功要么都失败。

什么时候使用事务

一个操作需要多条Sql语句一块完成,操作才能成功

事务在哪里说明

在业务方法上边

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class dao(){
public void insertA();
public void insertB():
}

public void Service(){

@Autowired
private dao dao1;

//事务下在这里
public void insertAB(){
dao1.insertA();
dao1.insertB();
}

}

Spring事务管理器

不同的数据库访问技术,处理事务是不同的,Spring统一管理事务,把不同的数据访问技术的事务处理统一起来,使用Spring的事务管理器,管理不同数据库访问技术处理事务,只需要掌握spring的事务处理一个方案,可以实现使用不同数据库访问技术的事务管理。

spring框架使用事务管理器管理对象,事务管理器的接口是PlatformTransacationManger,定义了事务的操作,主要是commit()rollback()事务管理器有很多实现类,一种数据库访问计数有一个实现类,有实现类具体完成事物的提交,回滚。 jdbc和mmybatis的事务管理器是DataSourceTranactionManager。hibernate事务管理器是hibernateTranactionManager

老师画的一张图,spring管理事务的工作方式,业务代码正常执行局提交了,运行时出现异常就回滚了

image.png
业务代码正常执行局提交了,运行时出现异常就回滚了

异常分类

  • Error 严重错误,回滚事务
  • Exception 异常类,可以处理的异常
  1. 运行时异常 : RuntimeException和它的子类都是运行时异常,在程序执行中抛出的异常,常见的有 NullPointException空指针异常,IndexOutOfBoundsException数组越界异常ClassCastException强制装换异常
  2. 受查异常: 编写java代码的时候,必须出来的异常,如IOException。SQLException

运行方法中只要出现了运行时异常事务回滚,其他情况(正常执行方法,受查异常)提交事务

事务使用的是AOP的环绕通知

环绕通知: 可以在目标方法的前后都机上增强,不需要修改代码,在业务代码前开启事务,在业务代码后提交事务,出现异常Catch回滚事务

事务定义接口 TransactionDefinition

定义了三类常量,定义了有关事务控制的属性

  • 隔离级别
  • 传播行为
  • 事物的超时

隔离级别

隔离级别:控制事物之间的影响程度

这些常量均是以 ISOLATION_开头。即形如 ISOLATION_XXX。

  • DEFAULT:采用 DB 默认的事务隔离级别。MySql 的默认为 REPEATABLE_READ(可重复读);Oracle默认为 READ_COMMITTED。(读已提交)
  • READ_UNCOMMITTED:读未提交。未解决任何并发问题。
  • READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。
  • REPEATABLE_READ:可重复读。解决脏读、不可重复读,存在幻读
  • SERIALIZABLE:串行化。不存在并发问题。

超时时间

以秒为单位 ,默认-1,表示一个业务方法最长的执行时间,到时见没有执行完毕,会回滚事务

传播行为

业务方法在执行时,事务在方法间的传递和使用,可以标志方法有无事务,有七个值PROPAGATION_XXXX开头

  • PROPAGATION_REQUIRED 默认传播行为,方法执行时,如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务,在新事务里执行
  • PROPAGATION_SUPPORTS 支持,如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。
  • PROPAGATION_REQUIRES_NEW 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。

  • PROPAGATION_MANDATORY 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
  • PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。
  • PROPAGATION_NEVER 总是非事务地执行,如果存在一个活动事务,则抛出异常
  • PROPAGATION_NESTED如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行

代码测试–不加事务

模拟买东西订单表添加商品,库存表减库存

准备两张表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码CREATE TABLE `sale` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`gid` int(11) DEFAULT NULL,
`nums` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8

CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL,
`amount` int(11) DEFAULT NULL,
`price` float DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1003 DEFAULT CHARSET=utf8

实体类

1
2
3
4
5
6
7
vbnet复制代码@Data
public class Sale {

private Integer id;
private Integer gid ;
private Integer nums;
}
1
2
3
4
5
6
7
8
9
10
11
vbnet复制代码@Data
public class Goods {

private Integer id;

private String name;

private Integer amount;

private float price;
}

spring配置文件

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
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:tx="http://www.springframework.org/schema/tx"
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/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--声明数据源-->

<context:annotation-config />
<context:component-scan base-package="com.study"/>
<bean id="MyDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_test?useSSL=false&amp;useUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=UTC"></property>
<property name="name" value="root"></property>
<property name="password" value="root"></property>
</bean>
<!--声明SqlSessionFactoryBean 在这个类的内部创建 SqlSessionFactory-->
<bean id="factory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--指定数据源-->
<property name="dataSource" ref="MyDataSource"></property>
<!--指定mybatis主配置文件-->
<property name="configLocation" value="classpath:mybatsi-config.xml"></property>
</bean>

<!--声明MapperScannerConfigurer-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--指定SqlSessionFactoryBean-->
<property name="sqlSessionFactoryBeanName" value="factory"></property>
<!--知道包名-->
<property name="basePackage" value="com.study.mapper"></property>
</bean>

</beans>

mybatis配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
<typeAliases>
<package name="com.study.domain"/>
</typeAliases>

<mappers>
<package name="org.study.Mapper"/>
</mappers>
</configuration>

mapper及xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--绑定mapping接口-->

<mapper namespace="com.study.mapper.saleMapper">

<insert id="buyGoods" >
insert into sale ( gid, nums)
VALUES(#{gid}, #{nums});
</insert>

</mapper>
1
2
3
4
5
java复制代码@Mapper
public interface saleMapper {

public void buyGoods(Sale sale1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--绑定mapping接口-->

<mapper namespace="com.study.mapper.goodsMapper">

<select id="findById" resultType="goods">
select * from goods where id=#{id}
</select>

<update id="updateAmount" >
update goods set amount = amount - #{amount} where id= #{id}
</update>

</mapper>
1
2
3
4
5
6
java复制代码@Mapper
public interface goodsMapper {
public Goods findById(Integer id);

public void updateAmount(Goods good);
}

业务方法

1
2
3
4
5
csharp复制代码public interface BuyService {

void Buy(Integer id,Integer num);

}
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
scss复制代码@Autowired
private goodsMapper goodsMapper1;

@Autowired
private saleMapper saleMapper1;


@Override
public void Buy(Integer id, Integer num) {

System.out.println("开始买了");

Sale sale = new Sale();
sale.setGid(id);
sale.setNums(num);
saleMapper1.buyGoods(sale);

Goods byId = goodsMapper1.findById(sale.getGid());

if (byId == null){
throw new NullPointerException("商品不存在");
} else if(byId.getAmount()<sale.getNums()){
throw new MyException("库存不足");
}

Goods goods = new Goods();
goods.setAmount(sale.getNums());
goods.setId(sale.getGid());
goodsMapper1.updateAmount(goods);


}

测试类

1
2
3
4
5
6
7
8
ini复制代码public void shouldAnswerWithTrue()
{
String config ="application.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
BuyService service = (BuyService) applicationContext.getBean("buyServiceImpl");
service.Buy(1001,1000);

}

以上代码我没加事务的情况下 去购买没有的产品,报错但是第一步订单表的信息还是会保存。

Spring框架使用自己的注解@Transaction控制事务

@Transaction使用注解的属性控制事务,隔离级别,传播行为,超时时间

@Transaction的属性

  • propagation : 事物的传播行为,他使用的Propagation类的枚举值 Propagation.REQUIRED
  • isolation : 隔离级别 使用Isolation枚举类表示隔离级别 默认Isolation.DEFAULT
  • readOnly : Boolean类型的值 标识数据库操作是不是只读,默认false
  • timeout : 事务超时,默认是-1 整数值单位是秒,例如 timeout =20
  • rollBackFor :表示回滚的异常类型
  • rollBackForClassName : 回滚的异常类列表。值时异常类名称,String类型的值
  • noRollackFor :不需要回滚的数据异常,是class类型
  • noRollBackForClassName : 不需要回滚的数据异常,是String类型
    位置
  • 在业务方法的上边,在public方法上边
  • 在类的上边
    特点
  • Spring自己的事务管理
  • 适合中小项目
  • 使用方便

使用注解实现事务

在Spring配置文件里声明事务管理器并开启注解

1
2
3
4
5
6
7
8
9
10
11
xml复制代码    <!--声明事务控制器-->
<bean id="TransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--指定事务操作的数据源-->
<property name="dataSource" ref="MyDataSource"></property>
</bean>

<!--开启事务注解驱动,告诉框架使用注解管理事务
transaction-manager:指定事务管理器的id
proxy-target-class:为true则是基于类的代理将起作用(需要cglib库),为false或者省略这个属性
-->
<tx:annotation-driven transaction-manager="TransactionManager" proxy-target-class="true"></tx:annotation-driven>

在业务代码上添加@Transactional注解并配置属性,直接写@Transactional也可以实现事务,都使用默认值。rollbackFor回滚的异常类型,指定的话先根据指定的找,找不到找是不是RuntimeException的子类都会回滚,那这个属性是不是有点多余,当我们要指定受查异常时就可以使用,还有一个属性noRollackFor,可以指定什么异常不需要回滚

1
2
3
4
5
6
ini复制代码@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
timeout = 20,
readOnly = false,
rollbackFor ={NullPointerException.class,MyException.class}
)

声明式事务

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
xml复制代码<!--声明式事务-->
<!--声明事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="MyDataSource"></property>
</bean>
<!--声明业务方法的事务属性(隔离级别,传播行为,超时)-->
<tx:advice id="Advice" transaction-manager="transactionManager">
<!--给具体的业务方法增加事物的说明-->
<tx:attributes>
<!--
给具体的业务方法添加其他事务属性
name:业务方法名称 配置name的值
1 业务方法的名称
2 带有部分通配符的业务方法的名称
3 使用*
propagation : 指定传播行为的值
isolation :隔离级别
read-only :是否只读
timeout :超时时间
rollback-for :指定回滚的异常类型
-->
<tx:method name="Buy" propagation="REQUIRED" isolation="DEFAULT" read-only="false" timeout="20"/>
<!--有命名规则的业务方法-->
<tx:method name="add*" propagation="REQUIRED"></tx:method>
<tx:method name="update*" propagation="REQUIRED"></tx:method>
<tx:method name="delect*" propagation="REQUIRED"></tx:method>
<!--以上方法以外的* -->
<tx:method name="*" propagation="SUPPORTS" read-only="true"></tx:method>
</tx:attributes>
</tx:advice>

<aop:config>
<!--声明切入点表达式
expression :声明那些类中的方法参与事务
id 名称
-->
<aop:pointcut id="servicePoint" expression="execution(* *..service..*.*(..))"/>
<!--切入点表达式和事务通知-->
<aop:advisor advice-ref="Advice" pointcut-ref="servicePoint"></aop:advisor>
</aop:config>

本文转载自: 掘金

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

Golang 协程与调度器原理

发表于 2021-11-22

思考从容器上该如何设置 GOMAXPROCS 大小引发,这个数字设置多少合理,其到底限制了什么,cpu 核数,系统线程数还是协程数?

背景

Go 语言可以说为并发而生。Go 从语言级别支持并发,通过轻量级协程 Goroutine 来实现程序并发运行,go关键字的强大与简洁是其它语言不可及的,接下来让我们一起来探索 Golang 中 Goroutine 与协程调度器设计的一些原理吧。

Go 协程

概念

进程: 操作系统分配系统资源(cpu 时间片,内存等)的最小单位

线程:轻量级进程,是操作系统调度的最小单位

协程:轻量级线程,协程的调度由程序控制

怎么理解

进程,线程的两个最小单位如何理解?

在早期面向进程设计的计算机结构中,进程就是操作系统分配系统资源与操作系统调度的最小单位

但在现代的计算结构中,进程升级为线程的容器,多个线程共享一个进程内的系统资源,cpu 执行(调度)对象是线程

轻量级进程与轻量级线程如何理解

轻量级进程:如下图各个进程拥有独立的虚拟内存空间,里面的资源包括 栈,代码,数据,堆… 而线程拥有独立的栈,但是共享进程全部的资源。在 linux 的实现中,进程与线程的底层数据结构是一致的,只是同一进程下线程会共享资源。

img

轻量级线程:线程栈大小固定 8 M,协程栈:2 KB ,动态增长。一对线程里对应多个协程,可以减少线程的数量

进程-线程-协程| CLD的博客

协程相对线程有什么优势

  • 轻量级 (MB vs KB)
  • 切换代价低 (调度由程序控制,不需要进入内核空间。需要保存上下文一般少一些)
  • 切换频率低,协程协作式调度,线程调度由操作系统控制,需要保证公平性,当线程数量一多,切换频率相对比较高

协程调度器

在 Golang 中,goroutine 调度是由 Golang 运行时(runtime)负责的,不需要程序员编写代码时关注协程的调度。

GM 模型

goroutine 的调度其实是一个生产者-消费者模型。

生产者:程序起 goroutine(G)

消费者:系统线程(M)去消费(执行)goroutine

自然的,在生产者与消费者中间还需要有一个队列来暂存没有消费过的 goroutine。在 Go 1.1 版本,用的就是这种模型。

典藏版

GM 模型问题

  • M 是并发的,每次访问这个全局队列需要全局锁,锁竞争比较严重
  • 忽略了 G 之间的关系,例如,M1 执行 G1 时,G1 创建了 G2,为了执行 G2 很有可能放到 M2 中执行。而 G1,G2 是相关的,缓存大概率是比较接近的,这样会导致性能下降
  • 每一个 M 都分配一块缓存 MCache,比较浪费

GOMAXPROCS:在这个版本代表了最多同时支持 GOMAXPROCS 个活跃的线程。

GMP 模型

  • G:go 协程
  • M:系统线程
  • P:逻辑处理器,负责提供相关的上下文环境,内存缓存的管理, Goroutine任务队列等

上述的 GM 模型性能问题比较严重,于是如下图所示, Go 将一个全局队列拆成了多个本地队列,这个管理本地队列的结构被称作 P。

GMP

P 结构特点

  • M 通过 P 取 G 时,并发访问大大降低,本地队列不需要全局锁了。
  • 每个 P 的本地 G 队列长度限定在 256,而 goroutine 的数量是不定的,因此 Go 还保留了一个无限长度的全局队列。
  • 本地队列数据结构是数组,全局队列数据结构是链表
  • P 中除了本地队列,还加了一个 runnext 的结构,为了优先执行刚创建的 goroutine
  • MCache 从 M 移到了P
  • 通过设置 GOMAXPROCS 控制 P 的数量

M 的消费逻辑(获取G)

  1. 先从绑定的 P 本地队列(优先 runnext)获取 G
  2. 定期从全局队列中获取 G:每执行 61 次调度会看一下全部队列(保证公平),并且在这个过程会把全局队列中 G 分给各个 P
  3. work stealing,若全局队列没有 G,则随机选择一个 P 偷一半的任务过来,若没有任务可偷,线程休眠。偷任务会导致并发访问本地队列,因此操作本地队列需要加自旋锁

img

G 的生产逻辑

  1. 使用 go func, 产生了一个 G 结构体,会优先选择放在当前 P 的 runnext
  2. 若 runnext 满了,把当前 runnext 里的 goroutine 踢出,放在本地队列尾,再把当前 gorouine 放入 runnext
  3. 若本地队列也满了,把本地队列的一半的 G 与 被踢出 runnext 的 G,放到全局队列中

引入 P 解决的问题

  • 全局锁,P 中的协程是串行的
  • 数据局部性,G 创建时优先在 P 的本地队列,M 获取可用 P 时,优先之前绑定的 P
  • 内存消耗问题,多个线程共用 MCache

回到最开始的思考,容器上的 GOMAXPROCS 设置问题。GOMAXPROCS 代表着 P 的数量,也即代表着运行go 代码的最大线程数,默认为 CPU 的核数,有利于减少线程数量进而减少线程切换,但在容器中,不应该使用物理机的实际核数,应修改为容器限制的核数。

参考

  1. docs.google.com/document/d/…
  2. stackoverflow.com/questions/6…
  3. cloud.tencent.com/developer/a…
  4. yizhi.ren/2019/06/03/…
  5. segmentfault.com/a/119000004…
  6. www.zhihu.com/question/30…
  7. www.bookstack.cn/read/qcrao-…

本文转载自: 掘金

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

1…233234235…956

开发者博客

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