SegmentFault 秦怀杂货店最新的文章
2022-11-21T01:27:56+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
聊聊Go里面的闭包
https://segmentfault.com/a/1190000042854602
2022-11-21T01:27:56+08:00
2022-11-21T01:27:56+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>以前写 Java 的时候,听到前端同学谈论闭包,觉得甚是新奇,后面自己写了一小段时间 JS,虽只学到皮毛,也大概了解到闭包的概念,现在工作常用语言是 Go,很多优雅的代码中总是有闭包的身影,看来不了解个透是不可能的了,本文让我来科普(按照自己水平随便瞎扯)一下:</p><h2>1、什么是闭包?</h2><p>在真正讲述闭包之前,我们先铺垫一点知识点:</p><ul><li>函数式编程</li><li>函数作用域</li><li><p>作用域的继承关系</p><p>## 1.1 前提知识铺垫</p></li></ul><h4>1.2.1 函数式编程</h4><p>函数式编程是一种编程范式,看待问题的一种方式,每一个函数都是为了用小函数组织成为更大的函数,函数的参数也是函数,函数返回的也是函数。我们常见的编程范式有:</p><ul><li><p>命令式编程:</p><ul><li>主要思想为:关注计算机执行的步骤,也就是一步一步告诉计算机先做什么再做什么。</li><li>先把解决问题步骤规范化,抽象为某种算法,然后编写具体的算法去实现,一般只要支持过程化编程范式的语言,我们都可以称为过程化编程语言,比如 BASIC,C 等。</li></ul></li><li><p>声明式编程:</p><ul><li>主要思想为:告诉计算机应该做什么,但是不指定具体要怎么做,比如 SQL,网页编程的 HTML,CSS。</li></ul></li><li><p>函数式编程:</p><ul><li>只关注做什么而不关注怎么做,有一丝丝声明式编程的影子,但是更加侧重于”函数是第一位“的原则,也就是函数可以出现在任何地方,参数、变量、返回值等等。</li></ul></li></ul><p>函数式编程可以认为是面向对象编程的对立面,一般只有一些编程语言会强调一种特定的编程方式,大多数的语言都是多范式语言,可以支持多种不同的编程方式,比如 JavaScript ,Go 等。</p><p>函数式编程是一种思维方式,将电脑运算视为函数的计算,是一种写代码的方法论,<strong>其实我应该聊函数式编程,然后再聊到闭包,因为闭包本身就是函数式编程里面的一个特点之一。</strong></p><blockquote>在函数式编程中,函数是<a href="https://link.segmentfault.com/?enc=bQgZCKVpVjVHfnxgvOuS1A%3D%3D.02PLuklW8SBVr5G2LLFhmHnpxPgpPzlSMBXWL5lpHgctRqjv14wsY0RcRd5mLU89" rel="nofollow">头等对象</a>,意思是说一个函数,既可以作为其它函数的输入参数值,也可以从函数中返回值,被修改或者被分配给一个变量。(维基百科)</blockquote><p>一般纯函数编程语言是不允许直接使用程序状态以及可变对象的,函数式编程本身就是要避免使用 <strong>共享状态</strong>,<strong>可变状态</strong>,尽可能避免产生 <strong>副作用</strong>。</p><p>函数式编程一般具有以下特点:</p><ol><li>函数是第一等公民:函数的地位放在第一位,可以作为参数,可以赋值,可以传递,可以当做返回值。</li><li>没有副作用:函数要保持纯粹独立,不能修改外部变量的值,不修改外部状态。</li><li>引用透明:函数运行不依赖外部变量或者状态,相同的输入参数,任何情况,所得到的返回值都应该是一样的。</li></ol><h4>1.2.2 函数作用域</h4><p><strong>作用域</strong>(scope),程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的<em>作用域</em>。</p><p>通俗易懂的说,函数作用域是指函数可以起作用的范围。函数有点像盒子,一层套一层,作用域我们可以理解为是个封闭的盒子,也就是函数的局部变量,只能在盒子内部使用,成为独立作用域。</p><p><img src="/img/remote/1460000042854604" alt="image-20221112163921104" title="image-20221112163921104"></p><p>函数内的局部变量,出了函数就跳出了作用域,找不到该变量。(里层函数可以使用外层函数的局部变量,因为外层函数的作用域包括了里层函数),比如下面的 <code>innerTmep</code> 出了函数作用域就找不到该变量,但是 <code>outerTemp</code> 在内层函数里面还是可以使用。</p><p><img src="/img/remote/1460000042854605" alt="image-20221112164640101" title="image-20221112164640101"></p><p>不管是任何语言,基本存在一定的内存回收机制,也就是回收用不到的内存空间,回收的机制一般和上面说的函数的作用域是相关的,局部变量出了其作用域,就有可能被回收,如果还被引用着,那么就不会被回收。</p><h4>1.2.3 作用域的继承关系</h4><p>所谓作用域继承,就是前面说的小盒子可以继承外层大盒子的作用域,在小盒子可以直接取出大盒子的东西,但是大盒子不能取出小盒子的东西,除非发生了逃逸(逃逸可以理解为小盒子的东西给出了引用,大盒子拿到就可以使用)。一般而言,变量的作用域有以下两种:</p><ul><li>全局作用域:作用于任何地方</li><li>局部作用域:一般是代码块,函数、包内,<strong>函数内部</strong>声明/定义的变量叫<strong>局部变量</strong>,<strong>作用域仅限于函数内部</strong></li></ul><h3>1.2 闭包的定义</h3><p>“多数情况下我们并不是先理解后定义,而是先定义后理解“,先下定义,<strong>读不懂没关系</strong>:</p><blockquote>闭包(closure)是<strong>一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合</strong>。 换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。 闭包会随着函数的创建而被同时创建。</blockquote><p>一句话表述:</p><p>$$
闭包 = 函数 + 引用环境
$$</p><p>以上定义找不到 Go语言 这几个字眼,聪明的同学肯定知道,闭包是和语言无关的,不是 JavaScript 特有的,也不是 Go 特有的,而是<strong>函数式编程语言</strong>的特有的,是的,你没有看错,<strong>任何支持函数式编程的语言都支持闭包,Go 和 JavaScript 就是其中之二, 目前 Java 目前版本也是支持闭包的</strong>,但是有些人可能认为不是完美的闭包,详细情况文中讨论。</p><h3>1.3 闭包的写法</h3><h4>1.3.1 初看闭包</h4><p>下面是一段闭包的代码:</p><pre><code class="go">import "fmt"
func main() {
sumFunc := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
}
func lazySum(arr []int) func() int {
fmt.Println("先获取函数,不求结果")
var sum = func() int {
fmt.Println("求结果...")
result := 0
for _, v := range arr {
result = result + v
}
return result
}
return sum
}</code></pre><p>输出的结果:</p><pre><code class="shell">先获取函数,不求结果
等待一会
求结果...
结果: 15</code></pre><p>可以看出,里面的 <code>sum()</code> 方法可以引用外部函数 <code>lazySum()</code> 的参数以及局部变量,在<code>lazySum()</code>返回函数 <code>sum()</code> 的时候,相关的参数和变量都保存在返回的函数中,可以之后再进行调用。</p><p>上面的函数或许还可以更进一步,体现出捆绑函数和其周围的状态,我们加上一个次数 <code>count</code>:</p><pre><code class="go">import "fmt"
func main() {
sumFunc := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
fmt.Println("结果:", sumFunc())
fmt.Println("结果:", sumFunc())
}
func lazySum(arr []int) func() int {
fmt.Println("先获取函数,不求结果")
count := 0
var sum = func() int {
count++
fmt.Println("第", count, "次求结果...")
result := 0
for _, v := range arr {
result = result + v
}
return result
}
return sum
}
</code></pre><p>上面代码输出什么呢?次数 <code>count</code> 会不会发生变化,<code>count</code>明显是外层函数的局部变量,但是在内存函数引用(捆绑),内层函数被暴露出去了,执行结果如下:</p><pre><code class="shell">先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
第 2 次求结果...
结果: 15
第 3 次求结果...
结果: 15</code></pre><p>结果是 <code>count</code> 其实每次都会变化,这种情况总结一下:</p><ul><li>函数体内嵌套了另外一个函数,并且返回值是一个函数。</li><li>内层函数被暴露出去,被<strong>外层函数以外</strong>的地方引用着,形成了闭包。</li></ul><p>此时有人可能有疑问了,前面是<code>lazySum()</code>被创建了 1 次,执行了 3 次,但是如果是 3 次执行都是不同的创建,会是怎么样呢?实验一下:</p><pre><code class="go">import "fmt"
func main() {
sumFunc := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
sumFunc1 := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc1())
sumFunc2 := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc2())
}
func lazySum(arr []int) func() int {
fmt.Println("先获取函数,不求结果")
count := 0
var sum = func() int {
count++
fmt.Println("第", count, "次求结果...")
result := 0
for _, v := range arr {
result = result + v
}
return result
}
return sum
}
</code></pre><p>执行的结果如下,每次执行都是第 1 次:</p><pre><code class="shell">先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15</code></pre><p>从以上的执行结果可以看出:</p><p><strong>闭包被创建的时候,引用的外部变量<code>count</code>就已经被创建了 1 份,也就是各自调用是没有关系的</strong>。</p><p>继续抛出一个问题,<strong>如果一个函数返回了两个函数,这是一个闭包还是两个闭包呢?</strong>下面我们实践一下:</p><p>一次返回两个函数,一个用于计算加和的结果,一个计算乘积:</p><pre><code class="go">import "fmt"
func main() {
sumFunc, productSFunc := lazyCalculate([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
fmt.Println("结果:", productSFunc())
}
func lazyCalculate(arr []int) (func() int, func() int) {
fmt.Println("先获取函数,不求结果")
count := 0
var sum = func() int {
count++
fmt.Println("第", count, "次求加和...")
result := 0
for _, v := range arr {
result = result + v
}
return result
}
var product = func() int {
count++
fmt.Println("第", count, "次求乘积...")
result := 0
for _, v := range arr {
result = result * v
}
return result
}
return sum, product
}</code></pre><p>运行结果如下:</p><pre><code class="shell">先获取函数,不求结果
等待一会
第 1 次求加和...
结果: 15
第 2 次求乘积...
结果: 0</code></pre><p>从上面结果可以看出,闭包是函数返回函数的时候,不管多少个返回值(函数),都是一次闭包,如果返回的函数有使用外部函数变量,则会绑定到一起,相互影响:</p><p><img src="/img/remote/1460000042854606" alt="image-20221119001944927" title="image-20221119001944927"></p><p>闭包绑定了周围的状态,我理解此时的函数就拥有了状态,让函数具有了对象所有的能力,函数具有了状态。</p><h4>1.3.2 闭包中的指针和值</h4><p>上面的例子,我们闭包中用到的都是数值,如果我们传递指针,会是怎么样的呢?</p><pre><code class="go">import "fmt"
func main() {
i := 0
testFunc := test(&i)
testFunc()
fmt.Printf("outer i = %d\n", i)
}
func test(i *int) func() {
*i = *i + 1
fmt.Printf("test inner i = %d\n", *i)
return func() {
*i = *i + 1
fmt.Printf("func inner i = %d\n", *i)
}
}</code></pre><p>运行结果如下:</p><pre><code class="shell">test inner i = 1
func inner i = 2
outer i = 2</code></pre><p>可以看出如果是指针的话,闭包里面修改了指针对应的地址的值,也会影响闭包外面的值。这个其实很容易理解,Go 里面没有引用传递,只有值传递,那我们传递指针的时候,也是值传递,这里的值是指针的数值(可以理解为地址值)。</p><p>当我们函数的参数是指针的时候,参数会拷贝一份这个指针地址,当做参数进行传递,因为本质还是地址,所以内部修改的时候,仍然可以对外部产生影响。</p><p>闭包里面的数据其实地址也是一样的,下面的实验可以证明:</p><pre><code class="go">func main() {
i := 0
testFunc := test(&i)
testFunc()
fmt.Printf("outer i address %v\n", &i)
}
func test(i *int) func() {
*i = *i + 1
fmt.Printf("test inner i address %v\n", i)
return func() {
*i = *i + 1
fmt.Printf("func inner i address %v\n", i)
}
}</code></pre><p>输出如下, 因此可以推断出,闭包如果引用外部环境的指针数据,只是会拷贝一份指针地址数据,而不是拷贝一份真正的数据(==先留个问题:拷贝的时机是什么时候呢==):</p><pre><code class="go">test inner i address 0xc0003fab98
func inner i address 0xc0003fab98
outer i address 0xc0003fab98</code></pre><h4>1.3.2 闭包延迟化</h4><p>上面的例子仿佛都在告诉我们,闭包创建的时候,数据就已经拷贝了,但是真的是这样么?</p><p>下面是继续前面的实验:</p><pre><code class="go">func main() {
i := 0
testFunc := test(&i)
i = i + 100
fmt.Printf("outer i before testFunc %d\n", i)
testFunc()
fmt.Printf("outer i after testFunc %d\n", i)
}
func test(i *int) func() {
*i = *i + 1
fmt.Printf("test inner i = %d\n", *i)
return func() {
*i = *i + 1
fmt.Printf("func inner i = %d\n", *i)
}
}</code></pre><p>我们在创建闭包之后,把数据改了,之后执行闭包,答案肯定是真实影响闭包的执行,因为它们都是指针,都是指向同一份数据:</p><pre><code class="shell">test inner i = 1
outer i before testFunc 101
func inner i = 102
outer i after testFunc 102</code></pre><p>假设我们换个写法,让闭包外部环境中的变量在声明闭包函数的之后,进行修改:</p><pre><code class="go">import "fmt"
func main() {
sumFunc := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
}
func lazySum(arr []int) func() int {
fmt.Println("先获取函数,不求结果")
count := 0
var sum = func() int {
fmt.Println("第", count, "次求结果...")
result := 0
for _, v := range arr {
result = result + v
}
return result
}
count = count + 100
return sum
}</code></pre><p>实际执行结果,<code>count</code> 会是修改后的值:</p><pre><code class="shell">等待一会
第 100 次求结果...
结果: 15</code></pre><p>这也证明了,实际上闭包并不会在声明<code>var sum = func() int {...}</code>这句话之后,就将外部环境的 <code>count</code>绑定到闭包中,而是在函数返回闭包函数的时候,才绑定的,这就是<strong>延迟绑定</strong>。</p><p>如果还没看明白没关系,我们再来一个例子:</p><pre><code class="go">func main() {
funcs := testFunc(100)
for _, v := range funcs {
v()
}
}
func testFunc(x int) []func() {
var funcs []func()
values := []int{1, 2, 3}
for _, val := range values {
funcs = append(funcs, func() {
fmt.Printf("testFunc val = %d\n", x+val)
})
}
return funcs
}</code></pre><p>上面的例子,我们闭包返回的是函数数组,本意我们想入每一个 <code>val</code> 都不一样,但是实际上 <code>val</code>都是一个值,==也就是执行到<code>return funcs</code> 的时候(或者真正执行闭包函数的时候)才绑定的 <code>val</code>值==(关于这一点,后面还有个Demo可以证明),此时 <code>val</code>的值是最后一个 <code>3</code>,最终输出结果都是 <code>103</code>:</p><pre><code class="shell">testFunc val = 103
testFunc val = 103
testFunc val = 103</code></pre><p>以上两个例子,都是闭包延迟绑定的问题导致,这也可以说是 feature,到这里可能不少同学还是对闭包绑定外部变量的时机有疑惑,到底是返回闭包函数的时候绑定的呢?还是真正执行闭包函数的时候才绑定的呢?</p><p>下面的例子可以有效的解答:</p><pre><code class="go">import (
"fmt"
"time"
)
func main() {
sumFunc := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
time.Sleep(time.Duration(3) * time.Second)
fmt.Println("结果:", sumFunc())
}
func lazySum(arr []int) func() int {
fmt.Println("先获取函数,不求结果")
count := 0
var sum = func() int {
count++
fmt.Println("第", count, "次求结果...")
result := 0
for _, v := range arr {
result = result + v
}
return result
}
go func() {
time.Sleep(time.Duration(1) * time.Second)
count = count + 100
fmt.Println("go func 修改后的变量 count:", count)
}()
return sum
}</code></pre><p>输出结果如下:</p><pre><code class="shell">先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
go func 修改后的变量 count: 101
第 102 次求结果...
结果: 15</code></pre><p>第二次执行闭包函数的时候,明显 <code>count</code>被里面的 <code>go func()</code>修改了,也就是调用的时候,才真正的获取最新的外部环境,但是在声明的时候,就会把环境预留保存下来。</p><p>其实本质上,<strong>Go Routine的匿名函数的延迟绑定就是闭包的延迟绑定</strong>,上面的例子中,<code>go func(){}</code>获取到的就是最新的值,而不是原始值<code>0</code>。</p><p>总结一下上面的验证点:</p><ul><li>闭包每次返回都是一个新的实例,每个实例都有一份自己的环境。</li><li>同一个实例多次执行,会使用相同的环境。</li><li>闭包如果逃逸的是指针,会相互影响,因为绑定的是指针,相同指针的内容修改会相互影响。</li><li>闭包并不是在声明时绑定的值,声明后只是预留了外部环境(逃逸分析),真正执行闭包函数时,会获取最新的外部环境的值(也称为延迟绑定)。</li><li>Go Routine的匿名函数的延迟绑定本质上就是闭包的延迟绑定。</li></ul><h2>2、闭包的好处与坏处?</h2><h3>2.1 好处</h3><p>纯函数没有状态,而闭包则是让函数轻松拥有了状态。但是凡事都有两面性,一旦拥有状态,多次调用,可能会出现不一样的结果,就像是前面测试的 case 中一样。那么问题来了:</p><p>Q:<strong>如果不支持闭包的话,我们想要函数拥有状态,需要怎么做呢?</strong></p><p>A: 需要使用全局变量,让所有函数共享同一份变量。</p><p>但是我们都知道全局变量有以下的一些特点(在不同的场景,优点会变成缺点):</p><ul><li>常驻于内存之中,只要程序不停会一直在内存中。</li><li>污染全局,大家都可以访问,共享的同时不知道谁会改这个变量。</li></ul><p>闭包可以一定程度优化这个问题:</p><ul><li>不需要使用全局变量,外部函数局部变量在闭包的时候会创建一份,生命周期与函数生命周期一致,闭包函数不再被引用的时候,就可以回收了。</li><li>闭包暴露的局部变量,外界无法直接访问,只能通过函数操作,可以避免滥用。</li></ul><p>除了以上的好处,像在 JavaScript 中,没有原生支持私有方法,可以靠闭包来模拟私有方法,因为闭包都有自己的词法环境。</p><h3>2.2 坏处</h3><p>函数拥有状态,如果处理不当,会导致闭包中的变量被误改,但这是编码者应该考虑的问题,是预期中的场景。</p><p>闭包中如果随意创建,引用被持有,则无法销毁,同时闭包内的局部变量也无法销毁,过度使用闭包会占有更多的内存,导致性能下降。一般而言,能共享一份闭包(共享闭包局部变量数据),不需要多次创建闭包函数,是比较优雅的方式。</p><h2>3、闭包怎么实现的?</h2><p>从上面的实验中,我们可以知道,闭包实际上就是外部环境的逃逸,跟随着闭包函数一起暴露出去。</p><p>我们用以下的程序进行分析:</p><pre><code class="go">import "fmt"
func testFunc(i int) func() int {
i = i * 2
testFunc := func() int {
i++
return i
}
i = i * 2
return testFunc
}
func main() {
test := testFunc(1)
fmt.Println(test())
}</code></pre><p>执行结果如下:</p><pre><code class="shell">5</code></pre><p>先看看逃逸分析,用下面的命令行可以查看:</p><pre><code class="shell"> go build --gcflags=-m main.go</code></pre><p><img src="/img/remote/1460000042854607" alt="image-20221120223253318" title="image-20221120223253318"></p><p>可以看到 变量 <code>i</code>被移到堆中,也就是本来是局部变量,但是发生逃逸之后,从栈里面放到堆里面,同样的 <code>test()</code>函数由于是闭包函数,也逃逸到堆上。</p><p>下面我们用命令行来看看汇编代码:</p><pre><code class="shell">go tool compile -N -l -S main.go</code></pre><p>生成代码比较长,我截取一部分:</p><pre><code class="shell">"".testFunc STEXT size=218 args=0x8 locals=0x38 funcid=0x0 align=0x0
0x0000 00000 (main.go:5) TEXT "".testFunc(SB), ABIInternal, $56-8
0x0000 00000 (main.go:5) CMPQ SP, 16(R14)
0x0004 00004 (main.go:5) PCDATA $0, $-2
0x0004 00004 (main.go:5) JLS 198
0x000a 00010 (main.go:5) PCDATA $0, $-1
0x000a 00010 (main.go:5) SUBQ $56, SP
0x000e 00014 (main.go:5) MOVQ BP, 48(SP)
0x0013 00019 (main.go:5) LEAQ 48(SP), BP
0x0018 00024 (main.go:5) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0018 00024 (main.go:5) FUNCDATA $1, gclocals·d571c0f6cf0af59df28f76498f639cf2(SB)
0x0018 00024 (main.go:5) FUNCDATA $5, "".testFunc.arginfo1(SB)
0x0018 00024 (main.go:5) MOVQ AX, "".i+64(SP)
0x001d 00029 (main.go:5) MOVQ $0, "".~r0+16(SP)
0x0026 00038 (main.go:5) LEAQ type.int(SB), AX
0x002d 00045 (main.go:5) PCDATA $1, $0
0x002d 00045 (main.go:5) CALL runtime.newobject(SB)
0x0032 00050 (main.go:5) MOVQ AX, "".&i+40(SP)
0x0037 00055 (main.go:5) MOVQ "".i+64(SP), CX
0x003c 00060 (main.go:5) MOVQ CX, (AX)
0x003f 00063 (main.go:6) MOVQ "".&i+40(SP), CX
0x0044 00068 (main.go:6) MOVQ "".&i+40(SP), DX
0x0049 00073 (main.go:6) MOVQ (DX), DX
0x004c 00076 (main.go:6) SHLQ $1, DX
0x004f 00079 (main.go:6) MOVQ DX, (CX)
0x0052 00082 (main.go:7) LEAQ type.noalg.struct { F uintptr; "".i *int }(SB), AX
0x0059 00089 (main.go:7) PCDATA $1, $1
0x0059 00089 (main.go:7) CALL runtime.newobject(SB)
0x005e 00094 (main.go:7) MOVQ AX, ""..autotmp_3+32(SP)
0x0063 00099 (main.go:7) LEAQ "".testFunc.func1(SB), CX
0x006a 00106 (main.go:7) MOVQ CX, (AX)
0x006d 00109 (main.go:7) MOVQ ""..autotmp_3+32(SP), CX
0x0072 00114 (main.go:7) TESTB AL, (CX)
0x0074 00116 (main.go:7) MOVQ "".&i+40(SP), DX
0x0079 00121 (main.go:7) LEAQ 8(CX), DI
0x007d 00125 (main.go:7) PCDATA $0, $-2
0x007d 00125 (main.go:7) CMPL runtime.writeBarrier(SB), $0
0x0084 00132 (main.go:7) JEQ 136
0x0086 00134 (main.go:7) JMP 142
0x0088 00136 (main.go:7) MOVQ DX, 8(CX)
0x008c 00140 (main.go:7) JMP 149
0x008e 00142 (main.go:7) CALL runtime.gcWriteBarrierDX(SB)
0x0093 00147 (main.go:7) JMP 149
0x0095 00149 (main.go:7) PCDATA $0, $-1
0x0095 00149 (main.go:7) MOVQ ""..autotmp_3+32(SP), CX
0x009a 00154 (main.go:7) MOVQ CX, "".testFunc+24(SP)
0x009f 00159 (main.go:11) MOVQ "".&i+40(SP), CX
0x00a4 00164 (main.go:11) MOVQ "".&i+40(SP), DX
0x00a9 00169 (main.go:11) MOVQ (DX), DX
0x00ac 00172 (main.go:11) SHLQ $1, DX
0x00af 00175 (main.go:11) MOVQ DX, (CX)
0x00b2 00178 (main.go:12) MOVQ "".testFunc+24(SP), AX
0x00b7 00183 (main.go:12) MOVQ AX, "".~r0+16(SP)
0x00bc 00188 (main.go:12) MOVQ 48(SP), BP
0x00c1 00193 (main.go:12) ADDQ $56, SP
0x00c5 00197 (main.go:12) RET
0x00c6 00198 (main.go:12) NOP
0x00c6 00198 (main.go:5) PCDATA $1, $-1
0x00c6 00198 (main.go:5) PCDATA $0, $-2
0x00c6 00198 (main.go:5) MOVQ AX, 8(SP)
0x00cb 00203 (main.go:5) CALL runtime.morestack_noctxt(SB)
0x00d0 00208 (main.go:5) MOVQ 8(SP), AX
0x00d5 00213 (main.go:5) PCDATA $0, $-1
0x00d5 00213 (main.go:5) JMP 0
</code></pre><p>可以看到闭包函数实际上底层也是用结构体<code>new</code>创建出来的:</p><p><img src="/img/remote/1460000042854608" alt="image-20221120224413412" title="image-20221120224413412"></p><p>使用的就是堆上面的<code> i</code>:</p><p><img src="/img/remote/1460000042854609" alt="image-20221120225532865" title="image-20221120225532865"></p><p>也就是返回函数的时候,实际上返回结构体,结构体里面记录了函数的引用环境。</p><h2>4、浅聊一下</h2><p>## 4.1 Java 支不支持闭包?</p><p>网上有很多种看法,实际上 Java 虽然暂时不支持返回函数作为返参,但是Java 本质上还是实现了闭包的概念的,所使用的的方式是内部类的形式,因为是内部类,所以相当于自带了一个引用环境,算是一种不完整的闭包。</p><p>目前有一定限制,比如是 <code>final </code>声明的,或者是明确定义的值,才可以进行传递:</p><p>Stack Overflow上有相关答案:<a href="https://link.segmentfault.com/?enc=pO7U9WvN1TT%2Bh8sHh7G72Q%3D%3D.UVx1A4SlxIwzKcRlsgyd9si%2B360YhoXGlAezSochUY6PsE6igIi36SxHvXyFWn84VB9YmQteI1qsbZVQOT6hpQ%3D%3D" rel="nofollow">https://stackoverflow.com/que...</a></p><p><img src="/img/remote/1460000042854610" alt="image-20221120233223203" title="image-20221120233223203"></p><h3>4.2 函数式编程的前景怎么样?</h3><p>下面是Wiki的内容:</p><blockquote><p>函数式编程长期以来在学术界流行,但几乎没有工业应用。造成这种局面的主因是函数式编程常被认为严重耗费CPU和存储器资源<a href="https://link.segmentfault.com/?enc=GaXZPcuUENDxSwbmASglDw%3D%3D.NeWKcFkiyVbB6sQhUxEP%2F69gORhb7RR2ZVBcB%2FBsXIVpS04Q%2BKBmLTJGSMipPoWV6IDTvN4UNTNVzg5dpeT6rg%3D%3D" rel="nofollow">[18]</a> ,这是由于在早期实现函数式编程语言时并没有考虑过效率问题,而且面向函数式编程特性,如保证<a href="https://link.segmentfault.com/?enc=%2FZHPR4ICeCHcyNLymEk7Ew%3D%3D.p05j63hmrTdBqs%2FHKZ0UV1G%2Br2V1EfiLWcewOHc8G1bDKV%2B7jnqhQXct6mx1dontdzybUpwKncsMtqWmdSRLATnLgwCdUoiLEg44q%2BjihQFnTBSxNr6pV8O6uFfmCyb3" rel="nofollow">参照透明性</a>等,要求独特的数据结构和算法。<a href="https://link.segmentfault.com/?enc=3RCxSwO2IulC2xC2ewd%2FEw%3D%3D.m0uQj%2FlyOCXoSlG7x7Lq7ebZweHUIIksOt0XYLA%2B5BTph3Sd6i%2BzjcravEyLc034CN8NraoKKVQoTOPNFKvSedz3d3EL7oQ%2FLfcqE5LSItk%3D" rel="nofollow">[19]</a></p><p>然而,最近几种函数式编程语言已经在商业或工业系统中使用<a href="https://link.segmentfault.com/?enc=iRM1cFGsWU%2Bsy4%2FeoSWpsA%3D%3D.kQqA3XmLy%2Bs6t6uoQRfaIYeZhcZY5OfA%2Br3OQiz0EUXfcug51yk2ljO%2B9BDOyYab3W66pMDZMb9TttDtTH%2BXmQ%3D%3D" rel="nofollow">[20]</a>,例如:</p><ul><li><a href="https://link.segmentfault.com/?enc=NQZBdi9AwnFdSg2%2BtGB8iw%3D%3D.m0zuBg6QlWDMGYQf6CPIdsGTFxGIwqyGvykvgXz023J2nJheqkzypwV8eXVRVXZH" rel="nofollow">Erlang</a>,它由瑞典公司<a href="https://link.segmentfault.com/?enc=H8dFU2Ji%2BSb11kgpUA33Sw%3D%3D.8cDFkdeni7jI6jD1AwJ4XhDKd1Zia1FeR6fD1mE7y6b1lZavlhwjwCBzP0Jn1T2y" rel="nofollow">爱立信</a>在20世纪80年代后期开发,最初用于实现容错电信系统。此后,它已在<a href="https://link.segmentfault.com/?enc=g55%2BPhb%2FJsn3ieop6QP3hQ%3D%3D.RLNphxtmWpTkodZovHjsC4FMbgrMakSp%2F31z7Wijh1LBn5nS6x8Z9KZegVtl84C7" rel="nofollow">Nortel</a>、<a href="https://link.segmentfault.com/?enc=OsGTQF%2FOhmI2gh7f%2BRoc8g%3D%3D.XdEY555ljaerxvHh%2FNpVgvEkd5Zqe1X%2BPierLucqAefntn4YD4OghejHtSeimZXV" rel="nofollow">Facebook</a>、<a href="https://link.segmentfault.com/?enc=c20X2dk%2BQOYVUAhbPhnS2w%3D%3D.fYtuvbnP4ot9wnaPkUQ%2Ba3E26grtNa7z8R7ou4J9SrCKXnl6jUJAh7F35DYkRho1QMMUaTWL7dqyYa2io0PeRg%3D%3D" rel="nofollow">Électricité de France</a>和<a href="https://link.segmentfault.com/?enc=uUrt31GPsiTTkg6g9oYLrQ%3D%3D.U7CXQcgOufhwgvLAI5GeIEDcG6C3LGKFa0d%2FwOLUr4AfhngaNDF6o8GVzrlbm4CW" rel="nofollow">WhatsApp</a>等公司作为流行语言创建一系列应用程序。<a href="https://link.segmentfault.com/?enc=4ZTU2YcMR%2BRmMdOWkWPfUQ%3D%3D.kMMKQ2FFDoJu%2BMk2lBGr7SL4Kmr65lkP1WdOJ9iQs3RWrK7rds2fCwxykWSqSADWAtl2kBzpx9R3hBil4%2BqBhQ%3D%3D" rel="nofollow">[21]</a><a href="https://link.segmentfault.com/?enc=tLK44pq5V5b4jCrM434%2ByQ%3D%3D.yag3HvlYw6b5eh9KXdI9yj9DoYkL0NoQYZFFDIPJ%2FlUFmOV2M5wn%2FD2vlUkUOVb3N4CPtK1YCVAICecR6ulZ9H%2FLaxbJpZ2qIOj5S7oGt5k%3D" rel="nofollow">[22]</a></li><li><a href="https://link.segmentfault.com/?enc=dG%2FVnUYmdFtkkV4tpsXNHA%3D%3D.u8%2FkRoOu6VwN%2B2dRN9h3DedVIpTn82ftMtRCxQM%2FxhKrrHTmQX2QQxUpA9Iw7xnb" rel="nofollow">Scheme</a>,它被用作早期<a href="https://link.segmentfault.com/?enc=Kb3bE%2Fpwmv2s3wfmJiP%2FMw%3D%3D.znEE0gwjqAlYfE0w9kwDY23dhrs63Dn79NtUNliAXqd7SJns8R3pz7rTuvd1Q1Q%2F" rel="nofollow">Apple</a> <a href="https://link.segmentfault.com/?enc=GGc7ktiY%2BHEiVS%2B3t4xRoQ%3D%3D.CDnh9090PuGD77iEOgHy9EFmkXTvN4SupZDLBfB7fbl5ysbYW%2FPXjFMzm%2BT8j3tR" rel="nofollow">Macintosh</a>计算机上的几个应用程序的基础,并且最近已应用于诸如训练模拟软件和望远镜控制等方向。</li><li><a href="https://link.segmentfault.com/?enc=zSHvyry139pmZVhYcJnhRA%3D%3D.pRcVuSu20ly0SehIUtqV9AYO2qGd3MS0CZ%2F%2F6QXdJrvMdEAWAECWf3bMkuiMhyqz" rel="nofollow">OCaml</a>,它于20世纪90年代中期推出,已经在金融分析,驱动程序验证,工业机器人编程和嵌入式软件静态分析等领域得到了商业应用。</li><li><a href="https://link.segmentfault.com/?enc=HsTNWt76Meei42W%2BiFWysw%3D%3D.xTbDdhd%2Fh7Zx2bLw7xOI9JKBhO6rh%2Betn1Id8%2BCRL8j0Yf2o4UbIO5n0oMms%2BfDC" rel="nofollow">Haskell</a>,它虽然最初是作为一种研究语言,也已被一系列公司应用于航空航天系统,硬件设计和网络编程等领域。</li></ul><p>其他在工业中使用的函数式编程语言包括多范型的<a href="https://link.segmentfault.com/?enc=Id%2Fj0P%2Bv%2BDhXlYqIer264Q%3D%3D.KdQvNX3SvwHnINBCUfP42lHYLlsEP4iNKpX5PLbKKUCApkDtpdKkHErETTvgt9uc" rel="nofollow">Scala</a><a href="https://link.segmentfault.com/?enc=kZjnS3VGV73DXX4DwSikEw%3D%3D.BVEtHUYgfD4SPQZWgulK%2Btje0%2BGbtT1TLJk5sbVZyzgFIeBaa3F7oUa%2FMyrzoGAcYJUPqvcCeZ6YR5YzMUZ4oA%3D%3D" rel="nofollow">[23]</a>、<a href="https://link.segmentfault.com/?enc=xavDCFjc%2Fsg9yrrMaMn4eQ%3D%3D.l3DpcONHoLtycYYoQcji1K9PqIhu9SkH4vdA8QbktdxcVouzNviF7BXXFKXZAtqe" rel="nofollow">F#</a>,还有<a href="https://link.segmentfault.com/?enc=aI4%2FiqsyLU0vImEeUm6Q6Q%3D%3D.EK%2BGjLW0eZ9H%2BdcDrFHe%2BPF3CoSfy9mLrCkPKtLnu7ZgywHurceNfrHXZKxGOfGH" rel="nofollow">Wolfram语言</a>、<a href="https://link.segmentfault.com/?enc=CXC0tJjD9N5VFoxkFB%2FTXg%3D%3D.AcRklDBPMVWhK8FsvvMB0twWaoigUeszuf9dfbew2vQvJ3SjX43cKPBN%2BFgYd33i" rel="nofollow">Common Lisp</a>、<a href="https://link.segmentfault.com/?enc=VFMO9rk7Udtjwl6VUGoIwA%3D%3D.2alUMdUsJCfa2Vdh5quqc7TLzLdQlmaVI0di5I0Qc0V5cz01OngsCQMKzQ4YaImO" rel="nofollow">Standard ML</a>和<a href="https://link.segmentfault.com/?enc=pVRFNLlF6FTNyn3pLJeKkQ%3D%3D.tBiWnOYfsO30QbkeuguyQuIXDGRn2IyFH6l3uTDLW3L1jhq3Zw37UcOYKTdQ6wz3" rel="nofollow">Clojure</a>等。</p></blockquote><p>从我个人的看法,不看好纯函数编程,但是函数式编程的思想,我相信以后几乎每门高级编程需要都会具备,特别期待 Java 拥抱函数式编程。从我自己了解的语言看,像 Go,JavaScript 中的函数式编程的特性,都让开发者深爱不已(当然,如果写出了bug,就是深恶痛疾)。</p><p>最近突然火了一波的原因,也是因为世界不停的发展,内存也越来越大,这个因素的限制几乎要解放了。</p><p>我相信,世界就是绚丽多彩的,要是一种事物统治世界,绝无可能,更多的是百家争鸣,编程语言或者编程范式也一样,后续可能有集大成者,最终最终历史会筛选出最终符合人类社会发展的。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,个人网站:<a href="https://link.segmentfault.com/?enc=%2FXazPUEaxf1Ktu8LPehaSg%3D%3D.dga8tgryDDl4Nc8Lzvm0MMDCWEGnInIHAp7Kcm4F8JI%3D" rel="nofollow">http://aphysia.cn</a>,技术之路不在一时,山高水长,纵使缓慢,驰而不息。</p>
设计模式【15】--从审批流中学习责任链模式
https://segmentfault.com/a/1190000041411338
2022-02-16T09:01:18+08:00
2022-02-16T09:01:18+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
1
<p><img src="/img/remote/1460000041381639" alt="设计模式" title="设计模式"></p><p>已经来到了责任链模式,各位客官听我瞎扯......</p><h2>责任链模式是什么</h2><blockquote>责任链模式是一种设计模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。(百度百科)</blockquote><p>责任链模式是一种行为型设计模式,也就是重点是处理数据,假设我们有一份数据,需要经过很多个节点处理,那么就会是以下这个样子:</p><p><img src="/img/remote/1460000041411340" alt="" title=""></p><p>一个节点处理完之后,交给下一个节点,不知道大家有没有使用过审批流,当我们提完一个审批单后,你的<code>leader</code>审批,leader审批通过之后就是总监批,总监后面可能是高级总监,或者<code>cto</code>,或者<code>hr</code>。他们在同一个链条上,倘若你的<code>leader</code>没有审批完,后面的节点是不可能收到信息的。如果你的<code>leader</code>拒绝了你的申请,那数据也不会到达后面的审批节点。</p><p>如果你接触过前端,JS 中点击某个 <code>div</code> 的时候会产生冒泡事件,也就是点击下面的<code>A</code>, <code>A</code> 在<code>B</code>里面,<code>B</code>在<code>C</code>里面, <code>A</code>-> <code>B</code> -> <code>C</code> 会依次收到点击事件:</p><p><img src="/img/remote/1460000041411341" alt="" title=""></p><p>再举个例子,在 <code>SpringMVC</code>中,我们有时候会定义一些拦截器,对请求进行预处理,也就是请求过来的时候,会依次经历拦截器,通过拦截器之后才会进入我们的处理业务逻辑代码。</p><p>之前,在做人员管理的时候,有涉及到人员离职情况的处理流程,要交接工作,解除权限,禁用账号等等,这整个处理流程就很适合使用责任链来处理。当然,自动处理流程是会出错的,保存每一个阶段的状态,针对出错的场景,可以手动去从断开责任链的地方接着执行。这整个流程的框架就是应用了责任链,但是根据实际场景也添加了不少其他的东西。</p><h2>两点疑问</h2><ol><li>责任链的每一个节点是不是一定包含下一个节点的引用?</li></ol><p>答:不一定,要么把所有责任节点放在一个<code>list</code>里面,依次处理;要么每个节点包含下一个责任节点的引用,</p><ol start="2"><li>责任链到底是不允许中断还是不允许中断?</li></ol><p>答:两种都可以,不拘泥于细节,可以根据自己的场景使用。</p><h2>责任链模式中的角色</h2><p>责任链一般有以下的角色:</p><ul><li><code>Client</code>(客户端):调用责任链处理器的处理方法,或者在第一个链对象中调用<code>handle</code>方法。</li><li><code>Handler</code>(处理器):抽象类,提供给实际处理器继承然后实现<code>handle</code>方法,处理请求</li><li><code>ConcreteHandler</code>(具体处理器):实现<code>handler</code>的类,同时实现<code>handle</code>方法,负责处理业务逻辑类,不同业务模块有不同的<code>ConcreteHandler</code>。</li><li><code>HandlerChain</code>:负责组合责任链的所有节点以及流程(如果节点包含下一个节点的引用,那么<code>HandlerChain</code>可以不存在)</li></ul><h2>审批链的实现</h2><p>下面我们分别来实现不同的写法,假设现在有一个场景,秦怀入职了一家公司,哼哧哼哧干了一年,但是一直没调薪,又过了一年,总得加薪了吧,不加就要提桶跑路了,于是秦怀大胆去内部系统提了一个申请单:【加薪申请】</p><h3>不中断模式</h3><p>先演示不中断模式,得先弄个申请单的实体,里面包含了申请单的名字和申请人:</p><pre><code class="Java">public class Requisition {
// 名称
public String name;
// 申请人
public String applicant;
public Requisition(String name, String applicant) {
this.name = name;
this.applicant = applicant;
}
}</code></pre><p>责任链中的每个责任节点,也就是处理器,可以抽象成为一个接口:</p><pre><code class="Java">public interface Handler {
// 处理申请单
void process(Requisition requisition);
}</code></pre><p>我们依次实现了三个不同的责任节点,分别代表<code>leader</code>,总监,<code>hr</code>审批:</p><pre><code class="Java">public class ManagerHandler implements Handler {
@Override
public void process(Requisition requisition) {
System.out.println(String.format("Manager 审批来自[%s]的申请单[%s]...", requisition.applicant, requisition.name));
}
}</code></pre><pre><code class="Java">public class DirectorHandler implements Handler{
@Override
public void process(Requisition requisition) {
System.out.println(String.format("Director 审批来自[%s]的申请单[%s]...", requisition.applicant, requisition.name));
}
}</code></pre><pre><code class="Java">public class HrHandler implements Handler{
@Override
public void process(Requisition requisition) {
System.out.println(String.format("Hr 审批来自[%s]的申请单[%s]...", requisition.applicant, requisition.name));
}
}</code></pre><p>责任节点都有了,我们需要用一个责任链把它们组合起来:</p><pre><code class="Java">public class HandlerChain {
List<Handler> handlers = new ArrayList<>();
public void addHandler(Handler handler){
handlers.add(handler);
}
public void handle(Requisition requisition){
for(Handler handler:handlers){
handler.process(requisition);
}
System.out.println(String.format("来自[%s]的申请单[%s]审批完成", requisition.applicant, requisition.name));
}
}</code></pre><p>客户端测试类:</p><pre><code class="Java">public class ClientTest {
public static void main(String[] args) {
HandlerChain handlerChain = new HandlerChain();
handlerChain.addHandler(new ManagerHandler());
handlerChain.addHandler(new DirectorHandler());
handlerChain.addHandler(new HrHandler());
handlerChain.handle(new Requisition("加薪申请","秦怀"));
}
}</code></pre><p>运行结果:</p><pre><code class="txt">Manager 审批来自[秦怀]的申请单[加薪申请]...
Director 审批来自[秦怀]的申请单[加薪申请]...
Hr 审批来自[秦怀]的申请单[加薪申请]...
来自[秦怀]的申请单[加薪申请]审批完成</code></pre><p>从结果上来看,申请单确实经历过了每一个节点,形成了一条链条,这就是责任链的核心思想。每个节点拿到的都是同一个数据,同一个申请单。</p><h3>中断模式</h3><p>秦怀加薪的想法很美好,但是现实很骨感,上面的审批流程一路畅通,但是万一 Hr 想拒绝掉这个申请单了,上面的代码并没有赋予她这种能力,因此,代码得改!(Hr 内心:我就要这个功能,明天上线)。</p><p>既然是支持中断,也就是支持任何一个节点审批不通过就直接返回,不会再走到下一个节点,先给抽象的处理节点方法加上返回值:</p><pre><code class="Java">public interface Handler {
// 处理申请单
boolean process(Requisition requisition);
}</code></pre><p>三个处理节点也同步修改:</p><pre><code class="Java">public class ManagerHandler implements Handler {
@Override
public boolean process(Requisition requisition) {
System.out.println(String.format("Manager 审批通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name));
return true;
}
}</code></pre><pre><code class="Java">public class DirectorHandler implements Handler{
@Override
public boolean process(Requisition requisition) {
System.out.println(String.format("Director 审批通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name));
return true;
}
}</code></pre><pre><code class="Java">public class HrHandler implements Handler{
@Override
public boolean process(Requisition requisition) {
System.out.println(String.format("Hr 审批不通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name));
return false;
}
}</code></pre><p>处理链调整:</p><pre><code class="Java">public class HandlerChain {
List<Handler> handlers = new ArrayList<>();
public void addHandler(Handler handler) {
handlers.add(handler);
}
public void handle(Requisition requisition) {
for (Handler handler : handlers) {
if (!handler.process(requisition)) {
System.out.println(String.format("来自[%s]的申请单[%s]审批不通过", requisition.applicant, requisition.name));
return;
}
}
System.out.println(String.format("来自[%s]的申请单[%s]审批完成", requisition.applicant, requisition.name));
}
}</code></pre><p>修改完成之后的结果:</p><pre><code class="txt">Manager 审批通过来自[秦怀]的申请单[加薪申请]...
Director 审批通过来自[秦怀]的申请单[加薪申请]...
Hr 审批不通过来自[秦怀]的申请单[加薪申请]...
来自[秦怀]的申请单[加薪申请]审批不通过</code></pre><p>秦怀哭了,加薪的审批被 hr 拒绝了。虽然被拒绝了,但是秦怀也感受到了可以中断的责任链模式,这种写法在处理请求的时候也比较常见,因为我们不希望不合法的请求到正常的处理逻辑中。</p><h3>包含下一个节点的引用</h3><p>前面说过,<strong>在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。</strong>上面的写法都是不包含下一个节点引用的写法。下面我们实践一下,如何使用引用写法完成责任链。</p><p>改造<code>Handler</code>接口为抽象类:</p><pre><code class="Java">public abstract class Handler {
private Handler nextHandler;
public void setNextHandler(Handler handler) {
this.nextHandler = handler;
}
// 处理申请单
protected abstract boolean process(Requisition requisition);
// 暴露方法
public boolean handle(Requisition requisition) {
boolean result = process(requisition);
if (result) {
if (nextHandler != null) {
return nextHandler.handle(requisition);
} else {
return true;
}
}
return false;
}
}</code></pre><p>三个实现类不变:</p><pre><code class="Java">public class ManagerHandler extends Handler{
@Override
boolean process(Requisition requisition) {
System.out.println(String.format(
"Manager 审批通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name));
return true;
}
}
public class DirectorHandler extends Handler {
@Override
public boolean process(Requisition requisition) {
System.out.println(String.format(
"Director 审批通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name));
return true;
}
}
public class HrHandler extends Handler{
@Override
public boolean process(Requisition requisition) {
System.out.println(String.format("Hr 审批不通过来自[%s]的申请单[%s]...",
requisition.applicant, requisition.name));
return false;
}
}</code></pre><p>测试方法,构造嵌套引用:</p><pre><code class="Java">public class ClientTest {
public static void main(String[] args) {
HrHandler hrHandler = new HrHandler();
DirectorHandler directorHandler = new DirectorHandler();
directorHandler.setNextHandler(hrHandler);
ManagerHandler managerHandler = new ManagerHandler();
managerHandler.setNextHandler(directorHandler);
managerHandler.handle(new Requisition("加薪申请","秦怀"));
}
}</code></pre><p>可以看到运行结果也是一样:</p><pre><code class="txt">Manager 审批通过来自[秦怀]的申请单[加薪申请]...
Director 审批通过来自[秦怀]的申请单[加薪申请]...
Hr 审批不通过来自[秦怀]的申请单[加薪申请]...</code></pre><h3>拓展一下</h3><p>其实责任链配合上<code>Spring</code>更加好用,主要有两点:</p><p>1、可以使用注入,自动识别该接口的所有实现类。</p><pre><code class="Java">@Autowire
public List<Handler> handlers;</code></pre><p>2、可以使用<code>@Order</code>注解,让接口实现类按照顺序执行。</p><pre><code class="Java">@Order(1)
public class HrHandler extends Handler{
...
}</code></pre><h3>源码中的应用</h3><ul><li><code>Mybatis</code> 中的 <code>Plugin</code> 机制使用了责任链模式,配置各种官方或者自定义的 <code>Plugin</code>,与 <code>Filter</code> 类似,可以在执行 <code>Sql</code> 语句的时候执行一些操作。</li><li><code>Spring</code>中使用责任链模式来管理<code>Adviser</code>。</li></ul><p>比如<code>Mybatis</code>中可以添加若干的插件,比如<code>PageHelper</code>,多个插件对对象的包装采用的动态代理来实现,多层代理。</p><pre><code class="Java">//责任链插件
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
// 生成代理对象
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
//一层一层的拦截器
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}</code></pre><h2>总结</h2><p>责任链模式的优点:</p><ul><li>降低对象直接的耦合度,对象会自动传递到下一个责任节点,不管是引用方式,还是非引用方式。</li><li>增强拓展性,如果需要添加新的责任节点,也比较方便,实现特定的接口即可。</li><li>责任节点的顺序可控,可以指定一个顺序属性,排序即可。</li><li>每个责任节点职责专一,只处理自己的任务,符合类的单一职责原则。</li></ul><p>责任链的缺点:</p><ul><li>如果责任链比较长,性能会受影响。</li><li>责任链可能会中途断掉,请求不一定会被接收。</li></ul><p>责任链一般是在流程化的处理中,多个节点处理同一份数据,依次传递,可能有顺序要求,也可能没有,处理器的能力抽象成接口,方便拓展。</p><p>设计模式系列:</p><ul><li><a href="https://link.segmentfault.com/?enc=QtitGk75DG7UZLGcbqPNdQ%3D%3D.uaSqCNIOPfdokdhm7XyaE3rxWlDRC2cJX3W4yQbeDoUmTscSqRNowSTQEE6c7qTf" rel="nofollow">设计模式【1】-- 单例模式到底几种写法?</a></li><li><a href="https://link.segmentfault.com/?enc=yG%2FuIWs4h08Z8qpSQjYVyg%3D%3D.QxwpOUSA7MqkEoJV8hUd8Ibw5oM0fn0MVT6%2FPDsJ%2BCzoUs4pu3DQD6vlX7fe%2BUO%2B" rel="nofollow">设计模式【1.1】-- 你想如何破坏单例模式?</a></li><li><a href="https://link.segmentfault.com/?enc=xzkT9%2B80WQhf2MNAFTtuNA%3D%3D.f%2Brid1scSJIc%2FjNmw5eAVGK2Pt%2FZdnzxy3EJhYXpGxO6oJ2MQxhdK3ckeaMQYy91" rel="nofollow">设计模式【1.2】-- 枚举式单例有那么好用么?</a></li><li><a href="https://link.segmentfault.com/?enc=tCU4clABfMyVB%2BJLb9tgyQ%3D%3D.oIqCXH03JUenvekfoVIn9ZwCL1sAd%2F0h2WRUIfWIRDEQqJxd3ihn1A6WLG1fTqGy" rel="nofollow">设计模式【1.3】-- 为什么饿汉式单例是线程安全的?</a></li><li><a href="https://link.segmentfault.com/?enc=p%2BhUqjYNzlT57LJ58nyuCg%3D%3D.CIiWmfHCHyfCfVOz2zu3P5LayFVXfXrhSwI%2Fr86dVMgwaGKm%2FAhyBOmGF3f33Nbv" rel="nofollow">设计模式【2】-- 简单工厂模式了解一下?</a></li><li><a href="https://link.segmentfault.com/?enc=xBQuSt0cNA6EGBINkLuKfg%3D%3D.H7%2BzA4v22llln%2BPMFz5VrcFgJp2lXUUmzRN7JW4YsW5JyNc9I6R40NTSq5OT0z2E" rel="nofollow">设计模式【2.1】-- 简单工厂模式怎么演变成工厂方法模式?</a></li><li><a href="https://link.segmentfault.com/?enc=YhqP9KfE4%2FmDt44M13rGyA%3D%3D.bc9srKjow%2BO0GGND%2FiZQ8ul%2FTey2NR064LAbNRZ4%2F0gnuHDd5Mxj25bRDjw4wMrp" rel="nofollow">设计模式【2.2】-- 工厂模式怎么演变成抽象工厂模式?</a></li><li><a href="https://link.segmentfault.com/?enc=q2oBJ77e6s3YwvhOVsg96g%3D%3D.CZORNlCfzjkd9CJ0lvIQ0d6xfhcX3cE5y0male7WrlYNKzTknxvJD7oddavR4u5zu5oIk9xfTn%2B0FLw4GnjBSw%3D%3D" rel="nofollow">设计模式【3.1】-- 浅谈代理模式之静态、动态、cglib代理</a></li><li><a href="https://link.segmentfault.com/?enc=7lVIkZQoTgvCctaoVesauQ%3D%3D.7Pu%2FaJmeCcRnJzg%2B6mVUkUe6hCUTXIR%2BnTF0ssvcO6FGRp0nMdF73gRM%2FuOBTSYHhACcK96Z4HVZErxd2aq%2Bf1KLASsASWDTTAwjauMTQO%2B%2FUsx7Yk1Ycz8cUDvjbHmQ" rel="nofollow">设计模式【3.2】-- JDK动态代理源码分析有多香?</a></li><li><a href="https://link.segmentfault.com/?enc=HIxLDGoHDvRDItLQhwWa8w%3D%3D.qy1to3aQplfRTtyBPBbtvnyQvoXCkpGVlbYvaH%2F7wpI0FDZMid%2FVow%2FkYep21BjlET%2FWOcHSSOqqVS01lKWtRrhTPZrzgV15xgnI3X89kj7kxIGhUO7apeAV4WxRsUAm" rel="nofollow">设计模式【3.3】-- CGLIB动态代理源码解读</a></li><li><a href="https://link.segmentfault.com/?enc=HuSYEl9x6rWC8l0PYg84LA%3D%3D.QlQV%2FH7pbnzSDb2jPsfHIu2NHSjb0ITiEe6ZvE9qRS2zcXXgPcul3lLgZgV7rt%2BlYlGJhoaTfNsIlk0qQkSMkBSBMpEkQj1Z5eGfUh%2FJv5A%3D" rel="nofollow">设计模式【4】-- 建造者模式详解</a></li><li><a href="https://link.segmentfault.com/?enc=6U0cQUz4qUERlHnr64DMMg%3D%3D.3%2F7PP55D3zPRqLo0EEGKH%2FjElpGLMiht%2FyjC1GSpt5bamzkApqidWfIBBtrEkj%2ByYCzeI1z8JGvZbZ1K2ZXGZQ%3D%3D" rel="nofollow">设计模式【5】-- 原型模式</a></li><li><a href="https://link.segmentfault.com/?enc=2LduJ%2FfsejM19fmw63wv6Q%3D%3D.p7xEDE16RDnjzGtwBX6rdqB5ZVefMkClT3n0%2Fh3x4khnhNOWamWu6U5DVSQhfbr75jz8BjkgVuvaOxyUzrtV596d81VhpH17Ycb4HHKNy8s%3D" rel="nofollow">设计模式【6.1】-- 初探适配器模式</a></li><li><a href="https://link.segmentfault.com/?enc=zMDBZYez%2FV%2FgiUGtsSwFnw%3D%3D.ZYRT90ny%2FI9x6fwirjJOxcAw%2Ft3PXKxnamBQ1%2FNPrbhI6ulYJzM%2BGomfOuZuO4Nwf%2F1SVjx9uTZHQEdsnSPQ2sU6xNv90wZZtbAWsxfiI%2F0%3D" rel="nofollow">设计模式【6.2】-- 再聊聊适配器模式</a></li><li><a href="https://link.segmentfault.com/?enc=rYlgIzGBdvMBil60DL1gaw%3D%3D.FbjfRUgpD61oxHLnEQb4elkLIrMGKv3xZJMlPIGGcyHAlvCqaXQay6X3its68VN3tIuKSQL%2Fg6JVkRQEZxPMvMJhThuxzXvIG5%2FjP1OcSKg%3D" rel="nofollow">设计模式【7】-- 探索一下桥接模式</a></li><li><a href="https://link.segmentfault.com/?enc=QnhEReW%2F66w9De1aD4Gn1A%3D%3D.72LLG0CyAJfKfyFvhcM1Oqn%2B1NTN2pBn%2FgCihaoJkFcaM5aADGBrZAEtoCm59e%2FcikpA0X5thFkzbKoTDmtNNb1e%2BkxEP%2F1mOEGTNELcjQ3vQwufZoplIYhXzr6p49k2" rel="nofollow">设计模式【8】-- 手工耿教我写装饰器模式</a></li><li><a href="https://link.segmentfault.com/?enc=xpknuonaKhPmNqI7GG%2FZVQ%3D%3D.LKk9acaKwXtDLeiCARQA%2BCpr5Oo2D4Y0DkBFfGpBy%2FVOaTTuiRbEgtLPJxLNMfEMeSDL6GA0BYQi5E8xd0OL1Kzn0uuKhgHJNgyJKpsV9RF2%2FYGxYqPPFa0knvtUDU7I" rel="nofollow">设计模式【9】-- 外观模式?没那么高大上</a></li><li><a href="https://link.segmentfault.com/?enc=97%2BSlbZNpFZ8dMD4ihe5Pw%3D%3D.YtazvqFJTt1cDDzeKNH%2FoshAUiit0T3lc4hf%2F6RhDMrJUwg9sH0GHHe03XupOoZCnLdMxR%2FuUOs92BelFMF%2BhObVqxDvtO4zTpoKHzkSAnJjZvknQKDbS8b2c9lSkrWL" rel="nofollow">设计模式【10】-- 顺便看看享元模式</a></li><li><a href="https://link.segmentfault.com/?enc=bMnvWtDTmuVvIgCY8xwwbg%3D%3D.dFTpyXjikEmQaJDvq%2FwQsWiAKPLsjiqOV2ju63zGmpQZ%2BnZmClR09WRLFhY3bOb%2FZ52p0ShSFn7Ae%2B0Mq1hRWBJpkzuWkakcpF6ZiLX6zLc%3D" rel="nofollow">设计模式【11】-- 搞定组合模式</a></li><li><a href="https://link.segmentfault.com/?enc=C%2Fdz6s85apueUYz0p8upWA%3D%3D.wwkH10qrgP9ZraaFsu2NOo62a5L%2F08Z9TqwG%2Fm%2BnYyryAOF02ZsXIgXEkVRcBITqIbOQ7I0tkmxOkNBBVYlBbfnjZxOftUnuC3Gha82lunSnn%2BbE3aFKamN2o0CQjETq" rel="nofollow">设计模式【12】-- 搞定最近大火的策略模式</a></li><li><a href="https://link.segmentfault.com/?enc=WT8%2Bvh%2BifqEYJFpPetzdZA%3D%3D.%2FTKhqb2sHwdIt1RxdDE%2FZr6x1esko2gMz6J0EeLuI71ziHk1GzoD5I37N%2BAdEeYMk4rlTTc0O9yOf%2BWsAaea%2BUFj5VRBNas0do1ucDXBNNM%3D" rel="nofollow">设计模式【13】-- 模板模式怎么弄?</a></li><li><a href="https://link.segmentfault.com/?enc=cJcexWf7p6jlwkoHfmbd1Q%3D%3D.NffsRSRPrvNF21xzKpetOU7Csneawv02gv8kTHP3roVjfLSXaU1RaIesQWPff06lt34lYOjv0wfT2fABzjN0OkhDDnfZ7t3XQMQVh1%2BRufaV%2BxNT4eIHHSRPj6wY1LuZBp3WFl6tsG3Qhkyvu%2FVBbQ%3D%3D" rel="nofollow">设计模式【14】-- 从智能音箱中学习命令模式</a></li></ul><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,个人网站:<a href="https://link.segmentfault.com/?enc=Kh37ulu0McNLELv%2FZvT0%2Fg%3D%3D.2pt3JfTw3Cx0eiOpj32S%2ByG0E7gslQ%2FCGm4wAH%2FZx0g%3D" rel="nofollow">http://aphysia.cn</a>,技术之路不在一时,山高水长,纵使缓慢,驰而不息。</p><p><a href="https://link.segmentfault.com/?enc=hsxtnlpynShJyCxUjmXlfw%3D%3D.e2tO6bRdmwe%2Bvul8IbzkQTPgQGk2dP5AQ0ephT%2F5dYTDgHv5rUPiTn3eSxVQJILP" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=CbPJCXNpIMgiy22%2FolhQbw%3D%3D.zO4N1Omt9NdlkB%2BRj6QHnv3LGHPvuE0dAotAF3GjuAleqZWGhi%2FUI%2B8h0ClOovGJ" rel="nofollow">开源编程笔记</a></p>
设计模式【14】-- 从智能音箱中学习命令模式
https://segmentfault.com/a/1190000041388050
2022-02-11T09:00:27+08:00
2022-02-11T09:00:27+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p><img src="/img/remote/1460000041381639" alt="设计模式" title="设计模式"></p><p>开局还是那种图,各位客官往下看...</p><blockquote>张无忌学太极拳,忘记了所有招式,打倒了"玄冥二老",所谓"心中无招"。设计模式可谓招数,如果先学通了各种模式,又忘掉了所有模式而随心所欲,可谓OO之最高境界。</blockquote><h2>命令模式是什么?</h2><blockquote>在面向对象程式设计的范畴中,命令模式(Command Pattern)是一种设计模式,它尝试以物件来代表实际行动。</blockquote><p>命令模式是一种行为型模式,它将请求以命令的形式包裹在对象里面,传递给调用对象,调用对象寻找匹配该命令的对象,将命令给该对象执行。也就是分为了三步:</p><ul><li>1、命令被包裹在请求对象里,传递给调用对象。</li><li>2、调用对象查找匹配该命令(可以处理该命令)的对象,将该命令传递给匹配的对象。</li><li>3、该对象执行传递给它的命令。</li></ul><p>一般而言,在软件开发中,行为的请求者和行为的执行者是紧密耦合在一起的,调用关系简单易懂,但是这样不容易拓展,有些时候,我们需要记录、撤销、重做处理的时候,不易修改。因此我们需要将命令抽象化,封装起来,不直接调用真正的执行者方法,易于拓展。</p><p>举个现实中的例子,比如我们去餐厅吃饭:</p><p><code>点餐(写好菜单,发起请求) --> 订单系统处理,生成订单(创建命令) --> 厨师获取到订单,开始做菜(执行命令)</code>,在这个过程中我们并没有直接与厨师交谈,不知道那个厨师做,厨师也不知道是哪个顾客需要,只需要按照订单处理就可以。</p><p>又比如,我们经常使用智能音响,我经常叫它 ”小度小度,帮我打开空调“,”小度小度,帮我打开窗帘“等等,在整个过程中,<code>我发出命令 --> 小度接受到命令,包装成为请求 --> 让真正接收命令的对象处理(空调或者窗帘控制器)</code>,我没有手动去操作空调和窗帘,小度也可以接受各种各样的命令,只要接入它,我都通过它去操作。</p><h2>命令模式中的角色</h2><p>在命令模式里面,一共存在以下的角色:</p><ul><li><code>Command</code>(抽象命令):命令有通用的特性,将命令抽象成一个类,不同的命令做不同的实现</li><li><code>ConcreteCommand</code>(具体命令类):实现抽象命令,做具体的实现</li><li><code>Receiver</code>(接受者):真正执行命令的对象</li><li><code>Invoker</code>(调用者/请求者):请求的封装发送者,它通过命令对象来执行请求,不会直接操作接受者,而是直接关联命令对象,间接调用到接受者的相关操作。</li><li><code>Client</code>(客户端):一般我们在客户端中创建调用者对象,具体的命令类,去执行命令。</li></ul><p>命令模式的<code>UML</code>图如下:</p><p><img src="/img/remote/1460000041388052" alt="" title=""></p><h2>命令模式的实现</h2><p>下面我们模拟一下智能音响的场景:</p><p>先创建一个空调对象,也就是<code>Receiver</code>(接受者),它才是真正的操作对象:</p><pre><code class="Java">public class AirConditionerReceiver {
public void turnOn(){
System.out.println("打开空调...");
}
public void turnOff(){
System.out.println("关闭空调...");
}
}</code></pre><p>抽象命令类如下:</p><pre><code class="Java">public interface Command {
void execute();
}</code></pre><p>打开<code>TurnOnCommand</code>以及关闭<code>TurnOffCommand</code>空调的具体实现类:</p><pre><code class="Java">public class TurnOnCommand implements Command{
private AirConditionerReceiver airConditionerReceiver;
public TurnOnCommand(AirConditionerReceiver airConditionerReceiver) {
super();
this.airConditionerReceiver = airConditionerReceiver;
}
@Override
public void execute() {
airConditionerReceiver.turnOn();
}
}</code></pre><pre><code class="Java">public class TurnOffCommand implements Command{
private AirConditionerReceiver airConditionerReceiver;
public TurnOffCommand(AirConditionerReceiver airConditionerReceiver) {
super();
this.airConditionerReceiver = airConditionerReceiver;
}
@Override
public void execute() {
airConditionerReceiver.turnOff();
}
}</code></pre><p><code>Invoker</code>调用者,/请求者(智能音响):</p><pre><code class="Java">public class Invoker {
private Command command;
public Invoker(Command command) {
this.command = command;
}
public void action(){
System.out.print("小度智能家居为你服务 --> ");
command.execute();
}
}</code></pre><p>客户端测试一下:</p><pre><code class="Java">public class Client {
public static void main(String[] args) {
Command command = new TurnOnCommand(new AirConditionerReceiver());
Invoker invoker = new Invoker(command);
invoker.action();
Command turnOffCommand = new TurnOffCommand(new AirConditionerReceiver());
Invoker turnOff = new Invoker(turnOffCommand);
turnOff.action();
}
}</code></pre><p>测试结果如下:</p><pre><code class="txt">小度智能家居为你服务 --> 打开空调...
小度智能家居为你服务 --> 关闭空调...</code></pre><p>通过以上的测试,我们确实通过命令传递,就可以操作空调,客户端也没有直接关联空调,如果需要其他操作,那么另外实现一个命令实现类即可,拓展比较方便,不同命令之间不会相互影响。</p><h2>命令模式的拓展</h2><ol><li>多条命令</li></ol><p>如果我们需要执行多条命令,那么可以考虑在内部维护一个列表,添加之后依次执行即可。</p><ol start="2"><li>维护日志</li></ol><p>如果考虑到执行命令的日志,我们则需要将对象序列化保存起来(磁盘上),维护好执行的状态,在系统故障的时候,可以从断开的地方继续执行。</p><ol start="3"><li>撤销</li></ol><p>如果某个命令需要撤销,那么我们需要在命令的抽象类里面加一个<code>undo()</code>方法,类似于<code>Mysql</code>数据库的<code>undo</code>操作,执行它即可撤销操作,当然这个中间过程涉及到了状态的维护,细节需要具体的命令来实现。(类似于数据库的事务回滚)</p><h2>优缺点</h2><p>优点:</p><ul><li>降低系统耦合度</li><li>易于拓展新的命令</li></ul><p>缺点:</p><ul><li>具体命令如果过多,类数量爆炸</li></ul><p>命令模式主要是想让请求者与真正的接受者解耦合,其实用的伎俩也是加一层(命令层)的操作,有一句话说得好,不是我说的:</p><blockquote>计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。</blockquote><p>这句话几乎概括了计算机软件体系结构的设计要点.整个体系从上到下都是按照严格的层级结构设计的,体现了设计的层次感,而不是相互缠绕。个人觉得之所以这样设计,最根本的原因在于社会分工以及人类大脑思维对于分层体系认知能力比较强,毕竟<strong>代码是不同人写给不同人读的</strong>。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,个人网站:<a href="https://link.segmentfault.com/?enc=O7Pe7zZcmmIJdz%2F6klY71g%3D%3D.%2Ff%2FqMqmFk3KijbSbSVh86Ja%2BiF1PTv073zLH%2FbOxjfI%3D" rel="nofollow">http://aphysia.cn</a>,技术之路不在一时,山高水长,纵使缓慢,驰而不息。</p><p><a href="https://link.segmentfault.com/?enc=M8%2FKNCMFT1P7%2FhvG2FLYFw%3D%3D.fZ0MDXvnaIPejvk%2B7fjjLvza89IhsQV9sV%2BVgsZVjm2TAIt0PlkxEhqTKVyDAhLi" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=y19tX1rMK%2BtIh2hJeaMgAw%3D%3D.gc36UN6SIHpyBZZ6%2B75D%2Fev4PNNYVqpgdKJvJqss2JFfygskwwpBk3SIH6C6W4jA" rel="nofollow">开源编程笔记</a></p><p>设计模式系列:</p><ul><li><a href="https://link.segmentfault.com/?enc=tWWjQZM%2F5W7GTbAy4CULKQ%3D%3D.JYjzCkDAPfrQWYSr%2BQggBIdBD7hSfaWH9RqDE4DOoQ7qrorDoI6mVV%2B1B4ZUgdYX" rel="nofollow">设计模式【1】-- 单例模式到底几种写法?</a></li><li><a href="https://link.segmentfault.com/?enc=zmVrtSM2FwckTSPOYmgZ8g%3D%3D.khTXueI4bsAIahfk3tqCm69HS4MxWgeCIXQMqDAnKfj9UuHGYR25KkGebtisxD8n" rel="nofollow">设计模式【1.1】-- 你想如何破坏单例模式?</a></li><li><a href="https://link.segmentfault.com/?enc=yNM60MPLNj230dcyJGfUYw%3D%3D.jRJ%2B4WtSMqJnXlVDzLhVnjR%2BEAHcL7%2Ft6UTA7d9jhDkyFHciNKXJn1xb8dfN0Zx%2B" rel="nofollow">设计模式【1.2】-- 枚举式单例有那么好用么?</a></li><li><a href="https://link.segmentfault.com/?enc=HAee2ufzEsbBnlWGRGFrOA%3D%3D.fDXg%2BvLl9ap2LIio9EW94uOrHyv3IFJF9UbFjZ286TWL%2FP9jJpVfIj9Oh0X7jSQi" rel="nofollow">设计模式【1.3】-- 为什么饿汉式单例是线程安全的?</a></li><li><a href="https://link.segmentfault.com/?enc=nloT0FYCbzdhq8RujdC%2B%2Bw%3D%3D.ZXrTAlLb6sOYDYKunvdwelTJ2V8HhCso0soKlySOhL6Rv%2F0KBr5mLejdi%2BTtaRmn" rel="nofollow">设计模式【2】-- 简单工厂模式了解一下?</a></li><li><a href="https://link.segmentfault.com/?enc=hLKraAmuFoHyzZVgtiTemw%3D%3D.K8%2BXvbljPntBedmMRNIMavkIle3CA%2Fb5EgghBCWhT4ufQ31N1UFshr13KH%2BH0pXk" rel="nofollow">设计模式【2.1】-- 简单工厂模式怎么演变成工厂方法模式?</a></li><li><a href="https://link.segmentfault.com/?enc=g4rqqXGSCPhCV29R2W3%2BUQ%3D%3D.h5LghEsaPF1H20wjPHFpDGY%2BY9A97pGpSQkytuDuccahxGBN1iyGb5r9sgIzWo9x" rel="nofollow">设计模式【2.2】-- 工厂模式怎么演变成抽象工厂模式?</a></li><li><a href="https://link.segmentfault.com/?enc=j3qSFGCZ2VXqiFNfUvXOtQ%3D%3D.HbJKvwxplmIUvSOabnoFbqD8snQYArHzBYAB5cTcm%2FyS3FDxSc4grizMh0g9aWhAXiyBt%2BHQZjjUUs9R5I8PrQ%3D%3D" rel="nofollow">设计模式【3.1】-- 浅谈代理模式之静态、动态、cglib代理</a></li><li><a href="https://link.segmentfault.com/?enc=cIfWhorfq0nlpzQYquljGw%3D%3D.U99y7lEaGun%2FBKchnbUYDXskrNiFTVSIH87206MSRejnaBVedeglPclOVqjPGn0o9AuLKrogNDpOF7ZWSgc6BizpxWKGBpbAFGh84rsa98O48orcI71YCTN9p61k0LIL" rel="nofollow">设计模式【3.2】-- JDK动态代理源码分析有多香?</a></li><li><a href="https://link.segmentfault.com/?enc=HhnTsaJEhi%2B3uzC6y3yo7Q%3D%3D.6f6OPZjl8fsrOHdCAEcQEUZ6Q6HayvbKB34bn%2BCx%2B6M%2BKgwvO8Ggm4oDE81WjTd6iKHkpCUplK45KnjThVLsX6tiWKH6zMdz5poYSkQExJhh9bpd9b8jikj%2FT%2FJTWLg1" rel="nofollow">设计模式【3.3】-- CGLIB动态代理源码解读</a></li><li><a href="https://link.segmentfault.com/?enc=iw%2FS%2BGe6gtsI1VX0U3xMUA%3D%3D.2pTcQvI0nqBolLHXOdTWE9pRuE%2FDnwQ%2FgcHQl968xCU5tRgMo3hJD5mwOL7DIraqK3vV1Vut3R4GJRa0amyDsPv3DJYzAjSn6%2F6VvXUQVFg%3D" rel="nofollow">设计模式【4】-- 建造者模式详解</a></li><li><a href="https://link.segmentfault.com/?enc=IlK%2BXiE%2B3PVu2gAJeiJiYw%3D%3D.HEP4z3ra19qk8K5R52p%2FX5paFmCDSAuVNf%2FPp5AcubHakZNtu%2FxaJiR4jf5nVeL790Q3yp59%2FU9ZOypqIWLfVQ%3D%3D" rel="nofollow">设计模式【5】-- 原型模式</a></li><li><a href="https://link.segmentfault.com/?enc=4F05yKkeI245IQO617Loww%3D%3D.Bxf6MFuPxW%2FoDiyB3y0mjb32QRg71GWTJ9%2FR%2FrtpLnSvbNtdaQ0qJU26TJmQBk4bOHiiLqk8L6dOJwapC0iy0TQO%2Fk3MpRdrTpzUAxenoEk%3D" rel="nofollow">设计模式【6.1】-- 初探适配器模式</a></li><li><a href="https://link.segmentfault.com/?enc=24aJMwi7Hh%2BH6tSMojr3bQ%3D%3D.c%2B31lnn2%2Fpws6s9fHwFrXKL7pSDD%2BiQTy7pDcQBktFOiJZX92ph6fR7lOoiPdxW8v0ysR0MrNprgqqCvN14m1wVch4eg%2Fc83qYI%2BtwIGEC0%3D" rel="nofollow">设计模式【6.2】-- 再聊聊适配器模式</a></li><li><a href="https://link.segmentfault.com/?enc=3UTgGhSJSsuJi0hHF2w7Zw%3D%3D.21ZrexRD2tjWS%2Fg8WnxeyrIeS9JgYHalhTaUxP2bZ4QL4DxOJWeYeGpC10oHeEFkLUPJceiTXsaMNvF6lmowGvo3%2FKZ%2FTtI9Fyx4vxaCVz0%3D" rel="nofollow">设计模式【7】-- 探索一下桥接模式</a></li><li><a href="https://link.segmentfault.com/?enc=GxtaLOTpJI112NV%2FlM3q9g%3D%3D.H4e8knNwpgM1U5auVzuEZ4XTSBN0KeAhwuDD93PLbmUqtJg0sg6ugZjKSvC4qnx3QfIBBi0Fv%2FfLGzBRT%2Fx8EZaTA9P7m37eA%2BX7XwiwQj91ic8YPKLasZ1FY9SaA60r" rel="nofollow">设计模式【8】-- 手工耿教我写装饰器模式</a></li><li><a href="https://link.segmentfault.com/?enc=xfjqC%2B6%2B7shQpzOObDbhhQ%3D%3D.FAvTOnWp7TeAI4YKVIO5zT9PvBJRIMsLW5tvnX%2Ba9ZcgDZRJwlcqMQ7wuWK5CQerOHrINDbSUF5prbaKQQCxu9gEyoJ92DsqNmWg%2FcwfQ1ESZj0dlKz0q1t5Wo%2Fov6Np" rel="nofollow">设计模式【9】-- 外观模式?没那么高大上</a></li><li><a href="https://link.segmentfault.com/?enc=aId0g%2FofhOO0F8CV0%2FzAkA%3D%3D.dKMCw3hJsU1Fmotc%2FJ54l1%2BEqniFZN6tvLMAuB8mBAjMxjg%2BAtpmp2%2F0i0qMZ59pcgM7dOgTzlRvj9kdyQIYcpFO0intoRSSAHCIG4iXEUCNrY6K3Abs4%2B4C6nbt2T2K" rel="nofollow">设计模式【10】-- 顺便看看享元模式</a></li><li><a href="https://link.segmentfault.com/?enc=GWUXWCx9mhB4AtgTuxUoeg%3D%3D.j2iYVT8WMOac4gq%2BEHw28LvK%2B88RSEvXfYy26mPll4RWCLuOKgTJYOBgr32XbldUYhyubf0%2ByrgJAKufvLPEQ8ynA2e%2BtGzMskJbt9NKGWM%3D" rel="nofollow">设计模式【11】-- 搞定组合模式</a></li><li><a href="https://link.segmentfault.com/?enc=uyyTypEhNtFaAjcnWxH8Pg%3D%3D.oVg68xUc1mO566MbjtjK3wjLq1dmgtF6p4T1lrKWaD21GynH8r%2BbMulz%2B%2B%2FUbaSPSxt8AxXd8tjnF%2FTNxky5luJo0V7VtqcfSlVazPXUC78LpnqlAVYt12iYIi3XWtBt" rel="nofollow">设计模式【12】-- 搞定最近大火的策略模式</a></li><li><a href="https://link.segmentfault.com/?enc=7X%2BgFLbi6ELroUVXLA72gQ%3D%3D.iX6l%2FRacGN53yZheIzTpAvubNlcOAPestzNDgFAPyjYwn6ueZIyXIT%2B6iklS1BAJNcI534yvTQ4qy3GeLpSWEcE7IBhQgGKuMMZ2oWSWjDY%3D" rel="nofollow">设计模式【13】-- 模板模式怎么弄?</a></li></ul>
设计模式【13】-- 模板模式怎么弄?
https://segmentfault.com/a/1190000041381637
2022-02-10T08:27:05+08:00
2022-02-10T08:27:05+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p><img src="/img/remote/1460000041381639" alt="设计模式" title="设计模式"></p><p>开局还是那种图,各位客官往下看...</p><blockquote>张无忌学太极拳,忘记了所有招式,打倒了"玄冥二老",所谓"心中无招"。设计模式可谓招数,如果先学通了各种模式,又忘掉了所有模式而随心所欲,可谓OO之最高境界。</blockquote><h2>模板模式是什么?</h2><p>模板模式,同样是一种行为型模式,也就是关于<strong>对象做什么或者怎么做</strong>的设计模式。模板模式的本质需要定义操作中的算法的框架,但是有一些步骤,又不需要具体的实现,而是不同的子类各自实现。子类不能修改流程框架,但是部分的步骤可以做定制化的实现。</p><p>主要要解决一个问题:一些通用的方法,但是每一个子类却都重新写,冗余。</p><p>比如说,做菜的步骤一般是:<code>洗锅 --> 炒菜 --> 洗碗</code> ,不同的菜,只是炒菜这一个步骤具体细节是不同的,但是其他步骤确实几乎一模一样的,这样其实整体框架,以及重复的步骤,我们可以抽象到模板中,而不同的细节方法可以开放给每一种菜(具体实现)去定制。</p><p>又比如造房子的时候,很多地方的建造都是一样的:地基,墙壁,水管等等,但是不同的房子里面的内部的设计又有所不同。</p><h2>不使用模板模式</h2><p>就挑个简单的例子“炒菜”,如果不使用模板模式的话,糖醋鲤鱼:</p><pre><code class="Java">
public class SweetAndSourCarp {
public void cookFood(){
washPan();
cook();
eat();
washDishes();
System.out.println("");
}
private void washPan(){
System.out.print("洗锅 --> ");
}
private void cook(){
System.out.print("煮糖醋鲤鱼 --> ");
}
private void eat(){
System.out.print("吃饭 --> ");
}
private void washDishes(){
System.out.print("洗碗 --> ");
}
}
</code></pre><p>再弄一个农家小炒肉,需要写很多相同的方法:</p><pre><code class="Java">public class ShreddedPorkWithVegetables {
public void cookFood(){
washPan();
cook();
eat();
washDishes();
System.out.println("");
}
private void washPan(){
System.out.print("洗锅 --> ");
}
private void cook(){
System.out.print("炒农家小炒肉 --> ");
}
private void eat(){
System.out.print("吃饭 --> ");
}
private void washDishes(){
System.out.print("洗碗 --> ");
}
}
</code></pre><p>测试类如下:</p><pre><code class="Java">public class Test {
public static void main(String[] args) {
SweetAndSourCarp sweetAndSourCarp = new SweetAndSourCarp();
sweetAndSourCarp.cookFood();
ShreddedPorkWithVegetables shreddedPorkWithVegetables = new ShreddedPorkWithVegetables();
shreddedPorkWithVegetables.cookFood();
}
}</code></pre><p>测试结果:</p><pre><code class="txt">洗锅 --> 煮糖醋鲤鱼 --> 吃饭 --> 洗碗 -->
洗锅 --> 炒农家小炒肉 --> 吃饭 --> 洗碗 --> </code></pre><p>可以看到,整体流程是一样的,有些步骤一样,有些步骤不一样,但是不使用模板模式,需要每个类都重写一遍方法,即使是通用方法,整个流程都需要自己写一遍。</p><h2>使用模板模式优化</h2><p>如果使用模板模式,那么我们会抽象出一个抽象类,定义整体的流程,已经固定的步骤,开放需要定制的方法,让具体的实现类按照自己的需求来定制。</p><p><img src="/img/remote/1460000041381640" alt="" title=""></p><p>定义的抽象类:</p><pre><code class="Java">public abstract class CookFood {
public final void cookFood() {
washPan();
cook();
eat();
washDishes();
System.out.println("");
}
private final void washPan() {
System.out.print("洗锅 --> ");
}
public abstract void cook();
private final void eat() {
System.out.print("吃饭 --> ");
}
private final void washDishes() {
System.out.print("洗碗 --> ");
}
}
</code></pre><p>具体的实现类糖醋鲤鱼:</p><pre><code class="Java">public class SweetAndSourCarp extends CookFood {
@Override
public void cook() {
System.out.print("煮糖醋鲤鱼 --> ");
}
}</code></pre><p>农家小炒肉:</p><pre><code class="Java">public class ShreddedPorkWithVegetables extends CookFood {
@Override
public void cook() {
System.out.print("炒农家小炒肉 --> ");
}
}</code></pre><p>测试类与前面的一样,测试结果也一样,这里不再重复。</p><p>上面的方法中,其实我们只开放了<code>cook()</code>方法,这就是<strong>钩子方法</strong>:</p><blockquote>在模板方法模式的父类中,我们可以定义一个方法,它默认不做任何事,子类可以视情况要不要覆盖它,该方法称为 ”钩子方法”</blockquote><p>钩子方法是开放的,可以由子类随意覆盖,但是像上面的其他方法,我们不希望子类重写或者覆盖它,就可以用 <code>final</code> 关键字,防止子类重写模板方法。</p><h2>模板模式的应用</h2><p>其实在 <code>JDK</code> 的 <code>Thread</code> 实现中,就是使用了模板模式,我们知道创建线程有两个方式:</p><ul><li>创建 <code>Thread</code> 类</li><li>实现 <code>runnable</code> 接口</li></ul><p>我们实现的一般是 <code>run()</code> 方法, 但是调用的却是 <code>start()</code> 方法来启动线程,这个原因就是 <code>start()</code> 方法里面帮我们调用了<code>run() </code>方法, <code>run()</code>方法是开发的方法,我们可以覆盖重写它。</p><p><img src="/img/remote/1460000041381641" alt="" title=""></p><p><code>Start0()</code>是一个<code>native</code>方法,是由 c 语言去实现的,在调用的时候,真正调用了我们的 <code>run()</code> 方法,如果需要跟踪这个方法需要到 <code>HotSpot</code>底层去。这里介绍的目的是让大家了解,它同样是使用了模板模式。</p><pre><code class="C++"> private native void start0();</code></pre><p>了解 <code>native</code> 关键字可以参考:<a href="https://link.segmentfault.com/?enc=ieWGMDyx1%2BMdj7lm%2FcRPHA%3D%3D.hQGblf%2BzhMOyplKUgBSu8Dh2UC3j1%2BaBxkK2xaUAfEGvMz9GLgymGBHNnEaqdL6b" rel="nofollow">http://aphysia.cn/archives/na...</a></p><h2>模板模式的优缺点</h2><p>模板模式的<strong>优点</strong>:</p><ul><li>1、封装固定的部分,拓展需要定制修改的部分,符合开闭原则。</li><li>2、公共的代码在父类中,容易维护。</li><li>3、整个流程由父类把握,调整比较方便。</li></ul><p><strong>缺点:</strong></p><ul><li>1、子类可能会很多,系统复杂度上升。</li><li>2、子类只有一小部分实现,了解全部方法则需要在父类中阅读,影响代码阅读。</li></ul><p>总结:代码该隐藏的复杂细节隐藏起来,开放定制化部分,优雅!</p><p>设计模式系列:</p><ul><li><a href="https://link.segmentfault.com/?enc=B11%2Fk%2B7IoP32aEMAup0m6Q%3D%3D.3D7JOXPcVbnUSfHP3uapj8EAY3Z9sEyl6mk3qHJd%2F%2BeDMSeZXjRmvwpJMoDCHK6r" rel="nofollow">设计模式【1】-- 单例模式到底几种写法?</a></li><li><a href="https://link.segmentfault.com/?enc=XZE%2BQC6dcHil2NezP%2F7lJQ%3D%3D.0L8%2FtBG7wq54PrUh2eDzVbmCRe4ZoCIJWP4FPos%2BR3FiCB5S8CK5CnLegl5208%2Fk" rel="nofollow">设计模式【1.1】-- 你想如何破坏单例模式?</a></li><li><a href="https://link.segmentfault.com/?enc=202ZhBziXS43d8zH6OarMQ%3D%3D.pwHchCYG%2FKwkCWnPmSk18cQ0ZhZ1WIAtyKmpz0S2jhQdeuRYpFqNA5uX7YUXnwNE" rel="nofollow">设计模式【1.2】-- 枚举式单例有那么好用么?</a></li><li><a href="https://link.segmentfault.com/?enc=K4lRIhYGCqPoLq36hG4pNg%3D%3D.EzXJrdyRttGQnyWcjXdoQ9GdC9Zv5dpYRl0jsVACA4TiOoZ%2FhCiDK4k4BxeMGyrF" rel="nofollow">设计模式【1.3】-- 为什么饿汉式单例是线程安全的?</a></li><li><a href="https://link.segmentfault.com/?enc=d0vRst%2BolU9RvHpvJZsLyw%3D%3D.Si0hbuNBSenRlolVc98Twu9cpEQiRfs8lThGz2wWb%2Fye9iDk9WkQ1McvirfG%2Bmrq" rel="nofollow">设计模式【2】-- 简单工厂模式了解一下?</a></li><li><a href="https://link.segmentfault.com/?enc=yR1okw24iQ1VHM2L1vHCRQ%3D%3D.UowJkkCVLpEPxBaJvPIbOc2fc1XTlZy1Y9TiXdXxTBZGEcQdsvCBJl6bw%2B00ULst" rel="nofollow">设计模式【2.1】-- 简单工厂模式怎么演变成工厂方法模式?</a></li><li><a href="https://link.segmentfault.com/?enc=7WHFwXVT8dVDP%2FSLiF%2Fdkw%3D%3D.OwuCq6Z%2BGT8%2FMPtJ2PJkYJwn%2FFl9QR1OjjdeuK9612pL%2FAdwu8Co703%2FJvHPP0va" rel="nofollow">设计模式【2.2】-- 工厂模式怎么演变成抽象工厂模式?</a></li><li><a href="https://link.segmentfault.com/?enc=u65ertnLP9brNYauAc5Z0w%3D%3D.AyrYZSpcBnQNmyP6PnPXiZm9wZEzJvGJsapgO4TJ68YGwvkZXpBbTJgf%2BAFyqVkxxEn733Io5O4Oii9vnodWQw%3D%3D" rel="nofollow">设计模式【3.1】-- 浅谈代理模式之静态、动态、cglib代理</a></li><li><a href="https://link.segmentfault.com/?enc=IRqD4EiYJ%2Ba0h2T37pDiYg%3D%3D.MOUmWrAqMA9DlxJzQYcrUdYSTMR9LW34FIe%2FL%2BdA6%2B82yMK6NBOny2GhEdcZ5YDF2tL%2FiaFLP9Tc7y1SlKivq59pMbe0mayvb4PNTa0RbrdrsRixReIYBB0ANbniEyYE" rel="nofollow">设计模式【3.2】-- JDK动态代理源码分析有多香?</a></li><li><a href="https://link.segmentfault.com/?enc=mJMewWdVXcS68L2qFbExew%3D%3D.0BPkv67qi2vGf2yslMMTQ9LDy1ieIEVhHm3qYLg8AOyYq%2BvUQJ0zryqB%2Bz4YDxoOIBKfd3B5OvkJNCe1agAmIkCt3soE%2BHHSWvEYob8bNIb8QrKM18dlYB6qt6D4Rkiu" rel="nofollow">设计模式【3.3】-- CGLIB动态代理源码解读</a></li><li><a href="https://link.segmentfault.com/?enc=CMbggOOwxZbh4Rnd3TzFnQ%3D%3D.CbNqj2SP27cpffOP40rpjpnU7HrGfq1CI5tzb%2FJpcsbzpvRlUJhD1I2RVDxPtVlYWnbohcJLCax2J%2FCNMfHn7Syd4kEdLPQyU5p9d8t1k%2B4%3D" rel="nofollow">设计模式【4】-- 建造者模式详解</a></li><li><a href="https://link.segmentfault.com/?enc=e3a%2BM09PiC4HOS%2B8XohxGQ%3D%3D.GnXjWMYq7HTa8Hb67pF%2B5cNH2e3JkM54Sg%2Fj19mxD2K7dRtzGNQLHkp%2BuTkOYQYdYaGqrfjNRGG9VAeTEUU7BA%3D%3D" rel="nofollow">设计模式【5】-- 原型模式</a></li><li><a href="https://link.segmentfault.com/?enc=5B7im%2FXxqFlbrd%2B1ynIjRw%3D%3D.%2BF4S0Yn8qDBTSP4HACPzAmDq97N2KWzIy3wAFyuONveVMKPOGpalhFVdodr6N4S5wXdHek%2BFfT4E297uupTT9sdC0Stm6r3BDDcqXCczMVI%3D" rel="nofollow">设计模式【6.1】-- 初探适配器模式</a></li><li><a href="https://link.segmentfault.com/?enc=2vsuIt7BZIKILeF2U6CGgg%3D%3D.31dGNw486B%2FJOV0RGA8V%2FVjq7BRDi8MJ0OqsU3w5GRBJs8Bdym9X40sa006PGOBNWiW6AUCFsZSwkFXHDROVgnR6WP1mVyFxj9Ewb1YmpA8%3D" rel="nofollow">设计模式【6.2】-- 再聊聊适配器模式</a></li><li><a href="https://link.segmentfault.com/?enc=jF1UgrxMtmCZj21Kjxe9Aw%3D%3D.4y%2FwWeFbAzH312xvxYC904OFfv3%2BL2S%2FjAjbMKPZxlGefJCXNJBG3F8ExENl%2BHMmbn1UK4OgCkM6JqOLX2DH3ymJWk0rOliYopWZqQRWx9E%3D" rel="nofollow">设计模式【7】-- 探索一下桥接模式</a></li><li><a href="https://link.segmentfault.com/?enc=3c8e8DXdc8XLu6thr5oW9A%3D%3D.zgH68%2BOPrwz%2BDPCRdJMLWDH6OQ%2FS3inn1zPxTevp8M6G76mu5kbvqVSACxxNBxBA1ICN9dthtl%2B544pez%2F606KqzILwoFHGTgaweM1L%2F%2FV6EkjSDqUakN3GBoatzOtJh" rel="nofollow">设计模式【8】-- 手工耿教我写装饰器模式</a></li><li><a href="https://link.segmentfault.com/?enc=GeHOfB3c4aaIQ3mcXJR4kg%3D%3D.RNNSrlaOhopKnqXXQCqKc5LG4tFqMPTEw5qZpticNzZ2NiNCNHfPt%2B7JncmgHmdkWUiSouJEgbqBPnMehD5KGg5gjqV%2BxIjv0SYNIs4cdvw%2BrrK1L5FI7LwsvVP1v%2BaS" rel="nofollow">设计模式【9】-- 外观模式?没那么高大上</a></li><li><a href="https://link.segmentfault.com/?enc=sjbxGCZLxIe2AFf4uY2iiw%3D%3D.X86pheF3lRpu8%2Bg%2B4ieH9nz0HAc2aRIFfOS9Kw05DqUKUUwaINuNSFH7C3QESjDf9LdaRz1XZl38EbC0VR13%2Fik2Qupo7E5XJUAc4gh15XmVwkodlmsIjgJPGNrr0eGA" rel="nofollow">设计模式【10】-- 顺便看看享元模式</a></li><li><a href="https://link.segmentfault.com/?enc=gYFQc2ciVqz6T6P6tphIdA%3D%3D.KCptT2ozBVE4gK5nYNlELgbqfDEn5dzsiR9IRXn%2BP0EzbmEEMtZXrhtkRCQEhLvuUJ5XL%2FhDyPQWajEh9PTICrqV4Q3XuC3XDlvhwSb9fl4%3D" rel="nofollow">设计模式【11】-- 搞定组合模式</a></li><li><a href="https://link.segmentfault.com/?enc=o7e9haGRX9SwMdV%2BsP6jsg%3D%3D.%2BZWRd5Zi4HkxgaP%2FjkVucIr4Zr14LgBTFYBhsqx3kVZvVHJ%2F775f33xHVKkPorzBLhYwLa%2FmeFP57gni04f5dolVBAOQGAXGNkHdsWaKW6r3PuRECoj54Up0f1agznjz" rel="nofollow">设计模式【12】-- 搞定最近大火的策略模式</a></li></ul><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,个人网站:<a href="https://link.segmentfault.com/?enc=TGRB8Fhj7gbx1%2Fo4tqzxtg%3D%3D.7%2BIh3r1j2k9P1DYoXtf4cIvT9QWgz3%2F9j1Fse%2BlDAUc%3D" rel="nofollow">http://aphysia.cn</a>,技术之路不在一时,山高水长,纵使缓慢,驰而不息。</p><p><a href="https://link.segmentfault.com/?enc=Q%2F1fpAAiGbeXN0dDgoGazw%3D%3D.JtQCpj60MhfVNLdf8YBfCtdfXBrexD7NjX2W2lt3S%2BtJz9zGl1S4wzJC9NBXLFBO" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=JWDmvqzoaOvAe8ryaFt8Qw%3D%3D.aJwfciFA6cDQ2Wqp9412ZoQPpSBQ13aFWpZtfLeiR2AwFPdyG6p2kosqwYcYEqCI" rel="nofollow">开源编程笔记</a></p>
设计模式【12】-- 搞定最近大火的策略模式
https://segmentfault.com/a/1190000041328955
2022-01-23T21:45:31+08:00
2022-01-23T21:45:31+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
2
<p>开局还是那种图,最近策略模式貌似很火,各位客官往下看...</p><p><img src="/img/remote/1460000041100740" alt="设计模式" title="设计模式"></p><h2>策略模式到底是什么?</h2><p>前面我们其实已经将结构型模式讲解完了,剩下的全都是行为型模式,三种模式的区分:</p><ul><li>创建型模式:如何创建一个对象</li><li>结构型模式:对象内部的构造是如何构造的</li><li>行为型模式:对象是如何运行(可以做什么)</li></ul><p><strong>而提到策略模式,我们该如何理解呢?</strong></p><ul><li>从北京到上海,可以坐飞机,也可以坐动车,也可以坐绿皮,甚至可以骑自行车,这些不同的方式,就是策略。</li><li>一件商品,可以直接打 5 折,也可以加 100 再打 6 折,也可以打 5.5 折之后返回现金券,这些也是策略。</li><li>带了 200 去超市买东西,可以买零食,也可以买生活用品,也可以啥也不买,这些也是策略。</li></ul><p>上面的例子,其实我们可以发现不同的策略其实是可以互换的,甚至策略细节之间可以混搭,那么很多时候我们为了方便拓展,方便替换策略,对调用方屏蔽不同的策略细节,就可以使用策略模式。倘若所有的策略全部写在一个类里面,可以是可以,这样维护代码会越来越复杂,维护只会越来越困难,里面会充斥着各种<code>if-else</code>,算法如果日益复杂,动一发而牵全身,那就只剩下提桶跑路了。</p><blockquote>策略模式是指有一定行动内容的相对稳定的策略名称。策略模式在古代中又称“计策”,简称“计”,如《汉书·高帝纪上》:“汉王从其计”。这里的“计”指的就是计谋、策略。策略模式具有相对稳定的形式,如“避实就虚”、“出奇制胜”等。一定的策略模式,既可应用于战略决策,也可应用于战术决策;既可实施于大系统的全局性行动,也可实施于大系统的局部性行动。</blockquote><p>再说一个打工人都知道的例子,比如每个人都要交个人所得税,我们知道个人所得税的计算方式是很复杂的,税法计算就是一个行为,而可能存在不同实现算法,那每一种计算算法都是一种策略。</p><p>这些策略可能随时被替换掉,那么我们为了好替换,相互隔离,就定义一系列算法,封装起来,这就叫策略模式。</p><h2>策略模式的角色</h2><p>策略模式一般有三种角色:</p><ul><li>抽象的策略类(<code>Strategy</code>):将策略的行为抽象出公共接口</li><li>具体的策略类(<code>ConcreteStrategy</code>):以<code>Strategy</code>接口实现某些具体的算法,具体的策略可能多个,也可以相互替换</li><li>上下文(<code>Context</code>): 一般是封装了策略,以及策略的使用方法,对上层调用屏蔽了细节,直接调用即可。</li></ul><p><img src="/img/remote/1460000041328957" alt="" title=""></p><h2>策略模式的demo</h2><p>创建一个抽象的策略接口:</p><pre><code class="Java">public interface Strategy {
int doStrategy();
}
</code></pre><p>创建 2 个具体的策略类,分别执行策略:</p><pre><code class="Java">public class ConcreteStrategy1 implements Strategy{
@Override
public int doStrategy() {
System.out.println("执行策略1...");
return 1;
}
}</code></pre><pre><code class="Java">
public class ConcreteStrategy2 implements Strategy{
@Override
public int doStrategy() {
System.out.println("执行策略2...");
return 2;
}
}
</code></pre><p>创建上下文类,封装策略类:</p><pre><code class="java">public class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void executeStrategy(){
strategy.doStrategy();
}
}</code></pre><p>测试类:</p><pre><code class="Java">public class Test {
public static void main(String[] args) {
Context context1 = new Context(new ConcreteStrategy1());
context1.executeStrategy();
context1 = new Context(new ConcreteStrategy2());
context1.executeStrategy();
}
}
</code></pre><p>测试结果:</p><pre><code class="txt">执行策略1
执行策略2</code></pre><p>可以看到只要使用不同的策略,就可以执行不同的结果。</p><h2>分红包策略实战</h2><p>比如,工作中有涉及到分配的策略,场景就是有红包,分配给若干个人,但是会有不同分配策略。这里不同的分配策略其实就是用策略模式来实现。可以随机分配,也可以平均分配,还可以根据各种权重来分配等等。这里只演示两种分配:</p><p>首先定义一个红包类,包含红包金额(<strong>我们以分为单位,保存整数,避免金额小数不准确问题</strong>)</p><pre><code class="Java">public class RedPackage {
public int remainSize;
public int remainMoney;
public RedPackage(int remainSize, int remainMoney) {
this.remainSize = remainSize;
this.remainMoney = remainMoney;
}
}</code></pre><p>定义一个分配策略的抽象类:</p><pre><code class="Java">import java.util.List;
public interface AllocateStrategy {
List<Integer> allocate(RedPackage redPackage);
}</code></pre><p>平均分配的时候,如果不能平均,那么第一位会有所增减:</p><pre><code class="Java">import java.util.ArrayList;
import java.util.List;
public class AverageAllocateStrategy implements AllocateStrategy {
@Override
public List<Integer> allocate(RedPackage redPackage) {
List<Integer> results = new ArrayList<>();
Integer sum = redPackage.remainMoney;
Integer average = sum / redPackage.remainSize;
for (int i = 0; i < redPackage.remainSize; i++) {
sum = sum - average;
results.add(average);
}
if (sum != 0) {
results.set(0, results.get(0) + sum);
}
return results;
}
}
</code></pre><p>随机分配的时候,我们将每一份随机添加到红包里面去,但是这样不能保证每一份都有金额(<strong>注意这个不要在生产使用,仅限测试</strong>)</p><pre><code class="Java">
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class RandomAllocateStrategy implements AllocateStrategy {
@Override
public List<Integer> allocate(RedPackage redPackage) {
return ranRedPackage(redPackage.remainSize, redPackage.remainMoney);
}
public List<Integer> ranRedPackage(Integer count, Integer money) {
List<Integer> result = new ArrayList<>();
for (int i = 0; i < count; i++) {
result.add(0);
}
for (int i = 1; i <= money; i++) {
int n = new Random().nextInt(count);
result.set(n, result.get(n) + 1);
}
return result;
}
}
</code></pre><p>定义上下文类,封装不同的策略:</p><pre><code class="Java">import java.util.List;
public class Context {
private AllocateStrategy strategy;
public Context(AllocateStrategy strategy) {
this.strategy = strategy;
}
public List<Integer> executeStrategy(RedPackage redPackage){
return strategy.allocate(redPackage);
}
}
</code></pre><p>测试类:</p><pre><code class="Java">import java.util.List;
public class Test {
public static void main(String[] args) {
RedPackage redPackage = new RedPackage(10,102);
Context context = new Context(new RandomAllocateStrategy());
List<Integer> list =context.executeStrategy(redPackage);
list.forEach(l-> System.out.print(l+" "));
System.out.println("");
context = new Context(new AverageAllocateStrategy());
list =context.executeStrategy(redPackage);
list.forEach(l-> System.out.print(l+" "));
}
}
</code></pre><p>可以看到分配的金额确实会随着策略类的不同发生不同的变化:</p><pre><code class="Java">9 10 16 8 14 8 7 15 9 6
12 10 10 10 10 10 10 10 10 10 </code></pre><p><strong>注意这个不能在生产使用!!!</strong></p><h2>优缺点</h2><p>策略模式的优点:</p><ul><li>消除<code>if-else</code>语句,不同策略相互隔离,方便维护</li><li>切换自由</li><li>拓展性好,实现接口即可</li></ul><p>策略模式的缺点:</p><ul><li>策略一旦数量过多,维护也会相对比较难,复用代码比较难</li><li>所有的策略类都对外暴露了,虽然一般通过<code>Context</code>上下文调用</li></ul><p>策略模式比较常用,可能有些同学会混淆策略模式和状态模式:</p><p>相对于状态模式:策略模式只会执行一次方法,而状态模式会随着状态变化不停的执行状态变更方法。举个例子,我们从<code>A</code>地方去<code>B</code>地方,策略就是飞机或者火车,状态模式则是可能到某一个地方,换了一种交通方式,到另外一个地方又换了一种交通方式。</p><h2>小结</h2><p>策略模式比较常用,核心就是隔离不同的策略,将具体的算法封装在策略里面,抽象出一个策略抽象类,把不同具体策略类注入到上下文类中,达到选择不同策略的目的。</p><p>但是如果策略很多,需要考虑复用,策略类对外暴露的时候需要考虑滥用风险,会破坏封装性。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,个人网站:<a href="https://link.segmentfault.com/?enc=toE0mwsdBUt6TqW4TnRu5g%3D%3D.YonVF584v5xO%2BJrZOvxgwjvvyaVg%2FFN9ldugsAK76LA%3D" rel="nofollow">http://aphysia.cn</a>,技术之路不在一时,山高水长,纵使缓慢,驰而不息。</p><p>设计模式系列:</p><ul><li><a href="https://link.segmentfault.com/?enc=nIQ3p3p0%2BslUpTTKWM4RgQ%3D%3D.5iQYGOjZnl0FRGgbJPs7wQFFhCLYh7IbRbyV%2BI%2Bg1xJWSpXZEvGxN9QPTx3SWpLi" rel="nofollow">设计模式【1】-- 单例模式到底几种写法?</a></li><li><a href="https://link.segmentfault.com/?enc=Kea7GcivOy7HKGbCQxpy3Q%3D%3D.orho0U13oeAdNhZUCrgVzgtOPUsl5DNtQkLvOpshZaW%2BXVPMg3i3kljvU6hDKBZi" rel="nofollow">设计模式【1.1】-- 你想如何破坏单例模式?</a></li><li><a href="https://link.segmentfault.com/?enc=V2eHkQ4lu%2FwMnobgwOmypw%3D%3D.cA31yR%2FzrzDr05wiEkJiR5IktsEGZyGTu%2FgjO8VxCbSHp7Hw3YQtq9kmNg8kGzff" rel="nofollow">设计模式【1.2】-- 枚举式单例有那么好用么?</a></li><li><a href="https://link.segmentfault.com/?enc=Ct9CVcK2vGbT5HdhNlh5qA%3D%3D.vHynaaWXi5GOX%2Fs6JYOTWSpbAl3Urz5UVPg64kzP5kfsPmYPyjQt%2Fcq3wSg5ZsXy" rel="nofollow">设计模式【1.3】-- 为什么饿汉式单例是线程安全的?</a></li><li><a href="https://link.segmentfault.com/?enc=VT7sjPGOt0Q0yLyc4KFMUQ%3D%3D.YpHksqPSeiCIyzt8LNNW%2BOLFdEV8NTeKoz4SikmJP9MNkVIwfGrXhdifZIiG23oB" rel="nofollow">设计模式【2】-- 简单工厂模式了解一下?</a></li><li><a href="https://link.segmentfault.com/?enc=K%2Bw%2BHDuDvtAkW1j%2B4QoJLw%3D%3D.g%2B2w4LN5EnnezNdBzQ%2FIoH4DhMJ7xq6g2sCX%2Fx1d%2F%2BxBFnmahPPWDNJIYMMlnF%2Fv" rel="nofollow">设计模式【2.1】-- 简单工厂模式怎么演变成工厂方法模式?</a></li><li><a href="https://link.segmentfault.com/?enc=VYbQRqYjICjARMloiLRmhA%3D%3D.bkk7xlvy8ChnP6N34PCXu21UZPlBvzxeTYJuGOvBfq8bur1rxEye7cnWfCDAJeFo" rel="nofollow">设计模式【2.2】-- 工厂模式怎么演变成抽象工厂模式?</a></li><li><a href="https://link.segmentfault.com/?enc=fiU7KKIfVN6iIuzTzi7LCQ%3D%3D.Fs9XInWER0r3zlu9OEcleBA%2FC5niiv2W5cDoIWfSi7GzhnZIxtBTO6OJ0iFk2jg%2BtTovB54BLlqwcIAngqOGkg%3D%3D" rel="nofollow">设计模式【3.1】-- 浅谈代理模式之静态、动态、cglib代理</a></li><li><a href="https://link.segmentfault.com/?enc=zOu7yIIk%2FnKdJ5rTJ4NFDg%3D%3D.GAePPeZeaHxjPOgbEOtnAetugRKQLS381teruAs5u9o1fPfOHCk%2BUw7APbzHjYVxQ20r%2BFP%2BGfOIyFllq3oX0xpFIQmEh8jKuZFnLV6W8DyVRiBRfyuCJeZvn7rShLHT" rel="nofollow">设计模式【3.2】-- JDK动态代理源码分析有多香?</a></li><li><a href="https://link.segmentfault.com/?enc=WhZkIplvna0iAGBd2nR2RA%3D%3D.KVoXNIX2sX1qDUGHL6%2FVkGodAqOHGYY3YWIPNjfZL6uzSYJo2K5%2FHGGXRQJ2mDODzY85xFhwlRHo0UkFPSYE%2BhdYRQl6ZE1DSCTB64n3ZJz2SBYF8cwY%2BXAoheAr%2FjF2" rel="nofollow">设计模式【3.3】-- CGLIB动态代理源码解读</a></li><li><a href="https://link.segmentfault.com/?enc=L9PKZmjRy6kwVYAplVG6fQ%3D%3D.1X7XxvXl%2FuqyZl97d%2F6KYnYVmG9ekSg8L%2FfnmyFS4%2B94v2WoWGq%2FJK15l2NwHcVQKvzLo1UaroEt3fMU%2BbrA1xTEplM4KSsc5IXylBidmC8%3D" rel="nofollow">设计模式【4】-- 建造者模式详解</a></li><li><a href="https://link.segmentfault.com/?enc=B1ZrUY9ILhXiwrqxg9i%2Bvg%3D%3D.yQndYXIaZNXyz2rK2x6n9PdIxMfu%2F1NP24G0zF3z8x3MbEOUdh4Ff2CtEZtUwLBRwW65QBa%2BQLjhmq%2FV1jl1cA%3D%3D" rel="nofollow">设计模式【5】-- 原型模式</a></li><li><a href="https://link.segmentfault.com/?enc=h2EW8grUc%2BF%2FjbMHEYgFjg%3D%3D.8QEWEJ1wSaB2WCKSQfuEjtW9lJ%2BbQzCrLZMqBMM0Gv6WiyyoBvTkxd3ri8GCb3iT6Slr34PUWm75Chr8mjocLAf%2BqwnC91VWQFR9qDN%2FI5Y%3D" rel="nofollow">设计模式【6.1】-- 初探适配器模式</a></li><li><a href="https://link.segmentfault.com/?enc=mT0HMgUSvvGhDdRc7WAaFg%3D%3D.6kZf0pxvogv5UiIeIWUOBDKe2mADKhP6rsgGJYXJE9hD9SsoXdIxAY8U0b%2FoVArY2UTPCzv5dJQ4yeZErV%2BZu1A2F3L8GXIVO63vdbKmc8E%3D" rel="nofollow">设计模式【6.2】-- 再聊聊适配器模式</a></li><li><a href="https://link.segmentfault.com/?enc=0dvN76A6%2BiLp2EEswIyHcw%3D%3D.JBGDC%2F06LRnblOukRGxzc12Hphb%2F6kyHbkM7DqjfTHBupivDL4TvUdvyUO%2BDuyULIdmXwmofPjkQ6JcVt%2BA9SVxNoMyNOvxgmoORJ89z1FA%3D" rel="nofollow">设计模式【7】-- 探索一下桥接模式</a></li><li><a href="https://link.segmentfault.com/?enc=%2Fgja%2B%2B5W8YwBr8GkrXuwAw%3D%3D.WK1PxedkaX29JReEXvA6paG9%2FoJMK0NohL6l1oSs5ZeYcctRd7faAxbf6%2FUPp8D3mBFT%2F3PeUlqtx1n4GPtOLwvg68eWeWYpemqwxSScezsJS7In79GPuXQY%2B1oV6PtW" rel="nofollow">设计模式【8】-- 手工耿教我写装饰器模式</a></li><li><a href="https://link.segmentfault.com/?enc=ML%2Ff349mPwSqyNQo%2Bq9J%2BQ%3D%3D.aOXx1TBFXbJArJhyt4iC8IF%2BqJjF2GKVriyFaZ7JYqnpleim1XmWKvDg%2BnyIPq%2Bl7Ohi9WjQ7JKvi0weCAxUhAjKGZkVv7oaEV3TC5e8I5UU36fFOW02c1EYM7ayUoZH" rel="nofollow">设计模式【9】-- 外观模式?没那么高大上</a></li><li><a href="https://link.segmentfault.com/?enc=pQlJ6DulZhRZBjo1dzgdzA%3D%3D.oLA259C06HvTB5607qihWZDpGhpLXQjTfE19WZPuNE0q5eLISIJ%2FK8zUnrWsnsIW0IXqQJDXNwMZEXXQLlNyMflxpP5Xv%2BsZRKVoxJmozYzfl9MoOvenMXhUf%2F%2B8rZBw" rel="nofollow">设计模式【10】-- 顺便看看享元模式</a></li><li><a href="https://link.segmentfault.com/?enc=ZCaN1nd4vnbsNrH0f33gQw%3D%3D.a2FPDHjELgQcS1V42mz3w3kTYIaAHXT57WnbAe1kTK%2FEDoK438RHkZbxPRZ%2FbQRzRooJ80NpzoVvJUgKftP5zEqePUOWsQswegU7szpqGrI%3D" rel="nofollow">设计模式【11】-- 搞定组合模式</a></li></ul>
设计模式【11】-- 搞定组合模式
https://segmentfault.com/a/1190000041305667
2022-01-19T08:49:48+08:00
2022-01-19T08:49:48+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
1
<p><img src="/img/remote/1460000041100740" alt="设计模式" title="设计模式"></p><p>开局还是那种图,各位客官往下看...</p><h2>组合模式是什么?</h2><blockquote>组合模式,将对象组合成树形结构以表示“部分-整体”的层次结构。(百度百科)</blockquote><p>其实,组合模式,又称为部分整体模式,用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。</p><p>关键字:<code>一致性</code>,<code>整体</code>,<code>部分</code></p><p>比如公司的组织架构,就是树形的结构:</p><p><img src="/img/remote/1460000041305669" alt="" title=""></p><p>公司下面有部门与人,人是属于部门,部门可以拥有子部门,如果我们将上面的节点,不管是组织,还是人,统一抽象成为一个<code>node</code>,那么,我们并不需要关心当前节点到底是人,还是部门,统计人数的时候或者遍历的时候,一视同仁。</p><p>还有就是<code>Java Swing</code>编程中,一般也会容器的说法:<code>Container</code>,我们在<code>Container</code>里面可以放子的容器,也可以放具体的组件,比如<code>Button</code>或者<code>Checkbox</code>,其实这也是一种部分-整体的思维。</p><p>除此之外,最经典的是文件夹与文件的表示,一个文件夹(容器对象)既可以存放文件夹(容器对象),也可以存放文件(叶子对象)。如果把树的每个节点摊平,那就是<code>List</code>。而树结构,则是更能直观的体现每个节点与整体的关系。</p><p><strong>为什么需要这个模式呢?它的目的是什么?</strong></p><p>主要是想要对外提供一致性的使用方式,即使容器对象与叶子对象之间属性差别可能非常大,我们希望抽象出相同的地方,一致的处理。</p><h2>组合模式的角色</h2><p>组合模式中一般有以下三种角色:</p><ul><li>抽象构件(<code>Component</code>):一般是接口或者抽象类,是叶子构件和容器构件对象声明接口,抽象出访问以及管理子构件的方法。</li><li>叶子节点(<code>Leaf</code>):在组合中表示叶子节点对象,叶子节点没有子节点,也就没有子构件。</li><li>容器构件(<code>Composite</code>):容器节点可以包含子节点,子节点可以是叶子节点,也可以是容器节点。</li></ul><p>注意:关键点就是抽象构件,所有节点都统一,不再需要调用者关心叶子节点与非叶子节点的差异。</p><h2>组合模式的两种实现</h2><p>组合模式有两种不同的实现,分别是<strong>透明模式</strong>和<strong>安全模式</strong>:</p><p><strong>两者的区别在于透明模式将组合使用的方法放到抽象类中,而安全模式则是放到具体实现类中</strong></p><h3>透明模式</h3><p>透明模式是把组合的方法抽象到抽象类中,不管是叶子节点,还是组合节点,都有一样的方法,这样对外处理的时候是一致的,不过实际上有些方法对叶子节点而言,是没有用的,有些累赘。</p><p><img src="/img/remote/1460000041305670" alt="" title=""></p><p>下面是代码实现:</p><p>抽象类,要求实现三个方法,增加,删除,展示:</p><pre><code class="Java">package designpattern.composite;
public abstract class Component {
String name;
public Component(String name) {
this.name = name;
}
public abstract void add(Component component);
public abstract void remove(Component component);
public abstract void show(int depth);
}
</code></pre><p>组合类:</p><pre><code class="Java">import java.util.ArrayList;
import java.util.List;
public class Composite extends Component {
List<Component> childs = new ArrayList<>();
public Composite(String name) {
super(name);
}
@Override
public void add(Component component) {
this.childs.add(component);
}
@Override
public void remove(Component component) {
this.childs.remove(component);
}
@Override
public void show(int depth) {
for (int i = 0; i < depth; i++) {
System.out.print(" ");
}
System.out.println(name + ": ");
for (Component component : childs) {
component.show(depth + 1);
}
}
}
</code></pre><p>叶子类:</p><pre><code class="Java">
public class Leaf extends Component {
public Leaf(String name) {
super(name);
}
@Override
public void add(Component component) {
}
@Override
public void remove(Component component) {
}
@Override
public void show(int depth) {
for (int i = 0; i < depth; i++) {
System.out.print(" ");
}
System.out.println(name);
}
}
</code></pre><p>测试类:</p><pre><code class="Java">public class Test {
public static void main(String[] args) {
Composite folderRoot = new Composite("备忘录文件夹");
folderRoot.add(new Leaf("word 文件"));
folderRoot.add(new Leaf("ppt 文件"));
Composite folderLevel1 = new Composite("周报文件夹");
folderLevel1.add(new Leaf("20210101周报"));
folderRoot.add(folderLevel1);
Composite folderLevel2 = new Composite("笔记文件夹");
folderLevel2.add(new Leaf("jvm.ppt"));
folderLevel2.add(new Leaf("redis.txt"));
folderLevel1.add(folderLevel2);
folderRoot.add(new Leaf("需求.txt"));
Leaf leaf = new Leaf("bug单.txt");
folderRoot.add(leaf);
folderRoot.remove(leaf);
folderRoot.show(0);
}
}</code></pre><p>运行结果如下:</p><pre><code class="txt">备忘录文件夹:
word 文件
ppt 文件
周报文件夹:
20210101周报
笔记文件夹:
jvm.ppt
redis.txt
需求.txt</code></pre><p>可以看到以上是一棵树的结果,不管是叶子节点,还是组合节点,都是一样的操作。</p><h3>安全模式</h3><p>安全模式,就是叶子节点和组合节点的特性分开,只有组合节点才有增加和删除操作,而两者都会拥有展示操作。但是如果同时对外暴露叶子节点和组合节点的话,使用起来还需要做特殊的判断。</p><p>抽象组件:</p><pre><code class="Java">public abstract class Component {
String name;
public Component(String name) {
this.name = name;
}
public abstract void show(int depth);
}</code></pre><p>组件构件:</p><pre><code class="Java">public class Composite extends Component {
List<Component> childs = new ArrayList<>();
public Composite(String name) {
super(name);
}
public void add(Component component) {
this.childs.add(component);
}
public void remove(Component component) {
this.childs.remove(component);
}
@Override
public void show(int depth) {
for (int i = 0; i < depth; i++) {
System.out.print(" ");
}
System.out.println(name + ": ");
for (Component component : childs) {
component.show(depth + 1);
}
}
}
</code></pre><p>叶子节点:</p><pre><code class="Java">public class Leaf extends Component {
public Leaf(String name) {
super(name);
}
@Override
public void show(int depth) {
for (int i = 0; i < depth; i++) {
System.out.print(" ");
}
System.out.println(name);
}
}</code></pre><p>测试类不变,测试结果也一样:</p><pre><code class="txt">备忘录文件夹:
word 文件
ppt 文件
周报文件夹:
20210101周报
笔记文件夹:
jvm.ppt
redis.txt
需求.txt</code></pre><p>安全模式中,叶子节点没有多余的方法,没有空的方法,外面调用的时候,不会调用到空方法。但是需要对节点进行判断,才能知道哪一个方法能调,哪一个方法不能调。</p><h2>小结一下</h2><p>组合模式的优点:</p><ul><li>可以分层次定义复杂对象,表示局部和全部,客户端可以忽略不同的节点的差异。</li><li>从高层次调用,可以很顺畅的调用到每一个局部,一致性比较强。</li><li>节点自由搭配,灵活度比较高。</li></ul><p>缺点:</p><ul><li>在使用组合模式时,其叶子和组合节点的声明都是实现类,而不是接口,违反了依赖倒置原则。</li></ul><p>使用场景:</p><ul><li>希望忽略每个部分的差异,客户端一致使用</li><li>需要表现为树形结构,以表示“整体-部分”的结构层次。</li></ul><p>以一句网友的话结尾:</p><blockquote>“张无忌学太极拳,忘记了所有招式,打倒了"玄冥二老",所谓"心中无招"。设计模式可谓招数,如果先学通了各种模式,又忘掉了所有模式而随心所欲,可谓OO之最高境界。”</blockquote><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,个人网站:<a href="https://link.segmentfault.com/?enc=hV1a8lcg35nZb0e9MiAglw%3D%3D.8cbDo0bpl2SgCpSicBHpNXd0PJyYh1WCpH9YnVSVk4k%3D" rel="nofollow">http://aphysia.cn</a>,技术之路不在一时,山高水长,纵使缓慢,驰而不息。</p><p><a href="https://link.segmentfault.com/?enc=%2FwByeZb8zbziaKeI8o0ymw%3D%3D.hc8tS5apOpwvzbC0EVGjbQlgxdorb%2FWWVYRJlD3rzLeZCpPL%2F3U530ybONCQa0HT" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=v2UULJY8j1uIJz7d4LyvWg%3D%3D.ddxEgvPFu%2FodKcesD3lq0sxn3TBIvR3cC5tPfPk3%2BSxIgXuwwREPdGYvYNRx%2FwRm" rel="nofollow">开源编程笔记</a></p><p>设计模式系列:</p><ul><li><a href="https://link.segmentfault.com/?enc=mbR0Fld7Zj3eDQ5o82pZnA%3D%3D.j6voMMakUfWhpKacG6tXKybj%2BWpMQikw7%2F%2Bwe4Gqbqx0PZstGkphw79hUnK9wBqi" rel="nofollow">设计模式【1】-- 单例模式到底几种写法?</a></li><li><a href="https://link.segmentfault.com/?enc=EWuoOfDCBKSGjqwuGPbqfA%3D%3D.5ClKfBASDqoW4lP3hiwfi1xISjctWhYQRRZqhWXAcfWjljdeBRWRSkh835%2Bb6G4%2F" rel="nofollow">设计模式【1.1】-- 你想如何破坏单例模式?</a></li><li><a href="https://link.segmentfault.com/?enc=xYQtiUzZ8ZhNPehlocrcnw%3D%3D.xPPKmb4MEzMgQN0jexqY0s1hwab%2BpVDRq2QRSsMh12Eg1AUptmI2snya8Oi6bXlw" rel="nofollow">设计模式【1.2】-- 枚举式单例有那么好用么?</a></li><li><a href="https://link.segmentfault.com/?enc=XnM82WvjnKHumOqkuvja%2Bw%3D%3D.ykoLJ35Fuyb9JPKECCinl%2FiYHMrUoOodK%2BYCxgRryJXJRlnkPU7qBquH6422jpDk" rel="nofollow">设计模式【1.3】-- 为什么饿汉式单例是线程安全的?</a></li><li><a href="https://link.segmentfault.com/?enc=xVymfg02JCd%2FN8sZGv2I1Q%3D%3D.2uS7mr46JbutZYw7%2BtINPUwCDajBH9QOzndRWlKFOwa7nebe3mG8GBfVAGprZwVr" rel="nofollow">设计模式【2】-- 简单工厂模式了解一下?</a></li><li><a href="https://link.segmentfault.com/?enc=WC0IPzO1U5ZOVz69OKqXaQ%3D%3D.eQEf22M04kDEPC%2FBJ5Ep8FGAKPfc2PUPcW51NC5m0rQ22zLfQDsNcFCYiHnrcZZm" rel="nofollow">设计模式【2.1】-- 简单工厂模式怎么演变成工厂方法模式?</a></li><li><a href="https://link.segmentfault.com/?enc=vuISMCpMq7v%2BSOfHyllVhQ%3D%3D.SF0Pr8lU1W77846wYACmtz2ygexF0ofHCSC%2F2zZdHpLxCiZEvEf%2FKxJaxDAXRcnK" rel="nofollow">设计模式【2.2】-- 工厂模式怎么演变成抽象工厂模式?</a></li><li><a href="https://link.segmentfault.com/?enc=zf7sX2%2FUMkG3r%2BKme2YmKw%3D%3D.bOvUDr58ozcphbhUp7GRvalCU4PmaZbzJIpNNbkbKKY41lvx4rKyjMPaLS663p4Xw%2FEw7eLIHyQvU47MvWwddQ%3D%3D" rel="nofollow">设计模式【3.1】-- 浅谈代理模式之静态、动态、cglib代理</a></li><li><a href="https://link.segmentfault.com/?enc=7MzOBOcfwRMDQ2EbB2qdVA%3D%3D.Ty69065F7bvJua9S7WInKROw06w0CSv8XWZm%2FUSAXPSP%2BtqAB3XiUbkPBH4Lhuu2V7Nid8pFndyofGatlw%2FYzFmooNyCl7mFyQoe9tl9EETAK84xIa3fmeR5nxF4LqTn" rel="nofollow">设计模式【3.2】-- JDK动态代理源码分析有多香?</a></li><li><a href="https://link.segmentfault.com/?enc=MiTRS88jNHs3blfPxVsTaA%3D%3D.GaDKa8FPLRLhfWJTREmXZ93svC8r1uz6n%2BJ%2F%2BgtzW3%2Bl95o395UDwVcATp97dewvpnaZzuNAFgetBLzWa8QppRBmfDylGGdi3FStqkwtUBaNGZOh%2BMU8TDLFbZxeV6eK" rel="nofollow">设计模式【3.3】-- CGLIB动态代理源码解读</a></li><li><a href="https://link.segmentfault.com/?enc=E8nVG%2BtF41ziuUmR%2FLu4Sg%3D%3D.b9SJ0ScuqFSs2IAHmqSWQewGs3dFsbhFIhRLYADClwirpdmQ9KpcF0ClPu3hCJbnXxPxl%2F2w3zCmNJvLZpDcm9QSBtIJ2l791qcjw8m521o%3D" rel="nofollow">设计模式【4】-- 建造者模式详解</a></li><li><a href="https://link.segmentfault.com/?enc=RbvGIq%2B9%2BthtnwzrdHwUTw%3D%3D.vhQ%2F729D2o3GLQzMuTGwMYpTQ6PAXu21HR6RWPgXVOxpGsnEXu5dqLKXiRfgAWoYimWlqdLRCJ%2BFdfb8tvoz4Q%3D%3D" rel="nofollow">设计模式【5】-- 原型模式</a></li><li><a href="https://link.segmentfault.com/?enc=pR6%2BcFGOOmsI1YzrRrQl3A%3D%3D.4ddxotBT7byia8fkjiOFeXWhFODu1Oltxl4T%2FX5yB7w5cxmn3pa571XjtR1qhFIVwHrJ%2F%2Bp8dmnMAPPQZXh0wNNzs6eaKInKH7uJJheQdwk%3D" rel="nofollow">设计模式【6.1】-- 初探适配器模式</a></li><li><a href="https://link.segmentfault.com/?enc=ET67mVU4c4NEwEFKymb2aA%3D%3D.vjlA7WGmXKo9Z7eQCKyJ8oP6gzEOehnNvLEsQK4sZLZ35tSgpl0InkM4vl8I3Ht%2BP1FiTcb7z9AM39G8xgXJ4McAB5pDuxuuFokPfFEjjpg%3D" rel="nofollow">设计模式【6.2】-- 再聊聊适配器模式</a></li><li><a href="https://link.segmentfault.com/?enc=%2FbgyDpvACD6DxnLyMWjg6Q%3D%3D.10J0AigHyIZDchbSznvBp%2F8PoJjX6l24KtTmCW5FFTZ6A%2F8d%2F2owgz5zUhbAaSxAR6cPvPFF%2BVoszrMi50Hhi94R6PjZgc9hji6VTdD15sA%3D" rel="nofollow">设计模式【7】-- 探索一下桥接模式</a></li><li><a href="https://link.segmentfault.com/?enc=4BX4DxKqtXNQLDre%2F5vbpg%3D%3D.lStpbAFIrKYxbAVC%2BPCeV2HbVqnZ5itqgiWEZoUmQw5tJ43QLPPKgwiHljopMiWDwsOFwOpOOV%2FnGO0O%2FmUxSFanGSqC1E%2FGUJyZOKNt95%2FxkKiUEh9WNqPhDhFFDPLH" rel="nofollow">设计模式【8】-- 手工耿教我写装饰器模式</a></li><li><a href="https://link.segmentfault.com/?enc=Ix3bDEQ%2FVy3jqmJSwUkpSA%3D%3D.XsItBPlqsNdGSomNxidKWIt%2F8DD%2FHIVFKescQwsiC2eJ%2BiEFYE7cdmOu5PnuCvGl45bjx2Zq9XIzwonN3rWmSJt4vKPLbOIl0ITSfXF96lPDezPeh%2FwMXmCI4hFjGWb0" rel="nofollow">设计模式【9】-- 外观模式?没那么高大上</a></li><li><a href="https://link.segmentfault.com/?enc=2X1NQCaOaYhbrq%2Fd32Mj%2BQ%3D%3D.XQq17idOE%2Bdxvw3y%2BH1hOC93brbFB2VZS17svb%2Ba1YybUOazikzQuTVNbgNid5qxm4eopkBxd7j%2FjeYfBa6iavHeVZwU%2BCoAdsC%2FKe17aOkobaElGGjvYzAFFLK24WwW" rel="nofollow">设计模式【10】-- 顺便看看享元模式</a></li></ul>
万字长文带你漫游数据结构世界
https://segmentfault.com/a/1190000041267822
2022-01-12T08:27:34+08:00
2022-01-12T08:27:34+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p><img src="/img/remote/1460000041267824" alt="" title=""></p><h2>数据结构是什么?</h2><blockquote>程序 = 数据结构 + 算法</blockquote><p>是的,上面这句话是非常经典的,程序由数据结构以及算法组成,当然数据结构和算法也是相辅相成的,不能完全独立来看待,但是本文会相对重点聊聊那些常用的数据结构。</p><p><strong>数据结构是什么呢?</strong></p><p>首先得知道数据是什么?<strong>数据是对客观事务的符号表示</strong>,在计算机科学中是指所有能输入到计算机中并被计算机程序处理的符号总称。那为何加上<strong>“结构”</strong>两字?</p><p><strong>数据元素是数据的基本单位</strong>,而任何问题中,数据元素都不是独立存在的,它们之间总是存在着某种关系,这种<strong>数据元素之间的关系我们称之为结构</strong>。</p><p>因此,我们有了以下定义:</p><blockquote>数据结构是<a href="https://link.segmentfault.com/?enc=UbHgI884ux9DJCxzm2Jeag%3D%3D.Cn5I2ypb4DiaCOcZ1DuTX712dN%2FVUMatpZh3wIaYee1uwMAkmjL01c0XIrtN98%2BZ2FitF9UINxzPgHKwtt7cSw%3D%3D" rel="nofollow">计算机</a>存储、组织<a href="https://link.segmentfault.com/?enc=N9dLPAF4XPbqAqukxI4gTQ%3D%3D.%2BrF8cVDUzuza3qhBgoBAxVnk4oQZGXjZ9EFknwLTYfHy%2Bpxhhpzs25ge6AOq88M5" rel="nofollow">数据</a>的方式。数据结构是指相互之间存在一种或多种特定关系的<a href="https://link.segmentfault.com/?enc=Lsumi%2BXh9W3zPpXgU1DJ%2FQ%3D%3D.FlG6x%2FIf5KQ30I42UcCaieoXeTJL4Eu4LR1RdhmwRhu2GaB%2BzXMHqNui17YhuzldgPZggGuuB33XPW8axVVCTE61Qjtq9ojhMFs2621YMF8%3D" rel="nofollow">数据元素</a>的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储<a href="https://link.segmentfault.com/?enc=%2BXq7ZbjnboiNcdz%2FWTkaEQ%3D%3D.GDTrGZLvbswZqnBkpyn4zPzYjcZvieQ7CxXjkJ%2FltYBSuZAKVXSyO%2BE8JiM6TELnZdqg2x9BBQYJ8sGci7%2FnGQ%3D%3D" rel="nofollow">效率</a>。数据结构往往同高效的检索<a href="https://link.segmentfault.com/?enc=NGP3aeJsFbp3Z4zzLW5SVg%3D%3D.DPy9jHXKCEbs3PrXazU%2BQ7vIKAz2PMaadmDK3P4%2F%2FkfNWKRJvA2O%2FWtqItr9G2l0QArx%2FKZqadcMXGZljj%2BWxw%3D%3D" rel="nofollow">算法</a>和<a href="https://link.segmentfault.com/?enc=tD%2Byihfy0ByYRNaeXFRR3w%3D%3D.9%2FTA4wsmnfl0AleZVzO23wzVg8Muv1R5yvIWBg4BdIAyu1VHxvDAaWlR3IGmO68nn1uCohBKjmiyQDoncxk5qA%3D%3D" rel="nofollow">索引</a>技术有关。</blockquote><p>简单讲,数据结构就是组织,管理以及存储数据的方式。虽然理论上所有的数据都可以混杂,或者糅合,或者饥不择食,随便存储,但是计算机是追求高效的,如果我们能了解数据结构,找到较为适合当前问题场景的数据结构,将数据之间的关系表现在存储上,计算的时候可以较为高效的利用适配的算法,那么程序的运行效率肯定也会有所提高。</p><p>常用的4种数据结构有:</p><ul><li>集合:只有同属于一个集合的关系,没有其他关系</li><li>线性结构:结构中的数据元素之间存在一个对一个的关系</li><li>树形结构:结构中的数据元素之间存在一个对多个的关系</li><li>图状结构或者网状结构:图状结构或者网状结构</li></ul><p><img src="/img/remote/1460000041267825" alt="" title=""></p><p><strong>何为逻辑结构和存储结构?</strong></p><p><strong>数据元素之间的逻辑关系,称之为逻辑结构</strong>,也就是我们定义了对操作对象的一种数学描述。但是我们还必须知道在计算机中如何表示它。<strong>数据结构在计算机中的表示(又称为映像),称之为数据的物理结构,又称存储结构</strong>。</p><p>数据元素之前的关系在计算机中有两种不同的表示方法:<strong>顺序映像和非顺序映像</strong>,并且由此得到两种不同的存储结构:<strong>顺序存储结构</strong>和<strong>链式存储结构</strong>,比如顺序存储结构,我们要表示复数<code>z1 =3.0 - 2.3i </code>,可以直接借助元素在存储器中的相对位置来表示数据元素之间的逻辑关系:</p><p><img src="/img/remote/1460000041267826" alt="" title=""></p><p>而链式结构,则是以<strong>指针</strong>表示数据元素之间的逻辑关系,同样是<code>z1 =3.0 - 2.3i </code>,先找到下一个是 <code>100</code>,是一个地址,根据地址找到真实的数据<code>-2.3i</code>:</p><p><img src="/img/remote/1460000041267827" alt="" title=""></p><h3>位(bit)</h3><p>在计算机中表示信息的最小的单位是二进制数中的一位,叫做<strong>位</strong>。也就是我们常见的类似<code>01010101010</code>这种数据,计算机的底层就是各种晶体管,电路板,所以不管是什么数据,即使是图片,声音,在最底层也是<code>0</code>和<code>1</code>,如果有八条电路,那么每条电路有自己的闭合状态,有<code>8</code>个<code>2</code>相乘,2^8^,也就是<code>256</code>种不同的信号。</p><p>但是一般我们需要表示负数,也就是最高的一位表示符号位,<code>0</code>表示正数,<code>1</code>表示负数,也就是8位的最大值是<code>01111111</code>,也就是<code>127</code>。</p><p>值得我们注意的是,计算机的世界里,多了原码,反码,补码的概念:</p><ul><li>原码:用第一位表示符号,其余位表示值</li><li>反码:正数的补码反码是其本身,负数的反码是符号位保持不变,其余位取反。</li><li>补码:正数的补码是其本身,负数的补码是在其反码的基础上 + 1</li></ul><h4>为什么有了原码还要反码和补码?</h4><p>我们知道加减法是高频的运算,人可以很直观的看出加号减号,马上就可以算出来,但是计算机如果区分不同的符号,那么加减就会比较复杂,比如正数+正数,正数-正数,正数-负数,负数+负数...等等。于是,有人就想用同一个运算器(加号运算器),解决所有的加减法计算,可以减少很多复杂的电路,以及各种符号转换的开销,计算也更加高效。</p><p>我们可以看到,下面负数参加运算的结果也是符合补码的规则的:</p><pre><code class="txt"> 00100011 35
+ 11011101 -35
-------------------------
00000000 0</code></pre><pre><code class="txt"> 00100011 35
+ 11011011 -37
-------------------------
11111110 -2</code></pre><p>当然,如果计算结果超出了位数所能表示的范围,那就是溢出,就说明需要更多的位数才能正确表示。</p><p>一般能用位运算的,都尽量使用位运算,因为它比较高效, 常见的位运算:</p><ul><li><code>~</code>:按位取反</li><li><code>&</code>:按为与运算</li><li><code>|</code>:按位或运算</li><li><code>^</code>:按位异或</li><li><code><<</code>: 带符号左移,比如<code>35(00100011)</code>,左移一位为 <code>70(01000110)</code>,<code>-35(11011101)</code>左移一位为<code>-70(10111010)</code></li><li><code>>></code>:带符号右移,比如<code>35(00100011)</code>,右移一位为 <code>17(00010001)</code>,<code>-35(11011101)</code>左移一位为<code>-18(11101110)</code></li><li><code><<<</code>:无符号左移,比如<code>35(00100011)</code>,左移一位为<code>70(01000110)</code></li><li><code>>>></code>:无符号右移,比如<code>-35(11011101)</code>,右移一位为<code>110(01101110)</code></li><li><code>x ^= y; y ^= x; x ^= y;</code>:交换</li><li><code>s &= ~(1 << k)</code>:第<code>k</code>位置0</li></ul><p>要说哪里使用位运算比较经典,那么要数<strong>布隆过滤器</strong>,需要了解详情的可以参考:<a href="https://link.segmentfault.com/?enc=xpW8EjPQwC4E2Rh3MrM2sA%3D%3D.mys%2BLG4pzontR899L2t3%2F3qKeBWnWgMIV%2BJulL2N527Ujk37Z5uqLY5KRBl4%2BdUW" rel="nofollow">http://aphysia.cn/archives/ca...</a></p><h4>布隆过滤器是什么呢?</h4><p>布隆过滤器(<code>Bloom Filter</code>)是由布隆(<code>Burton Howard Bloom</code>)在1970年提出的,它实际上是由一个很长的二进制向量和一系列随机hash映射函数组成(说白了,就是用二进制数组存储数据的特征)。可以使用它来判断一个元素是否存在于集合中,它的优点在于查询效率高,空间小,缺点是存在一定的误差,以及我们想要剔除元素的时候,可能会相互影响。</p><p>也就是当一个元素被加入集合的时候,通过多个<code>hash</code>函数,将元素映射到位数组中的<code>k</code>个点,置为<code>1</code>。</p><p><strong>重点是多个hash函数,可以将数据hash到不同的位上,也只有这些位全部为1的时候,我们才能判断该数据已经存在</strong></p><p>假设有三个<code>hash</code>函数,那么不同的元素,都会使用三个<code>hash</code>函数,<code>hash</code>到三个位置上。</p><p><img src="/img/remote/1460000039724780" alt="" title=""></p><p>假设后面又来了一个张三,那么在<code>hash</code>的时候,同样会<code>hash</code>到以下位置,所有位都是<code>1</code>,我们就可以说张三已经存在在里面了。</p><p><img src="/img/remote/1460000039724781" alt="" title=""></p><p>那么有没有可能出现误判的情况呢?这是有可能的,比如现在只有张三,李四,王五,蔡八,<code>hash</code>映射值如下:</p><p><img src="/img/remote/1460000039724779" alt="" title=""></p><p>后面来了陈六,但是不凑巧的是,它<code>hash</code>的三个函数hash出来的位,刚刚好就是被别的元素<code>hash</code>之后,改成<code>1</code>了,判断它已经存在了,但是实际上,陈六之前是不存在的。</p><p><img src="/img/remote/1460000039724782" alt="" title=""></p><p>上面的情况,就是误判,布隆过滤器都会不可避免的出现误判。但是它有一个好处是,<strong>布隆过滤器,判断存在的元素,可能不存在,但是判断不存在的元素,一定不存在。</strong>,因为判断不存在说明至少有一位<code>hash</code>出来是对不上的。</p><p>也是由于会出现多个元素可能<code>hash</code>到一起,但有一个数据被踢出了集合,我们想把它映射的位,置为<code>0</code>,相当于删除该数据。这个时候,就会影响到其他的元素,可能会把别的元素映射的位,置为了<code>0</code>。这也就是为什么布隆过滤器不能删除的原因。</p><h3>数组</h3><p>线性表示最常用而且最为简单的一种数据结构,一个线性表示 n 个数据元素的有限序列,有以下特点:</p><ul><li>存在唯一的第一个的数据元素</li><li>存在唯一被称为最后一个的数据元素</li><li>除了第一个以外,集合中每一个元素均有一个前驱</li><li>除了最后一个元素之外,集合中的每一个数据元素都有一个后继元素</li></ul><p>线性表包括下面几种:</p><ul><li>数组:查询 / 更新快,查找/删除慢</li><li>链表</li><li>队列</li><li>栈</li></ul><p><strong>数组是线性表的一种,线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素</strong>:</p><p><img src="/img/remote/1460000041267828" alt="" title=""></p><p>在<code>Java</code>中表示为:</p><pre><code class="Java">int[] nums = new int[100];
int[] nums = {1,2,3,4,5};
Object[] Objects = new Object[100];</code></pre><p>在<code>C++</code> 中表示为:</p><pre><code class="C++">int nums[100];</code></pre><p>数组是一种线性的结构,一般在底层是连续的空间,存储相同类型的数据,由于连续紧凑结构以及天然索引支持,查询数据效率高:</p><p>假设我们知道数组<code>a</code>的第 1 个值是 地址是 <code>296</code>,里面的数据类型占 <code>2</code> 个 单位,那么我们如果期望得到第 5 个: <code>296+(5-1)*2 = 304</code>,<code>O(1)</code>的时间复杂度就可以获取到。</p><p>更新的本质也是查找,先查找到该元素,就可以动手更新了:</p><p><img src="/img/remote/1460000041267829" alt="" title=""></p><p>但是如果期望插入数据的话,需要移动后面的数据,比如下面的数组,插入元素<code>6</code>,最差的是移动所有的元素,时间复杂度为<code>O(n)</code></p><p><img src="/img/remote/1460000041267830" alt="image-20220104225524289" title="image-20220104225524289"></p><p>而删除元素则需要把后面的数据移动到前面,最差的时间复杂度同样为<code>O(n)</code>:</p><p><img src="/img/remote/1460000041267831" alt="" title=""></p><p>Java代码实现数组的增删改查:</p><pre><code class="Java">package datastruction;
import java.util.Arrays;
public class MyArray {
private int[] data;
private int elementCount;
private int length;
public MyArray(int max) {
length = max;
data = new int[max];
elementCount = 0;
}
public void add(int value) {
if (elementCount == length) {
length = 2 * length;
data = Arrays.copyOf(data, length);
}
data[elementCount] = value;
elementCount++;
}
public int find(int searchKey) {
int i;
for (i = 0; i < elementCount; i++) {
if (data[i] == searchKey)
break;
}
if (i == elementCount) {
return -1;
}
return i;
}
public boolean delete(int value) {
int i = find(value);
if (i == -1) {
return false;
}
for (int j = i; j < elementCount - 1; j++) {
data[j] = data[j + 1];
}
elementCount--;
return true;
}
public boolean update(int oldValue, int newValue) {
int i = find(oldValue);
if (i == -1) {
return false;
}
data[i] = newValue;
return true;
}
}
// 测试类
public class Test {
public static void main(String[] args) {
MyArray myArray = new MyArray(2);
myArray.add(1);
myArray.add(2);
myArray.add(3);
myArray.delete(2);
System.out.println(myArray);
}
}</code></pre><h3>链表</h3><p>上面的例子中,我们可以看到数组是需要连续的空间,这里面如果空间大小只有 <code>2</code>,放到第 <code>3</code> 个元素的时候,就不得不扩容,不仅如此,还得拷贝元素。一些删除,插入操作会引起较多的数据移动的操作。</p><p>链表,也就是链式数据结构,由于它不要求逻辑上相邻的数据元素在物理位置上也相邻,所以它没有顺序存储结构所具有的缺点,但是同时也失去了通过索引下标直接查找元素的优点。</p><p>重点:<strong>链表在计算机的存储中不是连续的,而是前一个节点存储了后一个节点的指针(地址),通过地址找到后一个节点。</strong></p><p>下面是单链表的结构:</p><p><img src="/img/remote/1460000041267832" alt="" title=""></p><p>一般我们会手动在单链表的前面设置一个前置结点,也可以称为头结点,但是这并非绝对:</p><p><img src="/img/remote/1460000041267833" alt="" title=""></p><p>一般链表结构分为以下几种:</p><ul><li><strong>单向链表</strong>:链表中的每一个结点,都有且只有一个指针指向下一个结点,并且最后一个节点指向空。</li><li><strong>双向链表</strong>:每个节点都有两个指针(为方便,我们称之为<strong>前指针</strong>,<strong>后指针</strong>),分别指向上一个节点和下一个节点,第一个节点的前指针指向<code>NULL</code>,最后一个节点的后指针指向<code>NULL</code></li><li><strong>循环链表</strong>:每一个节点的指针指向下一个节点,并且最后一个节点的指针指向第一个节点(虽然是循环链表,但是必要的时候还需要标识头结点或者尾节点,避免死循环)</li><li><strong>复杂链表</strong>:每一个链表有一个后指针,指向下一个节点,同时有一个随机指针,指向任意一个结点。</li></ul><p><img src="/img/remote/1460000041267834" alt="" title=""></p><p>链表操作的时间复杂度:</p><ul><li>查询:<code>O(n)</code>,需要遍历链表</li><li>插入:<code>O(1)</code>,修改前后指针即可</li><li>删除:<code>O(1)</code>,同样是修改前后指针即可</li><li>修改:不需要查询则为<code>O(1)</code>,需要查询则为<code>O(n)</code></li></ul><p><strong>链表的结构代码怎么表示呢?</strong></p><p>下面只表示单链表结构,<code>C++</code>表示:</p><pre><code class="C++">// 结点
typedef struct LNode{
// 数据
ElemType data;
// 下一个节点的指针
struct LNode *next;
}*Link,*Position;
// 链表
typedef struct{
// 头结点,尾节点
Link head,tail;
// 长度
int len;
}LinkList;</code></pre><p><code>Java</code> 代码表示:</p><pre><code class="Java"> public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}</code></pre><p>自己实现简单链表,实现增删改查功能:</p><pre><code class="Java">class ListNode<T> {
T val;
ListNode next = null;
ListNode(T val) {
this.val = val;
}
}
public class MyList<T> {
private ListNode<T> head;
private ListNode<T> tail;
private int size;
public MyList() {
this.head = null;
this.tail = null;
this.size = 0;
}
public void add(T element) {
add(size, element);
}
public void add(int index, T element) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("超出链表长度范围");
}
ListNode current = new ListNode(element);
if (index == 0) {
if (head == null) {
head = current;
tail = current;
} else {
current.next = head;
head = current;
}
} else if (index == size) {
tail.next = current;
tail = current;
} else {
ListNode preNode = get(index - 1);
current.next = preNode.next;
preNode.next = current;
}
size++;
}
public ListNode get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("超出链表长度");
}
ListNode temp = head;
for (int i = 0; i < index; i++) {
temp = temp.next;
}
return temp;
}
public ListNode delete(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("超出链表节点范围");
}
ListNode node = null;
if (index == 0) {
node = head;
head = head.next;
} else if (index == size - 1) {
ListNode preNode = get(index - 1);
node = tail;
preNode.next = null;
tail = preNode;
} else {
ListNode pre = get(index - 1);
pre.next = pre.next.next;
node = pre.next;
}
size--;
return node;
}
public void update(int index, T element) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("超出链表节点范围");
}
ListNode node = get(index);
node.val = element;
}
public void display() {
ListNode temp = head;
while (temp != null) {
System.out.print(temp.val + " -> ");
temp = temp.next;
}
System.out.println("");
}
}
</code></pre><p>测试代码如下:</p><pre><code class="java">public class Test {
public static void main(String[] args) {
MyList myList = new MyList();
myList.add(1);
myList.add(2);
// 1->2
myList.display();
// 1
System.out.println(myList.get(0).val);
myList.update(1,3);
// 1->3
myList.display();
myList.add(4);
// 1->3->4
myList.display();
myList.delete(1);
// 1->4
myList.display();
}
}</code></pre><p>输出结果:</p><pre><code class="java">1 -> 2 ->
1
1 -> 3 ->
1 -> 3 -> 4 ->
1 -> 4 -></code></pre><p>单向链表的查找更新比较简单,我们看看插入新节点的具体过程(这里只展示中间位置的插入,头尾插入比较简单):</p><p><img src="/img/remote/1460000041267835" alt="" title=""></p><p><img src="/img/remote/1460000041267836" alt="" title=""></p><p>那如何删除一个中间的节点呢?下面是具体的过程:</p><p><img src="/img/remote/1460000041267837" alt="image-20220108114627633" title="image-20220108114627633"></p><p>或许你会好奇,<code>a5</code>节点只是指针没有了,那它去哪里了?</p><p>如果是<code>Java</code>程序,垃圾回收器会收集这种没有被引用的节点,帮我们回收掉了这部分内存,但是为了加快垃圾回收的速度,一般不需要的节点我们需要置空,比如 <code>node = null</code>, 如果在<code>C++</code> 程序中,那么就需要手动回收了,否则容易造成内存泄漏等问题。</p><p>复杂链表的操作暂时讲到这里,后面我会单独把链表这一块的数据结构以及常用算法单独分享一下,本文章主要讲数据结构全貌。</p><h4>跳表</h4><p>上面我们可以观察到,链表如果搜索,是很麻烦的,如果这个节点在最后,需要遍历所有的节点,才能找到,查找效率实在太低,有没有什么好的办法呢?</p><p>办法总比问题多,但是想要绝对的”<code>多快好省</code>“是不存在的,有舍有得,计算机的世界里,充满哲学的味道。既然搜索效率有问题,那么我们不如给链表排个序。排序后的链表,还是只能知道头尾节点,知道中间的范围,但是要找到中间的节点,还是得走遍历的老路。如果我们把中间节点存储起来呢?存起来,确实我们就知道数据在前一半,还是在后一半。比如找<code>7</code>,肯定就从中间节点开始找。如果查找<code>4</code>,就得从头开始找,最差到中间节点,就停止查找。</p><p><img src="/img/remote/1460000041267838" alt="" title=""></p><p>但是如此,还是没有彻底解决问题,因为链表很长的情况,只能通过前后两部分查找。不如回到原则:<code>空间和时间,我们选择时间,那就要舍弃一部分空间</code>,我们每个节点再加一个指针,现在有 2 层指针(注意:<strong>节点只有一份,都是同一个节点,只是为了好看,弄了两份,实际上是同一个节点,有两个指针,比如 1 ,既指向2,也指向5</strong>):</p><p><img src="/img/remote/1460000041267839" alt="" title=""></p><p>两层指针,问题依然存在,那就不断加层,比如每两个节点,就加一层:</p><p><img src="/img/remote/1460000041267840" alt="" title=""></p><p>这就是跳表了,跳表的定义如下:</p><blockquote>跳表(SkipList,全称跳跃表)是用于有序元素序列快速搜索查找的一个数据结构,跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。它在性能上和红黑树,AVL树不相上下,但是跳表的原理非常简单,实现也比红黑树简单很多。</blockquote><p>主要的原理是用空间换时间,可以实现近乎二分查找的效率,实际上消耗的空间,假设每两个加一层, <code>1 + 2 + 4 + ... + n = 2n-1</code>,多出了差不多一倍的空间。你看它像不像书的目录,一级目录,二级,三级 ...</p><p><img src="/img/remote/1460000041267841" alt="" title=""></p><p>如果我们不断往跳表中插入数据,可能出现某一段节点会特别多的情况,这个时候就需要动态更新索引,除了插入数据,还要插入到上一层的链表中,保证查询效率。</p><p><code>redis</code> 中使用了跳表来实现<code>zset</code>,<code>redis</code>中使用一个随机算法来计算层级,计算出每个节点到底多少层索引,虽然不能绝对保证比较平衡,但是基本保证了效率,实现起来比那些平衡树,红黑树的算法简单一点。</p><h3>栈</h3><p>栈是一种数据结构,在<code>Java</code>里面体现是<code>Stack</code>类。它的本质是<strong>先进后出</strong>,就像是一个桶,只能不断的放在上面,取出来的时候,也只能不断的取出最上面的数据。要想取出底层的数据,只有等到上面的数据都取出来,才能做到。当然,如果有这种需求,我们一般会使用双向队列。</p><p>以下是栈的特性演示:</p><p><img src="/img/remote/1460000041252292" alt="" title=""></p><p>栈的底层用什么实现的?其实可以用链表,也可以用数组,但是<code>JDK</code>底层的栈,是用数组实现的,封装之后,通过<code>API</code>操作的永远都只能是最后一个元素,栈经常用来实现递归的功能。如果想要了解<code>Java</code>里面的栈或者其他集合实现分析,可以看看这系列文章:<a href="https://link.segmentfault.com/?enc=TO1KP64l%2BeQYR0kftmX83Q%3D%3D.qrAu7uyt0c%2BZBzAxlhxYcVivgGKhh%2FPulepVr6pso3lipKaQ507SyleZ%2FxFgKm4X" rel="nofollow">http://aphysia.cn/categories/...</a></p><p>元素加入称之为入栈(压栈),取出元素,称之为出栈,栈顶元素则是最后一次放进去的元素。</p><p>使用数组实现简单的栈(注意仅供参考测试,实际会有线程安全等问题):</p><pre><code class="Java">import java.util.Arrays;
public class MyStack<T> {
private T[] data;
private int length = 2;
private int maxIndex;
public MyStack() {
data = (T[]) new Object[length];
maxIndex = -1;
}
public void push(T element) {
if (isFull()) {
length = 2 * length;
data = Arrays.copyOf(data, length);
}
data[maxIndex + 1] = element;
maxIndex++;
}
public T pop() {
if (isEmpty()) {
throw new IndexOutOfBoundsException("栈内没有数据");
} else {
T[] newdata = (T[]) new Object[data.length - 1];
for (int i = 0; i < data.length - 1; i++) {
newdata[i] = data[i];
}
T element = data[maxIndex];
maxIndex--;
data = newdata;
return element;
}
}
private boolean isFull() {
return data.length - 1 == maxIndex;
}
public boolean isEmpty() {
return maxIndex == -1;
}
public void display() {
for (int i = 0; i < data.length; i++) {
System.out.print(data[i]+" ");
}
System.out.println("");
}
}
</code></pre><p>测试代码:</p><pre><code class="Java">public class MyStackTest {
public static void main(String[] args) {
MyStack<Integer> myStack = new MyStack<>();
myStack.push(1);
myStack.push(2);
myStack.push(3);
myStack.push(4);
myStack.display();
System.out.println(myStack.pop());
myStack.display();
}
}</code></pre><p>输出结果如下,符合预期:</p><pre><code class="text">1 2 3 4
4
1 2 3 </code></pre><p>栈的特点就是先进先出,但是如果需要随机取出前面的数据,效率会比较低,需要倒腾出来,但是如果底层使用数组,理论上是可以通过索引下标取出的,<code>Java</code>里面正是这样实现。</p><h3>队列</h3><p>既然前面有先进后出的数据结构,那我们必定也有先进先出的数据结构,疫情的时候,排队估计大家都有测过核酸,那排队老长了,排在前面先测,排在后面后测,这道理大家都懂。</p><p><img src="/img/remote/1460000041267842" alt="" title=""></p><blockquote>队列是一种特殊的<a href="https://link.segmentfault.com/?enc=6eJk8Cg81rzXdz9eGTqWhA%3D%3D.MOLRZe1iXzv1XtHr76f%2BREVjG9L%2Fl8I4p95UbG8yLY%2BEdlbUibxj34YayD66IkYzgymGwk4yikw1ek7r3vOyrQU9J24QHO5dJVy%2FVwSONl0%3D" rel="nofollow">线性表</a>,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。</blockquote><p>队列的特点是先进先出,以下是例子:</p><p><img src="/img/remote/1460000041267843" alt="" title=""></p><p>一般只要说到先进先出(<code>FIFO</code>),全称<code>First In First Out</code>,就会想到队列,但是如果你想拥有队列即可以从队头取出元素,又可以从队尾取出元素,那就需要用到特殊的队列(双向队列),双向队列一般使用双向链表实现会简单一点。</p><p>下面我们用<code>Java</code>实现简单的单向队列:</p><pre><code class="Java">class Node<T> {
public T data;
public Node next;
public Node(T data) {
this.data = data;
}
}
public class MyQueue<T> {
private Node<T> head;
private Node<T> rear;
private int size;
public MyQueue() {
size = 0;
}
public void pushBack(T element) {
Node newNode = new Node(element);
if (isEmpty()) {
head = newNode;
} else {
rear.next = newNode;
}
rear = newNode;
size++;
}
public boolean isEmpty() {
return head == null;
}
public T popFront() {
if (isEmpty()) {
throw new NullPointerException("队列没有数据");
} else {
Node<T> node = head;
head = head.next;
size--;
return node.data;
}
}
public void dispaly() {
Node temp = head;
while (temp != null) {
System.out.print(temp.data +" -> ");
temp = temp.next;
}
System.out.println("");
}
}</code></pre><p>测试代码如下:</p><pre><code class="Java">public class MyStackTest {
public static void main(String[] args) {
MyStack<Integer> myStack = new MyStack<>();
myStack.push(1);
myStack.push(2);
myStack.push(3);
myStack.push(4);
myStack.display();
System.out.println(myStack.pop());
myStack.display();
}
}</code></pre><p>运行结果:</p><pre><code class="Java">1 -> 2 -> 3 ->
1
2 -> 3 ->
2
3 -> </code></pre><p>常用的队列类型如下:</p><ul><li>单向队列:也就是我们说的普通队列,先进先出。</li><li>双向队列:可以从不同方向进出队列</li><li>优先队列:内部是自动排序的,按照一定顺序出队列</li><li>阻塞队列:从队列取出元素的时候,队列没有元素则会阻塞,同样如果队列满了,往队列里面放入元素也会被阻塞。</li><li>循环队列:可以理解为一个循环链表,但是一般需要标识出头尾节点,防止死循环,尾节点的<code>next</code>指向头结点。</li></ul><p>队列一般可以用来保存需要顺序的数据,或者保存任务,在树的层次遍历中可以使用队列解决,一般广度优先搜索都可以使用队列解决。</p><h3>哈希表</h3><p>前面的数据结构,查找的时候,一般都是使用<code>=</code>或者<code>!=</code>,在折半查找或者其他范围查询的时候,可能会使用<code><</code>和<code>></code>,理想的时候,我们肯定希望不经过任何的比较,直接能定位到某个位置(存储位置),这种在数组中,可以通过索引取得元素。那么,如果我们将需要存储的数据和数组的索引对应起来,并且是一对一的关系,那不就可以很快定位到元素的位置了么?</p><p>只要通过函数<code>f(k)</code>就能找到<code>k</code>对应的位置,这个函数<code>f(k)</code>就是<code>hash</code>函数。它表示的是一种映射关系,但是对不同的值,可能会映射到同一个值(同一个<code>hash</code>地址),也就是<code>f(k1) = f(k2)</code>,这种现象我们称之为<code>冲突</code>或者<code>碰撞</code>。</p><p><code>hash</code>表定义如下:</p><blockquote>散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存储存位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。</blockquote><p><img src="/img/remote/1460000041267844" alt="" title=""></p><p>一般常用的<code>hash</code> 函数有:</p><ul><li>直接定址法:取出关键字或者关键字的某个线性函数的值为哈希函数,比如<code>H(key) = key</code>或者<code>H(key) = a * key + b</code></li><li>数字分析法:对于可能出现的数值全部了解,取关键字的若干数位组成哈希地址</li><li>平方取中法:取关键字平方后的中间几位作为哈希地址</li><li>折叠法:将关键字分割成为位数相同的几部分(最后一部分的位数可以不同),取这几部分的叠加和(舍去进位),作为哈希地址。</li><li>除留余数法:取关键字被某个不大于散列表表长<code>m</code>的数<code>p</code>除后所得的余数为散列地址。即h<code>ash(k)=k mod p</code>,<code>p< =m</code>。不仅可以对关键字直接取模,也可在折叠法、平方取中法等运算之后取模。对<code>p</code>的选择很重要,一般取素数或<code>m</code>,若<code>p</code>选择不好,容易产生冲突。</li><li>随机数法:取关键字的随机函数值作为它的哈希地址。</li></ul><p>但是这些方法,都无法避免哈希冲突,只能有意识的减少。那处理<code>hash</code>冲突,一般有哪些方法呢?</p><ul><li>开放地址法:<code>hash</code>计算后,如果该位置已经有数据,那么对该地址<code>+1</code>,也就是往后找,知道找到一个空的位置。</li><li>重新<code>hash</code>法:发生哈希冲突后,可以使用另外的<code>hash</code>函数重新极计算,找到空的<code>hash</code>地址,如果有,还可以再叠加<code>hash</code>函数。</li><li>链地址法:所有<code>hash</code>值一样的,链接成为一个链表,挂在数组后面。</li><li>建立公共溢出区:不常见,意思是所有元素,如果和表中的元素<code>hash</code>冲突,都弄到另外一个表,也叫溢出表。</li></ul><p><code>Java</code>里面,用的就是链地址法:</p><p><img src="/img/remote/1460000041267845" alt="" title=""></p><p>但是如果<code>hash</code>冲突比较严重,链表会比较长,查询的时候,需要遍历后面的链表,因此<code>JDK</code>优化了一版,链表的长度超过阈值的时候,会变成<strong>红黑树</strong>,红黑树有一定的规则去平衡子树,避免退化成为链表,影响查询效率。</p><p><img src="/img/remote/1460000041267846" alt="" title=""></p><p>但是你肯定会想到,如果数组太小了,放了比较多数据了,怎么办?再放冲突的概率会越来越高,其实这个时候会触发一个扩容机制,将数组扩容成为 <code>2</code>倍大小,重新<code>hash</code>以前的数据,哈希到不同的数组中。</p><p><code>hash</code>表的优点是查找速度快,但是如果不断触发重新 <code>hash</code>, 响应速度也会变慢。同时,如果希望范围查询,<code>hash</code>表不是好的选择。</p><h3>树</h3><p>数组和链表都是线性结构,而这里要介绍的树,则是非线性结构。现实中树是金字塔结构,数据结构中的树,最上面称之为根节点。</p><p><img src="/img/remote/1460000041267847" alt="" title=""></p><p>我们该如何定义树结构呢?</p><blockquote><p><strong>树</strong>是一种<a href="https://link.segmentfault.com/?enc=KAAbnpnrHL1GKfabpQCKbg%3D%3D.4Di2I8QH10cEW8Orkidi%2FV4ll8ERZF0TUTUGCC%2BVhHtuTKOSYCo1t9vbpX8Cg17PKvbngv31B1l3kwayABKBJOZQ0%2FBBOnqQeAOApbIQs7I%3D" rel="nofollow">数据结构</a>,它是由<em>n(n≥1</em>)个有限节点组成一个具有层次关系的<a href="https://link.segmentfault.com/?enc=03KJMC3HXj6ZNgkHl635GQ%3D%3D.SApVklePxxh%2Bw67U1CAnqaTDCuqXKNZ21aYF0kE98PC8zgS%2FhE38P28EBLWnHb1IDbh3i4Fb6yojCYdzK2XHrA%3D%3D" rel="nofollow">集合</a>。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:</p><p>每个节点有零个或多个子节点;没有父节点的节点称为根节点;每一个非根节点有且只有一个父节点;除了根节点外,每个子节点可以分为多个不相交的子树。(百度百科)</p></blockquote><p>下面是树的基本术语(来自于清华大学数据结构<code>C</code>语言版):</p><ul><li>节点的度:一个节点含有的子树的个数称为该节点的度</li><li>树的度:一棵树中,最大的节点度称为树的度;</li><li>叶节点或终端节点:度为零的节点;</li><li>非终端节点或分支节点:度不为零的节点;</li><li>父亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;</li><li>孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;</li><li>兄弟节点:具有相同父节点的节点互称为兄弟节点;</li><li>节点的层次:从根开始定义起,根为第<code>1</code>层,根的子节点为第<code>2</code>层,以此类推;</li><li>深度:对于任意节点<code>n</code>,<code>n</code>的深度为从根到n的唯一路径长,根的深度为<code>0</code>;</li><li>高度:对于任意节点<code>n</code>,<code>n</code>的高度为从<code>n</code>到一片树叶的最长路径长,所有树叶的高度为<code>0</code>;</li><li>堂兄弟节点:父节点在同一层的节点互为堂兄弟;</li><li>节点的祖先:从根到该节点所经分支上的所有节点;</li><li>子孙:以某节点为根的子树中任一节点都称为该节点的子孙。</li><li>有序树:将树种的节点的各个子树看成从左至右是有次序的(不能互换),则应该称该树为有序树,否则为无序树</li><li>第一个孩子:在有序树中最左边的子树的根称为第一个孩子</li><li>最后一个孩子:在有序树种最右边的子树的根称为最后一个孩子</li><li>森林:由<code>m</code>(<code>m>=0</code>)棵互不相交的树的集合称为森林;</li></ul><p>树,其实我们最常用的是二叉树:</p><p><img src="/img/remote/1460000041267848" alt="" title=""></p><p>二叉树的特点是每个节点最多只有两个子树,并且子树有左右之分,左右子节点的次序不能任意颠倒。</p><p>二叉树在<code>Java</code>中表示:</p><pre><code class="Java">public class TreeLinkNode {
int val;
TreeLinkNode left = null;
TreeLinkNode right = null;
TreeLinkNode next = null;
TreeLinkNode(int val) {
this.val = val;
}
}</code></pre><p>满二叉树:一棵深度为 k 且有 2<sup>k</sup>-1 个节点的二叉树,称之为满二叉树</p><p>完全二叉树:深度为 k 的,有 n 个节点的二叉树,当且仅当其每一个节点都与深度为 k 的满二叉树中编号从 1 到 n 的节点一一对应是,称之为完全二叉树。</p><p><img src="/img/remote/1460000041267849" alt="" title=""></p><p>一般二叉树的遍历有几种:</p><ul><li>前序遍历:遍历顺序 根节点 --> 左子节点 --> 右子节点</li><li>中序遍历:遍历顺序 左子节点 --> 根节点 --> 右子节点</li><li>后序遍历:遍历顺序 左子节点 --> 右子节点 --> 根节点</li><li>广度 / 层次遍历: 从上往下,一层一层的遍历</li></ul><p>如果是一棵混乱的二叉树,那查找或者搜索的效率也会比较低,和一条混乱的链表没有什么区别,何必弄更加复杂的结构呢?</p><p>其实,二叉树是可以用在排序或者搜索中的,因为二叉树有严格的左右子树之分,我们可以定义根节点,左子节点,右子节点的大小之分。于是有了二叉搜索树:</p><blockquote><a href="https://link.segmentfault.com/?enc=nEiVdNReXDai92ANMSlQvw%3D%3D.HCbPtUXs16mgw6QGpmG8N0Af%2BFGipZYkSKBbmX%2FCW6tgFDSXwH5nuwnFhdd5tmJCZS0KwxOaKtqONvEnUgu8m5e%2FTEQ9IUxm2AovR77GWhKwaeTEw7oN5tNIx8YZCXW2" rel="nofollow">二叉查找树</a>(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的<a href="https://link.segmentfault.com/?enc=YZBvco0r7WnFNqfQjCW7Zg%3D%3D.jWtrsShqAGKzQgk1fupPdCotIDuGE15kKnSAy7%2BDNBmsLVN7DSLkiNEvtHImdujS8CBAV9KRd35bHYfmu2uNo5MpkYrRXlcjbpcatD2kFJs%3D" rel="nofollow">二叉树</a>: 若它的左子树不空,则左子树上所有结点的值均小于它的<a href="https://link.segmentfault.com/?enc=m3MTaRb5zPq%2BxQ3EFF2BJA%3D%3D.LdVdAdG7Q6dkD0KlI8tZUw7KlSiPrmSEvnvsgEIiL%2Fh7qGB0c6xT9%2FxPabPgr2BgcG1Aykw9lNYJ8HFN%2FgS5bI4lukO9RgY90Fc%2FMnymFNI%3D" rel="nofollow">根结点</a>的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为<a href="https://link.segmentfault.com/?enc=%2F1AaezeBWg0rRbkb%2B18QMQ%3D%3D.EZurFNW%2F12cHKDpJM4ODGLS27qHRZG1h7TMFSRsNI0hnP%2Frd8TzVPwhCvOsty%2F1wzAbMFyDGtf16J8mvTly41jMpDk6cGJaKHOPBH%2BuIZSRJ03xMOcDjwDFip24n9PRD" rel="nofollow">二叉排序树</a>。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。</blockquote><p>二叉查找树样例如下:</p><p><img src="/img/remote/1460000041267850" alt="" title=""></p><p>比如上面的树,如果我们需要查找到 <code>4</code>, 从 <code>5</code>开始,<code>4</code>比<code>5</code>小,往左子树走,查找到<code>3</code>,<code>4</code>比<code>3</code>大,往右子树走,找到了<code>4</code>,也就是一个 <code>7</code>个节点的树,我们只查找了<code>3</code>次,也就是层数,假设<code>n</code>个节点,那就是<code>log(n+1)</code>。</p><p>树维护好了,查询效率固然高,但是如果树没维护好,容易退化成为链表,查询效率也会下降,比如:</p><p><img src="/img/remote/1460000041267851" alt="" title=""></p><p>一棵对查询友好的二叉树,应该是一个平衡或者接近平衡的二叉树,何为平衡二叉树:</p><blockquote>平衡二叉搜索树的任何结点的左子树和右子树高度最多相差1。平衡二叉树也称为 AVL 树。</blockquote><p>为了保证插入或者删除数据等之后,二叉树还是平衡二叉树,那么就需要调整节点,这个也称为平衡过程,里面会涉及各种旋转调整,这里暂时不展开。</p><p>但是如果涉及大量的更新,删除操作,平衡树种的各种调整需要牺牲不小的性能,为了解决这个问题,有大佬提出了红黑树.</p><blockquote><p>红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在<a href="https://link.segmentfault.com/?enc=omthGV59VaNrRkDXWY1xBg%3D%3D.q3j2OFN77TbBhhshFsfOdJ25ifr4rHR8%2B2K33%2FDdglAdiNbuU9i0AsrwYu5YOUaKxrkKO71Goo%2BAIGzjJr3HiA%3D%3D" rel="nofollow">计算机</a>科学中用到的一种<a href="https://link.segmentfault.com/?enc=RUGh0F0UG3vfVBt5VobsPw%3D%3D.azRkYqGNlujb9QZZl%2FmVXsvDXMlItT6knOkOUCP73j8gvkagonAHDN7bUWseY%2FhpF1jvzNb7RPYlDF5nDRsHIeByHWiQUZ3cMukVs2F%2FSP4%3D" rel="nofollow">数据结构</a>,典型的用途是实现<a href="https://link.segmentfault.com/?enc=UNW6FyNpLwRyjZc9ChjeTg%3D%3D.rqG7oR1Har2remIWSAz%2F%2BifBTdWoNPTtuKq1sPFrFMC8%2BFU1ux%2FUaPJgxAYPtJsGOdWUQfUSz43aKuNtgyvDmaT%2BGnQJxQfnejBT5F9tMLs%3D" rel="nofollow">关联数组</a>。 [1] </p><p>红黑树是在1972年由<a href="https://link.segmentfault.com/?enc=FVGGy6%2B1y5RDxUbOHZF0yA%3D%3D.3MBS%2B95zBfYsAnyp%2BA4dZIWEa9uh%2BWZ865%2ByqwQUmBleTe7Dhtsxtq8EbfENIEc2" rel="nofollow" title="Bayer/3014716">Rudolf Bayer</a>发明的,当时被称为平衡二叉B树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的“红黑树”。 [2] </p><p>红黑树是一种特化的AVL树(<a href="https://link.segmentfault.com/?enc=w4GwAHhFuznVmKO5vHrXcQ%3D%3D.%2FxO077dpEYucbT4xXTBB%2FexvzWHvr2DwRvvSuncpxoD5EI9NcXPkN7JFB%2BdgCYph6X1Q4m20uOB3hsXEAqw085au0FDNmBnRJ8sAdk5Gn8UJmyQnRkmPYhAaYJOdfOxa" rel="nofollow">平衡二叉树</a>),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。</p></blockquote><p>红黑树有以下的特点:</p><ul><li>性质1. 结点是红色或黑色。</li><li>性质2. 根结点是黑色。</li><li>性质3. 所有叶子都是黑色。(叶子是NIL结点)</li><li>性质4. 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)</li><li>性质5. 从任一节结点其每个叶子的所有路径都包含相同数目的黑色结点。</li></ul><p>正是这些特性,让红黑树在调整的时候,不像普通的平衡二叉树调整那般困难,频繁。也就是加上了条条框框,让它符合一定的标准,减少平衡过程的混乱以及频次。</p><p>前面说的哈希表,<code>Java</code> 中的实现,正是应用了红黑树,在<code>hash</code>冲突较多的时候,会将链表转换成为红黑树。</p><p>上面说的都是二叉树,但是我们不得不扯一下多叉树,为什么呢?虽然二叉树中的各种搜索树,红黑树已经很优秀了,但是在与磁盘交互的时候,大多数是数据存储中,我们不得不考虑 IO 的因素,因为磁盘IO比内存慢太多了。如果索引树的层高有几千上万,那么磁盘读取的时候,需要次数太多了。B树更加适合磁盘存储。</p><blockquote><p>970年,R.Bayer和E.mccreight提出了一种适用于外查找的<a href="https://link.segmentfault.com/?enc=Q239ZBHeLwa5K%2FX%2FwFvjtg%3D%3D.T4Mf8OU3T05pHNBhOTjvz0%2BH5xDKKWg%2FraOOQXB1Ub0q3mYFIaMAN2XiBEaYZ2DO" rel="nofollow">树</a>,它是一种平衡的多叉树,称为B树(或B-树、B_树)。</p><p>一棵m阶B树(balanced tree of order m)是一棵平衡的m路搜索树。它或者是空树,或者是满足下列性质的树:</p><p>1、根结点至少有两个子女;</p><p>2、每个非根节点所包含的关键字个数 j 满足:m/2 - 1 <= j <= m - 1;</p><p>3、除根结点以外的所有结点(不包括叶子结点)的度数正好是关键字总数加1,故<strong>内部子树</strong>个数 k 满足:m/2 <= k <= m ;</p><p>4、所有的叶子结点都位于同一层。</p></blockquote><p>每个节点放多一点数据,查找的时候,内存中的操作比磁盘快很多,<code>b</code>树可以减少磁盘IO的次数。B 树:</p><p><img src="/img/remote/1460000041267852" alt="" title=""></p><p>而每个节点的<code>data</code>可能很大,这样会导致每一页查出来的数据很少,IO查询次数自然就增加了,那我们不如只在叶子节点中存储数据:</p><p><img src="/img/remote/1460000041267853" alt="" title=""></p><blockquote><p>B+树是B树的一种变形形式,B+树上的叶子结点存储关键字以及相应记录的地址,叶子结点以上各层作为索引使用。一棵m阶的B+树定义如下: </p><p>(1)每个结点至多有m个子女;</p><p>(2)除根结点外,每个结点至少有[m/2]个子女,根结点至少有两个子女; </p><p>(3)有k个子女的结点必有k个关键字。</p></blockquote><p><strong>一般b+树的叶子节点,会用链表连接起来,方便遍历以及范围遍历。</strong></p><p>这就是<code>b+</code>树,<code>b+</code>树相对于<code>B树</code>多了以下优势:</p><ol><li><code>b+</code>树的中间节点不保存数据,每次IO查询能查到更多的索引,,是一个矮胖的树。</li><li>对于范围查找来说,<code>b+</code>树只需遍历叶子节点链表即可,<code>b</code>树却需要从根节点都叶子节点。</li></ol><p>除了上面的树,其实还有一种叫<code>Huffman</code>树:给定N个权值作为N个<a href="https://link.segmentfault.com/?enc=8tTb6WMJX0i4uVg0Rbmyow%3D%3D.Hf4KEu38QBPoxZ%2FVKdEBcALrm3VCSgdWBfy4EFPKu7HOU4VfHmN8UpUi1PB3gO3hBpldct6aIc2d9qC0JfWWzyquCeror8U997vvXRMr%2BWc%3D" rel="nofollow">叶子结点</a>,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。</p><p>一般用来作为压缩使用,因为数据中,每个字符出现的频率不一样,出现频率越高的字符,我们用越短的编码保存,就可以达到压缩的目的。那这个编码怎么来的呢?</p><p>假设字符是<code>hello</code>,那么编码可能是(只是编码的大致雏形,高频率出现的字符,编码更短),编码就是从根节点到当前字符的路径的<code>01</code>串:</p><p><img src="/img/remote/1460000041267854" alt="" title=""></p><p>通过不同权值的编码,哈夫曼树到了有效的压缩。</p><h3>堆</h3><p>堆,其实也是二叉树中的一种,堆必须是完全二叉树,完全二叉树是:除了最后一层,其他层的节点个数都是满的,最后一层的节点都集中在左部连续位置。</p><p>而堆还有一个要求:堆中每一个节点的值都必须大于等于(或小于等于)其左右子节点的值。</p><p>堆主要分为两种:</p><ul><li>大顶堆:每个节点都大于等于其子树节点(堆顶是最大值)</li><li>小顶堆:每个节点都小于等于其子树节点(堆顶是最小值)</li></ul><p>一般情况下,我们都是用数组来表示堆,比如下面的小顶堆:</p><p><img src="/img/remote/1460000041267855" alt="image-20220109000632499" title="image-20220109000632499"></p><p>数组中父子节点以及左右节点的关系如下:</p><ul><li><code>i </code>结点的父结点 <code>parent = floor((i-1)/2) </code>(向下取整)</li><li><code>i </code>结点的左子结点 <code>2 * i +1</code></li><li><code>i </code>结点的右子结点 <code>2 * i + 2</code></li></ul><p>既然是存储数据的,那么一定会涉及到插入删除等操作,堆里面插入删除,会涉及到堆的调整,调整之后才能重新满足它的定义,这个调整的过程,叫做<strong>堆化</strong>。</p><p>用小顶堆举例,调整主要是为了保证:</p><ul><li>还是完全二叉树</li><li>堆中每一个节点都还小于等于其左右子节点</li></ul><p>对于小顶堆,调整的时候是:小元素往上浮,大元素往下沉,就是不断交换的过程。</p><p>堆一般可以用来求解<code>TOP K</code> 问题,或者前面我们说的优先队列等。</p><h3>图</h3><p>终于来到了图的讲解,图其实就是二维平面,之前写过扫雷,扫雷的整个方块区域,其实也可以说是图相关的。图是非线性的数据结构,主要是由边和顶点组成。</p><p><img src="/img/remote/1460000041267856" alt="image-20220109002114134" title="image-20220109002114134"></p><p>同时图又分为有向图与无向图,上面的是无向图,因为边没有指明方向,只是表示两者关联关系,而有向图则是这样:</p><p><img src="/img/remote/1460000041267857" alt="" title=""></p><p>如果每个顶点是一个地方,每条边是路径,那么这就是一张地图网络,因此图也经常被用于求解最短距离。先来看看图相关的概念:</p><ul><li>顶点:图最基本的单元,那些节点</li><li>边:顶点之间的关联关系</li><li>相邻顶点:由边直接关联的顶点</li><li>度:一个顶点直接连接的相邻顶点的数量</li><li>权重:边的权值</li></ul><p>一般表示图有以下几种方法:</p><ol><li>邻接矩阵,使用二维数组表示,为1 表示联通,0表示不连通,当然如果表示路径长度的时候,可以用大于<code>0</code>的数表示路径长度,用<code>-1</code>表示不连通。</li></ol><p>下面的图片中,0和 1,2连通,我们可以看到第 0行的第1,2列是1 ,表示连通。还有一点:顶点自身我们是标识了0,表示不连通,但是有些情况可以视为连通状态。</p><p><img src="/img/remote/1460000041267858" alt="" title=""></p><ol start="2"><li>邻接表</li></ol><blockquote><p>邻接表,存储方法跟树的孩子链表示法相类似,是一种顺序分配和链式分配相结合的<a href="https://link.segmentfault.com/?enc=H%2Blr%2FZwPD1CnYrt%2BNxm1Cw%3D%3D.XDhhUuyt9tPA7J7%2Bry515EOP%2BqKVi%2FTszE9WCQEW0fuY%2BW%2B6%2B%2FQdoDKr8hpce3zSDcEh1J8X%2FAx33%2Fw0i30zgL6j7Amsb1kb3d26CzmC0gE%3D" rel="nofollow">存储结构</a>。如这个表头结点所对应的顶点存在<a href="https://link.segmentfault.com/?enc=lJrNe7N7llKZe%2F8YvJryaA%3D%3D.Xxq1tRUp0grJs7PkFlgi10Ykiy55caLE1VQBa8IbRRnS8cOKmyDuhBma3SetHwHv6Nh3Oi2DnPNuEd7NaDJQIA%3D%3D" rel="nofollow">相邻</a>顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。</p><p>对于无向图来说,使用邻接表进行存储也会出现数据冗余,表头结点A所指链表中存在一个指向C的表结点的同时,表头结点C所指链表也会存在一个指向A的表结点。</p></blockquote><p><img src="/img/remote/1460000041267859" alt="" title=""></p><p>图里面遍历一般分为广度优先遍历和深度优先遍历,广度优先遍历是指优先遍历与当前顶点<strong>直接相关</strong>的顶点,一般借助队列实现。而深度优先遍历则是往一个方向一直走到不能再走,有点不撞南墙不回头的意思,一般使用递归实现。</p><p>图,除了用了计算最小路径以外,还有一个概念:最小生成树。</p><blockquote>一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。 最小生成树可以用kruskal(克鲁斯卡尔)算法或prim(普里姆)算法求出。</blockquote><p>有一种说法,图是平面上的点,我们把其中一个点拎起来,能将其他顶点带起来的边,取最小权值,多余的边去掉,就是最小生成树。</p><p><img src="/img/remote/1460000041267860" alt="" title=""></p><p>当然,最小生成树并不一定是唯一的,可能存在多种结果。</p><h3>秦怀@观点</h3><p>了解这些基本的数据结构,在写代码或者数据建模的时候,能够选择更加合适的,这是最大的用处。计算机是为人服务的,代码也是,数据结构的全部类型我们是无法一下子一一掌握的,但是基本的东西是变动不会很大,除非新一代革命性变化。</p><p>程序是由数据结构和算法组成,数据结构就像是基石,借助《数据结构C语言》版本中的一句话结尾:</p><blockquote>为了编写出一个”好“的程序,必须分析待处理的对象的特性以及各处理对象之间存在的关系,这就是”数据结构“这门学科和发展的背景。</blockquote><p><img src="/img/remote/1460000041267861" alt="" title=""></p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,个人网站:<a href="https://link.segmentfault.com/?enc=7bUwma764tE4eDx4Mivkiw%3D%3D.jXq7W2lJU3bqeFVd8YiIVr5hzKIvQuVVZHzQPlN2sZc%3D" rel="nofollow">http://aphysia.cn</a>,技术之路不在一时,山高水长,纵使缓慢,驰而不息。</p><p><a href="https://link.segmentfault.com/?enc=zP8h%2Fd%2Bu%2FjAczXxq4jiiKg%3D%3D.Bh7bFSu5fw4%2FErpSiyEjdh7GwHcaz4B%2BIgHX9Ue2UAE3qASybMM0uczLEANAq%2Frh" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=Pxx8Tw%2FFr4%2B2GGT1oU5dPQ%3D%3D.FvQLzef1MwTyMynwT9jc5sNTKNOtWd2B6Xt4tkipIaDZDquMcF45cm7kfxiLuXol" rel="nofollow">开源编程笔记</a></p>
无聊的周末用Java写个扫雷小游戏
https://segmentfault.com/a/1190000041261742
2022-01-11T09:03:31+08:00
2022-01-11T09:03:31+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>周末无聊,用<code>Java</code>写了一个扫雷程序,说起来,这个应该是在学校的时候,写会比较好玩,毕竟自己实现一个小游戏,还是比较好玩的。说实话,扫雷程序里面核心的东西,只有点击的时候,去触发更新数据这一步。</p><p><strong>Swing 是过时了,但是好玩不会过时,不喜勿喷</strong></p><p><img src="/img/remote/1460000041261744" alt="" title=""></p><p>源码的地址:<a href="https://link.segmentfault.com/?enc=igAXE8N%2F6JU3wxhsmmYqlQ%3D%3D.FI7z5yccdZPl960QzuCcr%2FiEHeJQA1oSTraocfflcLBugZMYHL8BQRVZx6neOIi8AAQSrPy5UIKuPPW%2FL%2BnsOg%3D%3D" rel="nofollow">https://github.com/Damaer/Gam...</a></p><p>下面讲讲里面的设计:</p><ul><li>数据结构设计</li><li>视图和数据尽可能分开</li><li>点击时候使用<code>BFS</code>扫描</li><li>判断成功失败</li></ul><h2>数据结构设计</h2><p>在这个程序里面,为了方便,使用了全局的数据类<code>Data</code>类来维护整个游戏的数据,直接设置为静态变量,也就是一次只能有一个游戏窗口运行,否则会有数据安全问题。(仅仅是为了方便)</p><p>有以下的数据(部分代码):</p><pre><code class="Java">public class Data {
// 游戏状态
public static Status status = Status.LOADING;
// 雷区大小
public static int size = 16;
// 雷的数量
public static int numOfMine = 0;
// 表示是否有雷,1:有,0没有
public static int[][] maps = null;
// 是否被访问
public static boolean[][] visited = null;
// 周边雷的数量
public static int[][] nums = null;
// 是否被标记
public static boolean[][] flags = null;
// 上次被访问的块坐标
public static Point lastVisitedPoint = null;
// 困难模式
private static DifficultModeEnum mode;
...
}</code></pre><p>需要维护的数据如下:</p><ul><li>游戏状态:是否开始,结束,成功,失败等等</li><li>模式:简单,中等或者困难,这个会影响自动生成的雷的数量</li><li>雷区的大小:16*16的小方块</li><li>雷的数量:与模式选择有关,是个随机数</li><li>标识每个方块是否有雷:最基础的数据,生成之后需要同步更新这个数据</li><li>标识每个方块是否被扫过:默认没有扫过</li><li>每个方块周边类雷的数量:生成的时候同步计算该结果,不想每次点击后再计算,毕竟是个不会更新的数据,一劳永逸</li><li>标识方块是否被标记:扫雷的时候我们使用小旗子标记方块,表示这里是雷,标识完所有的雷的时候,成功</li><li>上次访问的方块坐标:这个其实可以不记录,但是为了表示爆炸效果,与其他的雷展示不一样,故而记录下来</li></ul><h2>视图与数据分开</h2><p>尽量遵循一个原则,视图与数据或者数据变更分开,方便维护。我们知道<code>Java</code>里面是用<code>Swing</code>来画图形界面,这个东西确实难画,视图写得比较复杂但是画不出什么东西。</p><p><img src="/img/remote/1460000041261745" alt="" title=""></p><p>视图与数据分开,也是几乎所有框架的优秀特点,主要是方便维护,如果视图和数据糅合在一起,更新数据,还要操作视图,那就会比较乱。(当然我写的是粗糙版本,只是简单区分了一下)</p><p>在这个扫雷程序里面基本都是点击事件,触发了数据变更,数据变更后,调用视图刷新,视图渲染的逻辑与数据变更的逻辑分开维护。</p><p>每个小方块都添加了点击事件,<code>Data.visit(x, y)</code>是数据刷新,<code>repaintBlocks()</code>是刷新视图,具体的代码就不放了,有兴趣可以<code>Github</code>看看源代码:</p><pre><code class="Java">new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) {
if (Data.status == Status.GOING) {
int c = e.getButton(); // 得到按下的鼠标键
Block block = (Block) e.getComponent();
int x = block.getPoint_x();
int y = block.getPoint_y();
if (c == MouseEvent.BUTTON1) {
Data.visit(x, y);
} else if (c == MouseEvent.BUTTON3) {// 推断是鼠标右键按下
if (!Data.visited[x][y]) {
Data.flags[x][y] = !Data.flags[x][y];
}
}
}
repaintBlocks();
}
}</code></pre><p>这里很遗憾的一点是每个方块里面还有一个背景的<code>`url</code>没有抽取出来,这个是变化的数据,不应该放在视图里面:</p><pre><code class="Java">public class Block extends JPanel {
private int point_x;
private int point_y;
private String backgroundPath = ImgPath.DEFAULT;
public Block(int x, int y) {
this.point_x = x;
this.point_y = y;
setBorder(BorderFactory.createEtchedBorder());
}
}</code></pre><p>重新设置方块背景,需要居中处理,重新绘制,重写<code>void paintComponent(Graphics g)</code>方法即可:</p><pre><code class="Java"> @Override
protected void paintComponent(Graphics g) {
refreshBackground();
URL url = getClass().getClassLoader().getResource(backgroundPath);
ImageIcon icon = new ImageIcon(url);
if (backgroundPath.equals(ImgPath.DEFAULT) || backgroundPath.equals(ImgPath.FLAG)
|| backgroundPath.equals(String.format(ImgPath.NUM, 0))) {
g.drawImage(icon.getImage(), 0, 0, getWidth(), getHeight(), this);
} else {
int x = (int) (getWidth() * 0.1);
int y = (int) (getHeight() * 0.15);
g.drawImage(icon.getImage(), x, y, getWidth() - 2 * x, getHeight() - 2 * y, this);
}
}</code></pre><h2>BFS扫描</h2><p><code>BFS</code>,也称为广度优先搜索,这算是扫雷里面的核心知识点,也就是点击的时候,如果当前方块是空的,那么就会触发扫描周边的方块,同时周边方块如果也是空的,会继续递归下去,我用了广度优先搜索,也就是先将它们放到队列里面,取出来,再判断是否为空,再将周边符合的方块添加进去,进行一一处理。</p><p>广度优先搜索在这里不展开,其本质是优先搜索与其直接关联的数据,也就是方块周围的点,这也是为什么需要队列的原因,我们需要队列来保存遍历的顺序。</p><pre><code class="Java"> public static void visit(int x, int y) {
lastVisitedPoint.x = x;
lastVisitedPoint.y = y;
if (maps[x][y] == 1) {
status = Status.FAILED;
// 游戏结束,暴露所有的雷
} else {
// 点击的不是雷
Queue<Point> points = new LinkedList<>();
points.add(new Point(x, y));
while (!points.isEmpty()) {
Point point = points.poll();
visited[point.x][point.y] = true;
if (nums[point.x][point.y] == 0) {
addToVisited(points, point.x, point.y);
}
}
}
}
public static void addToVisited(Queue<Point> points, int i, int j) {
int x = i - 1;
while (x <= i + 1) {
if (x >= 0 && x < size) {
int y = j - 1;
while (y <= j + 1) {
if (y >= 0 && y < size) {
if (!(x == i && j == y)) {
// 没访问过且不是雷
if (!visited[x][y] && maps[x][y] == 0) {
points.add(new Point(x, y));
}
}
}
y++;
}
}
x++;
}
}</code></pre><p>值得注意的是,周边的点,如果它的周边没有雷,那么会继续拓展,但是只要周边有雷,就会停止拓展,只会显示数字。</p><p><img src="/img/remote/1460000041261746" alt="" title=""></p><h3>判断成功失败</h3><p>当挖到雷的时候,就失败了,同时会将所有的雷暴露出来,为了展示我们当前挖到的点,有爆炸效果,我们记录了上一步操作的点,在刷新视图后,弹窗提示:</p><p><img src="/img/remote/1460000041261747" alt="image-20211229091811385" title="image-20211229091811385"></p><p>判断成功则需要将所有的雷遍历一次,判断是否被标记出来,这是我简单想的规则,忘记了扫雷是不是这样了,或者可以实现将其他所有非雷区都挖空的时候,成功,也是可以的。</p><h3>总结</h3><p>扫雷,一个简单的游戏,无聊的时候可以尝试一下,但是<code>Java</code> 的<code>Swing</code>真的难用,想找一个数据驱动视图修改的框架,但是貌似没有,那就简单实现一下。其实大部分时间都在找图标,测试<code>UI</code>,核心的代码并没有多少。</p><p>在这里推荐一下<code>icon</code>网站:<code>https://www.iconfont.cn/</code>,即使是没有什么技术含量的扫雷,写一下还是挺有趣的。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,个人网站:<a href="https://link.segmentfault.com/?enc=IhpCn5O65G%2BwJWwowtkYbw%3D%3D.SAHJ%2By9oJu1FngOz2c4SsOgOmhmsouPgpUWgwsrC%2FIk%3D" rel="nofollow">http://aphysia.cn</a>,技术之路不在一时,山高水长,纵使缓慢,驰而不息。</p><p><a href="https://link.segmentfault.com/?enc=%2BgL0c9T%2Bk4qQpWY8Me2I7w%3D%3D.vr52AawvrVB0CABHDhltfD5nAGYjjlmKvFcKXUcR0U%2F%2Bcq1Yo0MWid1UPwvd2WJt" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=OZdAclaj1u6NnRpSfzuFXQ%3D%3D.NQphaJ1kRvUa63z5%2FPqhznj6MVqnWnWPZSxIHh0O37V5bpUtx%2FjVQIPbrRyYHYRp" rel="nofollow">开源编程笔记</a></p>
java集合【13】——— Stack源码分析走一波
https://segmentfault.com/a/1190000041252289
2022-01-10T08:38:02+08:00
2022-01-10T08:38:02+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<h3>前言</h3><p>集合源码分析系列:<a href="https://link.segmentfault.com/?enc=amkoxvmylQJE5UfczfjpHA%3D%3D.j8yZJAqc%2Bh4%2B1jsh3nrDsxhrcoAtc%2FVGhaczxO9kC%2B4Cpe4WP8nlL8iZNHP3Tc136dR7fjqnEaiGVGhoFOSLgYqxBVCuerxOVtv%2BjGs5UAOmRTo3HT%2FMRukAG6F%2B%2B7S3EELBqHTlO6m3MWCzFDlfmKjFst3nQnmLGxUq26T7A6eoH4lMXixsvdChBknN6KKN" rel="nofollow">Java集合源码分析</a></p><p>前面已经把<code>Vector</code>,<code>ArrayList</code>,<code>LinkedList</code>分析完了,本来是想开始<code>Map</code>这一块,但是看了下面这个接口设计框架图:整个接口框架关系如下(来自百度百科):</p><p><img src="/img/remote/1460000037509035" alt="" title=""></p><p>原来还有一个漏网之鱼,<code>Stack</code>栈的是挂在<code>Vector</code>下,前面我们已经分析过<code>Vector</code>了,那么顺便把<code>Stack</code>分析一遍。再不写就2022年了:</p><p><img src="/img/remote/1460000041252291" alt="" title=""></p><h3>Stack介绍</h3><p>栈是一种数据结构,并不是<code>Java</code>特有的,在<code>Java</code>里面体现是<code>Stack</code>类。它的本质是先进后出,就像是一个桶,只能不断的放在上面,取出来的时候,也只能不断的取出最上面的数据。要想取出底层的数据,只有等到上面的数据都取出来,才能做到。当然,如果有这种需求,我们一般会使用双向队列。</p><p>以下是栈的特性演示:</p><p><img src="/img/remote/1460000041252292" alt="" title=""></p><p><code>Stack</code>在<code>Java</code>中是继承于<code>Vector</code>,这里说的是<code>1.8</code>版本,共用了<code>Vector</code>底层的数据结构,底层都是使用数组实现的,具有以下的特点:</p><ul><li>先进后出(<code>`FILO</code>)</li><li>继承于<code>Vector</code>,同样基于数组实现</li><li>由于使用的几乎都是<code>Vector</code>,<code>Vector</code>的操作都是线程安全的,那么<code>Stack</code>操作也是线程安全的。</li></ul><p>类定义源码:</p><pre><code class="Java">public
class Stack<E> extends Vector<E> {
}</code></pre><p>成员变量只有一个序列化的变量:</p><pre><code class="Java"> private static final long serialVersionUID = 1224463164541339165L;</code></pre><h2>方法解读</h2><p><code>Stack</code>没有太多自己的方法,几乎都是继承于<code>Vector</code>,我们可以看看它自己拓展的 5 个方法:</p><ul><li><code>E push(E item)</code>: 压栈</li><li><code>synchronized E pop()</code>: 出栈</li><li><code>synchronized E peek()</code>:获取栈顶元素</li><li><code>boolean empty()</code>:判断栈是不是为空</li><li><code>int search(Object o)</code>: 搜索某个对象在栈中的索引位置</li></ul><h4>push 方法</h4><p>在底层实际上调用的是<code>addElement()</code>方法,这是<code>Vector</code>的方法:</p><pre><code class="Java"> public E push(E item) {
addElement(item);
return item;
}</code></pre><p>在前面我们已经分析过<code>Vecor</code>的源码,感兴趣可以参考:<a href="https://link.segmentfault.com/?enc=AuPXXjnJGnPd6aJLzkzLaQ%3D%3D.w6bipPdWlkaWJhCzY4SEEwRhffk3mUuPGZobQXqHwL5aSP5%2Fua7bR4xvyk0SP%2FgTSbIRRShBX7ri%2F%2FaMNpIhLqJjt9ry0af0JXrdvCJ4dzA%3D" rel="nofollow">http://aphysia.cn/archives/ja...</a></p><p><code>addElement</code>是线程安全的,在底层实际上就是往数组后面添加了一个元素,但是它帮我们保障了容量,如果容量不足,会触发自动扩容机制。</p><pre><code class="Java"> public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}</code></pre><h4>pop 方法</h4><p>底层是先调用<code>peek()</code>方法,获取到栈顶元素,再调用<code>removeElementAt()</code>方法移除掉栈顶元素,实现出栈效果。</p><pre><code class="Java"> public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}</code></pre><p><code>removeElementAt(int index)</code>也是<code>Vector</code>的方法,<code>synchronized</code>修饰,也是线程安全的,由于移除的是数组最后的元素,所以在这里不会触发元素复制,也就是<code>System.arraycopy(elementData, index + 1, elementData, index, j);</code>:</p><pre><code class="Java"> public synchronized void removeElementAt(int index) {
modCount++;
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " +
elementCount);
}
else if (index < 0) {
throw new ArrayIndexOutOfBoundsException(index);
}
int j = elementCount - index - 1;
if (j > 0) {
System.arraycopy(elementData, index + 1, elementData, index, j);
}
elementCount--;
elementData[elementCount] = null; /* to let gc do its work */
}</code></pre><h4>peek 方法</h4><p>获取栈顶元素,先获取数组的大小,然后再调用<code>Vector</code>的<code>E elementAt(int index)</code>获取该索引的元素:</p><pre><code class="Java"> public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}</code></pre><p><code>E elementAt(int index)</code>的源码如下,里面逻辑比较简单,只有数组越界的判断:</p><pre><code class="Java"> public synchronized E elementAt(int index) {
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
}
return elementData(index);
}</code></pre><h4>empty 方法</h4><p>主要是用来判空,判断元素栈里面有没有元素,主要调用的是<code>size()</code>方法:</p><pre><code class="Java"> public boolean empty() {
return size() == 0;
}</code></pre><p>这个<code>size()</code>方法其实也是<code>Vector</code>的方法,返回的其实也是一个类变量,元素的个数:</p><pre><code class="Java"> public synchronized int size() {
return elementCount;
}</code></pre><h4>search方法</h4><p>这个方法主要用来查询某个元素的索引,如果里面存在多个,那么将会返回最后一个元素的索引:</p><pre><code class="Java">
public synchronized int search(Object o) {
int i = lastIndexOf(o);
if (i >= 0) {
return size() - i;
}
return -1;
}</code></pre><p>使用<code>synchronized</code>修饰,也是线程安全的,为什么需要这个方法呢?</p><p>我们知道栈是先进先出的,如果需要查找一个元素在其中的位置,那么需要一个个取出来再判断,那就太麻烦了,而底层使用数组进行存储,可以直接利用这个特性,就可以快速查找到该元素的索引位置。</p><p>至此,回头一看,你是否会感到疑惑,<code>`Stack</code>里面没有任何的数据,但是却不断的在操作数据,这得益于<code>Vector</code>的数据结构:</p><pre><code class="Java"> // 底层数组
protected Object[] elementData;
// 元素数量
protected int elementCount;</code></pre><p>底层使用数组,保存了元素的个数,那么操作元素其实只是不断往数组中插入元素,或者取出最后一个元素即可。</p><p><img src="/img/remote/1460000041252293" alt="" title=""></p><h3>总结</h3><p><code>stack</code> 由于继承了<code>Vector</code>,因此也是线程安全的,底层是使用数组保存数据,大多数<code>API</code>都是使用<code>Vector</code>来保存。它最重要的属性是先进先出。至于数组扩容,沿用了<code>Vector</code>中的扩容逻辑。</p><p>如果让我们自己实现,底层不一定使用数组,使用链表也是能实现相同的功能的,只是在整个集合源码体系中,共有相同的部分,是不错的选择。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,个人网站:<a href="https://link.segmentfault.com/?enc=4X%2FbygLGH3nHcVazT%2BQtOw%3D%3D.QQMO9oCttqcPZBAnBaOjfbQoAljIfmQngEywLLLyZsY%3D" rel="nofollow">http://aphysia.cn</a>,技术之路不在一时,山高水长,纵使缓慢,驰而不息。</p><p><a href="https://link.segmentfault.com/?enc=SQLma40roFV9TRTTMYhDRQ%3D%3D.dwg7fWpGd6yb1qP89Dzm3jJKm%2BeuPmLMkcqyE1AsgvxjHOjKcHlhAJRd5AQCjSC%2F" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=zwS9STR8IaWtQwurfbGcIw%3D%3D.Z%2BM9ExyAEAKJYnjUj%2BWTRq0F2x7EigCkfYoJjp1B2jykrFBkkidTybwTeEA5SXPC" rel="nofollow">开源编程笔记</a></p>
设计模式【10】-- 顺便看看享元模式
https://segmentfault.com/a/1190000041250378
2022-01-09T15:23:08+08:00
2022-01-09T15:23:08+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>设计模式系列:<a href="https://link.segmentfault.com/?enc=J94BX0262k07emvWU2AlUw%3D%3D.eK4L7DJcyjwPduKveLMsEY9RjOe7lHFmwZGQsi12pCNUN%2FmCI8NZDpJatraRjikw" rel="nofollow">http://aphysia.cn/categories/...</a></p><p><img src="/img/remote/1460000041100740" alt="1" title="1"></p><p>开局还是那种图,各位客官往下看...</p><h2>享元模式是什么?</h2><p>享元模式(<code>FlyWeight</code>),是结构型模式的一种,主要是为了减少创建对象的数量,减少内存占用以及提高性能。说到这里,不知道你是否会想到池技术,比如<code>String</code> 常量池,数据库连接池,缓冲池等等,是的,这些都应用了享元模式。</p><p>比如,有一些对象,创建时候需要资源比较多,创建成本比较高,内存开销比较大,如果我们一直创建,机器吃不消,那么我们就想到了池化技术,把创建好的对象放在里面,需要时,去池子里面取就可以了,也就是大家共享了池子里面的对象,这就是共享。</p><p>听名字,就很共享单车:</p><p><img src="/img/remote/1460000041250380" alt="" title=""></p><h3>享元模式的特点</h3><p>一般而言,享元对象需要在不同的场景下使用,那状态如果可随意修改,就容易造成混乱,出错的概率大大增加。但是如果所有的内部属性都是不可修改的,貌似也不是十分灵活,因此为了在稳定和灵活性之间找到平衡点,一般的享元对象,都会将内部属性划分为两大类:</p><ul><li>内部状态:不可变,且在多个地方中共享,重复使用的部分,只能通过构造函数设值</li><li>外部状态:每个对象,在不同场景下,可能存在不一样的状态,可以修改</li></ul><blockquote><ul><li>单纯享元模式:在单纯享元模式中,所有的具体享元类都是可以共享的,不存在非共享具体享元类。</li><li>复合享元模式:将一些单纯享元对象使用组合模式加以组合,还可以形成复合享元对象,这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享</li></ul></blockquote><p>这里我们说的是单纯享元模式,享元模式一般会有几种对象:</p><ul><li>享元接口或则抽象类(<code>Flyweight</code>):在接口或者抽象类中声明定义了公共的方法,可以对外提供部分能力,或者按需提供数据。</li><li>具体的享元实现类(<code>ConcreteFlyweight</code>):实现了抽象享元类,在内部有一部分数据是不可变的,实现接口的时候,会对外提供一部分能力或者数据。</li><li>享元工厂(<code>FlyweightFactory</code>): 享元工厂主要是用来创建和管理享元对象的,将各种类型的享元对象放到一个池子里,一般是键值对的形式存在,当然也可以是其他的类型,如果初次获取一个对象,需要先创建,如果池子里已经有该对象,那么就可以直接返回了。</li></ul><h2>实现</h2><p>举个小栗子,比如我们出去玩耍需要购买飞机票,假设一架航班的唯一性是与航班号,出发时间,到达时间相关,用户喜欢通过航班号,来查询航班的相关信息,首先我们需要创建航班一个接口:</p><pre><code class="Java">public interface IFlight {
void info();
}</code></pre><p>具体的航班类<code>Flight</code>:</p><pre><code class="Java">public class Flight implements IFlight {
private String flightNo;
private String start;
private String end;
private boolean isDelay;
public Flight(String flightNo, String start, String end) {
this.flightNo = flightNo;
this.start = start;
this.end = end;
isDelay = Math.random() > 0.5;
}
@Override
public void info() {
System.out.println(String.format("从[%s]到[%s]的航班[%s]: %s ",
start, end, flightNo, isDelay ? "延误起飞" : "正常起飞"));
}
}</code></pre><p>航班搜索工厂类<code>FlightSearchFactory</code>:</p><pre><code class="Java">public class FlightSearchFactory {
public static IFlight searchFlight(String flightNo,String start,String end){
return new Flight(flightNo,start,end);
}
}</code></pre><p>模拟客户端请求:</p><pre><code class="Java">public class ClientTest {
public static void main(String[] args) {
IFlight flight = FlightSearchFactory.searchFlight("C9876","北京","上海");
flight.info();
}
}</code></pre><p>我们可以看到打印出了以下信息:</p><pre><code class="txt">从[北京]到[上海]的航班[C9876]: 延误起飞 </code></pre><p>但是,上面的有一个问题,每次来访问,都会创建一个对象,坐同一个航班的人,理论上查询的是相同的数据才对,这部分其实可以共享的,复用来提高效率,何乐而不为呢?</p><p>怎么缓存呢?</p><p>我们一般用<code>HashMap</code>来缓存,只需要将唯一识别的<code>key</code>定义好即可:</p><pre><code class="Java">import java.util.HashMap;
import java.util.Map;
public class FlightSearchFactory {
private static Map<String, IFlight> maps = new HashMap<>();
public static IFlight searchFlight(String flightNo, String start, String end) {
String key = getKey(flightNo, start, end);
IFlight flight = maps.get(key);
if (flight == null) {
System.out.print("缓存中没有,需要重新构建:");
flight = new Flight(flightNo, start, end);
maps.put(key, flight);
}else{
System.out.print("从缓存中读取数据:");
}
return flight;
}
private static String getKey(String flightNo, String start, String end) {
return String.format("%s_%s_%s", flightNo, start, end);
}
}</code></pre><p>测试代码:</p><pre><code class="Java">public class ClientTest {
public static void main(String[] args) {
IFlight flight = FlightSearchFactory.searchFlight("C9876","北京","上海");
flight.info();
IFlight flight1 = FlightSearchFactory.searchFlight("C9876","北京","上海");
flight1.info();
IFlight flight2 = FlightSearchFactory.searchFlight("H1213","北京","广州");
flight2.info();
}
}</code></pre><p>测试结果:</p><pre><code class="txt">缓存中没有,需要重新构建:从[北京]到[上海]的航班[C9876]: 正常起飞
从缓存中读取数据:从[北京]到[上海]的航班[C9876]: 正常起飞
缓存中没有,需要重新构建:从[北京]到[广州]的航班[H1213]: 正常起飞 </code></pre><p>可以看到如果缓存里面有,那么就不会重新构建对象,可以达到共享对象的目的,我们平时在项目里面使用的各种连接池,比如<code>Redis</code>连接池,<code>Mysql</code>连接池等等,这些资源本质上都比较宝贵,我们可以共享。</p><p><code>JDK</code>中<code>Integer</code>其实也用了缓存的技术,因为大家常用的都是较小的数值,所以默认<code>Integer</code>如果使用<code>valuesOf(int i)</code>方法获取,就会优先读取缓存内容:</p><pre><code class="Java"> public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}</code></pre><p>我们可以看到如果在<code>low</code> 和<code>high</code>范围内的数据,就会从缓存里面获取,否则会直接新建一个对象,那么<code>low</code>和<code>high</code>的范围多大呢?</p><pre><code class="Java"> static final int low = -128;
static final int high;</code></pre><p><code>high</code>是动态变化的,但是<code>high</code>是有断言的,必须大于等于<code>127</code>:<code>assert IntegerCache.high >= 127;</code>,而范围可以从<code>java.lang.Integer.IntegerCache.high</code>这个配置项读取出来:</p><pre><code class="Java"> static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}</code></pre><p>测试一下:</p><pre><code class="Java">public class IntegerTest {
public static void main(String[] args) {
// 不相等
Integer integer = Integer.valueOf(128);
Integer integer1 = Integer.valueOf(128);
System.out.println(integer == integer1);
// 相等
Integer integer2 = Integer.valueOf(127);
Integer integer3 = Integer.valueOf(127);
System.out.println(integer2 == integer3);
// 相等
Integer integer4 = Integer.valueOf(0);
Integer integer5 = Integer.valueOf(0);
System.out.println(integer4 == integer5);
// 相等
Integer integer6 = Integer.valueOf(-128);
Integer integer7 = Integer.valueOf(-128);
System.out.println(integer6 == integer7);
// 不相等
Integer integer8 = Integer.valueOf(-129);
Integer integer9 = Integer.valueOf(-129);
System.out.println(integer8 == integer9);
}
}</code></pre><p>从上面的结果可以看出实际上<code>Integer</code>从<code>-128</code>到<code>127</code>被缓存了,也验证了我们的结果,注意必须使用<code>Integer.valueOf()</code>这个办法,要是使用构造器<code>new Integer()</code>,创建出来必定是新的对象。</p><p><img src="/img/remote/1460000041250381" alt="" title=""></p><h2>总结</h2><ul><li>优点:如果有很多相似或者重复的对象,使用享元模式,可以节省空间</li><li>缺点:如果重用很多,不同地方还做了特殊化处理,代码复杂度增加</li></ul><p>设计模式其实是在软件工程的不断摸索中,总结出来的常用的一种设计思路,并不是非用不可,不是银弹,但是总有值得我们学习的地方,了解它这般设计的好处,不断的改进我们写代码,即使每次一点点改进。曾经听过一句话:看见别人写得不优雅的代码就有想重构它的冲动,可以多读读自己写的代码,然后写得更好(大致是这个意思)。共勉!</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,个人网站:<a href="https://link.segmentfault.com/?enc=KMPaVf3uZoJTTbqIMHGp6w%3D%3D.s707S7M05RXzmS5lzdIv5NNYGUFgr%2FpSDTm9n%2B%2FYLMY%3D" rel="nofollow">http://aphysia.cn</a>,技术之路不在一时,山高水长,纵使缓慢,驰而不息。</p><p><a href="https://link.segmentfault.com/?enc=Uavzg9EtRxxa%2BafMsY9ljg%3D%3D.h9OQJy0%2FmTu5qbI8fvB60rnbGkzA9%2FBcI8kxi8NtleMoEEdJed8sf89ZfmP9FPOZ" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=y%2FHoG6UCsOOgXgADyaX8%2FA%3D%3D.GAER939cPlcbqkuC%2BCP9fI4Mezjklm4knhC2K%2Fjp8ZtFd%2FE8bZlF1V6B5Z0yNmFR" rel="nofollow">开源编程笔记</a></p>
设计模式【9】-- 外观模式?没那么高大上
https://segmentfault.com/a/1190000041239722
2022-01-07T08:40:52+08:00
2022-01-07T08:40:52+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p><img src="/img/remote/1460000041100740" alt="1" title="1"></p><p>开局一张图,剩下全靠写...</p><h2>外观模式是什么</h2><p>外观模式,其实是用来隐藏系统的复杂性的,屏蔽掉了背后复杂的逻辑,向用户提供简单的可以访问系统的接口,也是属于结构型模式的一种 。</p><p>举个例子,比如我们的<code>Java</code> 三层<code>MVC</code>架构,对外提供的是<code>controller</code>,但是<code>controller</code>内部可能调用了很多<code>service</code>,<code>service</code>又调用了一些<code>mapper</code>,反正就是内部很复杂,但是对外只是一个接口,一个门面,外部看起来是简单的,外观很好看,实际上,你都懂。</p><p><img src="/img/remote/1460000041239724" alt="" title=""></p><p>再举个栗子,我们用的电脑,其实内部也是极其复杂的,但是我们操作的时候,已经不管内存,cpu,磁盘,显卡这些怎么工作了,甚至更加底层还有二进制,硬件之类的,我们只需要开机,做我们想做的事情,比如<code>Ctrl+C</code>,<code>Ctrl+V</code>,在美丽漂亮的界面上操作就可以了。</p><p><img src="/img/remote/1460000041239725" alt="" title=""></p><h3>外观模式的角色</h3><p>外观模式主要包括几个角色:</p><ul><li>外观角色:糅合多个子系统功能,对外提供一个共同的接口</li><li>子系统的角色:实现系统的部分功能</li><li>客户角色:通过外观角色访问各个子系统的功能</li></ul><h3>优点与缺点</h3><p>优点:</p><ul><li>减少系统依赖,这里指的是对外的系统依赖</li><li>提高灵活性</li><li>提高安全性</li></ul><p>缺点:</p><ul><li>把东西糅合到一个人身上,带来未知的风险</li><li>增加新的子系统可能需要修改外观类或者客户端的源代码,违反了“开闭原则”</li></ul><h2>测试例子</h2><p>我们以电脑为例子,先给电脑的每个部件抽象定义成为一个组件,赋予一个<code>work()</code>的方法:</p><pre><code class="java">public interface Component {
public void work();
}</code></pre><p>再定义内存,磁盘,cpu三种不同组件,分别实现上面的接口,各自工作:</p><pre><code class="java">public class Disk implements Component{
@Override
public void work() {
System.out.println("磁盘工作了...");
}
}
public class CPU implements Component{
@Override
public void work() {
System.out.println("CPU工作了...");
}
}
public class Memory implements Component{
@Override
public void work() {
System.out.println("内存工作了...");
}
}</code></pre><p>然后以上组件可能是交叉在一起工作的,我们模拟一下开机过程,操作系统分别调用他们:</p><pre><code class="java">public class OperationSystem {
private Component disk;
private Component memory;
private Component CPU;
public OperationSystem() {
this.disk = new Disk();
this.memory = new Memory();
this.CPU = new CPU();
}
public void startingUp(){
System.out.println("准备开机...");
disk.work();
memory.work();
CPU.work();
}
}</code></pre><p>而使用人调用的其实是操作系统的开机启动方法,不会直接调用到内部的方法,也就是屏蔽掉了所有的细节:</p><pre><code class="java">public class PersonTest {
public static void main(String[] args) {
OperationSystem operationSystem = new OperationSystem();
operationSystem.startingUp();
}
}</code></pre><p>执行结果如下:</p><pre><code class="txt">准备开机...
磁盘工作了...
内存工作了...
CPU工作了...</code></pre><p>最后简单小结一下,外观模式,可以成为门面模式,也就是屏蔽掉内部细节,只对外提供接口,实现所需的功能,内部功能可能很复杂,以上我们模拟的只是简单操作。学会了么?</p><p><img src="/img/remote/1460000041239726" alt="" title=""></p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,个人网站:<a href="https://link.segmentfault.com/?enc=QwAEok2eSbXs3jW5M9N%2F8w%3D%3D.ZKSvTk0gOooixq%2BpWXZMMI2DEbomGp8fbRWoEuxtGo4%3D" rel="nofollow">http://aphysia.cn</a>,技术之路不在一时,山高水长,纵使缓慢,驰而不息。</p><p><a href="https://link.segmentfault.com/?enc=kbodt4X0kVdpxE5XfB6vcQ%3D%3D.dJql%2BLk3edN7D%2BTiR7xRTh8UmxTU0MmRoaEhrzK0DpTKXetNsW%2F%2FgeJExl%2FAsx9R" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=Cm%2FUOoIEnhdyIRX%2BA2%2Bt1A%3D%3D.cpunhv4%2ByzR6EtE1k8BzojFeUMscYvEhN57KdjQ9wnYN2IAZoXKLft1xGyai79ON" rel="nofollow">开源编程笔记</a></p>
设计模式【8】-- 手工耿教我写装饰器模式
https://segmentfault.com/a/1190000041233064
2022-01-06T08:43:53+08:00
2022-01-06T08:43:53+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p><img src="/img/remote/1460000041100740" alt="https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/设计模式.png" title="https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/设计模式.png"></p><h2>装饰器模式</h2><p>前面学习了好几种设计模式,今天继续...</p><p><img src="/img/remote/1460000041233066" alt="" title=""></p><p>装饰器模式,属于结构型模式,用来包裹封装现在的类对象,希望可以在不修改现在类对象和类定义的前提下,能够拓展对象的功能。</p><p>调用的时候,使用的是装饰后的对象,而不是原对象。,提供了额外的功能。</p><p>不知道大家有没有看手工耿的自制钢琴烤串车视频【<a href="https://www.bilibili.com/video/BV1334y1Z7kq?spm_id_from=333.999.0.0">https://www.bilibili.com/vide...</a> 】, 本来是一个钢琴,但是为了边弹琴,边烤串,改造装饰了一下,变成了特殊的钢琴,提供了额外的烤串功能。<strong>典型的装饰器模式</strong></p><p><img src="/img/remote/1460000041233067" alt="" title=""></p><p><strong>目的:</strong> 为了灵活的拓展类对象的功能。</p><p>主要包括以下几种角色:</p><ul><li>抽象组件(<code>Component</code>): 被装饰的原始类的抽象,可以是抽象类,亦或是接口。</li><li>具体实现类(<code>ConcreteComponent</code>):具体的被抽象的类</li><li>抽象装饰器(<code>Decorator</code>): 通用抽象器</li><li>具体装饰器(<code>ConcreteDecorator</code>):<code>Decorator</code> 的具体实现类,理论上,每个 <code>ConcreteDecorator</code>都扩展了<code>Component</code>对象的一种功能。</li></ul><h2>优缺点</h2><p>优点:</p><ul><li>相对于类继承,包裹对象更加容易拓展,更加灵活</li><li>装饰类和被装饰类相互独立,耦合度比较低</li><li>完全遵守开闭原则。</li></ul><p>缺点:</p><ul><li>包裹对象层级较深的时候,理解难度较大。</li></ul><h2>实现</h2><p>先抽闲一个乐器接口类 <code>Instrument</code>:</p><pre><code class="java">public interface Instrument {
void play();
}</code></pre><p>弄两种乐器<code>Piano</code>和<code>Guitar</code> ,实现乐器接口:</p><pre><code class="java">public class Piano implements Instrument{
@Override
public void play() {
System.out.println("手工耿弹奏钢琴");
}
}</code></pre><pre><code class="java">public class Guitar implements Instrument{
@Override
public void play() {
System.out.println("手工耿弹吉他");
}
}</code></pre><p>不管手工耿要边弹吉他边烧烤,还是边弹钢琴边烧烤,还是边弹钢琴边洗澡,不管什么需求,我们抽象一个装饰器类,专门对乐器类进行包装,装饰。</p><pre><code class="java">public class InstrumentDecorator implements Instrument{
protected Instrument instrument;
public InstrumentDecorator(Instrument instrument) {
this.instrument = instrument;
}
@Override
public void play() {
instrument.play();
}
}</code></pre><p>上面的是抽象的装饰类,具体装饰成什么样,我们得搞点实际动作,那就搞个烧烤功能。</p><pre><code class="java">public class BarbecueInstrumentDecorator extends InstrumentDecorator {
public BarbecueInstrumentDecorator(Instrument instrument) {
super(instrument);
}
@Override
public void play() {
instrument.play();
barbecue();
}
public void barbecue(){
System.out.println("手工耿在烧烤");
}
}</code></pre><p>测试一下:</p><pre><code class="java">public class DecoratorDemo {
public static void main(String[] args) {
Instrument instrument = new Piano();
instrument.play();
System.out.println("----------------------------------------");
InstrumentDecorator barbecuePiano = new BarbecueInstrumentDecorator(new Piano());
barbecuePiano.play();
System.out.println("----------------------------------------");
InstrumentDecorator barbecueGuitar = new BarbecueInstrumentDecorator(new Guitar());
barbecueGuitar.play();
}
}</code></pre><p>测试结果如下,可以看到不装饰的时候,只能干一件事,装饰之后的对象,既可以弹奏乐器,也可以烧烤,不禁感叹:原来手工耿是设计模式高手:</p><pre><code class="txt">手工耿弹奏钢琴
----------------------------------------
手工耿弹奏钢琴
手工耿在烧烤
----------------------------------------
手工耿弹吉他
手工耿在烧烤</code></pre><h2>小结一下</h2><p>设计模式,不是银弹,只是在软件工程或者说编程中,演变出来的较好实践。我们不能为了设计模式而设计模式,学习理论只是为了更好的使用它,知道什么时候应该使用,什么时候不该使用。</p><p>装饰器模式是为了拓展其功能,但又不破坏原来的结构的前提下做的,其实在<code>Java IO</code>的源码里面有大量使用,比如:</p><pre><code class="java">DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream("test.txt")
)
);</code></pre><p>先把<code>FileInputStream</code>传递给<code>BufferedInputStream</code>弄成一个装饰对象,再把装饰对象传递给<code>DataInputStream</code>,再装饰一遍。最终,<code>FileInputStream</code> 被包装成了 <code>DataInputStream</code>,感兴趣的同学可以翻一下源码。</p><p><img src="/img/remote/1460000041233068" alt="" title=""></p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。</p><p><a href="https://link.segmentfault.com/?enc=BaCALs0OuoCOy%2F%2BtCglhVg%3D%3D.OuPigpWnK6wKvRP8xCdv%2Bm2elFQoGWjjqF70OQEtC%2FFJDSQjZ2zzAfPuI%2FBX0c%2Fa" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=sHuVc4Va9vNn%2BlsK%2F6n3Dw%3D%3D.AgMcpPYOWBfRICBlWgtgw%2F9WKoLfYPVvAdv8gDIOodgGXdbSmafFAs%2F2o4ZVVV6n" rel="nofollow">开源编程笔记</a></p>
设计模式【7】-- 探索一下桥接模式
https://segmentfault.com/a/1190000041225650
2022-01-05T08:23:59+08:00
2022-01-05T08:23:59+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>设计模式,写代码必备神器...</p><p><img src="/img/remote/1460000041100740" alt="设计模式" title="设计模式"></p><h2>桥接模式是什么?</h2><p><strong>桥接模式</strong>是把抽象化和实现化解耦,让两者可以独立,该设计模式属于结构性设计模式。何为将抽象化和实现化解耦,可以理解为将功能点抽象出来,功能的实现如何取决于不同的需求,但是抽象的功能点(接口)已经<strong>被桥接</strong>到原本的类型上,只用关注与实现。原本的类型变化,和抽象的功能点可以自由变化,<strong>中间的桥梁已经搭建起来了</strong>。</p><p>桥接模式其实就是不单单使用类继承的方式,而是重点使用类聚合的方式,进行桥接,把抽象的功能点,聚合(注入)到基类里面。</p><h2>桥接模式的好处</h2><p><strong>一般用于解决什么问题呢?</strong></p><p>主要是功能点实现种类多,多个维度的功能点,<strong>独立变化</strong>,没有什么关联,可以按照维度来管理。比如有 2 个维度,每个维度有 3 种实现,但是不同的维度之间其实没有关联,如果按照维度之间两两关联来搞,单单是实现类的数量就已经<code>2 * 3 = 6</code>个类了,是在不太合适,还耦合在一块。</p><p>用电脑举个例子,既会分成不同的品牌,比如戴尔,联想,又会分为台式机,笔记本,那么不同的类就会很多,功能可能比较重复。正是鉴于这一点,我们得剥离重复的功能,用桥接的方式,来维护抽象出来的共同功能点。</p><p><img src="/img/remote/1460000041225652" alt="image-20211204132503297" title="image-20211204132503297"></p><p>如果再新增一个品牌,比如,华硕,那么又得增加两个类,这明显不太合适,不同的类很多功能可能会重复。</p><p><img src="/img/remote/1460000041225653" alt="image-20211204131258227" title="image-20211204131258227"></p><p>那么桥接模式怎么处理呢?桥接模式把两个不同的维度 <strong>台式机</strong> 和 <strong>笔记本</strong>抽取出来,相当于作为一个通用的属性来维护。</p><p><img src="/img/remote/1460000041225654" alt="image-20211205224859234" title="image-20211205224859234"></p><h2>代码Demo演示</h2><p>首先,定义一个抽象的电脑类<code>AbstractComputer</code>,在其中有一个属性是<code>ComputerType</code>,表示电脑的类型:</p><pre><code class="java">public abstract class AbstractComputer {
protected ComputerType type;
public void setType(ComputerType type) {
this.type = type;
}
public abstract void work();
}</code></pre><p>再定义三种类型的电脑:<code>LenovoComputer</code>,<code>AsusComputer</code>,<code>DellComputer</code>:</p><pre><code class="java">public class LenovoComputer extends AbstractComputer{
@Override
public void work() {
System.out.print("联想");
type.feature();
}
}</code></pre><pre><code class="java">public class AsusComputer extends AbstractComputer{
@Override
public void work() {
System.out.print("华硕");
type.feature();
}
}</code></pre><pre><code class="java">public class DellComputer extends AbstractComputer{
@Override
public void work() {
System.out.print("戴尔");
type.feature();
}
}</code></pre><p>电脑类型这个维度同样需要一个抽象类<code>ComputerType</code>,里面有一个说明功能的方法<code>feature()</code>:</p><pre><code class="java">public abstract class ComputerType {
// 功能特性
public abstract void feature();
}</code></pre><p>电脑类型这个维度,我们定义台式机和笔记本电脑两种:</p><pre><code class="java">public class DesktopComputerType extends ComputerType{
@Override
public void feature() {
System.out.println(" 台式机:性能强大,拓展性强");
}
}</code></pre><pre><code class="java">public class LaptopComputerType extends ComputerType{
@Override
public void feature() {
System.out.println(" 笔记本电脑:小巧便携,办公不在话下");
}
}</code></pre><p>测试一下,我们需要不同的搭配的时候,只需要将一个维度<code>set</code>到对象中去即可,就可以聚合出不同品牌不同类型的电脑:</p><pre><code class="java">public class BridgeTest {
public static void main(String[] args) {
ComputerType desktop = new DesktopComputerType();
LenovoComputer lenovoComputer = new LenovoComputer();
lenovoComputer.setType(desktop);
lenovoComputer.work();
ComputerType laptop = new LaptopComputerType();
DellComputer dellComputer = new DellComputer();
dellComputer.setType(laptop);
dellComputer.work();
}
}</code></pre><p>测试结果:</p><pre><code class="txt">联想 台式机:性能强大,拓展性强
戴尔 笔记本电脑:小巧便携,办公不在话下</code></pre><h2>总结一下</h2><p>桥接模式,本质上就是将不同维度或者说功能,抽象出来,作为属性,聚合到对象里面,而不是通过继承。这样一定程度上减少了类的数量,但是如果不同的维度之间,变化是相关联的,这样使用起来还需要再做各种特殊判断,使用起来容易造成混乱,不宜使用。(重点:<strong>用组合/聚合关系代替继承关系来实现</strong>)</p><p><code>JDBC</code>,搞过<code>Java</code>的同学应该都知道,这是一种<code>Java</code>统一访问数据库的<code>API</code>,可以操作<code>Mysql</code>,<code>Oracle</code>等,主要用到的设计模式也是桥接模式,有兴趣可以了解一下<code>Driver</code>驱动类管理的源码。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=NNtEtVUrnChBokIWs47P8A%3D%3D.CaUSCdzZaVN2PZuLcUVFzshcUbfTvE3diAJr9shn1nRzAPi%2Bupgixh0SGrBAgUdk" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=sIryHDR3z2Mf1M23Lpy6FQ%3D%3D.2JJ1boEbxlH%2F6N0ZvGiGCb40ZIPcSbd%2FfJHGGWxt0ahKQPuLgq%2Bw8cVPIIHcqpS8" rel="nofollow">开源编程笔记</a></p>
数据库批量插入这么讲究的么?
https://segmentfault.com/a/1190000041218630
2022-01-04T08:40:08+08:00
2022-01-04T08:40:08+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
1
<p>最近新的项目写了不少各种 <code>insertBatch</code> 的代码,一直有人说,批量插入比循环插入效率高很多,那本文就来实验一下,到底是不是真的?</p><p>测试环境:</p><ul><li>SpringBoot 2.5</li><li>Mysql 8</li><li>JDK 8</li><li>Docker</li></ul><p>首先,多条数据的插入,可选的方案:</p><ul><li><code>foreach</code>循环插入</li><li>拼接<code>sql</code>,一次执行</li><li>使用批处理功能插入</li></ul><h2>搭建测试环境`</h2><p><code>sql</code>文件:</p><pre><code class="sql">drop database IF EXISTS test;
CREATE DATABASE test;
use test;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT "",
`age` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;</code></pre><p>应用的配置文件:</p><pre><code class="properties">server:
port: 8081
spring:
#数据库连接配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&&serverTimezone=UTC&setUnicode=true&characterEncoding=utf8&&nullCatalogMeansCurrent=true&&autoReconnect=true&&allowMultiQueries=true
username: root
password: 123456
#mybatis的相关配置
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.aphysia.spingbootdemo.model
#开启驼峰命名
configuration:
map-underscore-to-camel-case: true
logging:
level:
root: error
</code></pre><p>启动文件,配置了<code>Mapper</code>文件扫描的路径:</p><pre><code class="java">import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.aphysia.springdemo.mapper")
public class SpringdemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringdemoApplication.class, args);
}
}</code></pre><p><code>Mapper</code>文件一共准备了几个方法,插入单个对象,删除所有对象,拼接插入多个对象:</p><pre><code class="java">import com.aphysia.springdemo.model.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserMapper {
int insertUser(User user);
int deleteAllUsers();
int insertBatch(@Param("users") List<User>users);
}</code></pre><p><code>Mapper.xml</code>文件如下:</p><pre><code class="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" >
<mapper namespace="com.aphysia.springdemo.mapper.UserMapper">
<insert id="insertUser" parameterType="com.aphysia.springdemo.model.User">
insert into user(id,age) values(#{id},#{age})
</insert>
<delete id="deleteAllUsers">
delete from user where id>0;
</delete>
<insert id="insertBatch" parameterType="java.util.List">
insert into user(id,age) VALUES
<foreach collection="users" item="model" index="index" separator=",">
(#{model.id}, #{model.age})
</foreach>
</insert>
</mapper></code></pre><p>测试的时候,每次操作我们都删除掉所有的数据,保证测试的客观,不受之前的数据影响。</p><h2>不同的测试</h2><h3>1. foreach 插入</h3><p>先获取列表,然后每一条数据都执行一次数据库操作,插入数据:</p><pre><code class="java">@SpringBootTest
@MapperScan("com.aphysia.springdemo.mapper")
class SpringdemoApplicationTests {
@Autowired
SqlSessionFactory sqlSessionFactory;
@Resource
UserMapper userMapper;
static int num = 100000;
static int id = 1;
@Test
void insertForEachTest() {
List<User> users = getRandomUsers();
long start = System.currentTimeMillis();
for (int i = 0; i < users.size(); i++) {
userMapper.insertUser(users.get(i));
}
long end = System.currentTimeMillis();
System.out.println("time:" + (end - start));
}
}</code></pre><h3>2. 拼接sql插入</h3><p>其实就是用以下的方式插入数据:</p><pre><code class="sql">INSERT INTO `user` (`id`, `age`)
VALUES (1, 11),
(2, 12),
(3, 13),
(4, 14),
(5, 15);</code></pre><pre><code class="java"> @Test
void insertSplicingTest() {
List<User> users = getRandomUsers();
long start = System.currentTimeMillis();
userMapper.insertBatch(users);
long end = System.currentTimeMillis();
System.out.println("time:" + (end - start));
}</code></pre><h3>3. 使用Batch批量插入</h3><p>将<code>MyBatis session</code> 的 <code>executor type</code> 设为 <code>Batch </code>,使用<code>sqlSessionFactory</code>将执行方式置为批量,自动提交置为<code>false</code>,全部插入之后,再一次性提交:</p><pre><code class="java"> @Test
public void insertBatch(){
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> users = getRandomUsers();
long start = System.currentTimeMillis();
for(int i=0;i<users.size();i++){
mapper.insertUser(users.get(i));
}
sqlSession.commit();
sqlSession.close();
long end = System.currentTimeMillis();
System.out.println("time:" + (end - start));
}</code></pre><h3>4. 批量处理+分批提交</h3><p>在批处理的基础上,每1000条数据,先提交一下,也就是分批提交。</p><pre><code class="java"> @Test
public void insertBatchForEachTest(){
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> users = getRandomUsers();
long start = System.currentTimeMillis();
for(int i=0;i<users.size();i++){
mapper.insertUser(users.get(i));
if (i % 1000 == 0 || i == num - 1) {
sqlSession.commit();
sqlSession.clearCache();
}
}
sqlSession.close();
long end = System.currentTimeMillis();
System.out.println("time:" + (end - start));
}
</code></pre><h2>初次结果,明显不对?</h2><p>运行上面的代码,我们可以得到下面的结果,<code>for</code>循环插入的效率确实很差,拼接的<code>sql</code>效率相对高一点,看到有些资料说拼接<code>sql</code>可能会被<code>mysql</code>限制,但是我执行到<code>1000w</code>的时候,才看到堆内存溢出。</p><p><strong>下面是不正确的结果!!!</strong></p><table><thead><tr><th align="center">插入方式</th><th align="center">10</th><th align="center">100</th><th align="center">1000</th><th align="center">1w</th><th align="center">10w</th><th align="center">100w</th><th>1000w</th></tr></thead><tbody><tr><td align="center">for循环插入</td><td align="center">387</td><td align="center">1150</td><td align="center">7907</td><td align="center">70026</td><td align="center">635984</td><td align="center">太久了...</td><td>太久了...</td></tr><tr><td align="center">拼接sql插入</td><td align="center">308</td><td align="center">320</td><td align="center">392</td><td align="center">838</td><td align="center">3156</td><td align="center">24948</td><td>OutOfMemoryError: 堆内存溢出</td></tr><tr><td align="center">批处理</td><td align="center">392</td><td align="center">917</td><td align="center">5442</td><td align="center">51647</td><td align="center">470666</td><td align="center">太久了...</td><td>太久了...</td></tr><tr><td align="center">批处理 + 分批提交</td><td align="center">359</td><td align="center">893</td><td align="center">5275</td><td align="center">50270</td><td align="center">472462</td><td align="center">太久了...</td><td>太久了...</td></tr></tbody></table><h3>拼接sql并没有超过内存</h3><p>我们看一下<code>mysql</code>的限制:</p><pre><code class="txt">mysql> show VARIABLES like '%max_allowed_packet%';
+---------------------------+------------+
| Variable_name | Value |
+---------------------------+------------+
| max_allowed_packet | 67108864 |
| mysqlx_max_allowed_packet | 67108864 |
| slave_max_allowed_packet | 1073741824 |
+---------------------------+------------+
3 rows in set (0.12 sec)</code></pre><p>这<code>67108864</code>足足<code>600</code>多M,太大了,怪不得不会报错,那我们去改改一下它吧,改完重新测试:</p><ol><li>首先在启动<code>mysql</code>的情况下,进入容器内,也可以直接在<code>Docker</code>桌面版直接点<code>Cli</code>图标进入:</li></ol><pre><code class="shell">docker exec -it mysql bash</code></pre><ol start="2"><li>进入<code> /etc/mysql </code>目录,去修改<code>my.cnf</code>文件:</li></ol><pre><code class="shell">cd /etc/mysql</code></pre><ol start="3"><li>先按照<code>vim</code>,要不编辑不了文件:</li></ol><pre><code class="shell">apt-get update
apt-get install vim</code></pre><ol start="4"><li>修改<code>my.cnf</code></li></ol><pre><code class="shell">vim my.cnf</code></pre><ol start="5"><li>在最后一行添加<code>max_allowed_packet=20M</code>(按<code>i</code>编辑,编辑完按<code>esc</code>,输入<code>:wq</code>退出)</li></ol><pre><code class="shell">
[mysqld]
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
datadir = /var/lib/mysql
secure-file-priv= NULL
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
# Custom config should go here
!includedir /etc/mysql/conf.d/
max_allowed_packet=2M</code></pre><ol start="6"><li>退出容器</li></ol><pre><code class="shell"># exit</code></pre><ol start="7"><li>查看<code>mysql</code>容器<code>id</code></li></ol><pre><code class="shell">docker ps -a</code></pre><p><img src="/img/remote/1460000041218632" alt="image-20211130005909539" title="image-20211130005909539"></p><ol start="8"><li>重启<code>mysql</code></li></ol><pre><code class="shell">docker restart c178e8998e68</code></pre><p>重启成功后查看最大的<code>max_allowed_pactet</code>,发现已经修改成功:</p><pre><code class="shell">mysql> show VARIABLES like '%max_allowed_packet%';
+---------------------------+------------+
| Variable_name | Value |
+---------------------------+------------+
| max_allowed_packet | 2097152 |
| mysqlx_max_allowed_packet | 67108864 |
| slave_max_allowed_packet | 1073741824 |
+---------------------------+------------+</code></pre><p>我们再次执行拼接<code>sql</code>,发现<code>100w</code>的时候,<code>sql</code>就达到了<code>3.6M</code>左右,超过了我们设置的<code>2M</code>,成功的演示抛出了错误:</p><pre><code class="shell">org.springframework.dao.TransientDataAccessResourceException:
### Cause: com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (36,788,583 > 2,097,152). You can change this value on the server by setting the 'max_allowed_packet' variable.
; Packet for query is too large (36,788,583 > 2,097,152). You can change this value on the server by setting the 'max_allowed_packet' variable.; nested exception is com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (36,788,583 > 2,097,152). You can change this value on the server by setting the 'max_allowed_packet' variable.
</code></pre><h3>批量处理为什么这么慢?</h3><p>但是,仔细一看就会发现,上面的方式,怎么批处理的时候,并没有展示出优势了,和<code>for</code>循环没有什么区别?这是对的么?</p><p>这肯定是不对的,从官方文档中,我们可以看到它会批量更新,不会每次去创建预处理语句,理论是更快的。</p><p><img src="/img/remote/1460000041218633" alt="image-20211130011820487" title="image-20211130011820487"></p><p>然后我发现我的一个最重要的问题:数据库连接 <code>URL </code>地址少了<strong>rewriteBatchedStatements=true</strong></p><p>如果我们不写,<code>MySQL JDBC</code> 驱动在默认情况下会忽视 <code>executeBatch()</code> 语句,我们期望批量执行的一组 <code>sql</code> 语句拆散,但是执行的时候是一条一条地发给 <code>MySQL</code> 数据库,实际上是单条插入,直接造成较低的性能。我说怎么性能和循环去插入数据差不多。</p><p>只有将 <code>rewriteBatchedStatements</code> 参数置为 <code>true</code>, 数据库驱动才会帮我们批量执行 <code>SQL</code>。</p><p>正确的数据库连接:</p><pre><code class="shell">jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&&serverTimezone=UTC&setUnicode=true&characterEncoding=utf8&&nullCatalogMeansCurrent=true&&autoReconnect=true&&allowMultiQueries=true&&&rewriteBatchedStatements=true</code></pre><p>找到问题之后,我们重新测试批量测试,最终的结果如下:</p><table><thead><tr><th align="center">插入方式</th><th align="center">10</th><th align="center">100</th><th align="center">1000</th><th align="center">1w</th><th align="center">10w</th><th align="center">100w</th><th>1000w</th></tr></thead><tbody><tr><td align="center">for循环插入</td><td align="center">387</td><td align="center">1150</td><td align="center">7907</td><td align="center">70026</td><td align="center">635984</td><td align="center">太久了...</td><td>太久了...</td></tr><tr><td align="center">拼接sql插入</td><td align="center">308</td><td align="center">320</td><td align="center">392</td><td align="center">838</td><td align="center">3156</td><td align="center">24948(很可能超过sql长度限制)</td><td>OutOfMemoryError: 堆内存溢出</td></tr><tr><td align="center">批处理(重点)</td><td align="center">333</td><td align="center">323</td><td align="center">362</td><td align="center">636</td><td align="center">1638</td><td align="center">8978</td><td>OutOfMemoryError: 堆内存溢出</td></tr><tr><td align="center">批处理 + 分批提交</td><td align="center">359</td><td align="center">313</td><td align="center">394</td><td align="center">630</td><td align="center">2907</td><td align="center">18631</td><td>OutOfMemoryError: 堆内存溢出</td></tr></tbody></table><p>从上面的结果来看,确实批处理是要快很多的,当数量级太大的时候,其实都会超过内存溢出的,批处理加上分批提交并没有变快,和批处理差不多,反而变慢了,提交太多次了,拼接<code>sql</code>的方案在数量比较少的时候其实和批处理相差不大,最差的方案就是<code>for</code>循环插入数据,这真的特别的耗时。<code>100</code>条的时候就已经需要<code>1s</code>了,不能选择这种方案。</p><p>一开始发现批处理比较慢的时候,真的挺怀疑自己,后面发现是有一个参数,有一种拨开云雾的感觉,知道得越多,不知道的越多。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。</p><p><a href="https://link.segmentfault.com/?enc=LRtRxAnyJiQAXIytDcwveg%3D%3D.sED4mAYwQcOsui7RtDDfrYUQCb%2FbVEdlunSC7B7FaqaJvJOegD2%2BNniLCnZJY9VS" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=W02rLSMyzzsebkctIhCssg%3D%3D.bu9jqTAXRIsFGQzMtr3hxI5wWyTBM%2FgRBhTpZSVdncM%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=cuI%2BhVBl2SpfjzGC%2FAIAlw%3D%3D.h6cmp6dmr%2BKXPgglgXDN3xDnQxEUDYlbsXXwEDBlRM6FIOBC9J9w3cw6YlVACunD" rel="nofollow">开源编程笔记</a></p>
完蛋,我的事务怎么不生效?
https://segmentfault.com/a/1190000041189086
2021-12-28T09:12:09+08:00
2021-12-28T09:12:09+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<h2>前言</h2><p>事务大家平时应该都有写,之前写事务的时候遇到一点坑,居然不生效,后来排查了一下,复习了一下各种事务失效的场景,想着不如来一个总结,这样下次排查问题,就能有恃无恐了。那么先来复习一下事务相关知识,事务是指操作的最小工作单位,作为一个单独且不可切割的单元操作,要么全部成功,要么全部失败。事务有四大特性(<code>ACID</code>):</p><ul><li>原子性(<code>Atomicity</code>):事务包含的操作,要么全部成功,要么全部失败回滚,不会存在一半成功一半失败的中间状态。比如<code>A</code>和<code>B</code>一开始都有<code>500</code>元,<code>A</code>给<code>B</code>转账<code>100</code>,那么<code>A</code>的钱少了<code>100</code>,<code>B</code>的钱就必须多了<code>100</code>,不能<code>A</code>少了钱,<code>B</code>也没收到钱,那这个钱就不翼而飞了,不符合原子性了。</li><li>一致性(<code>Consistency</code>):一致性是指事务执行之前和之后,保持整体状态的一致,比如<code>A</code>和<code>B</code>一开始都有<code>500</code>元,加起来是<code>1000</code>元,这个是之前的状态,<code>A</code>给<code>B</code>转账<code>100</code>,那么最后<code>A</code>是<code>400</code>,<code>B</code>是<code>600</code>,两者加起来还是<code>1000</code>,这个整体状态需要保证。</li><li>隔离性(<code>Isolation</code>):前面两个特性都是针对同一个事务的,而隔离性指的是不同的事务,当多个事务同时在操作同一个数据的时候,需要隔离不同事务之间的影响,并发执行的事务之间不能相互干扰。</li><li>持久性(<code>Durability</code>):指事务如果一旦被提交了,那么对数据库的修改就是永久性的,就算是数据库发生故障了,已经发生的修改也必然存在。</li></ul><p>事务的几个特性并不是数据库事务专属的,广义上的事务是一种工作机制,是并发控制的基本单位,保证操作的结果,还会包括分布式事务之类的,但是一般我们谈论事务,不特指的话,说的就是与数据库相关的,因为我们平时说的事务基本都基于数据库来完成。</p><blockquote>事务不仅是适用于数据库。我们可以将此概念扩展到其他组件,类似队列服务或外部系统状态。因此,“一系列数据操作语句必须完全完成或完全失败,以一致的状态离开系统”</blockquote><h3>测试环境</h3><p>前面我们已经部署过了一些demo项目,以及用docker快速搭建环境,本文基于的也是之前的环境:</p><ul><li>JDK 1.8</li><li>Maven 3.6</li><li>Docker</li><li>Mysql</li></ul><h3>事务正常回滚的样例</h3><p>正常的事务样例,包含两个接口,一个是获取所有的用户中的数据,另外一个更新的,是<code>update</code>用户数据,其实就是每个用户的年龄<code>+1</code>,我们让一次操作完第一个之后,抛出异常,看看最后的结果:</p><pre><code class="java">@Service("userService")
public class UserServiceImpl implements UserService {
@Resource
UserMapper userMapper;
@Autowired
RedisUtil redisUtil;
@Override
public List<User> getAllUsers() {
List<User> users = userMapper.getAllUsers();
return users;
}
@Override
@Transactional
public void updateUserAge() {
userMapper.updateUserAge(1);
int i= 1/0;
userMapper.updateUserAge(2);
}
}</code></pre><p>数据库操作:</p><pre><code class="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" >
<mapper namespace="com.aphysia.springdocker.mapper.UserMapper">
<select id="getAllUsers" resultType="com.aphysia.springdocker.model.User">
SELECT * FROM user
</select>
<update id="updateUserAge" parameterType="java.lang.Integer">
update user set age=age+1 where id =#{id}
</update>
</mapper></code></pre><p>先获取<code>http://localhost:8081/getUserList</code>所有的用户看看:</p><p><img src="/img/remote/1460000041189088" alt="image-20211124233731699" title="image-20211124233731699"></p><p>在调用更新接口,页面抛出错误了:</p><p><img src="/img/remote/1460000041189089" alt="image-20211124233938596" title="image-20211124233938596"></p><p>控制台也出现了异常,意思是除以0,异常:</p><pre><code class="shell">java.lang.ArithmeticException: / by zero
at com.aphysia.springdocker.service.impl.UserServiceImpl.updateUserAge(UserServiceImpl.java:35) ~[classes/:na]
at com.aphysia.springdocker.service.impl.UserServiceImpl$$FastClassBySpringCGLIB$$c8cc4526.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.12.jar:5.3.12]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783) ~[spring-aop-5.3.12.jar:5.3.12]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.12.jar:5.3.12]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-5.3.12.jar:5.3.12]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) ~[spring-tx-5.3.12.jar:5.3.12]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.12.jar:5.3.12]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.12.jar:5.3.12]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) ~[spring-aop-5.3.12.jar:5.3.12]
at com.aphysia.springdocker.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$25070cf0.updateUserAge(<generated>) ~[classes/:na]</code></pre><p>然后我们再次请求<code>http://localhost:8081/getUserList</code>,看到数据两个都是<code>11</code>说明数据都没有发生变化,第一个操作完之后,异常,回滚成功了:</p><pre><code class="json">[{"id":1,"name":"李四","age":11},{"id":2,"name":"王五","age":11}]</code></pre><p>那什么时候事务不正常回滚呢?且听我细细道来:</p><h2>实验</h2><h3>1. 引擎设置不对</h3><p>我们知道,<code>Mysql</code>其实有一个数据库引擎的概念,我们可以用<code>show engines</code>来查看<code>Mysql</code>支持的数据引擎:</p><p><img src="/img/remote/1460000041189090" alt="image-20211124234913121" title="image-20211124234913121"></p><p>可以看到<code>Transactions</code>那一列,也就是事务支持,只有<code>InnoDB</code>,那就是只有<code>InnoDB</code>支持事务,所以要是引擎设置成其他的事务会无效。</p><p>我们可以用<code>show variables like 'default_storage_engine'</code>看默认的数据库引擎,可以看到默认是<code>InnoDB</code>:</p><pre><code class="shell">mysql> show variables like 'default_storage_engine';
+------------------------+--------+
| Variable_name | Value |
+------------------------+--------+
| default_storage_engine | InnoDB |
+------------------------+--------+</code></pre><p>那我们看看我们演示的数据表是不是也是用了<code>InnoDB</code>,可以看到确实是使用<code>InnoDB</code></p><p><img src="/img/remote/1460000041189091" alt="image-20211124235353205" title="image-20211124235353205"></p><p>那我们把该表的引擎修改成<code>MyISAM</code>会怎么样呢?试试,在这里我们只修改数据表的数据引擎:</p><pre><code class="shell">mysql> ALTER TABLE user ENGINE=MyISAM;
Query OK, 2 rows affected (0.06 sec)
Records: 2 Duplicates: 0 Warnings: 0</code></pre><p>然后再<code>update</code>,不出意料,还是会报错,看起来错误没有什么不同:</p><p><img src="/img/remote/1460000041189092" alt="image-20211125000554928" title="image-20211125000554928"></p><p>但是获取全部数据的时候,第一个数据更新成功了,第二个数据没有更新成功,说明事务没有生效。</p><pre><code class="json">[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]</code></pre><p>结论:必须设置为<code>InnoDB</code>引擎,事务才生效。</p><h3>2. 方法不能是 private</h3><p>事务必须是<code>public</code>方法,如果用在了<code>private</code>方法上,那么事务会自动失效,但是在<code>IDEA</code>中,只要我们写了就会报错:<code>Methods annotated with '@Transactional' must be overrideable</code>,意思是事务的注解加上的方法,必须是可以重写的,<code>private</code>方法是不可以重写的,所以报错了。</p><p><img src="/img/remote/1460000041189093" alt="image-20211125083648166" title="image-20211125083648166"></p><p>同样的<code>final</code>修饰的方法,如果加上了注解,也会报错,因为用<code>final</code>就是不想被重写:</p><p><img src="/img/remote/1460000041189094" alt="image-20211126084347611" title="image-20211126084347611"></p><p><code>Spring</code>中主要是用放射获取<code>Bean</code>的注解信息,然后利用基于动态代理技术的<code>AOP</code>来封装了整个事务,理论上我想调用<code>private</code>方法也是没有问题的,在方法级别使用<code>method.setAccessible(true);</code>就可以,但是可能<code>Spring</code>团队觉得<code>private</code>方法就是开发人员意愿上不愿意公开的接口,没有必要破坏封装性,这样容易导致混乱。</p><p><code>Protected</code>方法可不可以?不可以!</p><p>下面我们为了实现,魔改代码结构,因为接口不能用<code>Portected</code>,如果用了接口,就不可能用<code>protected</code>方法,会直接报错,而且必须在同一个包里面使用,我们把<code>controller</code>和<code>service</code>放到同一个包下:</p><p><img src="/img/remote/1460000041189095" alt="image-20211125090358299" title="image-20211125090358299"></p><p>测试后发现<strong>事务不生效</strong>,结果依然是一个更新了,另外一个没有更新:</p><pre><code class="json">[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]</code></pre><p>结论:必须使用在<code>public</code>方法上,不能用在<code>private</code>,<code>final</code>,<code>static</code>方法上,否则不会生效。</p><h3>3. 异常必须是运行期的异常</h3><p><code>Springboot</code>管理异常的时候,只会对运行时的异常(<code>RuntimeException</code> 以及它的子类) 进行回滚,比如我们前面写的<code>i=1/0;</code>,就会产生运行时的异常。</p><p>从源码来看也可以看到,<code>rollbackOn(ex)</code>方法会判断异常是<code>RuntimeException</code>或者<code>Error</code>:</p><pre><code class="java"> public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}</code></pre><p>异常主要分为以下类型:</p><p>所有的异常都是<code>Throwable</code>,而<code>Error</code>是错误信息,一般是程序发生了一些不可控的错误,比如没有这个文件,内存溢出,<code>IO</code>突然错误了。而<code>Exception</code>下,除了<code>RuntimeException</code>,其他的都是<code>CheckException</code>,也就是可以处理的异常,<code>Java</code>程序在编写的时候就必须处理这个异常,否则编译是通不过去的。</p><p><img src="/img/remote/1460000041189096" alt="" title=""></p><p>由下面的图我们可以看出,<code>CheckedException</code>,我列举了几个常见的<code>IOException</code> IO异常,<code>NoSuchMethodException</code>没有找到这个方法,<code>ClassNotFoundException</code> 没找到这个类,而<code>RunTimeException</code>有常见的几种:</p><ul><li>数组越界异常:<code>IndexOutOfBoundsException</code></li><li>类型转换异常:<code>ClassCastException</code></li><li>空指针异常:<code>NullPointerException</code></li></ul><p><img src="/img/remote/1460000041189097" alt="" title=""></p><p>事务默认回滚的是:运行时异常,也就是<code>RunTimeException</code>,如果抛出其他的异常是无法回滚的,比如下面的代码,事务就会失效:</p><pre><code class="java"> @Transactional
public void updateUserAge() throws Exception{
userMapper.updateUserAge(1);
try{
int i = 1/0;
}catch (Exception ex){
throw new IOException("IO异常");
}
userMapper.updateUserAge(2);
}</code></pre><h3>4. 配置不对导致</h3><ol><li>方法上需要使用<code>@Transactional</code>才能开启事务</li><li>多个数据源配置或者多个事务管理器的时候,注意如果操作数据库<code>A</code>,不能使用<code>B</code>的事务,虽然这个问题很幼稚,但是有时候用错难查找问题。</li><li>如果在<code>Spring</code>中,需要配置<code>@EnableTransactionManagement</code>来开启事务,等同于配置<code>xml</code>文件<code>*<tx:annotation-driven/>*</code>,但是在<code>Springboot</code>中已经不需要了,在<code>springboot</code>中<code>SpringBootApplication</code>注解包含了<code>@EnableAutoConfiguration</code>注解,会自动注入。</li></ol><p><code>@EnableAutoConfiguration</code>自动注入了哪些东西呢?在<code>jetbrains://idea/navigate/reference?project=springDocker&path=~/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.5.6/spring-boot-autoconfigure-2.5.6.jar!/META-INF/spring.factories</code>下有自动注入的配置:</p><pre><code class="properties"># Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
...
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
...</code></pre><p>里面配置了一个<code>TransactionAutoConfiguration</code>,这是事务自动配置类:</p><pre><code class="java">@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PlatformTransactionManager.class)
@AutoConfigureAfter({ JtaAutoConfiguration.class, HibernateJpaAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class, Neo4jDataAutoConfiguration.class })
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {
...
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(TransactionManager.class)
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
public static class EnableTransactionManagementConfiguration {
@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement(proxyTargetClass = false) // 这里开启了事务
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
public static class JdkDynamicAutoProxyConfiguration {
}
...
}
}</code></pre><p>值得注意的是,<code>@Transactional</code>除了可以用于方法,还可以用于类,表示这个类所有的<code>public</code>方法都会配置事务。</p><h3>5. 事务方法不能在同个类里面调用</h3><p>想要进行事务管理的方法只能在其他类里面被调用,不能在当前类被调用,否则会失效,为了实现这个目的,如果同一个类有不少事务方法,还有其他方法,这个时候有必要抽取出一个事务类,这样分层会比较清晰,避免后继者写的时候在同一个类调用事务方法,造成混乱。</p><p>事务失效的例子:</p><p>比如我们将<code>service</code>事务方法改成:</p><pre><code class="java"> public void testTransaction(){
updateUserAge();
}
@Transactional
public void updateUserAge(){
userMapper.updateUserAge(1);
int i = 1/0;
userMapper.updateUserAge(2);
}</code></pre><p>在<code>controller</code>里面调用的是没有事务注解的方法,再间接调用事务方法:</p><pre><code class="java"> @RequestMapping("/update")
@ResponseBody
public int update() throws Exception{
userService.testTransaction();
return 1;
}</code></pre><p>调用之后,发现事务失效,一个更新另外一个没有更新:</p><pre><code class="json">[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]</code></pre><p><strong>为什么会这样呢?</strong></p><p><code>Spring</code>用切面对方法进行包装,只对外部调用方法进行拦截,内部方法没有进行拦截。</p><p>看源码:实际上我们调用事务方法的时候,会进入<code>DynamicAdvisedInterceptor</code>的<code>public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)()</code>方法:</p><p><img src="/img/remote/1460000041189098" alt="image-20211128125711187" title="image-20211128125711187"></p><p>里面调用了<code>AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice()</code>,这里是获取调用调用链。而没有<code>@Transactional</code>注解的方法<code>userService.testTransaction()</code>,根本获取不到代理调用链,调用的还是原来的类的方法。</p><p><code>spring</code>里面要想对一个方法进行代理,用的就是<code>aop</code>,肯定需要一个标识,标识哪一个方法或者类需要被代理,<code>spring</code>里面定义了<code>@Transactional</code>作为切点,我们定义这个标识,就会被代理。</p><p><strong>代理的时机是什么时候呢?</strong></p><p><code>Spring</code>统一管理了我们的<code>bean</code>,代理的时机自然就是创建<code>bean</code>的过程,看看哪一个类带了这个标识,就生成代理对象。</p><p><code>SpringTransactionAnnotationParser</code>这个类有一个方法是用来判断<code>TransactionAttribute</code>注解的:</p><pre><code class="java"> @Override
@Nullable
public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) {
AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(
element, Transactional.class, false, false);
if (attributes != null) {
return parseTransactionAnnotation(attributes);
}
else {
return null;
}
}</code></pre><h3>6.多线程下事务失效</h3><p>假设我们在多线程里面像以下方式使用事务,那么事务是不能正常回滚的:</p><pre><code class="java"> @Transactional
public void updateUserAge() {
new Thread(
new Runnable() {
@Override
public void run() {
userMapper.updateUserAge(1);
}
}
).start();
int i = 1 / 0;
userMapper.updateUserAge(2);
}</code></pre><p>因为不同的线程使用的是不同<code>SqlSession</code>,相当于另外一个连接,根本不会用到同一个事务:</p><pre><code class="shell">2021-11-28 14:06:59.852 DEBUG 52764 --- [ Thread-2] org.mybatis.spring.SqlSessionUtils : Creating a new SqlSession
2021-11-28 14:06:59.930 DEBUG 52764 --- [ Thread-2] c.a.s.mapper.UserMapper.updateUserAge : <== Updates: 1
2021-11-28 14:06:59.931 DEBUG 52764 --- [ Thread-2] org.mybatis.spring.SqlSessionUtils : Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2e956409]</code></pre><h3>7. 注意合理使用事务嵌套</h3><p>首先事务是有传播机制的:</p><ul><li><code>REQUIRED</code>(默认):支持使用当前事务,如果当前事务不存在,创建一个新事务,如果有直接使用当前的事务。</li><li><code>SUPPORTS</code>:支持使用当前事务,如果当前事务不存在,就不会使用事务。</li><li><code>MANDATORY</code>:支持使用当前事务,如果当前事务不存在,则抛出<code>Exception</code>,也就是必须当前处于事务里面。</li><li><code>REQUIRES_NEW</code>:创建新事务,如果当前事务存在,把当前事务挂起。</li><li><code>NOT_SUPPORTED</code>:没有事务执行,如果当前事务存在,把当前事务挂起。</li><li><code>NEVER</code>:没有事务执行,如果当前有事务则抛出<code>Exception</code>。</li><li><code>NESTED</code>:嵌套事务,如果当前事务存在,那么在嵌套的事务中执行。如果当前事务不存在,则表现跟`REQUIRED</li></ul><p>查不多。</p><p>默认的是<code>REQUIRED</code>,也就是事务里面调用另外的事务,实际上不会重新创建事务,而是会重用当前的事务。那如果我们这样来写嵌套事务:</p><pre><code class="java">@Service("userService")
public class UserServiceImpl {
@Autowired
UserServiceImpl2 userServiceImpl2;
@Resource
UserMapper userMapper;
@Transactional
public void updateUserAge() {
try {
userMapper.updateUserAge(1);
userServiceImpl2.updateUserAge();
}catch (Exception ex){
ex.printStackTrace();
}
}
}</code></pre><p>调用的另外一个事务:</p><pre><code class="java">@Service("userService2")
public class UserServiceImpl2 {
@Resource
UserMapper userMapper;
@Transactional
public void updateUserAge() {
userMapper.updateUserAge(2);
int i = 1 / 0;
}
}</code></pre><p>会抛出以下错误:</p><pre><code class="shell">org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only</code></pre><p>我们但是实际事务是正常回滚掉了,结果是对的,之所以出现这个问题,是因为里面到方法抛出了异常,用的是同一个事务,说明事务必须被回滚掉的,但是外层被<code>catch</code>住了,本来就是同一个事务,一个说回滚,一个<code>catch</code>住不让<code>spring</code>感知到<code>Exception</code>,那不是自相矛盾么?所以<code>spring</code>报错说:这个事务被标识了必须回滚掉,<strong>最终还是回滚掉了</strong>。</p><p>怎么处理呢?</p><ul><li><ol><li>外层主动抛出错误,<code>throw new RuntimeException()</code></li></ol></li><li><ol start="2"><li>使用<code>TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();</code>主动标识回滚</li></ol></li></ul><pre><code class="java"> @Transactional
public void updateUserAge() {
try {
userMapper.updateUserAge(1);
userServiceImpl2.updateUserAge();
}catch (Exception ex){
ex.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}</code></pre><h3>8. 依赖外部网络请求回滚需要考虑</h3><p>有些时候,我们不仅操作自己的数据库,还需要同时考虑外部的请求,比如同步数据,同步失败,需要回滚掉自己的状态,在这种场景下,必须考虑网络请求是否会出错,出错如何处理,错误码是哪一个的时候才成功。</p><p>如果网络超时了,实际上成功了,但是我们判定为没有成功,回滚掉了,可能会导致数据不一致。这种需要被调用方支持重试,重试的时候,需要支持幂等,多次调用保存状态的一致,虽然整个主流程很简单,里面的细节还是比较多的。</p><p><img src="/img/remote/1460000041189099" alt="image-20211128153822791" title="image-20211128153822791"></p><h2>总结</h2><p>事务被<code>Spring</code>包裹了复杂性,很多东西可能源码很深,我们用的时候注意模拟测试一下调用是不是能正常回滚,不能理所当然,人是会出错的,而很多时候黑盒测试根本测试这种异常数据,如果没有正常回滚,后面需要手动处理,考虑到系统之间同步的问题,会造成很多不必要的麻烦,手动改数据库这流程就必须走。</p><p><img src="/img/remote/1460000041189100" alt="image-20211128154248397" title="image-20211128154248397"></p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=UoT2OZv7GCehZVz4S3qq8A%3D%3D.%2BxGqFLKWr39zldpd%2F4lkzEjj4yAC47XPPZW9ihwaiU4KM3DDZHFf%2BDuuf4WdxMYz" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=n7JO9H5UR9hKLajhEao%2BvA%3D%3D.6oko4kjJxRGbHcF9fOp5NOfd9QI2n5yLwuSX2SKFyDI%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=NiVJ9cRl14tzcOZPWBRddw%3D%3D.vUiIxbvEEm6y0ImIoFva4NqahHwqP5PE%2Fr9p%2FrxPCgk%2BdylPCFrwNbzGbii9PsmQ" rel="nofollow">开源编程笔记</a></p>
如何用Docker Compose部署项目?
https://segmentfault.com/a/1190000041151538
2021-12-21T09:03:32+08:00
2021-12-21T09:03:32+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>[TOC]</p><h2>前言</h2><p>之前我们用<code>docker</code>部署了<code>springboot</code>,<code>redis</code>,<code>mysql</code>的项目,但是是部署在三个不同的容器里,还需要先知道<code>redis</code>和<code>mysql</code>的<code>ip</code>地址,手动配置到<code>springboot</code>应用容器里,我只是想快速在本地进行测试啊,这样成本太高了,有没有什么办法,把他们集中管理呢?比如把它构建成为一个镜像。</p><p>办法总是有的,那就是<code>Docker Compose</code>。</p><p>之前的项目地址:<a href="https://link.segmentfault.com/?enc=MTSWa6RoSuck3K16UwshNw%3D%3D.YwKTIZ633Mq%2BctJznfiWZnzTfTZ%2BnCN7G91JiR42dnGtFsnhjb1RfYHbObUDxDj6FQ76xULvHD3RFARSoKgvPJ%2FWvR0jyy9U5c6zU9PrmA8%3D" rel="nofollow">https://github.com/Damaer/Dem...</a></p><h2>Docker Compose</h2><h3>1. Docker Compose是什么?</h3><p><code>Docker Compose</code>其实就是用来定义和运行复杂应用的<code>Docker</code>工具,什么叫复杂应用,比如前面写的<code>springboot</code>+<code>redis</code>+<code>mysql</code>,里面就有三个容器,这种多个容器的,用一个工具来管理,它不香么?</p><blockquote>docker compose 通过配置文件来管理多个 <code>Docker</code> 容器,在配置文件中,所有的容器通过<code>service</code>来进行定义,然后使用<code>docker-compose</code>脚本来启动、停止、重启应用以及应用中的服务和所依赖的容器等。</blockquote><h3>2. Docker Compose 的具体步骤</h3><p>一般是三个步骤:</p><ul><li>使用<code>Dockerfile</code> 来定义应用程序的环境</li><li>在 <code>docker-compose.yml</code> 定义构成应用程序的服务,这样它们可以在隔离环境中一起运行。</li><li>执行 <code>docker-compose up</code> 命令来启动并运行整个应用程序。</li></ul><p>我使用的是<code>Mac OS</code>,装<code>Docker</code>的时候已经把<code>Docker Compose</code>也安装好了,不需要单独安装。</p><h3>3. 如何在IDEA项目里面使用Docker Compose</h3><p>首先<code>pom.xml</code>文件中需要注意配置小写的<code>artifactId</code>:</p><pre><code class="xml"> <groupId>com.aphysia</groupId>
<artifactId>dockerdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>dockerdemo</name>
<packaging>jar</packaging></code></pre><p>除此之外还需要配置插件:</p><pre><code class="xml"> <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.0.0</version>
<!-- 将插件绑定在某个phase执行 -->
<executions>
<execution>
<id>build-image</id>
<!-- 用户只需执行mvn package ,就会自动执行mvn docker:build -->
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
<configuration>
<imageName>${docker.image.prefix}/${project.artifactId}</imageName>
<dockerDirectory>src/main/docker</dockerDirectory>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</build></code></pre><p>除此之外,<code>Dockerfile</code>是必要的,上面的插件中已经配置了我们<code>dockerFile</code>需要放在<code><dockerDirectory>src/main/docker</dockerDirectory></code>这个位置,<code>DockerFile</code>里面配置如下:</p><pre><code class="txt">FROM openjdk:8-jdk-alpine
EXPOSE 8081
VOLUME /tmp
# 重写命名为app.jar
ADD dockerdemo-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]</code></pre><p>理论上到这个时候,我们使用<code>mvn clean package</code>就会生成对应的<code>jar</code>包:</p><p><img src="/img/remote/1460000041141123" alt="image-20211128215558740" title="image-20211128215558740"></p><p><code>docker compose</code>最重要的是配置<code>docker-compose.yml</code>,这个文件我们放在项目的根目录就可以,和<code>pom.xml</code>平级:</p><pre><code class="java">version: "3"
services:
redis:
image: redis:latest
restart: always
ports:
- "6389:6379"
volumes:
- /tmp/redis.conf:/etc/redis/redis.conf
command: redis-server /etc/redis/redis.conf
mysql:
image: mysql:latest
restart: always
environment:
MYSQL_ROOT_PASSWORD: "123456"
MYSQL_USER: 'root'
MYSQL_PASS: '123456'
ports:
- "3306:3306"
volumes:
- "./db:/var/lib/mysql"
- "./conf/my.cnf:/etc/my.cnf"
- "./init:/docker-entrypoint-initdb.d/"
# 指定服务名称
webapp:
# 指定服务使用的镜像
image: aphysia/dockerdemo
# 指定容器名称
container_name: dockerdemo
# 指定服务运行的端口
ports:
- 8081:8081
# 指定容器中需要挂载的文件
volumes:
- /etc/localtime:/etc/localtime
- /tmp/dockerdemo/logs:/var/logs</code></pre><p>值得注意的点:</p><ol><li>service里面就是我们配置的镜像,包含了<code>redis</code>,<code>mysql</code>,<code>webapp</code>,<code>webapp</code>其实就是我们的应用。</li><li><code>"6389:6379"</code>中<code>6389</code>其实是我们主机的端口,也就是我的<code>Mac</code>连接<code>redis</code>容器需要使用<code>6389</code>,而容器之间连接需要使用<code>6379</code>,这是容器的端口。</li><li><p><code>/tmp/redis.conf:/etc/redis/redis.conf</code>中<code>/tmp/redis.conf</code>是主机的目录,而这个目录需要在docker里面配置才可以,要不就会报错(执行记得加管理员权限):</p><pre><code class="shell">docker ERROR: * start service *: Mounts denied</code></pre><p><img src="/img/remote/1460000041141124" alt="image-20211128220527229" title="image-20211128220527229"></p></li><li><code>mysql</code> 8.0 可能会报错<code>java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed</code>,这个是因为<code>url</code>链接少了一个参数:<code>allowPublicKeyRetrieval=true</code></li></ol><h4>启动可能出现的坑点</h4><p>启动后可能链接不上<code>mysql</code>或者<code>redis</code>,但是看容器运行情况又是正常的:</p><pre><code class="shell">DockerCompose % docker container ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
32fd6ce598ba aphysia/dockerdemo "java -jar /app.jar" 7 minutes ago Up 7 minutes 0.0.0.0:8081->8081/tcp, :::8081->8081/tcp dockerdemo
585b9b6bd71d redis:latest "docker-entrypoint.s…" 10 minutes ago Up 7 minutes 0.0.0.0:6379->6379/tcp, :::6379->6379/tcp dockercompose_redis_1
d96ba57941d9 mysql:latest "docker-entrypoint.s…" 16 minutes ago Up 7 minutes 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp dockercompose_mysql_1</code></pre><p>执行<code>docker-compose up</code> 没有报错,请求的时候报错:</p><pre><code class="shell"> io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: /127.0.0.1:6379</code></pre><p>这是因为容器之间的请求不能用<code>127.0.0.1</code>,必须用<code>mysql</code>,<code>redis</code>代表容器的网络,比如:<code>jdbc:mysql://mysql:3306/test?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true</code></p><p>完整的<code>application.yml</code>:</p><pre><code class="yml">server:
port: 8081
spring:
#数据库连接配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://mysql:3306/test?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 123456
redis:
host: redis ## redis所在的服务器IP
port: 6379
##密码,我这里没有设置,所以不填
password:
## 设置最大连接数,0为无限
pool:
max-active: 8
min-idle: 0
max-idle: 8
max-wait: -1
#mybatis的相关配置
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.aphysia.springdocker.model
#开启驼峰命名
configuration:
map-underscore-to-camel-case: true
logging:
level:
root: debug
</code></pre><p>还有一个问题,就是<code>docker-compose.yml</code>里面配置的镜像名字一定要对,要不<code>docker-compose up</code>执行的时候,就会出现:</p><pre><code class="shell">Pulling xxxx...
ERROR: The image for the service you're trying to recreate has been removed. If you continue, volume data could be lost. Consider backing up your data before continuing.
Continue with the new image? [yN]y
Pulling xxxx...
ERROR: pull access denied for postgresql, repository does not exist or may require 'docker login': denied: requested access to the resource is denied</code></pre><p>我还以为是登录的原因,本来是本地镜像,应该直接<code>create</code>而不是<code>pull</code>,如果不知道名字,可以通过以下命令查看,<code>REPOSITORY</code>就是名字:</p><pre><code class="shell">DockerCompose % docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
aphysia/dockerdemo latest 1429aa26790a 54 minutes ago 137MB
<none> <none> ceb493583d7c 57 minutes ago 137MB
<none> <none> dffcc47602a2 About an hour ago 137MB
<none> <none> a695cf2cd2df About an hour ago 137MB
<none> <none> 209ce4f96d34 2 hours ago 137MB
redis latest 40c68ed3a4d2 10 days ago 113MB
mysql latest e1d7dc9731da 14 months ago 544MB
openjdk 8-jdk-alpine a3562aa0b991 2 years ago 105MB</code></pre><p>最后启动命令:</p><pre><code class="shell">sudo docker-compose up</code></pre><p>成功启动:</p><p><img src="/img/remote/1460000041141125" alt="image-20211128221753624" title="image-20211128221753624"></p><p><strong>启动之后记得初始化一下数据库数据表!!!</strong></p><pre><code class="sql">drop database IF EXISTS test;
CREATE DATABASE test;
use test;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT "",
`age` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `user` VALUES (1, '李四', 11);
INSERT INTO `user` VALUES (2, '王五', 11);</code></pre><p><img src="/img/remote/1460000041141126" alt="image-20211128223429280" title="image-20211128223429280"></p><p>至此,大功告成,看似简单的命令,其实还是有不少坑点。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=B%2B1GyW%2FjHMo7ioqPPtF2rQ%3D%3D.qIDq9wc4ROQDcsYnKZ%2Fy7TVNQWhne8I3FgYQY%2F4Fm%2BWBEAETEKBDOUtvV1LM9awP" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=AOW9z%2FBhcuzKekSq5Imc4g%3D%3D.f%2Fm1qIpUqfjIlLaRuTHEw6sHeXytL6INiKW5MFeBFC0%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=Jm5%2F2bTQ5O%2BVjx73mL41BA%3D%3D.cUfgCxpUfmG5XOOIP0UGPvWHK0DhZgjUTotfdlMuXstRtkKsCCgKK8xJSQAGNJgd" rel="nofollow">开源编程笔记</a></p>
无快不破,在本地 docker 运行 IDEA 里面的项目?
https://segmentfault.com/a/1190000041141121
2021-12-18T19:04:18+08:00
2021-12-18T19:04:18+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>[TOC]</p><h2>前言</h2><p>之前我们用<code>docker</code>部署了<code>springboot</code>,<code>redis</code>,<code>mysql</code>的项目,但是是部署在三个不同的容器里,还需要先知道<code>redis</code>和<code>mysql</code>的<code>ip</code>地址,手动配置到<code>springboot</code>应用容器里,我只是想快速在本地进行测试啊,这样成本太高了,有没有什么办法,把他们集中管理呢?比如把它构建成为一个镜像。</p><p>办法总是有的,那就是<code>Docker Compose</code>。</p><p>之前的项目地址:<a href="https://link.segmentfault.com/?enc=yT8QF8IZKMwuVKhmWYQ8Lg%3D%3D.znhdNUTEjON34Q4ms8OcH0ufwPX%2BCqdNbMJIvaCCfpCaEOjqontlSMsVjSDQrmIohZszyiazkQYs%2BQ9A0j0FoMBDsw7KWokJrGf01bFgcZ8%3D" rel="nofollow">https://github.com/Damaer/Dem...</a></p><p>上一篇:<a href="https://link.segmentfault.com/?enc=XkJZMCpcfh1KLvUAxqqaSw%3D%3D.OfirU2ubqCXuWEUCbmxXhkw5ffmMpv19g8tanu3p2yslySCBiY1akvdAsmZnTXsSzj8DC2pShoOeeEIS7QQmW99oglQAnZb8N3DHw2fQ%2BB86uP0aUaJ%2Fg%2FYGjZlKgws9" rel="nofollow">http://aphysia.cn/archives/ru...</a></p><h2>Docker Compose</h2><h3>1. Docker Compose是什么?</h3><p><code>Docker Compose</code>其实就是用来定义和运行复杂应用的<code>Docker</code>工具,什么叫复杂应用,比如前面写的<code>springboot</code>+<code>redis</code>+<code>mysql</code>,里面就有三个容器,这种多个容器的,用一个工具来管理,它不香么?</p><blockquote>docker compose 通过配置文件来管理多个 <code>Docker</code> 容器,在配置文件中,所有的容器通过<code>service</code>来进行定义,然后使用<code>docker-compose</code>脚本来启动、停止、重启应用以及应用中的服务和所依赖的容器等。</blockquote><h3>2. Docker Compose 的具体步骤</h3><p>一般是三个步骤:</p><ul><li>使用<code>Dockerfile</code> 来定义应用程序的环境</li><li>在 <code>docker-compose.yml</code> 定义构成应用程序的服务,这样它们可以在隔离环境中一起运行。</li><li>执行 <code>docker-compose up</code> 命令来启动并运行整个应用程序。</li></ul><p>我使用的是<code>Mac OS</code>,装<code>Docker</code>的时候已经把<code>Docker Compose</code>也安装好了,不需要单独安装。</p><h3>3. 如何在IDEA项目里面使用Docker Compose</h3><p>首先<code>pom.xml</code>文件中需要注意配置小写的<code>artifactId</code>:</p><pre><code class="xml"> <groupId>com.aphysia</groupId>
<artifactId>dockerdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>dockerdemo</name>
<packaging>jar</packaging></code></pre><p>除此之外还需要配置插件:</p><pre><code class="xml"> <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.0.0</version>
<!-- 将插件绑定在某个phase执行 -->
<executions>
<execution>
<id>build-image</id>
<!-- 用户只需执行mvn package ,就会自动执行mvn docker:build -->
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
<configuration>
<imageName>${docker.image.prefix}/${project.artifactId}</imageName>
<dockerDirectory>src/main/docker</dockerDirectory>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</build></code></pre><p>除此之外,<code>Dockerfile</code>是必要的,上面的插件中已经配置了我们<code>dockerFile</code>需要放在<code><dockerDirectory>src/main/docker</dockerDirectory></code>这个位置,<code>DockerFile</code>里面配置如下:</p><pre><code class="txt">FROM openjdk:8-jdk-alpine
EXPOSE 8081
VOLUME /tmp
# 重写命名为app.jar
ADD dockerdemo-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]</code></pre><p>理论上到这个时候,我们使用<code>mvn clean package</code>就会生成对应的<code>jar</code>包:</p><p><img src="/img/remote/1460000041141123" alt="image-20211128215558740" title="image-20211128215558740"></p><p><code>docker compose</code>最重要的是配置<code>docker-compose.yml</code>,这个文件我们放在项目的根目录就可以,和<code>pom.xml</code>平级:</p><pre><code class="java">version: "3"
services:
redis:
image: redis:latest
restart: always
ports:
- "6389:6379"
volumes:
- /tmp/redis.conf:/etc/redis/redis.conf
command: redis-server /etc/redis/redis.conf
mysql:
image: mysql:latest
restart: always
environment:
MYSQL_ROOT_PASSWORD: "123456"
MYSQL_USER: 'root'
MYSQL_PASS: '123456'
ports:
- "3306:3306"
volumes:
- "./db:/var/lib/mysql"
- "./conf/my.cnf:/etc/my.cnf"
- "./init:/docker-entrypoint-initdb.d/"
# 指定服务名称
webapp:
# 指定服务使用的镜像
image: aphysia/dockerdemo
# 指定容器名称
container_name: dockerdemo
# 指定服务运行的端口
ports:
- 8081:8081
# 指定容器中需要挂载的文件
volumes:
- /etc/localtime:/etc/localtime
- /tmp/dockerdemo/logs:/var/logs</code></pre><p>值得注意的点:</p><ol><li>service里面就是我们配置的镜像,包含了<code>redis</code>,<code>mysql</code>,<code>webapp</code>,<code>webapp</code>其实就是我们的应用。</li><li><code>"6389:6379"</code>中<code>6389</code>其实是我们主机的端口,也就是我的<code>Mac</code>连接<code>redis</code>容器需要使用<code>6389</code>,而容器之间连接需要使用<code>6379</code>,这是容器的端口。</li><li><p><code>/tmp/redis.conf:/etc/redis/redis.conf</code>中<code>/tmp/redis.conf</code>是主机的目录,而这个目录需要在docker里面配置才可以,要不就会报错(执行记得加管理员权限):</p><pre><code class="shell">docker ERROR: * start service *: Mounts denied</code></pre><p><img src="/img/remote/1460000041141124" alt="image-20211128220527229" title="image-20211128220527229"></p></li><li><code>mysql</code> 8.0 可能会报错<code>java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed</code>,这个是因为<code>url</code>链接少了一个参数:<code>allowPublicKeyRetrieval=true</code></li></ol><h4>启动可能出现的坑点</h4><p>启动后可能链接不上<code>mysql</code>或者<code>redis</code>,但是看容器运行情况又是正常的:</p><pre><code class="shell">DockerCompose % docker container ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
32fd6ce598ba aphysia/dockerdemo "java -jar /app.jar" 7 minutes ago Up 7 minutes 0.0.0.0:8081->8081/tcp, :::8081->8081/tcp dockerdemo
585b9b6bd71d redis:latest "docker-entrypoint.s…" 10 minutes ago Up 7 minutes 0.0.0.0:6379->6379/tcp, :::6379->6379/tcp dockercompose_redis_1
d96ba57941d9 mysql:latest "docker-entrypoint.s…" 16 minutes ago Up 7 minutes 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp dockercompose_mysql_1</code></pre><p>执行<code>docker-compose up</code> 没有报错,请求的时候报错:</p><pre><code class="shell"> io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: /127.0.0.1:6379</code></pre><p>这是因为容器之间的请求不能用<code>127.0.0.1</code>,必须用<code>mysql</code>,<code>redis</code>代表容器的网络,比如:<code>jdbc:mysql://mysql:3306/test?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true</code></p><p>完整的<code>application.yml</code>:</p><pre><code class="yml">server:
port: 8081
spring:
#数据库连接配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://mysql:3306/test?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 123456
redis:
host: redis ## redis所在的服务器IP
port: 6379
##密码,我这里没有设置,所以不填
password:
## 设置最大连接数,0为无限
pool:
max-active: 8
min-idle: 0
max-idle: 8
max-wait: -1
#mybatis的相关配置
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.aphysia.springdocker.model
#开启驼峰命名
configuration:
map-underscore-to-camel-case: true
logging:
level:
root: debug
</code></pre><p>还有一个问题,就是<code>docker-compose.yml</code>里面配置的镜像名字一定要对,要不<code>docker-compose up</code>执行的时候,就会出现:</p><pre><code class="shell">Pulling xxxx...
ERROR: The image for the service you're trying to recreate has been removed. If you continue, volume data could be lost. Consider backing up your data before continuing.
Continue with the new image? [yN]y
Pulling xxxx...
ERROR: pull access denied for postgresql, repository does not exist or may require 'docker login': denied: requested access to the resource is denied</code></pre><p>我还以为是登录的原因,本来是本地镜像,应该直接<code>create</code>而不是<code>pull</code>,如果不知道名字,可以通过以下命令查看,<code>REPOSITORY</code>就是名字:</p><pre><code class="shell">DockerCompose % docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
aphysia/dockerdemo latest 1429aa26790a 54 minutes ago 137MB
<none> <none> ceb493583d7c 57 minutes ago 137MB
<none> <none> dffcc47602a2 About an hour ago 137MB
<none> <none> a695cf2cd2df About an hour ago 137MB
<none> <none> 209ce4f96d34 2 hours ago 137MB
redis latest 40c68ed3a4d2 10 days ago 113MB
mysql latest e1d7dc9731da 14 months ago 544MB
openjdk 8-jdk-alpine a3562aa0b991 2 years ago 105MB</code></pre><p>最后启动命令:</p><pre><code class="shell">sudo docker-compose up</code></pre><p>成功启动:</p><p><img src="/img/remote/1460000041141125" alt="image-20211128221753624" title="image-20211128221753624"></p><p><strong>启动之后记得初始化一下数据库数据表!!!</strong></p><pre><code class="sql">drop database IF EXISTS test;
CREATE DATABASE test;
use test;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT "",
`age` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `user` VALUES (1, '李四', 11);
INSERT INTO `user` VALUES (2, '王五', 11);</code></pre><p><img src="/img/remote/1460000041141126" alt="image-20211128223429280" title="image-20211128223429280"></p><p>至此,大功告成,看似简单的命令,其实还是有不少坑点。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=LzHtqrm60FYQh5u8%2F28QnA%3D%3D.J3X5qXmDStn7dcrguQ2xSwLkRUFWBAex7PZJrxruMvHyeqsPD0FaCwRV2wrEouq7" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=OnTZ%2BJ1fC7LKT4TNLogqXA%3D%3D.TpDMgXQwc8%2F3la40MR3o%2Fvv5h0y5ZS86uB2Sv34eDOE%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=Ik3gDLYDc9TumbqZnRijWg%3D%3D.jemMtZj8ZkywzW4BTXZ4fv%2FNQcxgnYUDME%2FyAtV8OLNVf9xFT459nH0QWHVyoTXK" rel="nofollow">开源编程笔记</a></p>
如何基于 Docker 快速搭建 Springboot + Mysql + Redis 项目
https://segmentfault.com/a/1190000041132421
2021-12-17T00:32:02+08:00
2021-12-17T00:32:02+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>[TOC]</p><h2>前言</h2><p>有时候我们需要快速启动一些项目,但是环境往往折腾了好久,因此弄一个可以重用的快速搭建的教程,<code>docker</code>简直就是这方面的神器,Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux或Windows操作系统的机器上,也可以实现虚拟化。</p><p>本教程基于的前提条件:</p><ul><li>机器已经安装配置好<code>JDK1.8</code>,并且环境变量已经配置成功</li><li><code>Maven</code>已经配置好,<code>IDEA</code>中项目使用的默认<code>Maven</code>也配置成功</li><li>本地机器安装好<code>Docker</code></li><li>顺便提一句,我用<code>navicat</code>作为数据库可视化操作工具</li></ul><p><strong>项目地址:<a href="https://link.segmentfault.com/?enc=krAl49Q05BcSXsx9%2Bm3gyQ%3D%3D.H8lVRfnKtX2Ty4WhPm3z1Y2zRLtxCrXGgKdzFsUE9K37ozoqVAGy%2BePOULfnwG6JmfIpbf%2BONoKkHhrupxKh%2FQ7qs6Xn4gQD1gp5V0DOAuc%3D" rel="nofollow">https://github.com/Damaer/Dem...</a></strong></p><h2>项目目录</h2><pre><code class="txt">├── src :源代码
| ├── main
| | ├── java
| | | ├── com.aphysia.springbootdemo
| | | | ├── config:配置
| | | | | ├── RedisConfig:redis配置
| | | | ├── constant:常量
| | | | | ├── RedisConfig:redis常量
| | | | ├── controller:控制器
| | | | ├── mapper:数据库操作接口
| | | | ├── model:实体类
| | | | ├── service:逻辑处理层,包括接口以及实现类
| | | | | ├── impl:接口实现类
| | | | ├──util:工具类
| | | | | ├── RedisUtil:redis工具类
| | | | ├──SpringdemoApplication:启动类
| | ├── resource
| | | ├── mapper 数据库操作sql
| | | ├── application.yml:全局配置类
| | | ├── user.sql: 初始化mysql
| ├── test: 测试类
├── pom.xml :项目maven依赖关系
</code></pre><p>整体的目录如下:</p><p><img src="/img/remote/1460000041132424" alt="" title=""></p><h2>搭建项目</h2><h3>1. docker安装启动mysql以及redis</h3><h5>1.1 安装mysql</h5><p>查询<code>mysql</code>最新的镜像:</p><pre><code class="shell">docker search mysql</code></pre><p>拉取最新的<code>mysql</code>版本</p><pre><code class="shell">docker pull mysql:latest</code></pre><p>启动<code>mysql</code>,用户名<code>root</code>,密码<code>123456</code></p><pre><code class="shell">docker run -itd --name mysql-test -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql</code></pre><p>可以通过<code>docker ps</code> 查看是否安装成功</p><pre><code class="txt">% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
574d30f17868 mysql "docker-entrypoint.s…" 14 months ago Up 2 days 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp mysql-test</code></pre><p><img src="/img/remote/1460000041132425" alt="" title=""></p><h5>1.2 安装redis</h5><p>查询<code>redis</code>的镜像</p><pre><code class="shell">docker search redis</code></pre><p>拉取<code>redis</code>的最新镜像</p><pre><code class="shell">% docker pull redis:latest
latest: Pulling from library/redis
eff15d958d66: Pull complete
1aca8391092b: Pull complete
06e460b3ba1b: Pull complete
def49df025c0: Pull complete
646c72a19e83: Pull complete
db2c789841df: Pull complete
Digest: sha256:619af14d3a95c30759a1978da1b2ce375504f1af70ff9eea2a8e35febc45d747
Status: Downloaded newer image for redis:latest
docker.io/library/redis:latest</code></pre><p><code>docker images</code>可以查看我们安装了哪些镜像,可以看到其实我之前也安装过<code>redis</code>的镜像:</p><pre><code class="shell">% docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 40c68ed3a4d2 3 days ago 113MB
redis <none> 84c5f6e03bf0 14 months ago 104MB
mysql latest e1d7dc9731da 14 months ago 544MB
docker/getting-started latest 1f32459ef038 16 months ago 26.8MB</code></pre><p>让我们启动一下<code>redis</code>的容器:</p><pre><code class="shell">% docker run -itd --name redis-test -p 6379:6379 redis
7267e14faf93a0e416c39eeaaf51705dc4b6dc3507a68733c20a2609ade6dcd6</code></pre><p>可以看到<code>docker</code>里面现在有<code>redis</code>和<code>mysql</code>两个容器在跑了:</p><p><img src="/img/remote/1460000041132426" alt="image-20211121194442579" title="image-20211121194442579"></p><h3>2. 初始化数据库</h3><p>主要是创建数据库以及测试使用的数据表,初始化数据库的语句:</p><pre><code class="mysql">drop database IF EXISTS test;
CREATE DATABASE test;
use test;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT "",
`age` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `user` VALUES (1, '李四', 11);
INSERT INTO `user` VALUES (2, '王五', 11);</code></pre><p>初始化数据如下:</p><p><img src="/img/remote/1460000041132427" alt="" title=""></p><h3>3.创建项目</h3><p>在IDEA中,File --> New --> Project --> Spring Initializr(选择JDK 8):</p><p><img src="/img/remote/1460000041132428" alt="" title=""></p><p>点击<code>Next</code>:</p><p><img src="/img/remote/1460000041132429" alt="" title=""></p><p>选择<code>Web</code> 下面的<code>Spring Web</code>,<code>SQL</code>下面的 <code>JDBC API</code>,<code>Mybatis</code>,<code>NoSQL</code>下的<code>Redis</code>,也可以不选,直接在<code>pom</code>文件里自己加入即可:</p><p><img src="https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20211121213058.png" style="zoom:70%;" /></p><p>一路点<code>Next</code>,最后<code>Finish</code>,创建好之后,记得更新一下<code>Maven</code>,安装依赖包。</p><h3>4.初始化代码</h3><h4>4.1 全局配置文件以及启动类</h4><p>全局配置文件<code>application.yml</code>:</p><pre><code class="yml">server:
port: 8081
spring:
#数据库连接配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&useSSL=false
username: root
password: 123456
redis:
host: 127.0.0.1 ## redis所在的服务器IP
port: 6379
##密码,我这里没有设置,所以不填
password:
## 设置最大连接数,0为无限
pool:
max-active: 8
min-idle: 0
max-idle: 8
max-wait: -1
#mybatis的相关配置
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.aphysia.spingbootdemo.model
#开启驼峰命名
configuration:
map-underscore-to-camel-case: true
logging:
level:
root: debug</code></pre><p>启动类<code>SpringdemoApplication</code>:</p><pre><code class="java">import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.aphysia.springdemo.mapper")
public class SpringdemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringdemoApplication.class, args);
}
}
</code></pre><h4>4.2 实体类</h4><p>与数据库中user表对应的实体类<code>User.java</code>:</p><pre><code class="java">package com.aphysia.springdemo.model;
public class User {
int id;
String name;
int age;
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 int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
</code></pre><h4>4.3 Redis工具类</h4><p><code>Redis</code>配置类<code>RedisConfig</code>:</p><pre><code class="java">package com.aphysia.springdemo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Autowired
private RedisTemplate redisTemplate;
@Bean
public RedisTemplate redisTemplateInit() {
//设置序列化Key的实例化对象
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置序列化Value的实例化对象
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
}</code></pre><p><code>Redis</code> 常量类 <code>RedisConstant</code>:</p><pre><code class="java">package com.aphysia.springdemo.constant;
public class RedisConstant {
public static String ALL_USER_KEY = "allUser";
}
</code></pre><p><code>Redis</code> 工具类 <code>RedisUtil</code>:</p><pre><code class="java">package com.aphysia.springdemo.util;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
@Component
public class RedisUtil {
@Resource
private RedisTemplate<String, Object> redisTemplate;
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
}
}
}
//============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
//================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
//============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}</code></pre><h4>4.4 Mysql 数据库操作</h4><p>数据库的<code>sql</code>文件<code>UserMapper.xml</code></p><pre><code class="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" >
<mapper namespace="com.aphysia.springdemo.mapper.UserMapper">
<select id="getAllUsers" resultType="com.aphysia.springdemo.model.User">
SELECT * FROM user
</select>
<update id="updateUserAge" parameterType="java.lang.Integer">
update user set age=age+1 where id =#{id}
</update>
</mapper></code></pre><p>对应的<code>mapper</code>接口<code>UserMapper.java</code></p><pre><code class="java">package com.aphysia.springdemo.mapper;
import com.aphysia.springdemo.model.User;
import java.util.List;
public interface UserMapper {
List<User> getAllUsers();
int updateUserAge(Integer id);
}
</code></pre><h4>4.5 Service层</h4><p>先定义一个操作<code>User</code>的接口类<code>UserService</code>,包含两个方法,查询所有的<code>user</code>以及更新<code>user</code>的年龄:</p><pre><code class="java">package com.aphysia.springdemo.service;
import com.aphysia.springdemo.model.User;
import java.util.List;
public interface UserService {
public List<User> getAllUsers();
public void updateUserAge();
}
</code></pre><p>接口实现类<code>UserServiceImpl</code>,<strong>为了证实Redis可用,我们查询所有的用户的时候,加入了Redis缓存,优先从Redis中加载数据</strong>:</p><pre><code class="java">package com.aphysia.springdemo.service.impl;
import com.aphysia.springdemo.constant.RedisConstant;
import com.aphysia.springdemo.mapper.UserMapper;
import com.aphysia.springdemo.model.User;
import com.aphysia.springdemo.service.UserService;
import com.aphysia.springdemo.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.List;
@Service("userService")
public class UserServiceImpl implements UserService {
@Resource
UserMapper userMapper;
@Autowired
RedisUtil redisUtil;
@Override
public List<User> getAllUsers() {
List<User> users = (List<User>) redisUtil.get(RedisConstant.ALL_USER_KEY);
if(CollectionUtils.isEmpty(users)){
users = userMapper.getAllUsers();
redisUtil.set(RedisConstant.ALL_USER_KEY,users);
}
return users;
}
@Override
@Transactional
public void updateUserAge() {
redisUtil.del(RedisConstant.ALL_USER_KEY);
userMapper.updateUserAge(1);
userMapper.updateUserAge(2);
}
}
</code></pre><h4>4.6 Controller 控制层</h4><p>增加一个测试层<code>TestController</code>:</p><pre><code class="java">package com.aphysia.springdemo.controller;
import com.aphysia.springdemo.model.User;
import com.aphysia.springdemo.service.UserService;
import com.aphysia.springdemo.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@Controller
public class TestController {
@Autowired
UserService userService;
@RequestMapping("/getUserList")
@ResponseBody
public List<User> getUserList() {
return userService.getAllUsers();
}
@RequestMapping("/update")
@ResponseBody
public int update() {
userService.updateUserAge();
return 1;
}
}</code></pre><h4>4.7 pom依赖</h4><pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.aphysia</groupId>
<artifactId>springdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springdemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
</code></pre><h2>测试</h2><p>启动项目,输入链接:<a href="https://link.segmentfault.com/?enc=C1U7eGXdcF8WXlHVtFtHag%3D%3D.WyZgjeZ9Aaz3f4poUYKeJKhPi2R%2BEr%2B3CjmBWiinlbFT4lgx9B4sI2AY7jiezl%2Fn" rel="nofollow">http://localhost:8081/getUser...</a>,可以获取到所有的<code>user</code>:</p><p><img src="/img/remote/1460000041132430" alt="image-20211121215506343" title="image-20211121215506343"></p><p>我们更新一下所有的用户年龄,调用<code>http://localhost:8081/update</code>,返回<code>1</code></p><p><img src="/img/remote/1460000041132431" alt="image-20211121215721110" title="image-20211121215721110"></p><p>再次访问<code>http://localhost:8081/getUserList</code>,可以看到年龄全部都变成<code>12</code>:</p><p><img src="/img/remote/1460000041132432" alt="image-20211121215814253" title="image-20211121215814253"></p><p>怎么知道<code>Redis</code>生效了呢?最好就是<code>debug</code>,或者直接看控制台,我们已经开启了<code>debug</code>级别的日志:</p><p><img src="/img/remote/1460000041132433" alt="image-20211121220647743" title="image-20211121220647743"></p><p>还有一种方式,下载<code>Redis-desktop-manager</code>,可以直接可视化查看:</p><p><img src="/img/remote/1460000041132434" alt="image-20211121221927400" title="image-20211121221927400"></p><p>至此,一个<code>demo</code>项目就完成了,可以每次<code>copy</code>出来初始化使用。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=Hz2XpKVqICaAB5%2Bb6XlYyw%3D%3D.%2FPn0AoIqVHY6OdxFdyz7g5XmMgxOvtf3lsGmHNo3XrUxE1O33kLsLQxPUbvey9g8" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=Dk8OqGeKHvRWE814Da16ZQ%3D%3D.mtfLVuFM9KRmOtlfIE9itojfOujVA4VMoa1%2BtouhMG8%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=FwaCXJVMLM7L25UxYR1jXg%3D%3D.ZNY95wSMY2J4equ2fXY07uPYnGaIaBiU5UeYaehfYBFwXFX39xfQj5A0e7Z2WDRE" rel="nofollow">开源编程笔记</a></p>
设计模式【6.2】-- 再聊聊适配器模式
https://segmentfault.com/a/1190000041112253
2021-12-14T08:55:51+08:00
2021-12-14T08:55:51+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p><strong>这里说的适配器不是通常所说的类适配器,对象适配器,接口适配器,这里实现的是把所有的类进行统一管理的适配器。如需要查找设计模式的三种主要适配器模式,请点击</strong><a href="https://link.segmentfault.com/?enc=yDALNmiBkxhGtI6YiG7otw%3D%3D.WOxyN4KQzZwlpbuL2LIW9yi5yUwQIJ934x3yIMQKC3PYOyyypjAf0cr4o4P9MxwYv5r9OdZmEnlxO3dQLMXlqg%3D%3D" rel="nofollow">https://blog.csdn.net/Aphysia...</a></p><p>适配器模式(百度百科):在计算机编程中,适配器模式(有时候也称包装样式或者包装)将一个类的接口适配成用户所期待的。一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。</p><p>可以这么理解,原来可能两个接口或者两个类不兼容,适配器模式要做的事情就是把它们统一管理,让他们可以一起工作。举个简单的例子:内存卡和笔记本,是不能直接连接工作的,但是我们使用读卡器,相当于适配器,把它们连接起来了。</p><h5>1.<strong>不使用适配器</strong>的例子:</h5><ul><li> 需求:程序猿的工作是<code>program()</code>,教师的工作是<code>teach()</code>,那么这些不同的职业,具体的工作都是不一样的,这些程序猿program()方法内容也可能是不一样的,比如说京东,阿里,腾讯等等,教师也是一样的,不同学校的老师工作内容也各异。所以我们必须定义接口,不同工作内容的也可以通过实现自己的接口去实现。<br><br> 代码结果如下:</li></ul><p><img src="/img/remote/1460000041112255" alt="" title=""></p><p><strong> IProgramer.class</strong>(程序猿撸代码的接口)</p><pre><code class="java">package com.noadapter;
public interface IProgramer {
public void program();
}
</code></pre><p><strong> Programer.class</strong>(程序猿的类,实现了撸代码的接口)</p><pre><code class="java">package com.noadapter;
public class Programer implements IProgramer {
@Override
public void program() {
System.out.println("我是一个优秀的程序猿,我整天撸代码");
}
}
</code></pre><p> 下面的教师接口以及实现教师的类也和上面程序猿的一样:</p><p><strong>ITeacher.class</strong>(教师教书接口):</p><pre><code class="java">package com.noadapter;
public interface ITeacher {
public void teach();
}
</code></pre><p> <strong>Teacher.class</strong>(实现了教书的教师类):</p><pre><code class="java">
package com.noadapter;
public class Teacher implements ITeacher {
@Override
public void teach() {
System.out.println("我是教师,我教育祖国的花朵");
}
}</code></pre><p> <strong>MyTest.class</strong> 测试类:</p><pre><code class="java">package com.noadapter;
public class MyTest {
public static void main(String []args){
ITeacher teacher = new Teacher();
IProgramer programer = new Programer();
//必须挨个访问他们的方法
teacher.teach();
programer.program();
}
}
</code></pre><p>运行结果:</p><p><img src="/img/remote/1460000041112256" alt="" title=""></p><p>理解:如果不是用适配器模糊,那么我们要定义出所有的工种对象(程序猿,教师等等),还要为他们实现各自的接口,然后对他们的方法进行调用,这样有多少个工种,就要写多少个方法调用,比较麻烦。</p><h5>2.只定义一个适配器实现类</h5><p>在前面的基础上修改,增加了<code>IWorkAdapter.class</code>以及它的实现类<code>WorkerAdapter.class</code>,以及更改了测试方法,其他的都没有改变,代码结构如下:</p><p><img src="/img/remote/1460000041112257" alt="" title=""></p><p><img src="/img/remote/1460000041112258" alt="" title=""></p><p>增加的<code>IWorkAdapter.class</code>(适配器的接口):</p><pre><code class="java">public interface IWorkAdapter {
//参数之所以是Object,是因为要兼容所有的工种对象
public void work(Object worker);
}</code></pre><p>增加的<code>WorkAdapter.class</code>(适配器的类):</p><pre><code class="java">
public class WorkAdaper implements IWorkAdapter {
@Override
public void work(Object worker) {
if(worker instanceof IProgramer){
((IProgramer) worker).program();
}
if(worker instanceof ITeacher){
((ITeacher) worker).teach();
}
}
}
</code></pre><p>更改过的测试类<code>MyTest.class</code>:</p><pre><code class="java">public class MyTest {
public static void main(String []args){
ITeacher teacher = new Teacher();
IProgramer programer = new Programer();
//把两个工种放到对象数组
Object[] workers = {teacher,programer};
//定义一个适配器
IWorkAdapter adapter = new WorkAdaper();
//适配器遍历对象
for(Object worker:workers){
adapter.work(worker);
}
}
}
</code></pre><p>结果依然不变:</p><p><img src="/img/remote/1460000041112256" alt="" title=""></p><p>分析:只写一个适配器,功能上就像是把接口集中到一起,在中间加了一层,这一层把调用不同工种(程序猿,教师)之间的差异屏蔽掉了,这样也达到了解耦合的作用。</p><h5>3.多个适配器的模式</h5><p>也就是为每一个工种都定义一个适配器(在一个适配器的基础上进行修改)</p><p><img src="/img/remote/1460000041112259" alt="" title=""></p><p>修改 <strong>IWorkAdapter.class</strong></p><pre><code class="java">public interface IWorkAdapter {
//参数之所以是Object,是因为要兼容所有的工种对象
public void work(Object worker);
//判断当前的适配器是否支持指定的工种对象
boolean supports(Object worker);
}</code></pre><p>定义一个<strong>TeacherAdapter.class</strong></p><pre><code class="java">public class TeacherAdapter implements IWorkAdapter{
@Override
public void work(Object worker) {
((ITeacher)worker).teach();
}
@Override
public boolean supports(Object worker) {
return (worker instanceof ITeacher);
}
}</code></pre><p>定义一个<strong>ProgrammerAdapter.class</strong></p><pre><code class="java">public class ProgrammerAdapter implements IWorkAdapter{
@Override
public void work(Object worker) {
((IProgramer)worker).program();
}
@Override
public boolean supports(Object worker) {
return (worker instanceof IProgramer);
}
}
</code></pre><p>测试类(<code>Test.class</code>):</p><pre><code class="java">public class MyTest {
public static void main(String []args){
ITeacher teacher = new Teacher();
IProgramer programer = new Programer();
//把两个工种放到对象数组
Object[] workers = {teacher,programer};
//适配器遍历对象
for(Object worker:workers){
IWorkAdapter adapter = getAdapter(worker);
adapter.work(worker);
}
}
public static IWorkAdapter getAdapter(Object object){
IWorkAdapter teacherAdapter = new TeacherAdapter();
IWorkAdapter programmerAdapter = new ProgrammerAdapter();
IWorkAdapter[] adapters = {teacherAdapter,programmerAdapter};
for(IWorkAdapter adapter:adapters){
if(adapter.supports(object)){
return adapter;
}
}
return null;
}
}</code></pre><p>个人理解:其实多个适配器的根本是去获取支持该对象的适配器,通过该适配器来使用这个对象。</p>
设计模式【6.1】-- 初探适配器模式
https://segmentfault.com/a/1190000041112214
2021-12-14T08:46:09+08:00
2021-12-14T08:46:09+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>设计模式文章集合:<a href="https://link.segmentfault.com/?enc=8V3JyxF4KxgtYyWUdiqdjg%3D%3D.Yoj3BWE80KjP%2Bmcd0uzXHjGOVT%2FIYi2QFmUfvtA2oQ%2BfgORNyDUdqEWF7EDMYqSK" rel="nofollow">http://aphysia.cn/categories/...</a></p><p>开局一张图,剩下全靠写...</p><p><img src="/img/remote/1460000041100740" alt="111" title="111"></p><h2>介绍</h2><blockquote>适配器模式(百度百科):在计算机编程中,适配器模式(有时候也称包装样式或者包装)将一个类的接口适配成用户所期待的。一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。</blockquote><p>适配器模式的主要目的就是为了<strong>兼容性</strong>,把原来不匹配的两个类或者接口可以协同工作,它属于结构型模式,主要分为三种:类适配器,对象适配器,接口适配器。</p><p>适配器模式灵活性比较好,可以提高复用性,但是如果滥用,系统调用关系会比较复杂,<strong>每一次的适配,本质上都是一种妥协</strong>。</p><p>不断妥协,最后迎来的,必定是重构。</p><h2>适配器模式类型</h2><h3>类适配器</h3><p>描述:适配器的类(<code>Adapter</code>),通过继承原有类,同时实现目标接口,完成的功能是拥有原有类的属性方法,同时可以调用目标接口。<br>例子:原来一种充电器(目标类)可以给<code>IPhone</code>充电,另一种充电器(接口)可以给<code>Android</code>手机充电,我们想实现一种适配器可以让<code>IPhone</code>充电器拥有<code>Android</code>充电器的功能。</p><p>代码结构如下:</p><p><img src="/img/remote/1460000041112216" alt="" title=""></p><ul><li><code>AndroidCharger.class</code>:</li></ul><pre><code class="java">//给android充电的接口
public interface AndroidCharger {
public void androidout();
}
</code></pre><ul><li><p><code>AppleCharger.class</code></p><pre><code class="java">//给苹果充电的类
public class AppleCharger {
public void iphoenOut(){
System.out.println("我是充电器,我可以给苹果充电...");
}
}
</code></pre></li><li><p><code>ChagerAdapater.class</code></p><pre><code class="java">//充电适配器
public class ChagerAdapater extends AppleCharger implements AndroidCharger {
@Override
public void androidout() {
iphoenOut();
System.out.println("适配器开始工作----");
System.out.print("我拥有了给Android充电的能力");
}
}</code></pre></li><li><p><code>Test.class</code></p><pre><code class="java">public class Test {
public static void main(String[]args){
ChagerAdapater chagerAdapater = new ChagerAdapater();
chagerAdapater.androidout();
}
}</code></pre><p>运行结果如下:</p></li></ul><p><img src="/img/remote/1460000041112217" alt="" title=""></p><blockquote>个人理解:这里之所以一个继承一个接口,是因为java只能单继承,要去适配多个类,只能一个继承,一个用接口实现,有一定局限性。重写它的方法,这也比较灵活,可以对接口方法进行修改。</blockquote><h3>2.对象适配器</h3><p>个人理解:上面所说的类适配器是通过继承与实现接口的方式实现(所继承的父类以及接口都是一个<code>class</code>),对象适配器就是根据“合成复用原则”,<strong>不使用继承关系</strong>,而是使用了<strong>关联关系</strong>,直接把另一个类的对象当成<strong>成员对象</strong>,也就是持有之前需要继承的类的实例。<br>代码结构没有改变,只是重新创建了一个包:</p><p><img src="/img/remote/1460000041112218" alt="" title=""></p><ul><li><p>更改后的 <code>ChagerAdapater.class</code></p><pre><code class="java"> //充电适配器
public class ChagerAdapater implements AndroidCharger {
//持有苹果充电器的实例
private AppleCharger appleCharger;
//构造器
public ChagerAdapater(AppleCharger appleCharger){
this.appleCharger = appleCharger;
}
@Override
public void androidout() {
System.out.println("适配器开始工作----");
System.out.print("我拥有了给Android充电的能力");
}
}</code></pre></li><li><p>更改后的 Test.class</p><pre><code class="java">public class Test {
public static void main(String[]args){
ChagerAdapater chagerAdapater = new ChagerAdapater(new AppleCharger());
chagerAdapater.androidout();
}
}</code></pre><p>运行结果没有改变:</p></li></ul><p><img src="/img/remote/1460000041112217" alt="" title=""></p><blockquote><ul><li>个人理解:这个和第一种类的适配器其实思想上差不多,只是实现的方式不一样,类适配器是通过继承类,实现接口,对象适配器是把要继承的类变成了<strong>属性对象</strong>,把实例与适配器关联起来,也就是适配器的类持有了原有父类的对象实例。一般而言,由于<code>java</code>是单继承,所以我们尽量不要把这一次使用继承的机会给浪费了,这样写也比较灵活。</li></ul></blockquote><h3>3.接口适配器</h3><p>接口适配器,也可以称为默认适配器模式,或者缺省适配器模式。当我们不需要全部实现接口所实现的方法的时候,我们可以设计一个抽象类去实现接口,然后再这个抽象类中为所有方法提供一个默认的实现,这个抽象类的子类就可以有选择地对方法进行实现了。<br>代码结构如下:</p><p><img src="/img/remote/1460000041112219" alt="" title=""></p><p>解释:<code>学生类</code>可以吃饭,学习,但是<code>教师类</code>也吃饭,但是教师不是学习,而是教书,所以我们把<code>学习</code>,<code>吃饭</code>,<code>教书</code>当成接口的方法,由于不是所有的类都需要实现所有接口,我们在中间实现了一个抽象类实现这些接口,所有的方法都提供了一个默认是实现方法。然后学生类或者教师类才去继承抽象类,从而实现自己所需要的一部分方法即可。</p><ul><li><code>myInterface.class</code></li></ul><pre><code class="java">//定义接口的方法
public interface myInterface {
//学习的接口方法
public void study();
//教书的接口方法
public void teach();
//吃饭的接口方法
public void eat();
}</code></pre><ul><li><p><code>myAbstractClass.class(抽象类)</code></p><pre><code class="java">public abstract class myAbstractClass implements myInterface{
//学习的接口方法
@Override
public void study(){}
@Override
//吃饭的接口方法
public void eat(){}
//教书的接口方法
@Override
public void teach(){}
}
</code></pre></li><li><p><code>Student.class(学生类)</code></p><pre><code class="java">public class Student extends myAbstractClass{
//学习的接口方法
@Override
public void study(){
System.out.println("我是学生,我要好好学习");
}
@Override
//吃饭的接口方法
public void eat(){
System.out.println("我是学生,我要吃饭");
}
}
</code></pre></li><li><p><code>Teacher.class(教师类)</code></p><pre><code class="java">public class Teacher extends myAbstractClass {
@Override
//吃饭的接口方法
public void eat(){
System.out.println("我是教师,我要吃饭");
}
//教书的接口方法
@Override
public void teach(){
System.out.println("我是教师,我要教育祖国的花朵");
}
}
</code></pre></li><li><p><code>Test.calss(测试类)</code></p><pre><code class="java">public class Test {
public static void main(String[] args){
Student student = new Student();
Teacher teacher = new Teacher();
student.eat();
student.study();
teacher.eat();
teacher.teach();
}
}
</code></pre><p>运行的结果:</p></li></ul><p><img src="/img/remote/1460000041112220" alt="" title=""></p><h4>4.总结</h4><p>1.类适配器模式主要是通过继承目标的类,实现要增加的接口方法,就可以把类与接口适配一起工作。</p><p>2.对象的适配器与类适配器功能大致一样,但是为了更加灵活,不再使用继承的方式,而是直接使用了成员变量这样的方法,将目标的类的对象持有,再实现要增加的接口方法,达到一样的目的。</p><p>3.接口适配器模式,是把所有的方法定义到一个接口里面,然后创建一个抽象类去实现所有的方法,再使用真正的类去继承抽象类,只需要重写需要的方法,就可以完成适配的功能。<br>如果有兴趣,可以了解一下另外一种说法的适配器模式[<a href="https://link.segmentfault.com/?enc=WAexmwItl2FO7fX2hTzKyw%3D%3D.WgZ2hIsGWxLQSFIcgq6tZUDckARJZWxLQptrscc9kZyrbMOmayeRevK7AmBTWncqTULqVd394gmhxDYcWa8EMA%3D%3D" rel="nofollow">https://blog.csdn.net/Aphysia...</a>]</p><p>4.建议尽量使用对象的适配器模式,少用继承。适配器模式也是一种包装模式,它与装饰模式同样具有包装的功能,此外,对象适配器模式还具有委托的意思。总的来说,适配器模式属于补偿模式,专门用来在系统后期扩展、修改时使用,但要注意不要过度使用适配器模式。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=h56%2FRqVjyeHiP4kBYTAiUA%3D%3D.fqGMMUkRWkFG2O8tV2wX4I0Z08KM8xJTrdW4AIM%2BkhHglj3yhZwBEYr4zugHbgLu" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=99vuigyAcxaylCapfPR2MQ%3D%3D.5hzbbfnbNYGN4mjL%2BGYDon%2FFM7Aa09U%2Fbm7Vnh0NDXU%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=JwfGsp3KJ2GOy8kSiPkzcA%3D%3D.OOmxrkLoGxQ76rG0PFmz4CaqK%2B%2FCaI0sE8L6g0A7Rs0x5%2FKozG6OMc3QUO%2FAHsRB" rel="nofollow">开源编程笔记</a></p><blockquote>关注公众号 ”秦怀杂货店“ 可以领取<code>剑指 Offer V1</code>版本的 <code>PDF</code>解法,V2版本增加了题目,还在哼哧哼哧的更新中,并且为每道题目增加了<code>C++</code>解法,敬请期待。</blockquote><p><img src="/img/remote/1460000041100743" alt="" title=""></p>
设计模式【5】-- 原型模式
https://segmentfault.com/a/1190000041100738
2021-12-11T10:07:40+08:00
2021-12-11T10:07:40+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>开局一张图,剩下全靠写...</p><p>设计模式文章集合:<a href="https://link.segmentfault.com/?enc=goOOwTSKwloau6vUVjabdg%3D%3D.TjTyS1VW3Alz58CQnqZpbI0EFObCa9H93ktc%2FzrU3Ksku2nYPLPQKX88ZGIlrkQH" rel="nofollow">http://aphysia.cn/categories/...</a></p><p><img src="/img/remote/1460000041100740" alt="111" title="111"></p><h2>前言</h2><p>接触过 <code>Spring</code> 或者 <code>Springboot</code> 的同学或许都了解, <code>Bean</code> 默认是单例的,也就是全局共用同一个对象,不会因为请求不同,使用不同的对象,这里我们不会讨论单例,前面已经讨论过单例模式的好处以及各种实现,有兴趣可以了解一下:<a href="https://link.segmentfault.com/?enc=QY479W4psbXCMjoDDC47hg%3D%3D.P%2F%2BFJT7Te174jMmx684LMMLVuaqcaz1UwKwpMXoWarocEnTFHa54D7CnShl3p9lp" rel="nofollow">http://aphysia.cn/archives/de...</a>。除了单例以外,<code>Spring</code>还可以设置其他的作用域,也就是<code>scope="prototype"</code>,这就是原型模式,每次来一个请求,都会新创建一个对象,这个对象就是按照原型实例创建的。</p><h2>原型模式的定义</h2><p>原型模式,也是创建型模式的一种,是指用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,简单来说,就是拷贝。一般适用于:</p><ul><li>实例比较复杂,完全创建成本高,直接复制比较简单</li><li><p>构造函数比较复杂,创建可能产生很多不必要的对象</p><p>优点:</p></li><li>隐藏了创建实例的具体细节</li><li>创建对象效率比较高</li><li>如果一个对象大量相同的属性,只有少量需要特殊化的时候,可以直接用原型模式拷贝的对象,加以修改,就可以达到目的。</li></ul><h2>原型模式的实现方式</h2><p>一般来说,原型模式就是用来复制对象的,那么复制对象必须有原型类,也就是<code>Prototype</code>,<code>Prototype</code>需要实现<code>Cloneable</code>接口,实现这个接口才能被拷贝,再重写<code>clone()</code>方法,还可以根据不同的类型来快速获取原型对象。</p><p>我们先定义一个原型类<code>Fruit</code>:</p><pre><code class="java">public abstract class Fruit implements Cloneable{
String name;
float price;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
public Object clone() {
Object clone = null;
try {
clone = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
@Override
public String toString() {
return "Fruit{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}</code></pre><p>以及拓展了<code>Fruit</code>类的实体类<code>Apple</code>,<code>Pear</code>,<code>Watermelon</code>:</p><pre><code class="java">public class Apple extends Fruit{
public Apple(float price){
name = "苹果";
this.price = price;
}
}</code></pre><pre><code class="java">public class Pear extends Fruit{
public Pear(float price){
name = "雪梨";
this.price = price;
}
}</code></pre><pre><code class="java">public class Watermelon extends Fruit{
public Watermelon(float price){
name = "西瓜";
this.price = price;
}
}</code></pre><p>创建一个获取不同水果类的缓存类,每次取的时候,根据不同的类型,取出来,拷贝一次返回即可:</p><pre><code class="java">public class FruitCache {
private static ConcurrentHashMap<String,Fruit> fruitMap =
new ConcurrentHashMap<String,Fruit>();
static {
Apple apple = new Apple(10);
fruitMap.put(apple.getName(),apple);
Pear pear = new Pear(8);
fruitMap.put(pear.getName(),pear);
Watermelon watermelon = new Watermelon(5);
fruitMap.put(watermelon.getName(),watermelon);
}
public static Fruit getFruit(String name){
Fruit fruit = fruitMap.get(name);
return (Fruit)fruit.clone();
}
}</code></pre><p>测试一下,分别获取不同的水果,以及对比两次获取同一种类型,可以发现,两次获取的同一种类型,不是同一个对象:</p><pre><code class="java">public class Test {
public static void main(String[] args) {
Fruit apple = FruitCache.getFruit("苹果");
System.out.println(apple);
Fruit pear = FruitCache.getFruit("雪梨");
System.out.println(pear);
Fruit watermelon = FruitCache.getFruit("西瓜");
System.out.println(watermelon);
Fruit apple1 = FruitCache.getFruit("苹果");
System.out.println("是否为同一个对象" + apple.equals(apple1));
}
}</code></pre><p>结果如下:</p><pre><code class="txt">
Fruit{name='苹果', price=10.0}
Fruit{name='雪梨', price=8.0}
Fruit{name='西瓜', price=5.0}
false</code></pre><p>再测试一下,我们看看里面的<code>name</code>属性是不是同一个对象:</p><pre><code class="java">public class Test {
public static void main(String[] args) {
Fruit apple = FruitCache.getFruit("苹果");
System.out.println(apple);
Fruit apple1 = FruitCache.getFruit("苹果");
System.out.println(apple1);
System.out.println("是否为同一个对象:" + apple.equals(apple1));
System.out.println("是否为同一个字符串对象:" + apple.name.equals(apple1.name));
}
}</code></pre><p>结果如下,里面的字符串确实还是用的是同一个对象:</p><pre><code class="txt">Fruit{name='苹果', price=10.0}
Fruit{name='苹果', price=10.0}
是否为同一个对象:false
是否为同一个字符串对象:true</code></pre><p>这是为什么呢?<strong>因为上面使用的clone()是浅拷贝!!!不过有一点,字符串在Java里面是不可变的,如果发生修改,也不会修改原来的字符串,由于这个属性的存在,类似于深拷贝</strong>。如果属性是其他自定义对象,那就得注意了,浅拷贝不会真的拷贝该对象,只会拷贝一份引用。</p><p><img src="/img/remote/1460000041100741" alt="" title=""></p><p>这里不得不介绍一下浅拷贝与深拷贝的区别:</p><ul><li>浅拷贝:没有真正的拷贝数据,只是拷贝了一个指向数据内存地址的指针</li><li>深拷贝:不仅新建了指针,还拷贝了一份数据内存</li></ul><p>如果我们使用<code>Fruit apple = apple1</code>,这样只是拷贝了对象的引用,其实本质上还是同一个对象,上面的情况虽然对象是不同的,但是<code>Apple</code>属性的拷贝还属于同一个引用,地址还是一样的,它们共享了原来的属性对象<code>name</code>。</p><p><img src="/img/remote/1460000041100742" alt="" title=""></p><p><strong>那如何进行深拷贝呢?</strong>一般有以下方案:</p><ul><li>直接 new 对象,这个不用考虑了</li><li>序列化与反序列化:先序列化之后,再反序列化回来,就可以得到一个新的对象,注意必须实现<code>Serializable</code>接口。</li><li>自己重写对象的<code>clone()</code>方法</li></ul><h3>序列化实现深拷贝</h3><p>序列化实现代码如下:</p><p>创建一个<code>Student</code>类和<code>School</code>类:</p><pre><code class="java">import java.io.Serializable;
public class Student implements Serializable {
String name;
School school;
public Student(String name, School school) {
this.name = name;
this.school = school;
}
}</code></pre><pre><code class="java">import java.io.Serializable;
public class School implements Serializable {
String name;
public School(String name) {
this.name = name;
}
}</code></pre><p>序列化拷贝的类:</p><pre><code class="java">import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class CloneUtil {
public static <T extends Serializable> T clone(T obj) {
T result = null;
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
objectOutputStream.close();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
// 返回生成的新对象
result = (T) objectInputStream.readObject();
objectInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}</code></pre><p>测试类:</p><pre><code class="java">
public class Test {
public static void main(String[] args) {
School school = new School("东方小学");
Student student =new Student("小明",school);
Student student1= CloneUtil.clone(student);
System.out.println(student.equals(student1));
System.out.println(student.school.equals(student1.school));
}
}</code></pre><p>上面的结果均是<code>false</code>,说明确实不是同一个对象,发生了深拷贝。</p><h3>clone实现深拷贝</h3><p>前面的<code>Student</code>和<code>School</code>都实现<code>Cloneable</code>接口,然后重写<code>clone()</code>方法:</p><pre><code class="java">
public class Student implements Cloneable {
String name;
School school;
public Student(String name, School school) {
this.name = name;
this.school = school;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Student student = (Student) super.clone();
student.school = (School) school.clone();
return student;
}
}</code></pre><pre><code class="java">
public class School implements Cloneable {
String name;
public School(String name) {
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}</code></pre><p>测试类:</p><pre><code class="java">public class Test {
public static void main(String[] args) throws Exception{
School school = new School("东方小学");
Student student =new Student("小明",school);
Student student1= (Student) student.clone();
System.out.println(student.equals(student1));
System.out.println(student.school.equals(student1.school));
}
}</code></pre><p>测试结果一样,同样都是<code>false</code>,也是发生了深拷贝。</p><h2>总结</h2><p>原型模式适用于创建对象需要很多步骤或者资源的场景,而不同的对象之间,只有一部分属性是需要定制化的,其他都是相同的,一般来说,原型模式不会单独存在,会和其他的模式一起使用。值得注意的是,拷贝分为浅拷贝和深拷贝,浅拷贝如果发生数据修改,不同对象的数据都会被修改,因为他们共享了元数据。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=W5FnBoEjpTHKAQxCe3XYsg%3D%3D.2yp2xzdPcxxpi15eUdRuoQyN6D%2BoSiAnaEJF9KwnhxoYY%2Bo6mZ8%2BOqbAZc5FX19%2F" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=bj%2BknjTTXa4ysEPJFwU63Q%3D%3D.5JdXePGIrlTqx597C8%2BTYxrApzbNjKmF9nyztSvbDI0%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=Pa8ngCJ8sTcHwn7JRstU%2BA%3D%3D.aswLnNkVBRpZqZU%2FLkfKZ7R57Z6OAQrPPw4wbwDPh6ijGKedPpc29cjwtNU0iR7T" rel="nofollow">开源编程笔记</a></p><blockquote>关注公众号 ”秦怀杂货店“ 可以领取<code>剑指 Offer V1</code>版本的 <code>PDF</code>解法,V2版本增加了题目,还在哼哧哼哧的更新中,并且为每道题目增加了<code>C++</code>解法,敬请期待。</blockquote><p><img src="/img/remote/1460000041100743" alt="" title=""></p><p><img src="/img/remote/1460000041100744" alt="" title=""></p>
面试官说:你来设计一个短链接生成系统吧
https://segmentfault.com/a/1190000041063901
2021-12-04T15:36:49+08:00
2021-12-04T15:36:49+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<h2>引言</h2><p>相信大家在生活中,特别是最近的双十一活动期间,会收到很多短信,而那些短信都有两个特征,第一个是几乎都是垃圾短信,这个特点此处可以忽略不计,第二个特点是<strong>链接很短</strong>,比如下面这个:</p><p><img src="/img/remote/1460000041063915" alt="20211110222405" title="20211110222405"></p><p>我们知道,短信有些是有字数限制的,直接放一个带满各种参数的链接,不合适,另外一点是,不想暴露参数。好处无非以下:</p><ul><li>太长的链接容易被限制长度</li><li>短链接看着简洁,长链接看着容易懵</li><li>安全,不想暴露参数</li><li>可以统一链接转换,当然也可以实现统计点击次数等操作</li></ul><p>那背后的原理是什么呢?怎么实现的?让你实现这样的系统,你会怎么设计呢?【来自于某鹅场面试官】</p><h2>短链接的原理</h2><h3>短链接展示的逻辑</h3><p>这里最重要的知识点是重定向,先复习一下<code>http</code>的状态码:</p><table><thead><tr><th align="left">分类</th><th align="left">含义</th></tr></thead><tbody><tr><td align="left">1**</td><td align="left">服务器收到请求,需要请求者继续执行操作</td></tr><tr><td align="left">2**</td><td align="left">成功,操作被成功接收并处理</td></tr><tr><td align="left">3**</td><td align="left">重定向,需要进一步的操作以完成请求</td></tr><tr><td align="left">4**</td><td align="left">客户端错误,请求包含语法错误或无法完成请求</td></tr><tr><td align="left">5**</td><td align="left">服务器错误,服务器在处理请求的过程中发生了错误</td></tr></tbody></table><p>那么以 3 开头的状态码都是关于重定向的:</p><ul><li>300:多种选择,可以在多个位置存在</li><li>301:永久重定向,浏览器会缓存,自动重定向到新的地址</li><li>302:临时重定向,客户端还是会继续使用旧的URL</li><li>303:查看其他的地址,类似于301</li><li>304:未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。</li><li>305:需要使用代理才能访问到资源</li><li>306:废弃的状态码</li><li>307:临时重定向,使用Get请求重定向</li></ul><p>整个跳转的流程:</p><ul><li>1.用户访问短链接,请求到达服务器</li><li><p>2.服务器将短链接装换成为长链接,然后给浏览器返回重定向的状态码301/302</p><ul><li>301永久重定向会导致浏览器缓存重定向地址,短链接系统统计访问次数会不正确</li><li>302临时重定向可以解决次数不准的问题,但是每次都会到短链接系统转换,服务器压力会变大。</li></ul></li><li>3.浏览器拿到重定向的状态码,以及真正需要访问的地址,重定向到真正的长链接上。</li></ul><p>从下图可以看出,确实链接被<code>302</code>重定向到新的地址上去,返回的头里面有一个字段<code>Location</code>就是所要重定向的地址:</p><p><img src="https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20211110235518.png" style="zoom:70%;" /></p><h3>短链接怎么设计的?</h3><h4>全局发号器</h4><p>肯定我们第一点想到的是压缩,像文件压缩那样,压缩之后再解压还原到原来的链接,重定向到原来的链接,但是很不幸的是,这个是行不通的,你有见过什么压缩方式能把这么长的数字直接压缩到这么短么?事实上不可能。就像是<code>Huffman</code>树,也只能对那种重复字符较多的字符串压缩时效率较高,像链接这种,可能带很多参数,而且各种不规则的情况都有,直接压缩算法不现实。</p><p>那<code>https://dx.10086.cn/tzHLFw</code>与<code>https://gd.10086.cn/gmccapp/webpage/payPhonemoney/index.html?channel=</code>之间的装换是怎么样的呢?前面路径不变,变化的是后面,也就是<code>tzHLFw</code>与<code>gmccapp/webpage/payPhonemoney/index.html?channel=</code>之间的转换。</p><p>实际也很简单,就是数据库里面的一条数据,一个<code>id</code>对应长链接(相当于全局的发号器,全局唯一的ID):</p><table><thead><tr><th align="center">id</th><th align="center">url</th></tr></thead><tbody><tr><td align="center">1</td><td align="center"><a href="https://link.segmentfault.com/?enc=MGsxDsUr9ctspMNbixcbZg%3D%3D.NRDykwxSx9KkesgomWHmHrVxVs45w68KS8rF2INdCw3Gxy2OjZiQqE6%2FUkVn%2FinoqFLkJCYTDJWy6C6I4paCcHPQlC6T2Abl08bw6ubUvpw%3D" rel="nofollow">https://gd.10086.cn/gmccapp/w...</a></td></tr></tbody></table><p>这里用到的,也就是我们之前说过的分布式全局唯一ID,如果我们直接用<code>id</code>作为参数,貌似也可以:<code>https://dx.10086.cn/1</code>,访问这个链接时,去数据库查询获得真正的url,再重定向。</p><p>单机的唯一<code>ID</code>很简单,用原子类<code>AtomicLong</code>就可以,但是分布式的就不行了,简单点可以用 <code>redis</code>,或者数据库自增,或者可以考虑<code>Zookeeper</code>之类的。</p><h4>id 转换策略</h4><p>但是直接用递增的数字,有两个坏处:</p><ul><li>数字很大的时候,还是很长</li><li>递增的数字,不安全,规律性太强了</li></ul><p>明显我们平时看到的链接也不是数字的,一般都是大小写字母加上数字。为了缩短链接的长度,我们必须把<code>id</code>转换掉,比如我们的短链接由<code>a-z</code>,<code>A-Z</code>,<code>0-9</code>组成,相当于<code>62</code>进制的数字,将<code>id</code>转换成为<code>62</code>进制的数字:</p><pre><code class="java">public class ShortUrl {
private static final String BASE = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static String toBase62(long num) {
StringBuilder result = new StringBuilder();
do {
int i = (int) (num % 62);
result.append(BASE.charAt(i));
num /= 62;
} while (num > 0);
return result.reverse().toString();
}
public static long toBase10(String str) {
long result = 0;
for (int i = 0; i < str.length(); i++) {
result = result * 62 + BASE.indexOf(str.charAt(i));
}
return result;
}
public static void main(String[] args) {
// tzHLFw
System.out.println(toBase10("tzHLFw"));
System.out.println(toBase62(27095455234L));
}
}</code></pre><p><code>id</code>转 <code>62</code>位的<code>key</code> 或者<code>key</code>装换成为<code>id</code>都已经实现了,不过计算还是比较耗时的,不如加个字段存起来,于是数据库变成了:</p><table><thead><tr><th align="center">id</th><th align="center">key</th><th align="center">url</th></tr></thead><tbody><tr><td align="center">27095455234</td><td align="center">tzHLFw</td><td align="center"><a href="https://link.segmentfault.com/?enc=Epn4UAvSxiV9I8eEioH2PQ%3D%3D.8C%2ByRYqmF2mXDnbIeH4rRYPl%2F2jeKV3HyNsok%2FAO5z405B8NMf82qhOqSW%2FxkoiXOEu5bQMd5zDHZ0Ge6CT5Fl2LXGRXEaxxLSN7Z1l6i3Q%3D" rel="nofollow">https://gd.10086.cn/gmccapp/w...</a></td></tr></tbody></table><p>但是这样还是很容易被猜出这个<code>id</code>和<code>key</code>的对应关系,要是被遍历访问,那还是很不安全的,如果担心,可以随机将短链接的字符顺序打乱,或者在适当的位置加上一些随机生成的字符,比如第<code>1,4,5 </code>位是随机字符,其他位置不变,只要我们计算的时候,将它对应的关系存到数据库,我们就可以通过连接的<code>key</code>找到对应的<code>url</code>。(值得注意的是,<code>key</code>必须是全局唯一的,如果冲突,必须重新生成)</p><p>一般短链接都有过期时间,那么我们也必须在数据库里面加上对应的字段,访问的时候,先判断是否过期,过期则不给予重定向。</p><p><img src="/img/remote/1460000041063903" alt="" title=""></p><h4>性能考虑</h4><p>如果有很多短链接暴露出去了,数据库里面数据很多,这个时候可以考虑使用缓存优化,生成的时候顺便把缓存写入,然后读取的时候,走缓存即可,因为一般短链接和长链接的关系不会修改,即使修改,也是很低频的事情。</p><p>如果系统的<code>id</code>用完了怎么办?这种概率很小,如果真的发生,可以重用旧的已经失效的<code>id</code>号。</p><p>如果被人疯狂请求一些不存在的短链接怎么办?其实这就是缓存穿透,缓存穿透是指,<strong>缓存和数据库都没有的数据</strong>,被大量请求,比如订单号不可能为<code>-1</code>,但是用户请求了大量订单号为<code>-1</code>的数据,由于数据不存在,缓存就也不会存在该数据,所有的请求都会直接穿透到数据库。如果被恶意用户利用,疯狂请求不存在的数据,就会导致数据库压力过大,甚至垮掉。</p><p>针对这种情况,一般可以用布隆过滤器过滤掉不存在的数据请求,但是我们这里<code>id</code>本来就是递增且有序的,其实我们范围大致都是已知的,更加容易判断,超出的肯定不存在,或者请求到的时候,缓存里面放一个空对象也是没有问题的。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=%2BoSWwnsvdxbb%2B6SoKhWSLA%3D%3D.Yh%2BOMuS5HoCAK4BN861kbKjo%2FC%2FUgjZwNHeFVuWq7xzx7OEi6HSfodh1AJkGZ52s" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=f8V8zXe2wwYOBlOwxn8gJg%3D%3D.vnzCgibckuB%2BkRxIbAkMcxncgp%2FEgUponLg%2BMKPSQxg%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=WlQqh82E%2FE5GzHkZSveN2Q%3D%3D.t6W5RT921BvqF%2BwyJGD7hnlTiJV0YxugSecAVFmO4SNUSkMht025oo6ntpL%2BwLwy" rel="nofollow">开源编程笔记</a></p>
设计模式【4】-- 建造者模式详解
https://segmentfault.com/a/1190000041050096
2021-12-02T09:06:00+08:00
2021-12-02T09:06:00+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>开局一张图,剩下全靠写...</p><p><img src="https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/设计模式.png" style="zoom: 33%;" ></p><h2>引言</h2><blockquote>设计模式集合:<a href="https://link.segmentfault.com/?enc=wLoXk9KgGcFR6XmjYlOGNA%3D%3D.RC31v0ZnMhyFCuh8ijsx7yxfLaXNr4eSs9SIgSaHh0ErZDUvVPv1Qgpsso28yihm" rel="nofollow">http://aphysia.cn/categories/...</a></blockquote><p>如果你用过 <code>Mybatis</code> ,相信你对以下代码的写法并不陌生,先创建一个<code>builder</code>对象,然后再调用<code>.build()</code>函数:</p><pre><code class="java">InputStream is = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();</code></pre><p>上面其实就是我们这篇文章所要讲解的 <strong>建造者模式</strong>,下面让我们一起来琢磨一下它。</p><h2>什么是建造者模式</h2><blockquote>建造者模式是设计模式的一种,将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。(来源于百度百科)</blockquote><p>建造者模式,其实是创建型模式的一种,也是23种设计模式中的一种,从上面的定义来看比较模糊,但是不得不承认,当我们有能力用简洁的话去定义一个东西的时候,我们才是真的了解它了,因为这个时候我们已经知道它的界限在哪。</p><p>所谓将一个复杂对象的构建与它的表示分离,就是将对象的构建器抽象出来,构造的过程一样,但是不一样的构造器可以实现不一样的表示。</p><h2>结构与例子</h2><p>建造者模式主要分为以下四种角色:</p><ul><li>产品(<code>Product</code>):具体生产器要构造的复杂对象</li><li>抽象生成器(<code>Bulider</code>):抽象生成器是一个接口,创建一个产品各个部件的接口方法,以及返回产品的方法</li><li>具体建造者(<code>ConcreteBuilder</code>):按照自己的产品特性,实现抽象建造者对应的接口</li><li>指挥者(<code>Director</code>):创建一个复杂的对象,控制具体的流程</li></ul><p>说到这里,可能会有点懵,毕竟全都是定义,下面从实际例子来讲讲,就拿程序员最喜欢的电脑来说,假设现在要生产多种电脑,电脑有屏幕,鼠标,cpu,主板,磁盘,内存等等,我们可能立马就能写出来:</p><pre><code class="java">public class Computer {
private String screen;
private String mouse;
private String cpu;
private String mainBoard;
private String disk;
private String memory;
...
public String getMouse() {
return mouse;
}
public void setMouse(String mouse) {
this.mouse = mouse;
}
public String getCpu() {
return cpu;
}
public void setCpu(String cpu) {
this.cpu = cpu;
}
...
}</code></pre><p>上面的例子中,每一种属性都使用单独的<code>set</code>方法,要是生产不同的电脑的不同部件,具体的实现还不太一样,这样一个类实现起来貌似不是很优雅,比如联想电脑和华硕电脑的屏幕的构建过程不一样,而且这些部件的构建,理论上都是电脑的一部分,我们可以考虑<strong>流水线式</strong>的处理。</p><p>当然,也有另外一种实现,就是多个构造函数,不同的构造函数带有不同的参数,实现了可选的参数:</p><pre><code class="java">public class Computer {
private String screen;
private String mouse;
private String cpu;
private String mainBoard;
private String disk;
private String memory;
public Computer(String screen) {
this.screen = screen;
}
public Computer(String screen, String mouse) {
this.screen = screen;
this.mouse = mouse;
}
public Computer(String screen, String mouse, String cpu) {
this.screen = screen;
this.mouse = mouse;
this.cpu = cpu;
}
...
}</code></pre><p>上面多种参数的构造方法,理论上满足了按需构造的要求,但是还是会有不足的地方:</p><ul><li>倘若构造每一个部件的过程都比较复杂,那么构造函数看起来就比较凌乱</li><li>如果有多种按需构造的要求,构造函数就太多了</li><li>构造不同的电脑类型,耦合在一块,必须抽象出来</li></ul><p>首先,我们先用流水线的方式,实现按需构造,不能重载那么多构造函数:</p><pre><code class="java">public class Computer {
private String screen;
private String mouse;
private String cpu;
private String mainBoard;
private String disk;
private String memory;
public Computer setScreen(String screen) {
this.screen = screen;
return this;
}
public Computer setMouse(String mouse) {
this.mouse = mouse;
return this;
}
public Computer setCpu(String cpu) {
this.cpu = cpu;
return this;
}
public Computer setMainBoard(String mainBoard) {
this.mainBoard = mainBoard;
return this;
}
public Computer setDisk(String disk) {
this.disk = disk;
return this;
}
public Computer setMemory(String memory) {
this.memory = memory;
return this;
}
}</code></pre><p>使用的时候,构造起来,就像是流水线一样,一步一步构造就可以:</p><pre><code class="java"> Computer computer = new Computer()
.setScreen("高清屏幕")
.setMouse("罗技鼠标")
.setCpu("i7处理器")
.setMainBoard("联想主板")
.setMemory("32G内存")
.setDisk("512G磁盘");</code></pre><p>但是以上的写法不够优雅,既然构造过程可能很复杂,为何不用一个特定的类来构造呢?这样构造的过程和主类就分离了,职责更加清晰,在这里内部类就可以了:</p><pre><code class="java">package designpattern.builder;
import javax.swing.*;
public class Computer {
private String screen;
private String mouse;
private String cpu;
private String mainBoard;
private String disk;
private String memory;
Computer(Builder builder) {
this.screen = builder.screen;
this.cpu = builder.cpu;
this.disk = builder.disk;
this.mainBoard = builder.mainBoard;
this.memory = builder.memory;
this.mouse = builder.mouse;
}
public static class Builder {
private String screen;
private String mouse;
private String cpu;
private String mainBoard;
private String disk;
private String memory;
public Builder setScreen(String screen) {
this.screen = screen;
return this;
}
public Builder setMouse(String mouse) {
this.mouse = mouse;
return this;
}
public Builder setCpu(String cpu) {
this.cpu = cpu;
return this;
}
public Builder setMainBoard(String mainBoard) {
this.mainBoard = mainBoard;
return this;
}
public Builder setDisk(String disk) {
this.disk = disk;
return this;
}
public Builder setMemory(String memory) {
this.memory = memory;
return this;
}
public Computer build() {
return new Computer(this);
}
}
}
</code></pre><p>使用的时候,使用<code>builder</code>来构建,构建完成之后,调用build的时候,再将具体的值,赋予我们需要的对象(这里是<code>Computer</code>):</p><pre><code class="java">public class Test {
public static void main(String[] args) {
Computer computer = new Computer.Builder()
.setScreen("高清屏幕")
.setMouse("罗技鼠标")
.setCpu("i7处理器")
.setMainBoard("联想主板")
.setMemory("32G内存")
.setDisk("512G磁盘")
.build();
System.out.println(computer.toString());
}
}</code></pre><p>但是上面的写法,如果我们构造多种电脑,每种电脑的配置不太一样,构建的过程也不一样,那么我们就必须将构造器抽象出来,变成一个抽象类。</p><p>首先我们定义产品类<code>Computer</code>:</p><pre><code class="java">public class Computer {
private String screen;
private String mouse;
private String cpu;
public void setScreen(String screen) {
this.screen = screen;
}
public void setMouse(String mouse) {
this.mouse = mouse;
}
public void setCpu(String cpu) {
this.cpu = cpu;
}
@Override
public String toString() {
return "Computer{" +
"screen='" + screen + '\'' +
", mouse='" + mouse + '\'' +
", cpu='" + cpu + '\'' +
'}';
}
}</code></pre><p>定义一个抽象的构造类,用于所有的电脑类构造:</p><pre><code class="java">public abstract class Builder {
abstract Builder buildScreen(String screen);
abstract Builder buildMouse(String mouse);
abstract Builder buildCpu(String cpu);
abstract Computer build();
}</code></pre><p>先构造一台联想电脑,那联想电脑必须实现自己的构造器,每一款电脑总有自己特殊的地方:</p><pre><code class="java">public class LenovoBuilder extends Builder {
private Computer computer = new Computer();
@Override
Builder buildScreen(String screen) {
computer.setScreen(screen);
return this;
}
@Override
Builder buildMouse(String mouse) {
computer.setMouse(mouse);
return this;
}
@Override
Builder buildCpu(String cpu) {
computer.setCpu(cpu);
return this;
}
@Override
Computer build() {
System.out.println("构建中...");
return computer;
}
}
</code></pre><p>构建器有了,还需要有个指挥者,它负责去构建我们具体的电脑:</p><pre><code class="java">public class Director {
Builder builder = null;
public Director(Builder builder){
this.builder = builder;
}
public void doProcess(String screen,String mouse,String cpu){
builder.buildScreen(screen)
.buildMouse(mouse)
.buildCpu(cpu);
}
}</code></pre><p>使用的时候,我们只需要先构建<code>builder</code>,然后把<code>builder</code>传递给指挥者,他负责具体的构建,构建完之后,构建器调用一下<code>.build()</code>方法,就可以创建出一台电脑。</p><pre><code class="java">public class Test {
public static void main(String[] args) {
LenovoBuilder builder = new LenovoBuilder();
Director director = new Director(builder);
director.doProcess("联想屏幕","游戏鼠标","高性能cpu");
Computer computer = builder.build();
System.out.println(computer);
}
}</code></pre><p>打印结果:</p><pre><code class="shell">构建中...
Computer{screen='联想屏幕', mouse='游戏鼠标', cpu='高性能cpu'}</code></pre><p>以上其实就是完整的建造者模式,但是我们平时用的,大部分都是自己直接调用构建器<code>Builder</code>,一路<code>set()</code>,最后<code>build()</code>,就创建出了一个对象。</p><h2>使用场景</h2><p>构建这模式的好处是什么?首先想到的应该是将构建的过程解耦了,构建的过程如果很复杂,单独拎出来写,清晰简洁。其次,每个部分的构建,其实都是可以独立去创建的,不需要多个构造方法,构建的工作交给了构建器,而不是对象本身。专业的人做专业的事。同样,构建者模式也比较适用于不同的构造方法或者构造顺序,可能会产生不同的构造结果的场景。</p><p>但是缺点还是有的,需要维护多出来的<code>Builder</code>对象,如果多种产品之间的共性不多,那么抽象的构建器将会失去它该有的作用。如果产品类型很多,那么定义太多的构建类来实现这种变化,代码也会变得比较复杂。</p><p>最近在公司用<code>GRPC</code>,里面的对象几乎都是基于构建者模式,链式的构建确实写着很舒服,也比较优雅,代码是写给人看的,我们所做的一切设计模式,都是为了拓展,解耦,以及避免代码只能口口相传。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=uQyCFY7DB8LpEQIJZXOTHg%3D%3D.c6R7%2B9y3psMbb1zro0h%2FP7o7GqovixvneGzkEcGbcTb5hzibh8X2zv5w3bmH3vTn" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=iO4UE7U27F9OdUBajCJOqg%3D%3D.wDnGFdVZPNJTTYq%2FX6v9W%2F3SbXpauWzhUVfKeo%2Ftd3c%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=cjcHtaIpNY72o1MBBNVlkw%3D%3D.qgtLQsM90S%2F6W%2FTUeIgycaacdNOYW%2FuKBmV4GZeU1PQMbBt9eNrjR63gO6k%2BNJP0" rel="nofollow">开源编程笔记</a></p>
雪花算法对System.currentTimeMillis()优化真的有用么?
https://segmentfault.com/a/1190000041042963
2021-11-30T22:44:33+08:00
2021-11-30T22:44:33+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
1
<p>前面已经讲过了雪花算法,里面使用了<code>System.currentTimeMillis()</code>获取时间,有一种说法是认为<code>System.currentTimeMillis()</code>慢,是因为每次调用都会去跟系统打一次交道,在高并发情况下,大量并发的系统调用容易会影响性能(对它的调用甚至比<code>new</code>一个普通对象都要耗时,毕竟<code>new</code>产生的对象只是在<code>Java</code>内存中的堆中)。我们可以看到它调用的是<code>native</code> 方法:</p><pre><code class="java">// 返回当前时间,以毫秒为单位。注意,虽然返回值的时间单位是毫秒,但值的粒度取决于底层操作系统,可能更大。例如,许多操作系统以数十毫秒为单位度量时间。
public static native long currentTimeMillis();</code></pre><p>所以有人提议,用后台线程定时去更新时钟,并且是单例的,避免每次都与系统打交道,也避免了频繁的线程切换,这样或许可以提高效率。</p><h2>这个优化成立么?</h2><p>先上优化代码:</p><pre><code class="java">package snowflake;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class SystemClock {
private final int period;
private final AtomicLong now;
private static final SystemClock INSTANCE = new SystemClock(1);
private SystemClock(int period) {
this.period = period;
now = new AtomicLong(System.currentTimeMillis());
scheduleClockUpdating();
}
private void scheduleClockUpdating() {
ScheduledExecutorService scheduleService = Executors.newSingleThreadScheduledExecutor((r) -> {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
});
scheduleService.scheduleAtFixedRate(() -> {
now.set(System.currentTimeMillis());
}, 0, period, TimeUnit.MILLISECONDS);
}
private long get() {
return now.get();
}
public static long now() {
return INSTANCE.get();
}
}</code></pre><p>只需要用<code>SystemClock.now()</code>替换<code>System.currentTimeMillis()</code>即可。</p><p>雪花算法<code>SnowFlake</code>的代码也放在这里:</p><pre><code class="java">package snowflake;
public class SnowFlake {
// 数据中心(机房) id
private long datacenterId;
// 机器ID
private long workerId;
// 同一时间的序列
private long sequence;
public SnowFlake(long workerId, long datacenterId) {
this(workerId, datacenterId, 0);
}
public SnowFlake(long workerId, long datacenterId, long sequence) {
// 合法判断
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
// 开始时间戳(2021-10-16 22:03:32)
private long twepoch = 1634393012000L;
// 机房号,的ID所占的位数 5个bit 最大:11111(2进制)--> 31(10进制)
private long datacenterIdBits = 5L;
// 机器ID所占的位数 5个bit 最大:11111(2进制)--> 31(10进制)
private long workerIdBits = 5L;
// 5 bit最多只能有31个数字,就是说机器id最多只能是32以内
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 5 bit最多只能有31个数字,机房id最多只能是32以内
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 同一时间的序列所占的位数 12个bit 111111111111 = 4095 最多就是同一毫秒生成4096个
private long sequenceBits = 12L;
// workerId的偏移量
private long workerIdShift = sequenceBits;
// datacenterId的偏移量
private long datacenterIdShift = sequenceBits + workerIdBits;
// timestampLeft的偏移量
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
// 序列号掩码 4095 (0b111111111111=0xfff=4095)
// 用于序号的与运算,保证序号最大值在0-4095之间
private long sequenceMask = -1L ^ (-1L << sequenceBits);
// 最近一次时间戳
private long lastTimestamp = -1L;
// 获取机器ID
public long getWorkerId() {
return workerId;
}
// 获取机房ID
public long getDatacenterId() {
return datacenterId;
}
// 获取最新一次获取的时间戳
public long getLastTimestamp() {
return lastTimestamp;
}
// 获取下一个随机的ID
public synchronized long nextId() {
// 获取当前时间戳,单位毫秒
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
// 去重
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
// sequence序列大于4095
if (sequence == 0) {
// 调用到下一个时间戳的方法
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 如果是当前时间的第一次获取,那么就置为0
sequence = 0;
}
// 记录上一次的时间戳
lastTimestamp = timestamp;
// 偏移计算
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
private long tilNextMillis(long lastTimestamp) {
// 获取最新时间戳
long timestamp = timeGen();
// 如果发现最新的时间戳小于或者等于序列号已经超4095的那个时间戳
while (timestamp <= lastTimestamp) {
// 不符合则继续
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return SystemClock.now();
// return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowFlake worker = new SnowFlake(1, 1);
long timer = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
worker.nextId();
}
System.out.println(System.currentTimeMillis());
System.out.println(System.currentTimeMillis() - timer);
}
}</code></pre><p>Windows:i5-4590 16G内存 4核 512固态</p><p>Mac: Mac pro 2020 512G固态 16G内存</p><p>Linux:deepin系统,虚拟机,160G磁盘,内存8G</p><p>单线程环境测试一下 <code>System.currentTimeMillis()</code>:</p><table><thead><tr><th align="center">平台/数据量</th><th align="center">10000</th><th align="center">1000000</th><th align="center">10000000</th><th align="center">100000000</th></tr></thead><tbody><tr><td align="center">mac</td><td align="center">5</td><td align="center">247</td><td align="center">2444</td><td align="center">24416</td></tr><tr><td align="center">windows</td><td align="center">3</td><td align="center">249</td><td align="center">2448</td><td align="center">24426</td></tr><tr><td align="center">linux(deepin)</td><td align="center">135</td><td align="center">598</td><td align="center">4076</td><td align="center">26388</td></tr></tbody></table><p>单线程环境测试一下 <code>SystemClock.now()</code>:</p><table><thead><tr><th align="center">平台/数据量</th><th align="center">10000</th><th align="center">1000000</th><th align="center">10000000</th><th align="center">100000000</th></tr></thead><tbody><tr><td align="center">mac</td><td align="center">52</td><td align="center">299</td><td align="center">2501</td><td align="center">24674</td></tr><tr><td align="center">windows</td><td align="center">56</td><td align="center">3942</td><td align="center">38934</td><td align="center">389983</td></tr><tr><td align="center">linux(deepin)</td><td align="center">336</td><td align="center">1226</td><td align="center">4454</td><td align="center">27639</td></tr></tbody></table><p>上面的单线程测试并没有体现出后台时钟线程处理的优势,反而在windows下,数据量大的时候,变得异常的慢,linux系统上,也并没有快,反而变慢了一点。</p><p>多线程测试代码:</p><pre><code class="java"> public static void main(String[] args) throws InterruptedException {
int threadNum = 16;
CountDownLatch countDownLatch = new CountDownLatch(threadNum);
int num = 100000000 / threadNum;
long timer = System.currentTimeMillis();
thread(num, countDownLatch);
countDownLatch.await();
System.out.println(System.currentTimeMillis() - timer);
}
public static void thread(int num, CountDownLatch countDownLatch) {
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < countDownLatch.getCount(); i++) {
Thread cur = new Thread(new Runnable() {
@Override
public void run() {
SnowFlake worker = new SnowFlake(1, 1);
for (int i = 0; i < num; i++) {
worker.nextId();
}
countDownLatch.countDown();
}
});
threadList.add(cur);
}
for (Thread t : threadList) {
t.start();
}
}</code></pre><p>下面我们用不同线程数来测试 100000000(一亿) 数据量 <code>System.currentTimeMillis()</code>:</p><table><thead><tr><th align="center">平台/线程</th><th align="center">2</th><th align="center">4</th><th align="center">8</th><th align="center">16</th></tr></thead><tbody><tr><td align="center">mac</td><td align="center">14373</td><td align="center">6132</td><td align="center">3410</td><td align="center">3247</td></tr><tr><td align="center">windows</td><td align="center">12408</td><td align="center">6862</td><td align="center">6791</td><td align="center">7114</td></tr><tr><td align="center">linux</td><td align="center">20753</td><td align="center">19055</td><td align="center">18919</td><td align="center">19602</td></tr></tbody></table><p>用不同线程数来测试 100000000(一亿) 数据量 <code>SystemClock.now()</code>:</p><table><thead><tr><th align="center">平台/线程</th><th align="center">2</th><th align="center">4</th><th align="center">8</th><th align="center">16</th></tr></thead><tbody><tr><td align="center">mac</td><td align="center">12319</td><td align="center">6275</td><td align="center">3691</td><td align="center">3746</td></tr><tr><td align="center">windows</td><td align="center">194763</td><td align="center">110442</td><td align="center">153960</td><td align="center">174974</td></tr><tr><td align="center">linux</td><td align="center">26516</td><td align="center">25313</td><td align="center">25497</td><td align="center">25544</td></tr></tbody></table><p>在多线程的情况下,我们可以看到mac上没有什么太大变化,随着线程数增加,速度还变快了,直到超过 8 的时候,但是windows上明显变慢了,测试的时候我都开始刷起了小视频,才跑出来结果。而且这个数据和处理器的核心也是相关的,当windows的线程数超过了 4 之后,就变慢了,原因是我的机器只有四核,超过了就会发生很多上下文切换的情况。</p><p>linux上由于虚拟机,核数增加的时候,并无太多作用,但是时间对比于直接调用 <code>System.currentTimeMillis()</code>其实是变慢的。</p><p><strong>但是还有个问题,到底不同方法调用,时间重复的概率哪一个大呢?</strong></p><pre><code class="java"> static AtomicLong atomicLong = new AtomicLong(0);
private long timeGen() {
atomicLong.incrementAndGet();
// return SystemClock.now();
return System.currentTimeMillis();
}</code></pre><p>下面是1千万id,八个线程,测出来调用<code>timeGen()</code>的次数,也就是可以看出时间冲突的次数:</p><table><thead><tr><th align="center">平台/方法</th><th align="center">SystemClock.now()</th><th align="center">System.currentTimeMillis()</th></tr></thead><tbody><tr><td align="center">mac</td><td align="center">23067209</td><td align="center">12896314</td></tr><tr><td align="center">windows</td><td align="center">705460039</td><td align="center">35164476</td></tr><tr><td align="center">linux</td><td align="center">1165552352</td><td align="center">81422626</td></tr></tbody></table><p>可以看出确实<code>SystemClock.now()</code>自己维护时间,获取的时间相同的可能性更大,会触发更多次数的重复调用,冲突次数变多,这个是不利因素!还有一个残酷的事实,那就是自己定义的后台时间刷新,获取的时间不是那么的准确。在linux中的这个差距就更大了,时间冲突次数太多了。</p><h2>结果</h2><p>实际测试下来,并没有发现<code>SystemClock.now()</code>能够优化很大的效率,反而会由于竞争,获取时间冲突的可能性更大。<code>JDK</code>开发人员真的不傻,他们应该也经过了很长时间的测试,比我们自己的测试靠谱得多,因此,个人观点,最终证明这个优化并不是那么的可靠。 </p><p>不要轻易相信某一个结论,如果有疑问,请一定做做实验,或者找足够权威的说法。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=zOairtmmMJDp0AF%2BYqJ38w%3D%3D.oRUZtfJA9UhSbj4IHjHKCr7YOjDMu82Zh8aylUOf1cWDy73a8KtRYmIQHN7tQsOh" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=qGypp2WOrj%2BjN13vshp6fw%3D%3D.Wc5jfGMFJqwCpHroFh%2BdqZxPiZUKOEZn1FHzKZB0lcQ%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=xkColKE8kH3g8liM3Jmg2A%3D%3D.3%2Bv8gR8tN1y5v%2BFWkqTydTv2htnclL5Sf0A1%2F0%2F27i9rtPn8tRE4HbjOWHYxwum3" rel="nofollow">开源编程笔记</a></p>
面试官:讲讲雪花算法,越详细越好
https://segmentfault.com/a/1190000040964518
2021-11-15T23:55:28+08:00
2021-11-15T23:55:28+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
9
<p>前面文章在谈论分布式唯一ID生成的时候,有提到雪花算法,这一次,我们详细点讲解,只讲它。</p><h2>SnowFlake算法</h2><blockquote><p>据国家大气研究中心的查尔斯·奈特称,一般的雪花大约由10^19个水分子组成。在雪花形成过程中,会形成不同的结构分支,所以说大自然中不存在两片完全一样的雪花,每一片雪花都拥有自己漂亮独特的形状。雪花算法表示生成的id如雪花般独一无二。 </p><p>snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。</p></blockquote><p>核心思想:分布式,唯一。</p><h2>算法具体介绍</h2><p>雪花算法是 64 位 的二进制,一共包含了四部分:</p><ul><li>1位是符号位,也就是最高位,始终是0,没有任何意义,因为要是唯一计算机二进制补码中就是负数,0才是正数。</li><li>41位是时间戳,具体到毫秒,41位的二进制可以使用69年,因为时间理论上永恒递增,所以根据这个排序是可以的。</li><li>10位是机器标识,可以全部用作机器ID,也可以用来标识机房ID + 机器ID,10位最多可以表示1024台机器。</li><li>12位是计数序列号,也就是同一台机器上同一时间,理论上还可以同时生成不同的ID,12位的序列号能够区分出4096个ID。</li></ul><p><img src="/img/remote/1460000040964520" alt="" title=""></p><h3>优化</h3><p>由于41位是时间戳,我们的时间计算是从1970年开始的,只能使用69年,为了不浪费,其实我们可以用时间的相对值,也就是以项目开始的时间为基准时间,往后可以使用69年。获取唯一ID的服务,对处理速度要求比较高,所以我们全部使用位运算以及位移操作,获取当前时间可以使用<code>System.currentTimeMillis()</code>。</p><h3>时间回拨问题</h3><p>在获取时间的时候,可能会出现<code>时间回拨</code>的问题,什么是时间回拨问题呢?就是服务器上的时间突然倒退到之前的时间。</p><ol><li>人为原因,把系统环境的时间改了。</li><li>有时候不同的机器上需要同步时间,可能不同机器之间存在误差,那么可能会出现时间回拨问题。</li></ol><p><strong>解决方案</strong></p><ol><li>回拨时间小的时候,不生成 ID,循环等待到时间点到达。</li><li>上面的方案只适合时钟回拨较小的,如果间隔过大,阻塞等待,肯定是不可取的,因此要么超过一定大小的回拨直接报错,拒绝服务,或者有一种方案是利用拓展位,回拨之后在拓展位上加1就可以了,这样ID依然可以保持唯一。但是这个要求我们提前预留出位数,要么从机器id中,要么从序列号中,腾出一定的位,在时间回拨的时候,这个位置 <code>+1</code>。</li></ol><p>由于时间回拨导致的生产重复的ID的问题,其实百度和美团都有自己的解决方案了,有兴趣可以去看看,下面不是它们官网文档的信息:</p><ul><li><p>百度UIDGenerator:<a href="https://link.segmentfault.com/?enc=kCl5EBPZn5lOQRGZJF9wGw%3D%3D.tVRsbCGe%2FIrz%2FCbXj5Z5NP7agkWd%2F5sQB5PpygrB5LWd57VdOx3ljbp2Zzf5RGWiHRCTkmblB5%2B7YnLQwaHGva1q8ybhH5Hzd5Xe71Sk0YE%3D" rel="nofollow">https://github.com/baidu/uid-...</a></p><ul><li>UidGenerator是Java实现的, 基于<a href="https://link.segmentfault.com/?enc=mkzb6kZDE6G3lo338pT20A%3D%3D.O5uxBj7xCXJud%2Bby7vIU9EYmVwRDCAinZVsibnfBfOUhjOsuF2s%2By9UINCr8m3sX" rel="nofollow">Snowflake</a>算法的唯一ID生成器。UidGenerator以组件形式工作在应用项目中, 支持自定义workerId位数和初始化策略, 从而适用于<a href="https://link.segmentfault.com/?enc=VrxwQO1D%2FEw1daaV2%2FyKLA%3D%3D.lfWpeUhmixAk5j%2FLQo66KmYXuaghAmkAm2S3VtM0du0%3D" rel="nofollow">docker</a>等虚拟化环境下实例自动重启、漂移等场景。 在实现上, UidGenerator通过借用未来时间来解决sequence天然存在的并发限制; 采用RingBuffer来缓存已生成的UID, 并行化UID的生产和消费, 同时对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题. 最终单机QPS可达600万。</li></ul></li><li><p>美团Leaf:<a href="https://link.segmentfault.com/?enc=sZFen2xiMUWsC8j3kD7U1w%3D%3D.V9IbKvCJSNBR1MfapYXsHT6WlfEphpUzBD2%2FcSOuj8Sja6hrxZ1XYy%2Bdb3CM5LJ7Qbc3PjxbyiQU9dvsshYH%2BdxDsjOw2xtAH6ByuMDjalA%3D" rel="nofollow">https://tech.meituan.com/2019...</a></p><ul><li><p>leaf-segment 方案</p><ul><li>优化:双buffer + 预分配</li><li>容灾:Mysql DB 一主两从,异地机房,半同步方式</li><li>缺点:如果用segment号段式方案:id是递增,可计算的,不适用于订单ID生成场景,比如竞对在两天中午12点分别下单,通过订单id号相减就能大致计算出公司一天的订单量,这个是不能忍受的。</li></ul></li><li><p>leaf-snowflake方案</p><ul><li><p>使用Zookeeper持久顺序节点的特性自动对snowflake节点配置workerID</p><ul><li>1.启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。</li><li>2.如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。</li><li>3.如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。</li></ul></li><li>缓存workerID,减少第三方组件的依赖</li><li>由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。<strong>或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警</strong></li></ul></li></ul></li></ul><h2>代码展示</h2><pre><code class="java">public class SnowFlake {
// 数据中心(机房) id
private long datacenterId;
// 机器ID
private long workerId;
// 同一时间的序列
private long sequence;
public SnowFlake(long workerId, long datacenterId) {
this(workerId, datacenterId, 0);
}
public SnowFlake(long workerId, long datacenterId, long sequence) {
// 合法判断
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
// 开始时间戳(2021-10-16 22:03:32)
private long twepoch = 1634393012000L;
// 机房号,的ID所占的位数 5个bit 最大:11111(2进制)--> 31(10进制)
private long datacenterIdBits = 5L;
// 机器ID所占的位数 5个bit 最大:11111(2进制)--> 31(10进制)
private long workerIdBits = 5L;
// 5 bit最多只能有31个数字,就是说机器id最多只能是32以内
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 5 bit最多只能有31个数字,机房id最多只能是32以内
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 同一时间的序列所占的位数 12个bit 111111111111 = 4095 最多就是同一毫秒生成4096个
private long sequenceBits = 12L;
// workerId的偏移量
private long workerIdShift = sequenceBits;
// datacenterId的偏移量
private long datacenterIdShift = sequenceBits + workerIdBits;
// timestampLeft的偏移量
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
// 序列号掩码 4095 (0b111111111111=0xfff=4095)
// 用于序号的与运算,保证序号最大值在0-4095之间
private long sequenceMask = -1L ^ (-1L << sequenceBits);
// 最近一次时间戳
private long lastTimestamp = -1L;
// 获取机器ID
public long getWorkerId() {
return workerId;
}
// 获取机房ID
public long getDatacenterId() {
return datacenterId;
}
// 获取最新一次获取的时间戳
public long getLastTimestamp() {
return lastTimestamp;
}
// 获取下一个随机的ID
public synchronized long nextId() {
// 获取当前时间戳,单位毫秒
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
// 去重
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
// sequence序列大于4095
if (sequence == 0) {
// 调用到下一个时间戳的方法
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 如果是当前时间的第一次获取,那么就置为0
sequence = 0;
}
// 记录上一次的时间戳
lastTimestamp = timestamp;
// 偏移计算
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
private long tilNextMillis(long lastTimestamp) {
// 获取最新时间戳
long timestamp = timeGen();
// 如果发现最新的时间戳小于或者等于序列号已经超4095的那个时间戳
while (timestamp <= lastTimestamp) {
// 不符合则继续
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowFlake worker = new SnowFlake(1, 1);
long timer = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
worker.nextId();
}
System.out.println(System.currentTimeMillis());
System.out.println(System.currentTimeMillis() - timer);
}
}
</code></pre><h2>问题分析</h2><h3>1. 第一位为什么不使用?</h3><p>在计算机的表示中,第一位是符号位,0表示整数,第一位如果是1则表示负数,我们用的ID默认就是正数,所以默认就是0,那么这一位默认就没有意义。</p><h3>2.机器位怎么用?</h3><p>机器位或者机房位,一共10 bit,如果全部表示机器,那么可以表示1024台机器,如果拆分,5 bit 表示机房,5bit表示机房里面的机器,那么可以有32个机房,每个机房可以用32台机器。</p><h3>3. twepoch表示什么?</h3><p>由于时间戳只能用69年,我们的计时又是从1970年开始的,所以这个<code>twepoch</code>表示从项目开始的时间,用生成ID的时间减去<code>twepoch</code>作为时间戳,可以使用更久。</p><h3>4. -1L ^ (-1L << x) 表示什么?</h3><p>表示 x 位二进制可以表示多少个数值,假设x为3:</p><p>在计算机中,第一位是符号位,负数的反码是除了符号位,1变0,0变1, 而补码则是反码+1:</p><pre><code class="txt">-1L 原码:1000 0001
-1L 反码:1111 1110
-1L 补码:1111 1111</code></pre><p>从上面的结果可以知道,<strong>-1L其实在二进制里面其实就是全部为1</strong>,那么 -1L 左移动 3位,其实得到 <code>1111 1000</code>,也就是最后3位是0,再与<code>-1L</code>异或计算之后,其实得到的,就是后面3位全是1。<code>-1L ^ (-1L << x) </code>表示的其实就是x位全是1的值,也就是x位的二进制能表示的最大数值。</p><h3>5.时间戳比较</h3><p>在获取时间戳小于上一次获取的时间戳的时候,不能生成ID,而是继续循环,直到生成可用的ID,这里没有使用拓展位防止时钟回拨。</p><h3>6.前端直接使用发生精度丢失</h3><p>如果前端直接使用服务端生成的long 类型 id,会发生精度丢失的问题,因为 JS 中Number是16位的(指的是十进制的数字),而雪花算法计算出来最长的数字是19位的,这个时候需要用 String 作为中间转换,输出到前端即可。</p><h2>秦怀の观点</h2><p>雪花算法其实是依赖于时间的一致性的,如果时间回拨,就可能有问题,一般使用拓展位解决。而只能使用69年这个时间限制,其实可以根据自己的需要,把时间戳的位数设置得更多一点,比如42位可以用139年,但是很多公司首先得活下来。当然雪花算法也不是银弹,它也有缺点,在单机上递增,而多台机器只是大致递增趋势,并不是严格递增的。</p><p><strong>没有最好的设计方案,只有合适和不合适的方案。</strong></p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=2CnSdMqUpbqX%2BDqD3fXizA%3D%3D.5tXhCbqnX8L4faM%2F1W75GuITbzq8bIvgWCwrw3ILHH3tfnV1wCN8LAantVXH4YQ7" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=W7zeiWLkVOmIRp04CM6kqA%3D%3D.Xg9AjwORFmoGM%2FoZybNoYfv%2B1n8isg97ZhMD%2BnsXAHg%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=OdNkTiJ0mN4bwzExStZuqQ%3D%3D.IhPMYskQ2naQDd8Mi1e%2FZQUviYDIcFg4VzcDBTy7SOzO7NYB%2BaacXEIHUXAtlufr" rel="nofollow">开源编程笔记</a></p>
讲分布式唯一id,这篇文章很实在
https://segmentfault.com/a/1190000040935281
2021-11-09T22:34:00+08:00
2021-11-09T22:34:00+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<h2>分布式唯一ID介绍</h2><p>分布式系统全局唯一的 id 是所有系统都会遇到的场景,往往会被用在搜索,存储方面,用于作为唯一的标识或者排序,比如全局唯一的订单号,优惠券的券码等,如果出现两个相同的订单号,对于用户无疑将是一个巨大的bug。</p><p>在单体的系统中,生成唯一的 id 没有什么挑战,因为只有一台机器一个应用,直接使用单例加上一个原子操作自增即可。而在分布式系统中,不同的应用,不同的机房,不同的机器,要想生成的 ID 都是唯一的,确实需要下点功夫。</p><p>一句话总结:</p><blockquote><strong>分布式唯一ID是为了给数据进行唯一标识。</strong></blockquote><h3>分布式唯一ID的特征</h3><p>分布式唯一ID的核心是唯一性,其他的都是附加属性,一般来说,一个优秀的全局唯一ID方案有以下的特点,仅供参考:</p><ul><li>全局唯一:不可以重复,核心特点!</li><li>大致有序或者单调递增:自增的特性有利于搜索,排序,或者范围查询等</li><li>高性能:生成ID响应要快,延迟低</li><li>高可用:要是只能单机,挂了,全公司依赖全局唯一ID的服务,全部都不可用了,所以生成ID的服务必须高可用</li><li>方便使用:对接入者友好,能封装到开箱即用最好</li><li>信息安全:有些场景,如果连续,那么很容易被猜到,攻击也是有可能的,这得取舍。</li></ul><h2>分布式唯一ID的生成方案</h2><h3>UUID直接生成</h3><p>写过 Java 的朋友都知道,有时候我们写日志会用到一个类 UUID,会生成一个随机的ID,去作为当前用户请求记录的唯一识别码,只要用以下的代码:</p><pre><code class="java">String uuid = UUID.randomUUID();</code></pre><p>用法简单粗暴,UUID的全称其实是<code>Universally Unique IDentifier</code>,或者<code>GUID(Globally Unique IDentifier)</code>,它本质上是一个 128 位的二进制整数,通常我们会表示成为 32 个 16 进制数组成的字符串,几乎不会重复,2 的 128 次方,那是无比庞大的数字。</p><p>以下是百度百科说明:</p><blockquote><p>UUID由以下几部分的组合:</p><p>(1)UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。</p><p>(2)时钟序列。</p><p>(3)全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。</p><p>UUID的唯一缺陷在于生成的结果串会比较长。关于UUID这个标准使用最普遍的是微软的GUID(Globals Unique Identifiers)。在ColdFusion中可以用CreateUUID()函数很简单地生成UUID,其格式为:xxxxxxxx-xxxx- xxxx-xxxxxxxxxxxxxxxx(8-4-4-16),其中每个 x 是 0-9 或 a-f 范围内的一个十六进制的数字。而标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),可以从cflib 下载CreateGUID() UDF进行转换。 [2] </p><p>(4)在 hibernate(Java orm框架)中, 采用 IP-JVM启动时间-当前时间右移32位-当前时间-内部计数(8-8-4-8-4)来组成UUID</p></blockquote><p>要想重复,两台完全相同的虚拟机,开机时间一致,随机种子一致,同一时间生成uuid,才有极小的概率会重复,因此我们可认为,理论上会重复,实际不可能重复!!!</p><p>uuid优点:</p><ul><li>性能好,效率高</li><li>不用网络请求,直接本地生成</li><li>不同的机器个干个的,不会重复</li></ul><p>uuid 这么好,难不成是银弹?当然缺点也很突出:</p><ul><li>没办法保证递增趋势,没法排序</li><li>uuid太长了,存储占用空间大,特别落在数据库,对建立索引不友好</li><li>没有业务属性,这东西就是一串数字,没啥意义,或者说规律</li></ul><p>当然也有人想要改进这家伙,比如不可读性改造,用<code>uuid to int64</code>,把它转成 long 类型:</p><pre><code class="java">byte[] bytes = Guid.NewGuid().ToByteArray();
return BitConverter.ToInt64(bytes, 0);</code></pre><p>又比如,改造无序性,比如 <code>NHibernate</code> 的 <code>Comb</code> 算法,把 uuid 的前 20 个字符保留下来,后面 12 个字符用 <code>guid</code> 生成的时间,时间是大致有序的,是一种小改进。</p><p>点评:<strong>UUID不存在数据库当索引,作为一些日志,上下文的识别,还是挺香的,但是要是这玩意用来当订单号,真是令人崩溃</strong></p><h3>数据库自增序列</h3><h4>单机的数据库</h4><p>数据库的主键本身就拥有一个自增的天然特性,只要设置ID为主键并且自增,我们就可以向数据库中插入一条记录,可以返回自增的ID,比如以下的建表语句:</p><pre><code class="sql">CREATE DATABASE `test`;
use test;
CREATE TABLE id_table (
id bigint(20) unsigned NOT NULL auto_increment,
value char(10) NOT NULL default '',
PRIMARY KEY (id),
) ENGINE=MyISAM;</code></pre><p>插入语句:</p><pre><code class="sql">insert into id_table(value) VALUES ('v1');</code></pre><p>优点:</p><ul><li>单机,简单,速度也很快</li><li>天然自增,原子性</li><li>数字id排序,搜索,分页都比较有利</li></ul><p>缺点也很明显:</p><ul><li>单机,挂了就要提桶跑路了</li><li>一台机器,高并发也不可能</li></ul><h4>集群的数据库</h4><p>既然单机高并发和高可用搞不定,那就加机器,搞集群模式的数据库,既然集群模式,如果有多个master,那肯定不能每台机器自己生成自己的id,这样会导致重复的id。</p><p>这个时候,每台机器设置<strong>起始值</strong>和<strong>步长</strong>,就尤为重要。比如三台机器V1,V2,V3:</p><pre><code class="txt">统一步长:3
V1起始值:1
V2起始值:2
V3起始值:3</code></pre><p>生成的ID:</p><pre><code class="txt">V1:1, 4, 7, 10...
V2:2, 5, 8, 11...
V3:3, 6, 9, 12...</code></pre><p>设置命令行可以使用:</p><pre><code class="sql">set @@auto_increment_offset = 1; // 起始值
set @@auto_increment_increment = 3; // 步长</code></pre><p>这样确实在master足够多的情况下,高性能保证了,就算有的机器宕机了,slave 也可以补充上来,基于主从复制就可以,可以大大降低对单台机器的压力。但是这样做还是有缺点:</p><ul><li>主从复制延迟了,master宕机了,从节点切换成为主节点之后,可能会重复发号。</li><li>起始值和步长设置好之后,要是后面需要增加机器(水平拓展),要调整很麻烦,很多时候可能需要停机更新</li></ul><h4>批量号段式数据库</h4><p>上面的访问数据库太频繁了,并发量一上来,很多小概率问题都可能发生,那为什么我们不直接一次性拿出一段id呢?直接放在内存里,以供使用,用完了再申请一段就可以了。同样也可以保留集群模式的优点,每次从数据库取出一个范围的id,比如3台机器,发号:</p><pre><code class="txt">每次取1000,每台步长3000
V1:1-1000,3001-4000,
V2:1001-2000,4001-5000
V3:2001-3000,5001-6000</code></pre><p>当然,如果不搞多台机器,也是可以的,一次申请10000个号码,用乐观锁实现,加一个版本号,</p><pre><code class="sql">CREATE TABLE id_table (
id int(10) NOT NULL,
max_id bigint(20) NOT NULL COMMENT '当前最大id',
step int(20) NOT NULL COMMENT '号段的步长',
version int(20) NOT NULL COMMENT '版本号',
`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) </code></pre><p>只有用完的时候,才会重新去数据库申请,竞争的时候乐观锁保证只能一个请求成功,其他的直接等着别人取出来放在应用内存里面,再取就可以了,取的时候其实就是一个update操作:</p><pre><code class="sql">update id_table set max_id = #{max_id+step}, version = version + 1 where version = # {version}</code></pre><p>重点:</p><ul><li>批量获取,减少数据库请求</li><li>乐观锁,保证数据准确</li><li>获取只能从数据库中获取,批量获取可以做成异步定时任务,发现少于某个阈值,自动补充</li></ul><h3>Redis自增</h3><p>redis有一个原子命令<code>incr</code>,原子自增,redis速度快,基于内存:</p><pre><code class="shell">127.0.0.1:6379> set id 1
OK
127.0.0.1:6379> incr id
(integer) 2</code></pre><p>当然,redis 如果单机有问题,也可以上集群,同样可以用初始值 + 步长,可以用 <code>INCRBY</code> 命令,搞几台机器基本能抗住高并发。</p><p>优点:</p><ul><li>基于内存,速度快</li><li>天然排序,自增,有利于排序搜索</li></ul><p>缺点:</p><ul><li>步长确定之后,增加机器也比较难调整</li><li><p>需要关注持久化,可用性等,增加系统复杂度</p><p>redis持久化如果是RDB,一段时间打一个快照,那么可能会有数据没来得及被持久化到磁盘,就挂掉了,重启可能会出现重复的ID,同时要是主从延迟,主节点挂掉了,主从切换,也可能出现重复的ID。如果使用AOF,一条命令持久化一次,可能会拖慢速度,一秒钟持久化一次,那么就可能最多丢失一秒钟的数据,同时,数据恢复也会比较慢,这是一个取舍的过程。</p></li></ul><h3>Zookeeper生成唯一ID</h3><p>zookeeper其实是可以用来生成唯一ID的,但是大家不用,因为性能不高。znode有数据版本,可以生成32或者64位的序列号,这个序列号是唯一的,但是如果竞争比较大,还需要加分布式锁,不值得,效率低。</p><h3>美团的Leaf</h3><p>下面均来自美团的官方文档:<a href="https://link.segmentfault.com/?enc=fZKtTwz9u482mwwBgH7QVg%3D%3D.NbH%2FJ4iBYv%2F2plPO%2BgoWnNKF%2Fha1g9amhbTwKiMq5wSPvnRt38nGCRrFXoa6IqGinPlndpS%2BWJPjTawQBOTP4ZRRkq8gX3L8oJI%2BS4VDUXE%3D" rel="nofollow">https://tech.meituan.com/2019...</a></p><blockquote><p>Leaf在设计之初就秉承着几点要求:</p><ol><li>全局唯一,绝对不会出现重复的ID,且ID整体趋势递增。</li><li>高可用,服务完全基于分布式架构,即使MySQL宕机,也能容忍一段时间的数据库不可用。</li><li>高并发低延时,在CentOS 4C8G的虚拟机上,远程调用QPS可达5W+,TP99在1ms内。</li><li>接入简单,直接通过公司RPC服务或者HTTP调用即可接入。</li></ol></blockquote><p>文档里面讲得很清晰,一共有两个版本:</p><ul><li>V1:预分发的方式提供ID,也就是前面说的号段式分发,表设计也差不多,意思就是批量的拉取id</li></ul><p><img src="/img/remote/1460000040935283" alt="image-20211012002835752" title="image-20211012002835752"></p><p>这样做的缺点就是更新号段的时候,耗时比较高,还有就是如果这时候宕机或者主从复制,就不可用。</p><p>优化:</p><ul><li>1.先做了一个双Buffer优化,就是异步更新,意思就是搞两个号段出来,一个号段比如被消耗10%的时候,就开始分配下一个号段,有种提前分配的意思,而且异步线程更新</li><li>2.上面的方案,号段可能固定,跨度可能太大或者太小,那就做成动态变化,根据流量来决定下一次的号段的大小,动态调整</li></ul><ul><li>V2:Leaf-snowflake,Leaf提供了Java版本的实现,同时对Zookeeper生成机器号做了弱依赖处理,即使Zookeeper有问题,也不会影响服务。Leaf在第一次从Zookeeper拿取workerID后,会在本机文件系统上缓存一个workerID文件。即使ZooKeeper出现问题,同时恰好机器也在重启,也能保证服务的正常运行。这样做到了对第三方组件的弱依赖,一定程度上提高了SLA。</li></ul><h3>snowflake(雪花算法)</h3><p>snowflake 是 twitter 公司内部分布式项目采用的 ID 生成算法,开源后广受欢迎,它生成的ID是 <code>Long</code> 类型,8个字节,一共64位,从左到右:</p><ul><li>1位:不使用,二进制中最高位是为1都是负数,但是要生成的唯一ID都是正整数,所以这个1位固定为0。</li><li>41位:记录时间戳(毫秒),这个位数可以用 $(2^{41}-1) / (1000 * 60 * 60 * 24 * 365) = 69$年</li><li>10位:记录工作机器的ID,可以机器ID,也可以机房ID + 机器ID</li><li>12位:序列号,就是某个机房某台机器上这一毫秒内同时生成的 id 序号</li></ul><p>那么每台机器按照上面的逻辑去生成ID,就会是趋势递增的,因为时间在递增,而且不需要搞个分布式的,简单很多。</p><p>可以看出 snowflake 是强依赖于时间的,因为时间理论上是不断往前的,所以这一部分的位数,也是趋势递增的。但是有一个问题,是时间回拨,也就是时间突然间倒退了,可能是故障,也可能是重启之后时间获取出问题了。那我们该如何解决时间回拨问题呢?</p><ul><li>第一种方案:获取时间的时候判断,如果小于上一次的时间戳,那么就不要分配,继续循环获取时间,直到时间符合条件。</li><li>第二种方案:上面的方案只适合时钟回拨较小的,如果间隔过大,阻塞等待,肯定是不可取的,因此要么超过一定大小的回拨直接报错,拒绝服务,或者有一种方案是利用拓展位,回拨之后在拓展位上加1就可以了,这样ID依然可以保持唯一。</li></ul><p>Java代码实现:</p><pre><code class="java">public class SnowFlake {
// 数据中心(机房) id
private long datacenterId;
// 机器ID
private long workerId;
// 同一时间的序列
private long sequence;
public SnowFlake(long workerId, long datacenterId) {
this(workerId, datacenterId, 0);
}
public SnowFlake(long workerId, long datacenterId, long sequence) {
// 合法判断
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
// 开始时间戳
private long twepoch = 1420041600000L;
// 机房号,的ID所占的位数 5个bit 最大:11111(2进制)--> 31(10进制)
private long datacenterIdBits = 5L;
// 机器ID所占的位数 5个bit 最大:11111(2进制)--> 31(10进制)
private long workerIdBits = 5L;
// 5 bit最多只能有31个数字,就是说机器id最多只能是32以内
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 5 bit最多只能有31个数字,机房id最多只能是32以内
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 同一时间的序列所占的位数 12个bit 111111111111 = 4095 最多就是同一毫秒生成4096个
private long sequenceBits = 12L;
// workerId的偏移量
private long workerIdShift = sequenceBits;
// datacenterId的偏移量
private long datacenterIdShift = sequenceBits + workerIdBits;
// timestampLeft的偏移量
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
// 序列号掩码 4095 (0b111111111111=0xfff=4095)
// 用于序号的与运算,保证序号最大值在0-4095之间
private long sequenceMask = -1L ^ (-1L << sequenceBits);
// 最近一次时间戳
private long lastTimestamp = -1L;
// 获取机器ID
public long getWorkerId() {
return workerId;
}
// 获取机房ID
public long getDatacenterId() {
return datacenterId;
}
// 获取最新一次获取的时间戳
public long getLastTimestamp() {
return lastTimestamp;
}
// 获取下一个随机的ID
public synchronized long nextId() {
// 获取当前时间戳,单位毫秒
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
// 去重
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
// sequence序列大于4095
if (sequence == 0) {
// 调用到下一个时间戳的方法
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 如果是当前时间的第一次获取,那么就置为0
sequence = 0;
}
// 记录上一次的时间戳
lastTimestamp = timestamp;
// 偏移计算
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
private long tilNextMillis(long lastTimestamp) {
// 获取最新时间戳
long timestamp = timeGen();
// 如果发现最新的时间戳小于或者等于序列号已经超4095的那个时间戳
while (timestamp <= lastTimestamp) {
// 不符合则继续
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowFlake worker = new SnowFlake(1, 1);
long timer = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
worker.nextId();
}
System.out.println(System.currentTimeMillis());
System.out.println(System.currentTimeMillis() - timer);
}
}
</code></pre><h3>百度 uid-generator</h3><p>换汤不换药,百度开发的,基于<code>Snowflake</code>算法,不同的地方是可以自己定义每部分的位数,也做了不少优化和拓展:<a href="https://link.segmentfault.com/?enc=Wos9UYWscpE5RlmFRkr2Mg%3D%3D.hd7Ma7U%2F8BXD%2B4SX5zY4dh%2FEa1m6KXdet1llFpikqwNVtLaZJ7%2B1l0NHeCbylRb4CN3zUSrfxaoGhTo%2BhC1oenqzzjmnDVmGVEvkqZMn0zI%3D" rel="nofollow">https://github.com/baidu/uid-...</a></p><blockquote>UidGenerator是Java实现的, 基于<a href="https://link.segmentfault.com/?enc=LWbYnL%2BviHA2pM7nZ0d3fA%3D%3D.EX%2Bil3gme%2B1aO7%2BAY8BFIV8wUFLsIY3smso1Nvn6BEBi%2Bm2sNi5%2BpCLC%2FHF%2FWRx2" rel="nofollow">Snowflake</a>算法的唯一ID生成器。UidGenerator以组件形式工作在应用项目中, 支持自定义workerId位数和初始化策略, 从而适用于<a href="https://link.segmentfault.com/?enc=JJE0bPh%2BV1f7J49nU8CMlQ%3D%3D.dbMIi4NtcEXHI3XA63B%2FaKUBFcEXLwn8olIhHXljwWY%3D" rel="nofollow">docker</a>等虚拟化环境下实例自动重启、漂移等场景。 在实现上, UidGenerator通过借用未来时间来解决sequence天然存在的并发限制; 采用RingBuffer来缓存已生成的UID, 并行化UID的生产和消费, 同时对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题. 最终单机QPS可达600万。</blockquote><h2>秦怀の观点</h2><p>不管哪一种uid生成器,保证唯一性是核心,在这个核心上才能去考虑其他的性能,或者高可用等问题,总体的方案分为两种:</p><ul><li><p>中心化:第三方的一个中心,比如 Mysql,Redis,Zookeeper</p><ul><li>优点:趋势自增</li><li>缺点:增加复杂度,一般得集群,提前约定步长之类</li></ul></li><li><p>无中心化:直接本地机器上生成,snowflake,uuid</p><ul><li>优点:简单,高效,没有性能瓶颈</li><li>缺点:数据比较长,自增属性较弱</li></ul></li></ul><p>没有哪一种是完美的,只有符合业务以及当前体量的方案,技术方案里面,没有最优解。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=aeuuzw4ko5dwIJESRZB4%2BQ%3D%3D.aC5dCDjxV%2FDGz5KztFF5ZLCOpyH9VEHksp4k%2FIbkDxhzGjl%2FZ%2FW3dVC5qi3UhZiD" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=XwAIowvlgwQu81ZNDsdRBg%3D%3D.UDWRPGVQgm%2BqEM4DOZ8%2Bpewq%2BMrbclz%2BNQ4Y2wP%2BEUA%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=Q43Zoc572pK01js08dRUEg%3D%3D.ERgF4UnjCW9kdlLrZct5NWpbMBzCGuxGzZPdLR4MZ0UAeeFQoUmHxu%2FjLmJ2ulu%2F" rel="nofollow">开源编程笔记</a></p>
设计模式【3.3】-- CGLIB动态代理源码解读
https://segmentfault.com/a/1190000040928979
2021-11-08T22:42:34+08:00
2021-11-08T22:42:34+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
1
<p><img src="https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210925233820.png" width = "500" height = "400" alt="图片名称" align=center /></p><h2>cglib 动态代理</h2><h3>cglib介绍</h3><p>CGLIB 是一个开源项目,一个强大高性能高质量的代码生成库,可以在运行期拓展 Java 类,实现 Java 接口等等。底层是使用一个小而快的字节码处理框架 ASM,从而转换字节码和生成新的类。</p><p>理论上我们也可以直接用 ASM 来直接生成代码,但是要求我们对 JVM 内部,class 文件格式,以及字节码的指令集都很熟悉。</p><p><strong>这玩意不在 JDK 的包里面,需要自己下载导入或者 Maven 坐标导入。</strong></p><p>我选择 <code>Maven</code> 导入, 加到 <code>pom.xml</code> 文件:</p><pre><code class="xml"><dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
</dependencies>
</code></pre><p><code>Student.java</code>:</p><pre><code class="java">public class Student {
public void learn() {
System.out.println("我是学生,我想学习");
}
}</code></pre><p><code>MyProxy.java</code>(代理类)</p><pre><code class="java">
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class StudentProxy implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// TODO Auto-generated method stub
System.out.println("代理前 -------");
proxy.invokeSuper(obj, args);
System.out.println("代理后 -------");
return null;
}
}
</code></pre><p>测试类(<code>Test.java</code>)</p><pre><code class="java">
import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;
public class Test {
public static void main(String[] args) {
// 代理类class文件存入本地磁盘方便我们反编译查看源码
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/aphysia/Desktop");
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Student.class);
enhancer.setCallback(new StudentProxy());
Student student = (Student) enhancer.create();
student.learn();
}
}
</code></pre><p>运行之后的结果是:</p><pre><code class="shell">CGLIB debugging enabled, writing to '/Users/xuwenhao/Desktop'
代理前 -------
我是学生,我想学习
代理后 -------</code></pre><p>在我们选择的文件夹里面,生成了代理类的代码:</p><p><img src="/img/remote/1460000040928981" alt="" title=""></p><h3>源码分析</h3><p>我们先要代理的类,需要实现<code>MethodInterceptor</code>(方法拦截器)接口,这个接口只有一个方法 <code>intercept</code>,参数分别是:</p><ul><li>obj:需要增强的对象</li><li>method:需要拦截的方法</li><li>args:要被拦截的方法参数</li><li>proxy:表示要触发父类的方法对象</li></ul><pre><code class="java">package net.sf.cglib.proxy;
import java.lang.reflect.Method;
public interface MethodInterceptor extends Callback {
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
MethodProxy proxy) throws Throwable;
}</code></pre><p>再看回我们要创建代理类的方法 <code>enhancer.create()</code>,这个方法的意思:如果需要,生成一个新类,并使用指定的回调(如果有的话)来创建一个新的对象实例。使用超类的无参数构造函数。</p><pre><code class="java"> public Object create() {
classOnly = false;
argumentTypes = null;
return createHelper();
}</code></pre><p>主要的方法逻辑我们得看 <code>createHelper()</code>,除了校验,就是调用 <code>KEY_FACTORY.newInstance()</code> 方法生成 <code>EnhancerKey</code>对象,<code>KEY_FACTORY</code> 是静态 <code>EnhancerKey</code> 接口,<code>newInstance()</code>是接口里面的一个方法,重点在<code>super.create(key)</code>里面,调用的是父类的方法:</p><pre><code class="java"> private Object createHelper() {
// 校验
preValidate();
Object key = KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null,
ReflectUtils.getNames(interfaces),
filter == ALL_ZERO ? null : new WeakCacheKey<CallbackFilter>(filter),
callbackTypes,
useFactory,
interceptDuringConstruction,
serialVersionUID);
this.currentKey = key;
Object result = super.create(key);
return result;
}</code></pre><p><code>AbstractClassGenerator</code> 是 <code>Enhancer</code> 的父类,<code>create(key)</code> 方法的主要逻辑是获取类加载器,缓存获取类加载数据,然后再反射构造对象,里面有两个创造实例对象的方法:</p><ul><li><code>fistInstance()</code>: 不应该在常规流中调用此方法。从技术上讲,<code>{@link #wrapCachedClass(Class)}</code>使用<code>{@link EnhancerFactoryData}</code>作为缓存值,后者支持比普通的旧反射查找和调用更快的实例化。出于向后兼容性的原因,这个方法保持不变:只是以防它曾经被使用过。(我的理解是目前的逻辑不会走到这个分支,因为它比较忙,但是为了兼容,这个case还保存着),内部逻辑其实用的是<code>ReflectUtils.newInstance(type)</code>。</li><li><code>nextInstance()</code>: 真正的创建代理对象的类</li></ul><pre><code class="java"> protected Object create(Object key) {
try {
ClassLoader loader = getClassLoader();
Map<ClassLoader, ClassLoaderData> cache = CACHE;
ClassLoaderData data = cache.get(loader);
if (data == null) {
synchronized (AbstractClassGenerator.class) {
cache = CACHE;
data = cache.get(loader);
if (data == null) {
Map<ClassLoader, ClassLoaderData> newCache = new WeakHashMap<ClassLoader, ClassLoaderData>(cache);
data = new ClassLoaderData(loader);
newCache.put(loader, data);
CACHE = newCache;
}
}
}
this.key = key;
Object obj = data.get(this, getUseCache());
if (obj instanceof Class) {
return firstInstance((Class) obj);
}
// 真正创建对象的方法
return nextInstance(obj);
} catch (RuntimeException e) {
throw e;
} catch (Error e) {
throw e;
} catch (Exception e) {
throw new CodeGenerationException(e);
}
}</code></pre><p>这个方法定义在<code>AbstractClassGenerator</code>,但是实际上是调用子类 <code>Enhancer</code>的实现,主要是通过获取参数类型,参数,以及回调对象,用这些参数反射生成代理对象。</p><pre><code class="java"> protected Object nextInstance(Object instance) {
EnhancerFactoryData data = (EnhancerFactoryData) instance;
if (classOnly) {
return data.generatedClass;
}
Class[] argumentTypes = this.argumentTypes;
Object[] arguments = this.arguments;
if (argumentTypes == null) {
argumentTypes = Constants.EMPTY_CLASS_ARRAY;
arguments = null;
}
// 构造
return data.newInstance(argumentTypes, arguments, callbacks);
}</code></pre><p>内部实现逻辑,调用的都是<code>ReflectUtils.newInstance()</code>, 参数种类不一样:</p><pre><code class="java"> public Object newInstance(Class[] argumentTypes, Object[] arguments, Callback[] callbacks) {
setThreadCallbacks(callbacks);
try {
if (primaryConstructorArgTypes == argumentTypes ||
Arrays.equals(primaryConstructorArgTypes, argumentTypes)) {
return ReflectUtils.newInstance(primaryConstructor, arguments);
}
return ReflectUtils.newInstance(generatedClass, argumentTypes, arguments);
} finally {
setThreadCallbacks(null);
}
}</code></pre><p>跟进去到底,就是获取构造器方法,反射方式构造代理对象,最终调用到的是 JDK 提供的方法:</p><pre><code class="java"> public static Object newInstance(Class type, Class[] parameterTypes, Object[] args) {
return newInstance(getConstructor(type, parameterTypes), args);
}
public static Object newInstance(final Constructor cstruct, final Object[] args) {
boolean flag = cstruct.isAccessible();
try {
if (!flag) {
cstruct.setAccessible(true);
}
Object result = cstruct.newInstance(args);
return result;
} catch (InstantiationException e) {
throw new CodeGenerationException(e);
} catch (IllegalAccessException e) {
throw new CodeGenerationException(e);
} catch (InvocationTargetException e) {
throw new CodeGenerationException(e.getTargetException());
} finally {
if (!flag) {
cstruct.setAccessible(flag);
}
}
}</code></pre><p>打开它自动生成的代理类文件看看,就会发现其实也是生成那些方法,加上了一些增强方法:</p><p><img src="/img/remote/1460000040928982" alt="" title=""></p><p>生成的代理类继承了原来的类:</p><pre><code class="java">public class Student$$EnhancerByCGLIB$$929cb5fe extends Student implements Factory {
...
}</code></pre><p>看看生成的增强方法,其实是调用到 <code>intercept()</code>方法,这个方法由我们前面自己实现,因此就完成了代理对象增强的功能:</p><pre><code class="java"> public final void learn() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
var10000.intercept(this, CGLIB$learn$0$Method, CGLIB$emptyArgs, CGLIB$learn$0$Proxy);
} else {
super.learn();
}
}</code></pre><h2>cglib 和 jdk 动态代理有什么区别</h2><ol><li>jdk 动态代理是利用拦截器加上反射生成了一个代理接口的匿名类,执行方法的时候交给 InvokeHandler 处理。CGLIB 动态代理是使用了 ASM框架,修改原来的字节码,然后生成新的子类来处理。</li><li>JDK 代理需要实现接口,但是CGLIB不强制。</li><li>在JDK1.6之前,cglib因为用了字节码生成技术,比反射效率高,但是之后jdk也进行了一些优化,效率上已经提升了。</li></ol><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=NKB3D75qZxTNe%2BFoIAkz8Q%3D%3D.9VVMNBo8OoYJSgpaIoll8HaGT2Sbnavqk6QK%2BI6Go2qWCPiFPWmfOYQUrb7kX9dh" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=rwrkKk6%2B8xCuwHOOf0zyEg%3D%3D.nV%2BCRx7sC78r6WAGS2%2FQvOuWlqL%2FDn4FBKjhP0bjPKw%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=a7Wd9h7wIf72pwGxbcwQ%2FA%3D%3D.0XfOPjrOfqFDJPerKzG07NmmUPrOHGb9cHoLyb7k3BvGTGX2oHZ22Go3s1bkEb0q" rel="nofollow">开源编程笔记</a></p>
设计模式【3.2】-- JDK动态代理源码分析有多香?
https://segmentfault.com/a/1190000040920958
2021-11-06T21:05:00+08:00
2021-11-06T21:05:00+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>前面文章有说到代理模式:<a href="https://link.segmentfault.com/?enc=vNn3gaQZWqJw1EA1TrN4TQ%3D%3D.GQv2CbOl%2FbumOsM%2BF50gvXStDmHQ3tasDbX3aiPppFiqXLQhJrnWNxF3UpJ61swQJnn%2Bc3PmCdkPNBoFjPonmA%3D%3D" rel="nofollow">http://aphysia.cn/archives/dy...</a></p><p>那么回顾一下,代理模式怎么来的?假设有个需求:</p><blockquote>在系统中所有的 <code>controller</code> 类调用方法之前以及之后,打印一下日志。</blockquote><p>假设原来的代码:</p><pre><code class="java">public class Person{
public void method(){
// 表示自己的业务逻辑
process();
}
}</code></pre><p>如果在所有的类里面都添加打印方法,这样肯定是不现实的,如果我有几百个这样的类,写到崩溃,况且重复代码太多,冗余,还耦合到一块了,要是我下次不打日志了,做其他的,那几百个类又全部改一遍。</p><pre><code class="java">public class Person{
public void method(){
log();
// 表示自己的业务逻辑
process();
log();
}
}</code></pre><h2>静态代理</h2><p>怎么样写比较优美呢?<strong>静态代理</strong> 这时候出场了,先把方法抽象成为接口:</p><pre><code class="java">public class IProxy(){
public void method();
}</code></pre><p>让具体的类去实现 <code>IProxy</code>,写自己的业务逻辑,比如:</p><pre><code class="java">public class Person implements IProxy(){
public void method(){
// 表示自己的业务逻辑
process();
}
}</code></pre><p>然后弄个代理类,对方法进行增强:</p><pre><code class="java">public class PersonProxy implements IProxy{
private IProxy target;
public PersonProxy(IProxy target){
this.target = target;
}
@Override
public void method() {
log();
target.method();
log();
}
}</code></pre><p>调用的时候,把真实的对象放到代理类的构造器里面,就可以得到一个代理类,对它的方法进行增强,好处就是,如果下次我要改,不打日志,做其他事情,我改代理类就可以了,不用到处改我的目标类的方法,而坏处还是很明显,要增强哪一个类,就要为它写一个代理类,这样好像不是很合理。</p><h2>动态代理</h2><p><strong>怎么样能让他自动生成代理对象呢?</strong> 动态代理做的就是这个事情,它可以 <strong>动态</strong> 的根据我们提供的类,生成代理类的对象。</p><p>最主要的,是在运行时,动态生成,只要传入我们要代理增强的类相关的信息,比如类对象本身,类加载器,类的接口等,就可以生成,不用提前知道它是 A 类,B 类还是 C 类。</p><p>动态代理主要有三种实现方法,今天我们重点分析 JDK 动态代理:</p><ul><li>JDK 代理:使用 JDK 提供的官方的 Proxy</li><li>第三方 CGlib 代理:使用 CGLib 的 Enhancer 类创建代理对象</li><li>javassit:Javassist 是一个开源的分析、编辑和创建 Java 字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。</li></ul><h3>JDK 动态代理</h3><h4>使用步骤</h4><ol><li>新建一个接口</li><li>新建一个类,实现该接口</li><li>创建代理类,实现 <code>java.lang.reflect.InvocationHandler</code> 接口</li></ol><p>代码如下:</p><p><code>IPlayDao.java</code>(玩的接口)</p><pre><code class="java">public interface IPlayDao {
void play();
}</code></pre><p><code>StudentDao.java</code>(实现了买东西,玩的接口的学生类)</p><pre><code class="java">public class StudentDao implements IPlayDao {
@Override
public void play() {
System.out.println("我是学生,我想出去玩");
}
}
</code></pre><p>MyProxy.java 代理类:</p><pre><code class="java">import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class MyProxy {
private Object target;
public MyProxy(Object target){
this.target=target;
}
public Object getProxyInstance(){
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
// 一个接口可能很多方法,要是需要针对某一个方法,那么需要在函数里判断 method
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始事务 2");
// 执行目标对象方法
Object returnValue = method.invoke(target, args);
System.out.println("提交事务 2");
return returnValue;
}
}
);
}
}</code></pre><p>测试类(Test.java)</p><pre><code class="java">public class Test {
public static void main(String [] args){
// 保存生成代理类的字节码文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
IPlayDao target =new StudentDao();
System.out.println(target.getClass());
IPlayDao proxy = (IPlayDao) new MyProxy(target).getProxyInstance();
System.out.println(proxy.getClass());
// 执行方法 【代理对象】
proxy.play();
}
}
由于加了这句代码,我们可以把生成的代理类的字节码文件保存下来, 其实通过输出也可以看到,两个对象不是同一个类,代理类是动态生成的:
</code></pre><p>System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");</p><pre><code>
![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210924001916.png)
### 源码分析
跟着源码一步步看,先从调用的地方 `Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)`:
![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210924002757.png)
进入方法里面,**省略各种异常处理**,主要剩下了**生成代理类字节码** 以及 **通过构造函数反射构造新对象**:</code></pre><pre><code>public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
// 判空
Objects.requireNonNull(h);
// 安全检查
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* 查找或者生成代理对象
*/
Class<?> cl = getProxyClass0(loader, intfs);
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
// 获取构造器
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
// 反射构造代理对象
return cons.newInstance(new Object[]{h});
}</code></pre><pre><code>
上面注释里面说查找或者生成代理对象,为什么有查找?因为并不是每一次都生成,生成的代理对象实际上会缓存起来,如果没有,才会生成,看源码 `Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces)`:
</code></pre><pre><code>private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length> 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// 调用缓存代理类的 cache 来获取类加载器
return proxyClassCache.get(loader, interfaces);
}</code></pre><pre><code>
如果由实现给定接口的给定加载器定义的代理类存在,这将简单地返回缓存的副本; 否则,它将通过 ProxyClassFactory 创建代理类,`proxyClassCache` 其实就是个 `weakCache`:</code></pre><p>private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());</p><pre><code>
初始化的时候,proxyClassCache 指定了两个属性,一个是 `KeyFactory`, 另外一个是 `ProxyClassFactory`, 从名字就是猜到 `ProxyClassFactory` 是代理类工厂:
</code></pre><pre><code>public WeakCache(BiFunction<K, P, ?> subKeyFactory,
BiFunction<K, P, V> valueFactory) {
this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
this.valueFactory = Objects.requireNonNull(valueFactory);
}</code></pre><pre><code>**记住这里的 subKeyFactory,实际上就是传入的 ProxyClassFactory**,那前面 `proxyClassCache.get(loader, interfaces);` 到底是怎么操作的?
![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210924010200.png)
上面调用到了 `subKeyFactory.apply(key, parameter)`,这个 `subKeyFactory` 实际上是我们传的 `ProxyClassFactory`, 进入里面去看:
</code></pre><pre><code>private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
// 生成的代理类前缀
private static final String proxyClassNamePrefix = "$Proxy";
// 下一个生成的代理类的名字的计数器,一般是 $Proxy0,$Proxy1
private static final AtomicLong nextUniqueNumber = new AtomicLong();
@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class<?> intf : interfaces) {
/*
* 检验类加载器是否能通过接口名称加载该类
*/
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + "is not visible from class loader");
}
/*
* 判断接口类型
*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + "is not an interface");
}
/*
* 判断是否重复
*/
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface:" + interfaceClass.getName());
}
}
// 代理包名字
String proxyPkg = null;
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
/*
* 记录非公共代理接口的包,以便在同一个包中定义代理类。确认所有非公共代理接口都在同一个包中。
*/
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}
if (proxyPkg == null) {
// 如果没有非公共代理接口,请使用 com.sun.proxy 包
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
/*
* 为要生成的代理类选择一个名称。
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
/*
* 生成指定的代理类。(这里是重点!!!)
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* 这里的 ClassFormatError 意味着 (禁止代理类生成代码中的错误) 提供给代理类创建的参数有其他一些无效方面 (比如超出了虚拟机限制)。
*/
throw new IllegalArgumentException(e.toString());
}
}
}
</code></pre><pre><code>
上面调用一个方法生成代理类,我们看看 IDEA 反编译的代码:</code></pre><pre><code>public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) {
ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
// 生成文件
final byte[] var4 = var3.generateClassFile();
// 判断是否要写入磁盘!!!
if (saveGeneratedFiles) {
// 开启高权限写入
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
try {
int var1 = var0.lastIndexOf(46);
Path var2;
if (var1> 0) {
Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar));
Files.createDirectories(var3);
var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
} else {
var2 = Paths.get(var0 + ".class");
}
Files.write(var2, var4, new OpenOption[0]);
return null;
} catch (IOException var4x) {
throw new InternalError("I/O exception saving generated file:" + var4x);
}
}
});
}
return var4;
}</code></pre><pre><code>生成代理文件实际上和我们想的差不多,就是一些 hashCode(),toString(),equals(), 原方法,代理方法等:
![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210924011447.png)
这与我们之前看到的文件一致:
![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210924011628.png)
然后之所以我们代码要设置写入磁盘,是因为这个变量, 控制了写磁盘操作:
</code></pre><pre><code>private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));</code></pre><pre><code>
**为什么只支持接口实现,不支持继承普通类?**
因为代理类继承了 Proxy 类,并且实现了接口,Java 不允许多继承,所以不能代理普通类的方式,并且在静态代码块里面,用反射方式获取了所有的代理方法。
JDK 代理看起来像是个黑盒,实际上,每一句代码,都有其缘由。其实本质上也是动态的为我们的原始类,动态生成代理类。
生成的代理类里面其实对原始类进行增强(比如 `play()` 方法)的时候, 调用了 `super.h.invok()` 方法,其实这里的 `h` 是什么呢?
`h` 是父类的 `h`, 生成的代理类的父类是 `Proxy`,Proxy 的 `h`,就是我们传入的 `InvocationHandler`:
</code></pre><pre><code>public final void play() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}</code></pre><pre><code>
生成的代码里面通过反射调用到的其实是我们自己重写的那部分逻辑,所以就有了增强的功能,不得不说,这种设计确实巧妙:
![](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20210925020135.png)
## 动态代理有多香
动态代理是Java语言里面的一个很强大的特性,可以用来做一些切面,比如拦截器,登录验证等等,但是它并不是独立存在的,任何一个知识点都不能独立说明语言的强大,重要的是它们的组合。
动态代理要实现强大的功能,一般需要和反射,注解等一起合作,比如对某些请求进行拦截,拦截后做一些登录验证,或者日志功能。最重要的一点,它能够在减少耦合度的前提下实现增强。
**【作者简介】**:
秦怀,公众号【**秦怀杂货店**】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:`Java源码解析`,`JDBC`,`Mybatis`,`Spring`,`redis`,`分布式`,`剑指Offer`,`LeetCode`等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。
[剑指Offer全部题解PDF](http://aphysia.cn/archives/jianzhiofferpdf)
[2020年我写了什么?](http://aphysia.cn/archives/2020)
[开源编程笔记](https://damaer.github.io/Coding/#/)
</code></pre>
马拉车算法,其实并不难!!!
https://segmentfault.com/a/1190000040789504
2021-10-10T18:30:46+08:00
2021-10-10T18:30:46+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
1
<p>要说马拉车算法,必须说说这道题,查找最长回文子串,马拉车算法是其中一种解法,狠人话不多,直接往下看:</p><h2>题目描述</h2><p>给你一个字符串 s,找到 s 中最长的回文子串。</p><h2>例子</h2><pre><code class="txt">示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
示例 3:
输入:s = "a"
输出:"a"
示例 4:
输入:s = "ac"
输出:"a"</code></pre><h2>马拉车算法</h2><p>这是一个奇妙的算法,是1957年一个叫Manacher的人发明的,所以叫<code>Manacher‘s Algorithm</code>,主要是用来查找一个字符串的最长回文子串,这个算法最大的贡献是将时间复杂度提升到线性,前面我们说的动态规划的时间复杂度为 O(n<sup>2</sup>)。</p><p>前面说的中心拓展法,中心可能是字符也可能是字符的间隙,这样如果有 n 个字符,就有 <code>n+n+1</code> 个中心:</p><p><img src="/img/remote/1460000040789506" alt="" title=""></p><p>为了解决上面说的中心可能是间隙的问题,我们往每个字符间隙插入”<code>#</code>“,为了让拓展结束边界更加清晰,左边的边界插入”<code>^</code>“,右边的边界插入 "<code>$</code>":</p><p><img src="/img/remote/1460000040789507" alt="" title=""></p><p><code>S</code> 表示插入"<code>#</code>","<code>^</code>","<code>$</code>"等符号之后的字符串,我们用一个数组<code>P</code>表示<code>S</code>中每一个字符能够往两边拓展的长度:</p><p><img src="/img/remote/1460000040789508" alt="" title=""></p><p>比如 <code>P[8] = 3</code>,表示可以往两边分别拓展3个字符,<strong>也就是回文串的长度为 3</strong>,去掉 <code>#</code> 之后的字符串为<code>aca</code>:</p><p><img src="/img/remote/1460000040789509" alt="" title=""></p><p><code>P[11]= 4</code>,表示可以往两边分别拓展4个字符,<strong>也就是回文串的长度为 4</strong>,去掉 <code>#</code> 之后的字符串为<code>caac</code>:</p><p><img src="/img/remote/1460000040789510" alt="" title=""></p><p><strong>假设我们已经得知数组P,那么我们怎么得到回文串?</strong></p><p>用 <code>P</code> 的下标 <code>index</code> ,减去<code> P[i]</code>(也就是回文串的长度),可以得到回文串开头字符在拓展后的字符串 <code>S</code> 中的下标,除以2,就可以得到在原字符串中的下标了。</p><p>那么现在的问题是:<strong>如何求解数组P[i]</strong></p><p>其实,马拉车算法的关键是:<strong>它充分利用了回文串的对称性,用已有的结果来帮助计算后续的结果。</strong></p><p>假设已经计算出字符索引位置 P 的最大回文串,左边界是P<sub>L</sub>,右边界是P<sub>R</sub>:</p><p><img src="/img/remote/1460000040789511" alt="" title=""></p><p>那么当我们求因为一个位置 <code>i</code> 的时候,<code>i</code> 小于等于 P<sub>R</sub>,其实我们可以找到 <code>i</code> 关于 <code>P</code> 的对称点 <code>j</code>:</p><p><img src="/img/remote/1460000040789512" alt="" title=""></p><p>那么假设 j 为中心的最长回文串长度为 len,并且在 P<sub>L</sub> 到 P 的范围内,则 i 为中心的最长回文串也是如此:</p><p><strong>以 i 为中心的最长回文子串长度等于以 j 为中心的最长回文子串的长度</strong></p><p><img src="/img/remote/1460000040789513" alt="" title=""></p><p>但是这里有两个问题:</p><ul><li>前一个回文字符串P,是哪一个?</li><li>有哪些特殊情况?特殊情况怎么处理?</li></ul><p>(1) 前一个回文字符串 <code>P</code>,是指的前面计算出来的<strong>右边界最靠右的回文串</strong>,因为这样它最可能覆盖我们现在要计算的 i 为中心的索引,可以尽量重用之前的结果的对称性。</p><p>也正因为如此,我们在计算的时候,需要不断保存更新 P 的中心和右边界,用于每一次计算。</p><p>(2) 特殊情况其实就是当前 i 的最长回文字符串计算不能再利用 P 点的对称,例如:</p><ol><li>以 <code>i</code> 的回文串的右边界超出了 <code>P</code> 的右边界 P<sub>R</sub>:</li></ol><p><img src="/img/remote/1460000040789514" alt="" title=""></p><p>这种情况的解决方案是:超过的部分,需要按照中心拓展法来一一拓展。</p><ol start="2"><li><code>i</code> 不在 以 <code>P</code> 为中心的回文串里面,只能按照中心拓展法来处理。</li></ol><p><img src="/img/remote/1460000040789515" alt="" title=""></p><p>具体的代码实现如下:</p><pre><code class="java"> // 构造字符串
public String preProcess(String s) {
int n = s.length();
if (n == 0) {
return "^$";
}
String ret = "^";
for (int i = 0; i < n; i++)
ret = ret + "#" + s.charAt(i);
ret = ret + "#$";
return ret;
}
// 马拉车算法
public String longestPalindrome(String str) {
String S = preProcess(str);
int n = S.length();
// 保存回文串的长度
int[] P = new int[n];
// 保存边界最右的回文中心以及右边界
int center = 0, right = 0;
// 从第 1 个字符开始
for (int i = 1; i < n - 1; i++) {
// 找出i关于前面中心的对称
int mirror = 2 * center - i;
if (right > i) {
// i 在右边界的范围内,看看i的对称点的回文串长度,以及i到右边界的长度,取两个较小的那个
// 不能溢出之前的边界,否则就得中心拓展
P[i] = Math.min(right - i, P[mirror]);
} else {
// 超过范围了,中心拓展
P[i] = 0;
}
// 中心拓展
while (S.charAt(i + 1 + P[i]) == S.charAt(i - 1 - P[i])) {
P[i]++;
}
// 看看新的索引是不是比之前保存的最右边界的回文串还要靠右
if (i + P[i] > right) {
// 更新中心
center = i;
// 更新右边界
right = i + P[i];
}
}
// 通过回文长度数组找出最长的回文串
int maxLen = 0;
int centerIndex = 0;
for (int i = 1; i < n - 1; i++) {
if (P[i] > maxLen) {
maxLen = P[i];
centerIndex = i;
}
}
int start = (centerIndex - maxLen) / 2;
return str.substring(start, start + maxLen);
}</code></pre><p>至于算法的复杂度,空间复杂度借助了大小为n的数组,为O(n),而时间复杂度,看似是用了两层循环,实则不是 O(n<sup>2</sup>),而是 <code>O(n)</code>,因为绝大多数索引位置会直接利用前面的结果以及对称性获得结果,常数次就可以得到结果,而那些需要中心拓展的,是因为超出前面结果覆盖的范围,才需要拓展,拓展所得的结果,有利于下一个索引位置的计算,因此拓展实际上较少。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=k83infEdBDxC6QS%2FY5F3kQ%3D%3D.3N3RkDQIiM6ULfQdjO524oDzcy7eez5WJ8TAt%2Bu0I5c4d6GTmQWhfes%2FJY5l5VCC" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=C%2BUyKfJiyQoWYphLb%2FNcgw%3D%3D.WHI2stmSAxT%2Fuf3VMB79lL28DN5HWuXGCL4pyK0Q9II%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=lEXlMebSpDlm8ik3NEjxeg%3D%3D.L35UZjA7hiDzSWq%2FJYk8WXWfsOtGj%2BglA55DFk3OlNl0UV%2FYTQgdrt%2FDVl7yKEDV" rel="nofollow">开源编程笔记</a></p>
100台机器上海量IP如何查找出现频率 Top 100?
https://segmentfault.com/a/1190000040782132
2021-10-09T00:04:40+08:00
2021-10-09T00:04:40+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<h2>场景题</h2><blockquote>有 100 机器,每个机器的磁盘特别大,磁盘大小为 1T,但是内存大小只有 4G,现在每台机器上都产生了很多 ip 日志文件,每个文件假设有50G,那么如果计算出这 100 太机器上访问量最多的 100 ip 呢?也就是Top 100。</blockquote><h2>思路</h2><p>其实,一开始我有往布隆过滤器那边考虑,但是布隆过滤器只能大致的判断一个 ip 是否已经存在,而不能去统计数量,不符合该场景。</p><p>那么一般这种大数据的问题,都是因为一次不能完全加载到内存,因此需要拆分,那怎么拆呢?ip是32位的,也就是最多就 2<sup>32</sup> 个, 常见的拆分方法都是 <code>哈希</code>:</p><ul><li>把大文件通过哈希算法分配到不同的机器</li><li>把大文件通过哈希算法分配到不同的小文件</li></ul><p>上面所说,一台机器的内存肯定不能把所有的 ip 全部加载进去,必须在不同机器上先 hash 区分,先看每台机器上,50G 文件,假设我们分成 100 个小文件,那么平均每个就500M,使用 Hash 函数将所有的 ip 分流到不同的文件中。</p><p>这个时候相同的 ip 一定在相同的文件中,当然不能排除数据全部倾斜于一个文件的情况,也就是虽然 hash了,但是由于个别ip或者hash值相同的ip太多了,都分到了个别文件上,那么这个时候分流后的文件依旧很大。这种情况我能想到的就是要是文件还是很大,需要再hash,如果基本属于同一个ip,那么这个时候就可以分批次读取,比如一次只读 1G 到内存。</p><p>在处理每个小文件时,使用 HashMap 来统计每个 ip 出现的频率,统计完成后,遍历,用最小根堆,获取出现频率最大的100个ip。这个时候,每个小文件都获取到了出现频率最大的100个 ip,然后每个文件的 Top 100 个ip 再进行==排序==即可(每个文件的top100 都是不一样的,因为前面进行 hash 之后保证相同的 ip 只会落到同一个文件里)。这样就可以得到每台机器上的 Top 100。</p><p>不同机器的 Top 100 再进行 <code>加和</code> 并 <code>排序</code>,就可以得到Top 100 的ip。</p><p>为什么加和? 因为不同机器上可能存在同样的ip,前面的hash操作只是确保同一个机器的不同文件里面的ip一定不一样。</p><p><strong>但是上面的操作有什么瑕疵么?当然有!</strong></p><p>假设我们又两台机器,有一台机器 <code>C1</code> 的top 100 的ip是 <code>192.128.1.1</code>,top 101 是 <code>192.128.1.2</code>,那么就可能存在另一台机器 <code>C2</code> 上 <code>192.128.1.1</code> 可能从来没有出现过,但是 <code>192.128.1.2</code> 却也排在 top 101,其实总数上 <code>192.128.1.2</code> 是超过<code>192.128.1.1</code>,但是很不幸的是,我们每台机器只保存了 top100,所以它在计算过程中被淘汰了,导致结果不准确。</p><p><strong>解决方案:</strong></p><p>先用 hash 算法,把 ip 按照 hash 值哈希到不同的机器上,保证相同的ip在相同的机器上,再对每个机器上的ip文件再hash成小文件,这个时候再分别统计小文件的出现频次,用最小根堆处理,不同文件的结果排序,就可以得到每台机器的top 100,再进行不同机器之间的结果排序,就可以得到真正的 top 100。</p><p><img src="/img/remote/1460000040782135" alt="" title=""></p><hr><p>一般而言,像这种海量数据,比如 <code>有一个包含100亿个URL的大文件,假设每个URL占用64B,请找出其中所有重复的URL.</code> ,内存一次性读不下,只能通过 ==分而治之==。</p><p>hash 到不同的小文件,一直这样划分,直到满足资源的限制:</p><ul><li>hash分流</li><li>hash表统计</li><li>最小堆/外部排序</li></ul><p>如果允许一定的误差存在,其实还可以考虑使用布隆过滤器(Bloom filter),将URL挨个映射到每一个Bit,在此之前判断该位置是否映射过,来证明它是否已经存在。(<code>有一定的概率出现误判,因为其他的URL也可能会映射到同一位置</code>)</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=FdiuXxsiMR7f7eO5fk9Ivw%3D%3D.PVOmYMtZBFzo6WnyHX4dvyF1kSZKwP8BKoey3cEdxo9gplmi6sgxE2V2G%2Frk8vNo" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=mZyjq8xMyswrsrjEvzbXJw%3D%3D.zQZ4JWk4KV747YCiVTkDhvAScoTrvTKsVae%2F7HMe8E0%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=ehRklSlNJdMBFqfSa8NV6A%3D%3D.6S4aJqn8zksZpLC%2FQh28PoxOXPbrN2daBhDUvjcUJO%2F7miy%2FYQH6cUp48yZ31nZJ" rel="nofollow">开源编程笔记</a></p>
最长回文子串 -- 三种解答
https://segmentfault.com/a/1190000040782073
2021-10-08T23:42:46+08:00
2021-10-08T23:42:46+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
1
<h3>题目描述</h3><p>给你一个字符串 s,找到 s 中最长的回文子串。</p><h3>例子</h3><pre><code class="txt">示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
示例 3:
输入:s = "a"
输出:"a"
示例 4:
输入:s = "ac"
输出:"a"</code></pre><p>来源:力扣(LeetCode)<br>链接:<a href="https://link.segmentfault.com/?enc=98LrBkVr%2BKpLOsNxWX911Q%3D%3D.BsMctRXzs57ovfNUHhxDMfoIhNw04L98RDh1XQXkZS6eiKotcgKoROjOsUrnWs45g0sPsH3PJ8kkMXztyBuF2A%3D%3D" rel="nofollow">https://leetcode-cn.com/probl...</a>,著作权归领扣网络所有。</p><h3>思路以及解答</h3><h4>暴力破解</h4><p>暴力破解,即是针对里面每一个子串,都去判断是否为回文串。<br><img src="/img/remote/1460000040782075" alt="" title=""></p><p>判断每一个字符是不是回文串,比如用 <code>cbac</code> 判断,左右两个指针,对称判断,相等则往中间移动,继续判断,不相等则直接返回 false 。<br><img src="/img/remote/1460000040782076" alt="" title=""></p><pre><code class="java"> public static String longestPalindrome(String s) {
if (s == null || s.length() == 0) {
return s;
}
String result = s.substring(0, 1);
for (int i=0; i < s.length() - 1; i++) {
for (int j = i + 1; j < s.length(); j++) {
if (judge(s, i, j) && j - i + 1 > result.length()) {
result = s.substring(i, j+1);
}
}
}
return result;
}
// 判断每个子串是不是回文
public static boolean judge(String source, int start, int end) {
// 对称轴对比
while (start <= end) {
if (source.charAt(start) != source.charAt(end)) {
return false;
}
start++;
end--;
}
return true;
}</code></pre><p>暴力破解复杂度过高,会超时,不推荐使用。<br><img src="/img/remote/1460000040782077" alt="" title=""></p><h4>中心拓展法</h4><p>回文串总是中心对称的,前面使用暴力法的时候,都是截取出子串之后再判断,只有判断到全部对称,才能证明回文,这样其实走了很多弯路,只要最后一个不对称,前功尽弃。</p><p>反过来想,我们不如在每一个点,都尝试往两边拓展,这样只要不匹配,就可以及时止顺。</p><p><img src="/img/remote/1460000040782078" alt="" title=""></p><p>值得注意的是,中心拓展法的中心怎么找?3个字符有多少个中心呢?</p><p><img src="/img/remote/1460000040782079" alt="" title=""></p><p>一共有五个中心,有些中心可能是两个字符的间隙,有些中心可能是字符。那么设计的时候,我们用 <code>left</code> 和 <code>right</code> 表示两个指针:</p><ul><li><code>left = right</code>:对称中心为字符</li><li><code>left + 1 = right</code>: 对称中心为两个字符的间隙</li></ul><p>具体实现如下:</p><pre><code class="java">class Solution {
// 开始下标
public static int start = -1;
// 最大长度
public static int maxLen= 0;
public String longestPalindrome(String s) {
start = -1;
maxLen = 0;
if(s==null||s.length()==0){
return "";
}
for(int i=0;i<s.length();i++){
// 以当前字符为对称轴
judge(s,i,i);
// 以当前字符和下一个字符的间隙为对称轴
judge(s,i,i+1);
}
if(start == -1){
return "";
}
return s.substring(start,start+maxLen);
}
public void judge(String s,int left,int right){
while(left>=0 && right<s.length() && s.charAt(left)==s.charAt(right)){
left--;
right++;
}
int size = right-left-1;
if(size > maxLen){
maxLen = size;
start = left+1;
}
}
}
</code></pre><h4>动态规划</h4><p>其实,一个字符串是回文串的话,那么它倒过来读也是一样的,也就是说,它与它反转后的字符串,其实是完全匹配的,那么要是我们用一个字符串和它反转字符串一一统计匹配,是不是就可以得到结果呢?</p><p>答案是肯定的!假设原字符串为 <code>s1</code>,反转后的字符串为 <code>s2</code>,字符串长度为 <code>n</code>,我们用数组 <code>nums[n][n]</code> 来记录匹配的数量,<code>nums[i][j]</code>表示以 <code>s1[i]</code> 结尾的字符子串,和以 <code>s2[j]</code>结尾的字符子串,两者的匹配字符的最大数值。</p><ul><li><p>当 <code>s1[i] == s2[j]</code>:</p><ul><li>如果 <code>i == 0</code> 或者 <code>j == 0</code>: <code>nums[i][j] = 1</code></li><li>否则 <code>nums[i][j] = nums[i - 1][j - 1] + 1;</code></li></ul></li><li>如果 <code>s1[i] != s2[j]</code>,则 <code>nums[i][j]=0</code></li></ul><p>前面说的其实就是状态转移表达式,也就是 <code>nums[i][j]</code> 是怎么求解的?<code>nums[i][j]</code> 是依赖于 <code>nums[i - 1][j - 1]</code> 和 当前字符是否匹配,如果当前字符不匹配,直接赋值为 0,只有在当前字符匹配的情况下,才会需要看前面一位的匹配数值 <code>nums[i - 1][j - 1]</code>。</p><p>假设以 <code>babad</code> 为例子:</p><p><img src="/img/remote/1460000040782080" alt="" title=""></p><p>最后两行的计算:</p><p><img src="/img/remote/1460000040782081" alt="" title=""></p><p>实现的代码如下:</p><pre><code class="java">class Solution {
public static String longestPalindrome(String s) {
if (s == null || s.length() == 0) {
return "";
}
if (s.length() == 1) {
return s;
}
int len = s.length();
String s1 = new StringBuffer(s).reverse().toString();
int[][] nums = new int[len][len];
int end = 0, max = 0;
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < nums.length; j++) {
if (s1.charAt(i) == s.charAt(j)) {
if (i == 0 || j == 0) {
nums[i][j] = 1;
} else {
nums[i][j] = nums[i - 1][j - 1] + 1;
}
}
if (nums[i][j] > max) {
if (len - i - 1 + nums[i][j] - 1 == j) {
end = j;
max = nums[i][j];
}
}
}
}
return s.substring(end - max+1, end+1);
}
}</code></pre><h2>作者简介</h2><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:<code>Java源码解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>剑指Offer</code>,<code>LeetCode</code>等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=swOBQ4m6DkIobnjd4mWrBQ%3D%3D.jfeuQVvn5eU40COE%2B2o49zy3Ijd9XhgoxfeoocYqFezjIvz4Ykrep1SdIj0HbdtP" rel="nofollow">剑指Offer全部题解PDF</a></p><p><a href="https://link.segmentfault.com/?enc=D5esk4n8YuT9hqbqAaInnA%3D%3D.fDLJSAmLpOy8AdV3LcRZXW6Uy5XGHNY%2FSoIQDxg87cU%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=asg%2FdIfLuFJqmzDVfV66dw%3D%3D.w3rlOsvATh7cWBrBjn4d53rI%2Ffht%2B04nnBQqByvE4Aoa8bdMvomXQX9ahTnJlOex" rel="nofollow">开源编程笔记</a></p>
面试题 -- 如何设计一个线程池
https://segmentfault.com/a/1190000040631931
2021-09-05T02:29:35+08:00
2021-09-05T02:29:35+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
1
<blockquote>以前,我总觉得的买一件东西,做一件事,或者从某一个时间节点开始,我的生命就会发生转折,一切就会无比顺利,立马变厉害。但是,事实上并不是如此。我不可能马上变厉害,也不可能一口吃成一个胖子。看一篇文章也不能让你从此走上人生巅峰,越来越相信,这是一个长期的过程,只有量变引起质变,纵使缓慢,驰而不息。</blockquote><p>[TOC]</p><h2>如何设计一个线程池?</h2><h3>三个步骤</h3><p>这是一个常见的问题,如果在比较熟悉线程池运作原理的情况下,这个问题并不难。设计实现一个东西,三步走:是什么?为什么?怎么做?</p><h4>线程池是什么?</h4><p>线程池使用了池化技术,将线程存储起来放在一个 "池子"(容器)里面,来了任务可以用已有的空闲的线程进行处理, 处理完成之后,归还到容器,可以复用。如果线程不够,还可以根据规则动态增加,线程多余的时候,亦可以让多余的线程死亡。</p><h4>为什么要用线程池?</h4><p>实现线程池有什么好处呢?</p><ul><li>降低资源消耗:池化技术可以重复利用已经创建的线程,降低线程创建和销毁的损耗。</li><li>提高响应速度:利用已经存在的线程进行处理,少去了创建线程的时间</li><li>管理线程可控:线程是稀缺资源,不能无限创建,线程池可以做到统一分配和监控</li><li>拓展其他功能:比如定时线程池,可以定时执行任务</li></ul><h4>需要考虑的点</h4><p>那线程池设计需要考虑的点:</p><ul><li><p>线程池状态:</p><ul><li>有哪些状态?如何维护状态?</li></ul></li><li><p>线程</p><ul><li>线程怎么封装?线程放在哪个池子里?</li><li>线程怎么取得任务?</li><li>线程有哪些状态?</li><li>线程的数量怎么限制?动态变化?自动伸缩?</li><li>线程怎么消亡?如何重复利用?</li></ul></li><li><p>任务</p><ul><li>任务少可以直接处理,多的时候,放在哪里?</li><li>任务队列满了,怎么办?</li><li>用什么队列?</li></ul></li></ul><p>如果从任务的阶段来看,分为以下几个阶段:</p><ul><li>如何存任务?</li><li>如何取任务?</li><li>如何执行任务?</li><li>如何拒绝任务?</li></ul><h3>线程池状态</h3><h4>状态有哪些?如何维护状态?</h4><p>状态可以设置为以下几种:</p><ul><li>RUNNING:运行状态,可以接受任务,也可以处理任务</li><li>SHUTDOWN:不可以接受任务,但是可以处理任务</li><li>STOP:不可以接受任务,也不可以处理任务,中断当前任务</li><li>TIDYING:所有线程停止</li><li>TERMINATED:线程池的最后状态</li></ul><p>各种状态之间是不一样的,他们的状态之间变化如下:</p><p><img src="/img/remote/1460000040212106" alt="" title=""></p><p>而维护状态的话,可以用一个变量单独存储,并且需要保证修改时的<strong>原子性</strong>,在底层操作系统中,对int的修改是原子的,而在32位的操作系统里面,对<code>double</code>,<code>long</code>这种64位数值的操作不是原子的。<strong>除此之外,实际上JDK里面实现的状态和线程池的线程数是同一个变量,高3位表示线程池的状态,而低29位则表示线程的数量。</strong></p><p>这样设计的好处是节省空间,并且同时更新的时候有优势。</p><h3>线程相关</h3><h4>线程怎么封装?线程放在哪个池子里?</h4><p>线程,即是实现了<code>Runnable</code>接口,执行的时候,调用的是<code>start()</code>方法,但是<code>start()</code>方法内部编译后调用的是 <code>run()</code> 方法,这个方法只能调用一次,调用多次会报错。因此线程池里面的线程跑起来之后,不可能终止再启动,只能一直运行着。<strong>既然不可以停止,那么执行完任务之后,没有任务过来,只能是轮询取出任务的过程</strong></p><p>线程可以运行任务,因此封装线程的时候,假设封装成为 <code>Worker</code>, <code>Worker</code>里面必定是包含一个 <code>Thread</code>,表示当前线程,除了当前线程之外,封装的线程类还应该持有任务,初始化可能直接给予任务,当前的任务是null的时候才需要去获取任务。</p><p>可以考虑使用 <code>HashSet</code> 来存储线程,也就是充当线程池的角色,当然,<code>HashSet</code> 会有线程安全的问题需要考虑,那么我们可以考虑使用一个可重入锁比如 <code>ReentrantLock</code>,凡是增删线程池的线程,都需要锁住。</p><pre><code class="java"> private final ReentrantLock mainLock = new ReentrantLock();</code></pre><h4>线程怎么取得任务?</h4><p>(1)初始化线程的时候可以直接指定任务,譬如<code>Runnable firstTask</code>,将任务封装到 <code>worker</code> 中,然后获取 <code>worker</code> 里面的 <code>thread</code>,<code>thread.run()</code>的时候,其实就是 跑的是 <code>worker</code> 本身的 <code>run()</code> 方法,因为 <code>worker</code> 本身就是实现了 <code>Runnable</code> 接口,里面的线程其实就是其本身。因此也可以实现对 <code>ThreadFactory</code> 线程工厂的定制化。</p><pre><code class="java"> private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
final Thread thread;
Runnable firstTask;
...
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
// 从线程池创建线程,传入的是其本身
this.thread = getThreadFactory().newThread(this);
}
}</code></pre><p>(2)运行完任务的线程,应该继续取任务,取任务肯定需要从任务队列里面取,要是任务队列里面没有任务,由于是阻塞队列,那么可以等待,如果等待若干时间后,仍没有任务,倘若该线程池的线程数已经超过核心线程数,并且允许线程消亡的话,应该将该线程从线程池中移除,并结束掉该线程。</p><blockquote>取任务和执行任务,对于线程池里面的线程而言,就是一个周而复始的工作,除非它会消亡。</blockquote><h4>线程有哪些状态?</h4><p>现在我们所说的是<code>Java</code>中的线程<code>Thread</code>,一个线程在一个给定的时间点,只能处于一种状态,这些状态都是虚拟机的状态,不能反映任何操作系统的线程状态,一共有六种/七种状态:</p><ul><li><code>NEW</code>:创建了线程对象,但是还没有调用<code>Start()</code>方法,还没有启动的线程处于这种状态。</li><li><p><code>Running</code>:运行状态,其实包含了两种状态,但是<code>Java</code>线程将就绪和运行中统称为可运行</p><ul><li><p><code>Runnable</code>:就绪状态:创建对象后,调用了<code>start()</code>方法,该状态的线程还位于可运行线程池中,等待调度,获取<code>CPU</code>的使用权</p><ul><li>只是有资格执行,不一定会执行</li><li><code>start()</code>之后进入就绪状态,<code>sleep()</code>结束或者<code>join()</code>结束,线程获得对象锁等都会进入该状态。</li><li><code>CPU</code>时间片结束或者主动调用<code>yield()</code>方法,也会进入该状态</li></ul></li><li><code>Running</code> :获取到<code>CPU</code>的使用权(获得CPU时间片),变成运行中</li></ul></li><li><code>BLOCKED</code> :阻塞,线程阻塞于锁,等待监视器锁,一般是<code>Synchronize</code>关键字修饰的方法或者代码块</li><li><code>WAITING</code> :进入该状态,需要等待其他线程通知(<code>notify</code>)或者中断,一个线程无限期地等待另一个线程。</li><li><code>TIMED_WAITING</code> :超时等待,在指定时间后自动唤醒,返回,不会一直等待</li><li><code>TERMINATED</code> :线程执行完毕,已经退出。如果已终止再调用start(),将会抛出<code>java.lang.IllegalThreadStateException</code>异常。</li></ul><p><img src="/img/remote/1460000040631934" alt="image-20210509224848865" title="image-20210509224848865"></p><h4>线程的数量怎么限制?动态变化?自动伸缩?</h4><p>线程池本身,就是为了限制和充分使用线程资的,因此有了两个概念:核心线程数,最大线程数。</p><p>要想让线程数根据任务数量动态变化,那么我们可以考虑以下设计(假设不断有任务):</p><ul><li>来一个任务创建一个线程处理,直到线程数达到核心线程数。</li><li>达到核心线程数之后且没有空闲线程,来了任务直接放到任务队列。</li><li>任务队列如果是无界的,会被撑爆。</li><li>任务队列如果是有界的,任务队列满了之后,还有任务过来,会继续创建线程处理,此时线程数大于核心线程数,直到线程数等于最大线程数。</li><li>达到最大线程数之后,还有任务不断过来,会触发拒绝策略,根据不同策略进行处理。</li><li>如果任务不断处理完成,任务队列空了,线程空闲没任务,会在一定时间内,销毁,让线程数保持在核心线程数即可。</li></ul><p>由上面可以看出,主要控制伸缩的参数是<code>核心线程数</code>,<code>最大线程数</code>,<code>任务队列</code>,<code>拒绝策略</code>。</p><h4>线程怎么消亡?如何重复利用?</h4><p>线程不能被重新调用多次<code>start()</code>,因此只能调用一次,也就是线程不可能停下来,再启动。那么就说明线程复用只是在不断的循环罢了。</p><p>消亡只是结束了它的<code>run()</code>方法,当线程池数量需要自动缩容的,就会让一部分空闲的线程结束。</p><p>而重复利用,其实是执行完任务之后,再去去任务队列取任务,取不到任务会等待,任务队列是一个阻塞队列,这是一个<code>不断循环</code>的过程。</p><h3>任务相关</h3><h4>任务少可以直接处理,多的时候,放在哪里?</h4><p>任务少的时候,来了直接创建,赋予线程初始化任务,就可开始执行,任务多的时候,把它放进队列里面,先进先出。</p><h4>任务队列满了,怎么办?</h4><p>任务队列满了,会继续增加线程,直到达到最大的线程数。</p><h4>用什么队列?</h4><p>一般的队列,只是一个有限长度的缓冲区,要是满了,就不能保存当前的任务,阻塞队列可以通过阻塞,保留出当前需要入队的任务,只是会阻塞等待。同样的,阻塞队列也可以保证任务队列没有任务的时候,阻塞当前获取任务的线程,让它进入<code>wait</code>状态,释放<code>cpu</code>的资源。因此在线程池的场景下,阻塞队列其实是比较有必要的。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。如果有帮助,顺手点个赞,对我,是莫大的鼓励和认可。</p>
线程与线程池的那些事之线程池篇(万字长文)
https://segmentfault.com/a/1190000040212099
2021-06-21T17:33:00+08:00
2021-06-21T17:33:00+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
1
<p><strong>本文关键字:</strong></p><p><code>线程</code>,<code>线程池</code>,<code>单线程</code>,<code>多线程</code>,<code>线程池的好处</code>,<code>线程回收</code>,<code>创建方式</code>,<code>核心参数</code>,<code>底层机制</code>,<code>拒绝策略</code>,<code>参数设置</code>,<code>动态监控</code>,<code>线程隔离</code></p><p>线程和线程池相关的知识,是Java学习或者面试中一定会遇到的知识点,本篇我们会从线程和进程,并行与并发,单线程和多线程等,一直讲解到线程池,线程池的好处,创建方式,重要的核心参数,几个重要的方法,底层实现,拒绝策略,参数设置,动态调整,线程隔离等等。主要的大纲如下:</p><p><img src="/img/remote/1460000040212101" alt="" title=""></p><h2>线程池的好处</h2><p>线程池,使用了池化思想来管理线程,池化技术就是为了最大化效益,最小化用户风险,将资源统一放在一起管理的思想。这种思想在很多地方都有使用到,不仅仅是计算机,比如金融,企业管理,设备管理等。</p><p>为什么要线程池?如果在并发的场景,编码人员根据需求来创建线程池,可能会有以下的问题:</p><ul><li>我们很难确定系统有多少线程在运行,如果使用就创建,不使用就销毁,那么创建和销毁线程的消耗也是比较大的</li><li>假设来了很多请求,可能是爬虫,疯狂创建线程,可能把系统资源耗尽。</li></ul><p>实现线程池有什么好处呢?</p><ul><li>降低资源消耗:池化技术可以重复利用已经创建的线程,降低线程创建和销毁的损耗。</li><li>提高响应速度:利用已经存在的线程进行处理,少去了创建线程的时间</li><li>管理线程可控:线程是稀缺资源,不能无限创建,线程池可以做到统一分配和监控</li><li>拓展其他功能:比如定时线程池,可以定时执行任务</li></ul><p>其实池化技术,用在比较多地方,比如:</p><ul><li>数据库连接池:数据库连接是稀缺资源,先创建好,提高响应速度,重复利用已有的连接</li><li>实例池:先创建好对象放到池子里面,循环利用,减少来回创建和销毁的消耗</li></ul><h2>线程池相关的类</h2><p>下面是与线程池相关的类的继承关系:</p><p><img src="/img/remote/1460000040212102" alt="" title=""></p><h3>Executor</h3><p><code>Executor</code> 是顶级接口,里面只有一个方法<code>execute(Runnable command)</code>,定义的是调度线程池来执行任务,它定义了线程池的基本规范,执行任务是它的天职。</p><h3>ExecutorService</h3><p><code>ExecutorService</code> 继承了<code>Executor</code>,但是它仍然是一个接口,它多了一些方法:</p><p><img src="/img/remote/1460000040212103" alt="" title=""></p><ul><li><code>void shutdown()</code>:关闭线程池,会等待任务执行完。</li><li><code>List<Runnable> shutdownNow()</code>:立刻关闭线程池,尝试停止所有正在积极执行的任务,停止等待任务的处理,并<strong>返回一个正在等待执行的任务列表(还没有执行的)</strong>。</li><li><code>boolean isShutdown()</code>:判断线程池是不是已经关闭,但是可能线程还在执行。</li><li><code>boolean isTerminated()</code>:在执行shutdown/shutdownNow之后,所有的任务已经完成,这个状态就是true。</li><li><code>boolean awaitTermination(long timeout, TimeUnit unit)</code>:执行shutdown之后,阻塞等到terminated状态,除非超时或者被打断。</li><li><code><T> Future<T> submit(Callable<T> task)</code>: 提交一个有返回值的任务,并且返回该任务尚未有结果的Future,调用future.get()方法,可以返回任务完成的时候的结果。</li><li><code><T> Future<T> submit(Runnable task, T result)</code>:提交一个任务,传入返回结果,这个result没有什么作用,只是指定类型和一个返回的结果。</li><li><code>Future<?> submit(Runnable task)</code>: 提交任务,返回Future</li><li><code><T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)</code>:批量执行tasks,获取Future的list,可以批量提交任务。</li><li><code><T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)</code>:批量提交任务,并指定超时时间</li><li><code><T> T invokeAny(Collection<? extends Callable<T>> tasks)</code>: 阻塞,获取第一个完成任务的结果值,</li><li><code><T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)</code>:阻塞,获取第一个完成结果的值,指定超时时间</li></ul><p>可能有同学对前面的<code><T> Future<T> submit(Runnable task, T result)</code>有疑问,这个reuslt有什么作用?</p><p>其实它没有什么作用,只是持有它,任务完成后,还是调用 <code>future.get()</code>返回这个结果,用<code>result</code> new 了一个 <code>ftask</code>,其内部其实是使用了Runnable的包装类 <code>RunnableAdapter</code>,没有对result做特殊的处理,调用 <code>call()</code> 方法的时候,直接返回这个结果。(Executors 中具体的实现)</p><pre><code class="java"> public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
// 返回传入的结果
return result;
}
}</code></pre><p>还有一个方法值得一提:<code>invokeAny()</code>: 在 <code>ThreadPoolExecutor</code>中使用<code>ExecutorService</code> 中的方法 <code>invokeAny()</code> 取得第一个完成的任务的结果,当第一个任务执行完成后,会调用 <code>interrupt()</code> 方法将其他任务中断。</p><p>注意,<code>ExecutorService</code>是接口,里面都是定义,并没有涉及实现,而前面的讲解都是基于它的名字(规定的规范)以及它的普遍实现来说的。</p><p>可以看到 <code>ExecutorService</code> 定义的是线程池的一些操作,包括关闭,判断是否关闭,是否停止,提交任务,批量提交任务等等。</p><h3>AbstractExecutorService</h3><p><code>AbstractExecutorService</code> 是一个抽象类,实现了 <code>ExecutorService</code>接口,这是大部分线程池的基本实现,定时的线程池先不关注,主要的方法如下:</p><p><img src="/img/remote/1460000040212104" alt="" title=""></p><p>不仅实现了<code>submit</code>,<code>invokeAll</code>,<code>invokeAny</code> 等方法,而且提供了一个 <code>newTaskFor</code> 方法用于构建 <code>RunnableFuture</code> 对象,那些能够获取到任务返回结果的对象都是通过 <code>newTaskFor</code> 来获取的。不展开里面所有的源码的介绍,仅以submit()方法为例:</p><pre><code class="java"> public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
// 封装任务
RunnableFuture<Void> ftask = newTaskFor(task, null);
// 执行任务
execute(ftask);
// 返回 RunnableFuture 对象
return ftask;
}</code></pre><p>但是在 <code>AbstractExecutorService</code> 是没有对最最重要的方法进行实现的,也就是 <code>execute()</code> 方法。线程池具体是怎么执行的,这个不同的线程池可以有不同的实现,一般都是继承 <code>AbstractExecutorService</code> (定时任务有其他的接口),我们最最常用的就是<code>ThreadPoolExecutor</code>。</p><p><img src="/img/remote/1460000040212105" alt="" title=""></p><h3>ThreadPoolExecutor</h3><p><strong>重点来了!!!</strong> <code>ThreadPoolExecutor</code> 一般就是我们平时常用到的线程池类,所谓创建线程池,如果不是定时线程池,就是使用它。</p><p>先看<code>ThreadPoolExecutor</code>的内部结构(属性):</p><pre><code class="java">public class ThreadPoolExecutor extends AbstractExecutorService {
// 状态控制,主要用来控制线程池的状态,是核心的遍历,使用的是原子类
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 用来表示线程数量的位数(使用的是位运算,一部分表示线程的数量,一部分表示线程池的状态)
// SIZE = 32 表示32位,那么COUNT_BITS就是29位
private static final int COUNT_BITS = Integer.SIZE - 3;
// 线程池的容量,也就是27位表示的最大值
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 状态量,存储在高位,32位中的前3位
// 111(第一位是符号位,1表示负数),线程池运行中
private static final int RUNNING = -1 << COUNT_BITS;
// 000
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 001
private static final int STOP = 1 << COUNT_BITS;
// 010
private static final int TIDYING = 2 << COUNT_BITS;
// 011
private static final int TERMINATED = 3 << COUNT_BITS;
// 取出运行状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 取出线程数量
private static int workerCountOf(int c) { return c & CAPACITY; }
// 用运行状态和线程数获取ctl
private static int ctlOf(int rs, int wc) { return rs | wc; }
// 任务等待队列
private final BlockingQueue<Runnable> workQueue;
// 可重入主锁(保证一些操作的线程安全)
private final ReentrantLock mainLock = new ReentrantLock();
// 线程的集合
private final HashSet<Worker> workers = new HashSet<Worker>();
// 在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),
// 传统线程的通信方式,Condition都可以实现,Condition和传统的线程通信没什么区别,Condition的强大之处在于它可以为多个线程间建立不同的Condition
private final Condition termination = mainLock.newCondition();
// 最大线程池大小
private int largestPoolSize;
// 完成的任务数量
private long completedTaskCount;
// 线程工厂
private volatile ThreadFactory threadFactory;
// 任务拒绝处理器
private volatile RejectedExecutionHandler handler;
// 非核心线程的存活时间
private volatile long keepAliveTime;
// 允许核心线程的超时时间
private volatile boolean allowCoreThreadTimeOut;
// 核心线程数
private volatile int corePoolSize;
// 工作线程最大容量
private volatile int maximumPoolSize;
// 默认的拒绝处理器(丢弃任务)
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
// 运行时关闭许可
private static final RuntimePermission shutdownPerm =
new RuntimePermission("modifyThread");
// 上下文
private final AccessControlContext acc;
// 只有一个线程
private static final boolean ONLY_ONE = true;
}</code></pre><h4>线程池状态</h4><p>从上面的代码可以看出,用一个32位的对象保存线程池的状态以及线程池的容量,高3位是线程池的状态,而剩下的29位,则是保存线程的数量:</p><pre><code class="java"> // 状态量,存储在高位,32位中的前3位
// 111(第一位是符号位,1表示负数),线程池运行中
private static final int RUNNING = -1 << COUNT_BITS;
// 000
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 001
private static final int STOP = 1 << COUNT_BITS;
// 010
private static final int TIDYING = 2 << COUNT_BITS;
// 011
private static final int TERMINATED = 3 << COUNT_BITS;</code></pre><p>各种状态之间是不一样的,他们的状态之间变化如下:</p><p><img src="/img/remote/1460000040212106" alt="" title=""></p><ul><li>RUNNING:运行状态,可以接受任务,也可以处理任务</li><li>SHUTDOWN:不可以接受任务,但是可以处理任务</li><li>STOP:不可以接受任务,也不可以处理任务,中断当前任务</li><li>TIDYING:所有线程停止</li><li>TERMINATED:线程池的最后状态</li></ul><h4>Worker 实现</h4><p>线程池,肯定得有池子,并且是放线程的地方,在 <code>ThreadPoolExecutor</code> 中表现为 <code>Worker</code>,这是内部类:</p><p><img src="/img/remote/1460000040212107" alt="" title=""></p><p>线程池其实就是 <code>Worker</code> (打工人,不断的领取任务,完成任务)的集合,这里使用的是 <code>HashSet</code>:</p><pre><code class="java">private final HashSet<Worker> workers = new HashSet<Worker>();</code></pre><p><code>Worker</code> 怎么实现的呢?</p><p><code>Worker</code> 除了继承了 <code>AbstractQueuedSynchronizer</code>,也就是 <code>AQS</code> , <code>AQS</code> 本质上就是个队列锁,一个简单的互斥锁,一般是在中断或者修改 <code>worker</code> 状态的时候使用。</p><p>内部引入<code>AQS</code>,是为了线程安全,线程执行任务的时候,调用的是<code>runWorker(Worker w)</code>,这个方法不是worker的方法,而是 <code>ThreadPoolExecutor</code>的方法。从下面的代码可以看出,每次修改<code>Worke</code>r的状态的时候,都是线程安全的。<code>Worker</code>里面,持有了一个线程<code>Thread</code>,可以理解为是对线程的封装。</p><p>至于<code>runWorker(Worker w)</code>是怎么运行的?先保持这个疑问,后面详细讲解。</p><pre><code class="java"> // 实现 Runnable,封装了线程
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
// 序列化id
private static final long serialVersionUID = 6138294804551838833L;
// worker运行的线程
final Thread thread;
// 初始化任务,有可能是空的,如果任务不为空的时候,其他进来的任务,可以直接运行,不在添加到任务队列
Runnable firstTask;
// 线程任务计数器
volatile long completedTasks;
// 指定一个任务让工人忙碌起来,这个任务可能是空的
Worker(Runnable firstTask) {
// 初始化AQS队列锁的状态
setState(-1); // 禁止中断直到 runWorker
this.firstTask = firstTask;
// 从线程工厂,取出一个线程初始化
this.thread = getThreadFactory().newThread(this);
}
// 实际上运行调用的是runWorker
public void run() {
// 不断循环获取任务进行执行
runWorker(this);
}
// 0表示没有被锁
// 1表示被锁的状态
protected boolean isHeldExclusively() {
return getState() != 0;
}
// 独占,尝试获取锁,如果成功返回true,失败返回false
protected boolean tryAcquire(int unused) {
// CAS 乐观锁
if (compareAndSetState(0, 1)) {
// 成功,当前线程独占锁
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 独占方式,尝试释放锁
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 上锁,调用的是AQS的方法
public void lock() { acquire(1); }
// 尝试上锁
public boolean tryLock() { return tryAcquire(1); }
// 解锁
public void unlock() { release(1); }
// 是否锁住
public boolean isLocked() { return isHeldExclusively(); }
// 如果开始可就中断
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}</code></pre><h4>任务队列</h4><p>除了放线程池的地方,要是任务很多,没有那么多线程,肯定需要一个地方放任务,充当缓冲作用,也就是任务队列,在代码中表现为:</p><pre><code class="java">private final BlockingQueue<Runnable> workQueue;</code></pre><h4>拒绝策略和处理器</h4><p>计算机的内存总是有限的,我们不可能一直往队列里面增加内容,所以线程池为我们提供了选择,可以选择多种队列。同时当任务实在太多,占满了线程,并且把任务队列也占满的时候,我们需要做出一定的反应,那就是拒绝还是抛出错误,丢掉任务?丢掉哪些任务,这些都是可能需要定制的内容。</p><h2>如何创建线程池</h2><p>关于如何创建线程池,其实 <code>ThreadPoolExecutor</code>提供了构造方法,主要参数如下,不传的话会使用默认的:</p><ul><li>核心线程数:核心线程数,一般是指常驻的线程,没有任务的时候通常也不会销毁</li><li>最大线程数:线程池允许创建的最大的线程数量</li><li>非核心线程的存活时间:指的是没有任务的时候,非核心线程能够存活多久</li><li>时间的单位:存活时间的单位</li><li>存放任务的队列:用来存放任务</li><li>线程工厂</li><li>拒绝处理器:如果添加任务失败,将由该处理器处理</li></ul><pre><code class="java"> // 指定核心线程数,最大线程数,非核心线程没有任务的存活时间,时间单位,任务队列
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
// 指定核心线程数,最大线程数,非核心线程没有任务的存活时间,时间单位,任务队列,线程池工厂
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
// 指定核心线程数,最大线程数,非核心线程没有任务的存活时间,时间单位,任务队列,拒绝任务处理器
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
// 最后其实都是调用了这个方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}</code></pre><p>其实,除了显示的指定上面的参数之外,JDK也封装了一些直接创建线程池的方法给我们,那就是<code>Executors</code>:</p><pre><code class="java"> // 固定线程数量的线程池,无界的队列
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 单个线程的线程池,无界的队列,按照任务提交的顺序,串行执行
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
// 动态调节,没有核心线程,全部都是普通线程,每个线程存活60s,使用容量为1的阻塞队列
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
// 定时任务线程池
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}</code></pre><p>但是一般是不推荐使用上面别人封装的线程池的哈!!!</p><h2>线程池的底层参数以及核心方法</h2><p>看完上面的创建参数大家可能会有点懵,但是没关系,一一为大家道来:</p><p><img src="/img/remote/1460000040212108" alt="" title=""></p><p>可以看出,当有任务进来的时候,先判断核心线程池是不是已经满了,如果还没有,将会继续创建线程。注意,如果一个任务进来,创建线程执行,执行完成,线程空闲下来,这时候再来一个任务,是会继续使用之前的线程,还是重新创建一个线程来执行呢?</p><p>答案是重新创建线程,这样线程池可以快速达到核心线程数的规模大小,以便快速响应后面的任务。</p><p>如果线程数量已经到达核心线程数,来了任务,线程池的线程又都不是空闲状态,那么就会判断队列是不是满的,倘若队列还有空间,那么就会把任务放进去队列中,等待线程领取执行。</p><p>如果任务队列已经满了,放不下任务,那么就会判断线程数是不是已经到最大线程数了,要是还没有到达,就会继续创建线程并执行任务,这个时候创建的是非核心部分线程。</p><p>如果已经到达最大线程数,那么就不能继续创建线程了,只能执行拒绝策略,默认的拒绝策略是丢弃任务,我们可以自定义拒绝策略。</p><p>值得注意的是,倘若之前任务比较多,创建出了一些非核心线程,那么任务少了之后,领取不到任务,过了一定时间,非核心线程就会销毁,只剩下核心线程池的数量的线程。这个时间就是前面说的<code>keepAliveTime</code>。</p><h3>提交任务</h3><p>提交任务,我们看<code>execute()</code>,会先获取线程池的状态和个数,要是线程个数还没达到核心线程数,会直接添加线程,否则会放到任务队列,如果任务队列放不下,会继续增加线程,但是不是增加核心线程。</p><pre><code class="java"> public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// 获取状态和个数
int c = ctl.get();
// 如果个数小于核心线程数
if (workerCountOf(c) < corePoolSize) {
// 直接添加
if (addWorker(command, true))
return;
// 添加失败则继续获取
c = ctl.get();
}
// 判断线程池状态是不是运行中,任务放到队列中
if (isRunning(c) && workQueue.offer(command)) {
// 再次检查
int recheck = ctl.get();
// 判断线程池是不是还在运行
if (! isRunning(recheck) && remove(command))
// 如果不是,那么就拒绝并移除任务
reject(command);
else if (workerCountOf(recheck) == 0)
// 如果线程数为0,并且还在运行,那么就直接添加
addWorker(null, false);
}else if (!addWorker(command, false))
// 添加任务队列失败,拒绝
reject(command);
}</code></pre><p>上面的源码中,调用了一个重要的方法:<code>addWorker(Runnable firstTask, boolean core)</code>,该方法主要是为了增加工作的线程,我们来看看它是如何执行的:</p><pre><code class="java"> private boolean addWorker(Runnable firstTask, boolean core) {
// 回到当前位置重试
retry:
for (;;) {
// 获取状态
int c = ctl.get();
int rs = runStateOf(c);
// 大于SHUTDOWN说明线程池已经停止
// ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()) 表示三个条件至少有一个不满足
// 不等于SHUTDOWN说明是大于shutdown
// firstTask != null 任务不是空的
// workQueue.isEmpty() 队列是空的
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
// 工作线程数
int wc = workerCountOf(c);
// 是否符合容量
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 添加成功,跳出循环
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
// cas失败,重新尝试
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// 前面线程计数增加成功
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 创建了一个worker,包装了任务
w = new Worker(firstTask);
final Thread t = w.thread;
// 线程创建成功
if (t != null) {
// 获取锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 再次确认状态
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// 如果线程已经启动,失败
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// 新增线程到集合
workers.add(w);
// 获取大小
int s = workers.size();
// 判断最大线程池数量
if (s > largestPoolSize)
largestPoolSize = s;
// 已经添加工作线程
workerAdded = true;
}
} finally {
// 解锁
mainLock.unlock();
}
// 如果已经添加
if (workerAdded) {
// 启动线程
t.start();
workerStarted = true;
}
}
} finally {
// 如果没有启动
if (! workerStarted)
// 失败处理
addWorkerFailed(w);
}
return workerStarted;
}</code></pre><h3>处理任务</h3><p>前面在介绍<code>Worker</code>这个类的时候,我们讲解到其实它的<code>run()</code>方法调用的是外部的<code>runWorker()</code>方法,那么我们来看看<code>runWorkder()</code>方法:</p><p>首先,它会直接处理自己的firstTask,这个任务并没有在任务队列里面,而是它自己持有的:</p><pre><code class="java">final void runWorker(Worker w) {
// 当前线程
Thread wt = Thread.currentThread();
// 第一个任务
Runnable task = w.firstTask;
// 重置为null
w.firstTask = null;
// 允许打断
w.unlock();
boolean completedAbruptly = true;
try {
// 任务不为空,或者获取的任务不为空
while (task != null || (task = getTask()) != null) {
// 加锁
w.lock();
//如果线程池停止,确保线程被中断;
//如果不是,确保线程没有被中断。这
//在第二种情况下需要复查处理
// shutdown - now竞赛同时清除中断
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
// 执行之前回调方法(可以由我们自己实现)
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 执行任务
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
// 执行之后回调方法
afterExecute(task, thrown);
}
} finally {
// 置为null
task = null;
// 更新完成任务
w.completedTasks++;
w.unlock();
}
}
// 完成
completedAbruptly = false;
} finally {
// 处理线程退出相关工作
processWorkerExit(w, completedAbruptly);
}
}</code></pre><p>上面可以看到如果当前的任务是null,会去获取一个task,我们看看<code>getTask()</code>,里面涉及到了两个参数,一个是是不是允许核心线程销毁,另外一个是线程数是不是大于核心线程数,如果满足条件,就从队列中取出任务,如果超时取不到,那就返回空,表示没有取到任务,没有取到任务,就不会执行前面的循环,就会触发线程销毁<code>processWorkerExit()</code>等工作。</p><pre><code class="java">private Runnable getTask() {
// 是否超时
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// SHUTDOWN状态继续处理队列中的任务,但是不接收新的任务
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
// 线程数
int wc = workerCountOf(c);
// 是否允许核心线程超时或者线程数大于核心线程数
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
// 减少线程成功,就返回null,后面由processWorkerExit()处理
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 如果允许核心线程关闭,或者超过了核心线程,就可以在超时的时间内获取任务,或者直接取出任务
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
// 如果能取到任务,那就肯定可以执行
if (r != null)
return r;
// 否则就获取不到任务,超时了
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}</code></pre><h3>销毁线程</h3><p>前面提到,如果线程当前任务为空,又允许核心线程销毁,或者线程超过了核心线程数,等待了一定时间,超时了却没有从任务队列获取到任务的话,就会跳出循环执行到后面的线程销毁(结束)程序。那销毁线程的时候怎么做呢?</p><pre><code class="java"> private void processWorkerExit(Worker w, boolean completedAbruptly) {
// 如果是突然结束的线程,那么之前的线程数是没有调整的,这里需要调整
if (completedAbruptly)
decrementWorkerCount();
// 获取锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 完成的任务数
completedTaskCount += w.completedTasks;
// 移除线程
workers.remove(w);
} finally {
// 解锁
mainLock.unlock();
}
// 试图停止
tryTerminate();
// 获取状态
int c = ctl.get();
// 比stop小,至少是shutdown
if (runStateLessThan(c, STOP)) {
// 如果不是突然完成
if (!completedAbruptly) {
// 最小值要么是0,要么是核心线程数,要是允许核心线程超时销毁,那么就是0
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
// 如果最小的是0或者队列不是空的,那么保留一个线程
if (min == 0 && ! workQueue.isEmpty())
min = 1;
// 只要大于等于最小的线程数,就结束当前线程
if (workerCountOf(c) >= min)
return; // replacement not needed
}
// 否则的话,可能还需要新增工作线程
addWorker(null, false);
}
}</code></pre><h3>如何停止线程池</h3><p>停止线程池可以使用<code>shutdown()</code>或者<code>shutdownNow()</code>,<code>shutdown()</code>可以继续处理队列中的任务,而<code>shutdownNow()</code>会立即清理任务,并返回未执行的任务。</p><pre><code class="java"> public void shutdown() {
// 获取锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 检查停止权限
checkShutdownAccess();
// 更新状态
advanceRunState(SHUTDOWN);
// 中断所有线程
interruptIdleWorkers();
// 回调钩子
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
// 立刻停止
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
// 获取锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 检查停止权限
checkShutdownAccess();
// 更新状态到stop
advanceRunState(STOP);
// 中断所有线程
interruptWorkers();
// 清理队列
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
// 返回任务列表(未完成)
return tasks;
}</code></pre><h2>execute()和submit()方法</h2><ul><li><code>execute() </code>方法可以提交不需要返回值的任务,无法判断任务是否被线程池执行是否成功</li><li><code>submit()</code>方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个对象,我们调用<code>get()</code>方法就可以<strong>阻塞</strong>,直到获取到线程执行完成的结果,同时我们也可以使用有超时时间的等待方法<code>get(long timeout,TimeUnit unit)</code>,这样不管线程有没有执行完成,如果到时间,也不会阻塞,直接返回null。返回的是<code>RunnableFuture</code>对象,继承了<code>Runnable, Future<V></code>两个接口:</li></ul><pre><code class="java">public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}</code></pre><h2>线程池为什么使用阻塞队列?</h2><p>阻塞队列,首先是一个队列,肯定具有先进先出的属性。</p><p>而阻塞,则是这个模型的演化,一般队列,可以用在生产消费者模型,也就是数据共享,有人往里面放任务,有人不断的往里面取出任务,这是一个理想的状态。</p><p>但是倘若不理想,产生任务和消费任务的速度不一样,要是任务放在队列里面比较多,消费比较慢,还可以慢慢消费,或者生产者得暂停一下产生任务(阻塞生产者线程)。可以使用 <code>offer(E o, long timeout, TimeUnit unit)</code>设定等待的时间,如果在指定的时间内,还不能往队列中加入<code>BlockingQueue</code>,则返回失败,也可以使用<code>put(Object)</code>,将对象放到阻塞队列里面,如果没有空间,那么这个方法会阻塞到有空间才会放进去。</p><p>如果消费速度快,生产者来不及生产,获取任务的时候,可以使用<code>poll(time)</code>,有数据则直接取出来,没数据则可以等待<code>time</code>时间后,返回<code>null</code>。也可以使用<code>take()</code>取出第一个任务,没有任务就会一直阻塞到队列有任务为止。</p><p>上面说了阻塞队列的属性,那么为啥要用呢?</p><ul><li>如果产生任务,来了就往队列里面放,资源很容易被耗尽。</li><li>创建线程需要获取锁,这个一个线程池的全局锁,如果各个线程不断的获取锁,解锁,线程上下文切换之类的开销也比较大,不如在队列为空的时候,然一个线程阻塞等待。</li></ul><h3>常见的阻塞队列</h3><p><img src="/img/remote/1460000040212109" alt="" title=""></p><ul><li><strong>ArrayBlockingQueue</strong>:基于数组实现,内部有一个定长的数组,同时保存着队列头和尾部的位置。</li><li><strong>LinkedBlockingQueue</strong>:基于链表的阻塞对垒,生产者和消费者使用独立的锁,并行能力强,如果不指定容量,默认是无效容量,容易系统内存耗尽。</li><li><strong>DelayQueue</strong>:延迟队列,没有大小限制,生产数据不会被阻塞,消费数据会,只有指定的延迟时间到了,才能从队列中获取到该元素。</li><li><strong>PriorityBlockingQueue</strong>:基于优先级的阻塞队列,按照优先级进行消费,内部控制同步的是公平锁。</li><li><strong>SynchronousQueue</strong>:没有缓冲,生产者直接把任务交给消费者,少了中间的缓存区。</li></ul><h2>线程池如何复用线程的?执行完成的线程怎么处理</h2><p>前面的源码分析,其实已经讲解过这个问题了,线程池的线程调用的<code>run()</code>方法,其实调用的是<code>runWorker()</code>,里面是死循环,除非获取不到任务,如果没有了任务firstTask并且从任务队列中获取不到任务,超时的时候,会再判断是不是可以销毁核心线程,或者超过了核心线程数,满足条件的时候,才会让当前的线程结束。</p><p>否则,一直都在一个循环中,不会结束。</p><p>我们知道<code>start()</code>方法只能调用一次,因此调用到<code>run()</code>方法的时候,调用外面的<code>runWorker()</code>,让其在<code>runWorker()</code>的时候,不断的循环,获取任务。获取到任务,调用任务的<code>run()</code>方法。</p><p>执行完成的线程会调用<code>processWorkerExit()</code>,前面有分析,里面会获取锁,把线程数减少,从工作线程从集合中移除,移除掉之后,会判断线程是不是太少了,如果是,会再加回来,个人以为是一种补救。</p><h2>如何配置线程池参数?</h2><p>一般而言,有个公式,如果是计算(CPU)密集型的任务,那么核心线程数设置为<code>处理器核数-1</code>,如果是io密集型(很多网络请求),那么就可以设置为<code>2*处理器核数</code>。但是这并不是一个银弹,一切要从实际出发,最好就是在测试环境进行压测,实践出真知,并且很多时候一台机器不止一个线程池或者还会有其他的线程,因此参数不可设置得太过饱满。</p><p>一般 8 核的机器,设置 10-12 个核心线程就差不多了,这一切必须按照业务具体值进行计算。设置过多的线程数,上下文切换,竞争激烈,设置过少,没有办法充分利用计算机的资源。</p><blockquote><p>计算(CPU)密集型消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。</p><p>io密集型系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。</p></blockquote><h2>为什么不推荐默认的线程池创建方式?</h2><p>阿里的编程规范里面,不建议使用默认的方式来创建线程,是因为这样创建出来的线程很多时候参数都是默认的,可能创建者不太了解,很容易出问题,最好通过<code>new ThreadPoolExecutor()</code>来创建,方便控制参数。默认的方式创建的问题如下:</p><ul><li>Executors.newFixedThreadPool():无界队列,内存可能被打爆</li><li>Executors.newSingleThreadExecutor():单个线程,效率低,串行。</li><li>Executors.newCachedThreadPool():没有核心线程,最大线程数可能为无限大,内存可能还会爆掉。</li></ul><p>使用具体的参数创建线程池,开发者必须了解每个参数的作用,不会胡乱设置参数,减少内存溢出等问题。</p><p>一般体现在几个问题:</p><ul><li>任务队列怎么设置?</li><li>核心线程多少个?</li><li>最大线程数多少?</li><li>怎么拒绝任务?</li><li>创建线程的时候没有名称,追溯问题不好找。</li></ul><h2>线程池的拒绝策略</h2><p>线程池一般有以下四种拒绝策略,其实我们可以从它的内部类看出来:</p><p><img src="/img/remote/1460000040212110" alt="" title=""></p><ul><li>AbortPolicy: 不执行新的任务,直接抛出异常,提示线程池已满</li><li>DisCardPolicy:不执行新的任务,但是也不会抛出异常,默默的</li><li>DisCardOldSetPolicy:丢弃消息队列中最老的任务,变成新进来的任务</li><li>CallerRunsPolicy:直接调用当前的execute来执行任务</li></ul><p>一般而言,上面的拒绝策略都不会特别理想,一般要是任务满了,首先需要做的就是看任务是不是必要的,如果非必要,非核心,可以考虑拒绝掉,并报错提醒,如果是必须的,必须把它保存起来,不管是使用mq消息,还是其他手段,不能丢任务。在这些过程中,日志是非常必要的。既要保护线程池,也要对业务负责。</p><h2>线程池监控与动态调整</h2><p>线程池提供了一些API,可以动态获取线程池的状态,并且还可以设置线程池的参数,以及状态:</p><p>查看线程池的状态:</p><p><img src="/img/remote/1460000040212111" alt="" title=""></p><p>修改线程池的状态:</p><p><img src="/img/remote/1460000040212112" alt="" title=""></p><p>关于这一点,美团的线程池文章讲得很清楚,甚至做了一个实时调整线程池参数的平台,可以进行跟踪监控,线程池活跃度、任务的执行Transaction(频率、耗时)、Reject异常、线程池内部统计信息等等。这里我就不展开了,原文:<a href="https://link.segmentfault.com/?enc=aEHzBiFc4uIACy6hSTcv5g%3D%3D.%2Fa335Iv7phBNcE3zCJ6sN2t0gbDKQvr%2Bg5ys0OjFW8aKNf46LvqYMKp4uS7cBy3RwHkqt%2BNjQYwdbZIFOTrcGBnS8Qa84pzac50aj61MRAI%3D" rel="nofollow">https://tech.meituan.com/2020...</a> ,这是我们可以参考的思路。</p><h2>线程池隔离</h2><p>线程隔离,很多同学可能知道,就是不同的任务放在不同的线程里面运行,而线程池隔离,一般是按照业务类型来隔离,比如订单的处理线程放在一个线程池,会员相关的处理放在一个线程池。</p><p>也可以通过核心和非核心来隔离,核心处理流程放在一起,非核心放在一起,两个使用不一样的参数,不一样的拒绝策略,尽量保证多个线程池之间不影响,并且最大可能保住核心线程的运行,非核心线程可以忍受失败。</p><p><code>Hystrix</code>里面运用到这个技术,<code>Hystrix</code>的线程隔离技术,来防止不同的网络请求之间的雪崩,即使依赖的一个服务的线程池满了,也不会影响到应用程序的其他部分。</p><h3>关于作者</h3><p>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=yKMQyKlAqjbXHOChOlMnfA%3D%3D.0872X9b5GVaMBkgLT%2FZ1GbAzl48BCT9EIFeLyKrXbMc%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=8mlsEJKTA8HXOE%2FXMCajCw%3D%3D.DMXtbjl63RO2fZIm9dEH1zVSQCqwtlRWeT687E7QQHClq4lPTApUxdhDjl50JH1Y" rel="nofollow">开源编程笔记</a></p>
线程与线程池的那些事之线程篇
https://segmentfault.com/a/1190000040038039
2021-05-20T22:09:29+08:00
2021-05-20T22:09:29+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p><strong>本文关键字:</strong></p><p><code>线程</code>,<code>线程池</code>,<code>单线程</code>,<code>多线程</code>,<code>线程池的好处</code>,<code>线程回收</code>,<code>创建方式</code>,<code>核心参数</code>,<code>底层机制</code>,<code>拒绝策略</code>,<code>参数设置</code>,<code>动态监控</code>,<code>线程隔离</code></p><p>线程和线程池相关的知识,是Java学习或者面试中一定会遇到的知识点,本篇我们会从线程和进程,并行与并发,单线程和多线程等,一直讲解到线程池,线程池的好处,创建方式,重要的核心参数,几个重要的方法,底层实现,拒绝策略,参数设置,动态调整,线程隔离等等。主要的大纲如下(<strong>本文只涉及线程部分,线程池下篇讲</strong>):</p><p><img src="/img/remote/1460000040038041" alt="" title=""></p><h2>进程和线程</h2><h3>从线程到进程</h3><p>要说线程池,就不得不先讲讲线程,什么是线程?</p><blockquote><strong>线程</strong>(英语:thread)是<a href="https://link.segmentfault.com/?enc=sCr%2Bg2jcDJqpHXD1CQ5c0A%3D%3D.5%2FSIc6fIL7K99YEpmd2f1SGaC7YYWkbboG4o5udoxxSWtDcYRTzNFT3JXqHDV6Ii" rel="nofollow">操作系统</a>能够进行运算<a href="https://link.segmentfault.com/?enc=6lExpPeFNAhUEa7xkDpVtg%3D%3D.jGIgV033oPQW8vPgxQoB54QwiV7Dz41njMmnSgaps4hiqrQUnOxnFIShSZJvgsMZ" rel="nofollow">调度</a>的最小单位。它被包含在<a href="https://link.segmentfault.com/?enc=aASiO7N1eGI%2Boc9bA1mVjQ%3D%3D.KbPnZ5yW%2BQ0xB8Hs75NXX2g%2FPqv4wBKKFs5h8cHOVRZbceEqaCVCHsujm1faxCwZ" rel="nofollow">进程</a>之中,是<a href="https://link.segmentfault.com/?enc=kePrCSTcqpHQ2BQmNUHgBQ%3D%3D.MmMi4vVpdG5NzVA7TPxLydr50LNrtP0P8g7YjUOvhE9tbggB6Lkwb7ihg75NrL0E" rel="nofollow">进程</a>中的实际运作单位。</blockquote><p>那么问题来了,进程又是什么?</p><blockquote>进程是操作系统中进行保护和资源分配的基本单位。</blockquote><p>是不是有点懵,进程摸得着看得见么?具体怎么表现?打开<code>Windows</code>的任务管理器或者<code>Mac</code>的活动监视器,就可以看到,基本每一个打开的<code>App</code>就是一个进程,但是并不是一定的,<strong>一个应用程序可能存在多个进程</strong>。</p><p>比如下面的<code>Typora</code>就显示了两个进程,每个进程后面有一个<code>PID</code>是唯一的标识,也是由系统分配的。除此之外,每个进程都可以看到有多少个线程在执行,比如微信有<code>32</code>个线程在执行。<strong>重要的一句话:</strong>一个程序运行之后至少有一个进程,一个进程可以包含多个线程。</p><p><img src="https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210508225417275.png" alt="image-20210508225417275" style="zoom:50%;" /></p><h3>为什么需要进程?</h3><p>程序,就是指令的集合,指令的集合说白了就是文件,让程序跑起来,在执行的程序,才是进程。程序是静态的描述文本,而进程是程序的一次执行活动,是动态的。进程是拥有计算机分配的资源的运行程序。</p><p>我们不可能一个计算机只有一个进程,就跟我们全国不可能只有一个市或者一个部门,计算机是一个庞然大物,里面的运转需要有条理,就需要按照功能划分出比较独立的单位,分开管理。每个进程有自己的职责,也有自己的独立内存空间,不可能混着使用,要是所有的程序共用一个进程就会乱套。</p><p><strong>每个进程,都有各自独立的内存,进程之间内存地址隔离,进程的资源,比如:代码段,数据集,堆等等,还可能包括一些打开的文件或者信号量,这都是每个进程自己的数据。</strong>同时,由于进程的隔离性,即使有一个程序的进程出现问题了,一般不会影响到其他的进程的使用。</p><p>进程在Linux系统中,进程有一个比较重要的东西,叫进程控制块(<code>PCB</code>),仅做了解:</p><p><code>PCB</code>是进程的唯一标识,由链表实现,是为了动态的插入以及删除,创建进程的时候,生成一个<code>PCB</code>,进程结束的时候,回收这个<code>PCB</code>。<code>PCB</code>主要包括以下的信息:</p><ul><li>进程状态</li><li>进程标识信息</li><li>定时器</li><li>用户可见的寄存器,控制状态寄存区,栈指针等等。</li></ul><h5>进程怎么切换的呢?</h5><p>先明白计算机里面的一个事实:<strong>CPU运转得超级无敌快</strong>,快到其他的只有寄存器差不多能匹配它的速度,但是很多时候我们需要从磁盘或者内存读或者写数据,这些设备的速度太慢了,与之相差太远。(<strong>如果不特殊说明,默认是单核的CPU</strong>)</p><p>假设一个程序/进程的任务执行一段时间,要写磁盘,写磁盘不需要<code>CUP</code>进行计算,那<code>CPU</code>就空出来了,但是其他的程序也不能用,<code>CPU</code>就干等着,等到写完磁盘再接着执行。这多浪费,<code>CPU</code>又不是这个程序一家的,其他的应用也要使用。<code>CPU</code>你不用的时候,总有别人需要用。</p><p>所以<code>CPU</code>资源需要调度,程序<code>A</code>不用的时候,可以切出来,让程序<code>B</code>去使用,但是程序<code>A</code>切回来的时候怎么保证它能够接着之前的位置继续执行呢?这时候不得不提<strong>上下文</strong>的事。</p><p>当程序<code>A</code>(假设为单进程)放弃<code>CPU</code>的时候,需要保存当前的上下文,<strong>何为上下文?</strong>也就是除了<code>CPU</code>之外,寄存器或者其他的状态,就跟犯罪现场一样,需要拍个照,要不到时候别的程序执行完之后,怎么知道接下来怎么执行程序<code>A</code>,之前执行到哪一步了。<strong>总结一句话:保存当前程序的执行状态。</strong></p><p>上下文切换一般还涉及缓存的开销,也就是缓存会失效,一般执行的时候,CPU会缓存一些数据方便下次更快的执行,一旦进行上下文切换,原来的缓存就失效了,需要重新缓存。</p><p>调度一般有两种(一般是按照线程维度来调度),<code>CPU</code>的时间被分为特别小的时间片:</p><ul><li>分时调度:每个线程或者进程轮流的使用<code>CPU</code>,平均时间分配到每个线程或者进程。</li><li>抢占式调度:优先级高的线程/进程立即抢占下一个时间片,如果优先级相同,那么随机选择一个进程。</li></ul><p><strong>时间片超级短,CPU超级快,给我们无比丝滑的感觉,就像是多个任务在同时进行</strong></p><p>我们现在操作系统或者其他的系统,基本都是抢占式调度,为什么?</p><p>因为如果使用分时调度,很难做到实时响应,当后台的聊天程序在进行网络传输的时候,分配予它的时间片还没有使用完,那我点击浏览器,是没有办法实时响应的。除此之外,如果前面的进程挂了,但是一直占有<code>CPU</code>,那么后面的任务将永远得不到执行。</p><p>由于<code>CPU</code>的处理能力超级快,就算是单核的<code>CPU</code>,运行着多个程序,多个进程,经过抢占式的调度,每一个程序使用的时候都像是独享了<code>CPU</code>一样顺滑。进程有效的提高了<code>CPU</code>的使用率,但是进程在上下文切换的时候是存在着一定的成本的。</p><h3>线程和进程什么关系?</h3><p>前面说了进程,那有了进程,为啥还要线程,多个应用程序,假设我们每个应用程序要做<code>n</code>件事,就用<code>n</code>个进程不行么?</p><p><strong>可以,但是没必要。</strong></p><p>进程一般由程序,数据集合和进程控制块组成,同一个应用程序一般是需要使用同一个数据空间的,要是一个应用程序搞很多个进程,就算有能力做到数据空间共享,进程的上下文切换都会消耗很多资源。(一般一个应用程序不会有很多进程,大多数一个,少数有几个)</p><p>进程的颗粒度比较大,每次执行都需要上下文切换,如果同一个程序里面的代码段<code>A</code>,<code>B</code>,<code>C</code>,做不一样的东西,如果分给多个进程去处理,那么每次执行都有切换进程上下文。这太惨了。<strong>一个应用程序的任务是一家人,住在同一个屋子下(同一个内存空间),有必要每个房间都当成每一户,去派出所登记成一个户口么?</strong></p><p>进程缺点:</p><ul><li>信息共享难,空间独立</li><li>切换需要<code>fork()</code>,切换上下文,开销大</li><li>只能在一个时间点做一件事</li><li>如果进程阻塞了,要等待网络传过来数据,那么其他不依赖这个数据的任务也做不了</li></ul><p>但是有人会说,那我一个应用程序有很多事情要做,总不能只用一个进程,所有事情都等着它来处理啊?那不是会阻塞住么?</p><p>确实啊,单独一个进程处理不了问题,那么我们<strong>把进程分得更小</strong>,里面分成很多线程,一家人,每个人都有自己的事情做,那我们每个人就是一个线程,一家人就是一个进程,这样岂不是更好么?</p><p>进程是描述CPU时间片调度的时间片段,但是线程是更细小的时间片段,两者的颗粒度不一样。<strong>线程可以称为轻量级的进程</strong>。其实,线程也不是一开始就有的概念,而是随着计算机发展,对多个任务上下文切换要求越来越高,随之抽象出来的概念。<br>$$进程时间段 = CPU加载程序上下文的时间 + CPU执行时间 + CPU保存程序上下文的时间$$</p><p>$$
线程时间段 = CPU加载线程上下文的时间 + CPU执行时间 + CPU保存线程上下文的时间$$
**最重要的是,进程切换上下文的时间远比线程切换上下文的时间成本要高**,如果是同一个进程的不同线程之间抢占到`CPU`,切换成本会比较低,因为他们**共享了进程的地址空间**,线程间的通信容易很多,通过共享进程级全局变量即可实现。
况且,现在多核的处理器,让不同进程在不同核上跑,进程内的线程在同个核上做切换,尽量减少(不可以避免)进程的上下文切换,或者让不同线程跑在不同的处理器上,进一步提高效率。
进程和线程的模型如下:
![image-20210509163642149](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210509163642149.png)
### 线程和进程的区别或者优点
- 线程是程序执行的最小单位,进程是操作系统分配资源的最小单位。
- 一个应用可能多个进程,一个进程由一个或者多个线程组成
- 进程相互独立,通信或者沟通成本高,在同一个进程下的线程共享进程的内存等,相互之间沟通或者协作成本低。
- 线程切换上下文比进程切换上下文要快得多。
## 线程有哪些状态
现在我们所说的是`Java`中的线程`Thread`,一个线程在一个给定的时间点,只能处于一种状态,这些状态都是虚拟机的状态,不能反映任何操作系统的线程状态,一共有六种/七种状态:
- `NEW`:创建了线程对象,但是还没有调用`Start()`方法,还没有启动的线程处于这种状态。
- `Running`:运行状态,其实包含了两种状态,但是`Java`线程将就绪和运行中统称为可运行
- `Runnable`:就绪状态:创建对象后,调用了`start()`方法,该状态的线程还位于可运行线程池中,等待调度,获取`CPU`的使用权
- 只是有资格执行,不一定会执行
- `start()`之后进入就绪状态,`sleep()`结束或者`join()`结束,线程获得对象锁等都会进入该状态。
- `CPU`时间片结束或者主动调用`yield()`方法,也会进入该状态
- `Running` :获取到`CPU`的使用权(获得CPU时间片),变成运行中
- `BLOCKED` :阻塞,线程阻塞于锁,等待监视器锁,一般是`Synchronize`关键字修饰的方法或者代码块
- `WAITING` :进入该状态,需要等待其他线程通知(`notify`)或者中断,一个线程无限期地等待另一个线程。
- `TIMED_WAITING` :超时等待,在指定时间后自动唤醒,返回,不会一直等待
- `TERMINATED` :线程执行完毕,已经退出。如果已终止再调用start(),将会抛出`java.lang.IllegalThreadStateException`异常。
![image-20210509224848865](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210509224848865.png)
可以看到`Thread.java`里面有一个`State`枚举类,枚举了线程的各种状态(`Java`线程将**就绪**和**运行中**统称为**可运行**):
```Java
public enum State {
/**
* 尚未启动的线程的线程状态。
*/
NEW,
/**
* 可运行线程的线程状态,一个处于可运行状态的线程正在Java虚拟机中执行,但它可能正在等待来自操作系统(如处理器)的其他资源。
*/
RUNNABLE,
/**
* 等待监视器锁而阻塞的线程的线程状态。
* 处于阻塞状态的线程正在等待一个监视器锁进入一个同步的块/方法,或者在调用Oject.wait()方法之后重新进入一个同步代码块
*/
BLOCKED,
/**
* 等待线程的线程状态,线程由于调用其中一个线程而处于等待状态
*/
WAITING,
/**
* 具有指定等待时间的等待线程的线程状态,线程由于调用其中一个线程而处于定时等待状态。
*/
TIMED_WAITING,
/**
* 终止线程的线程状态,线程已经完成执行。
*/
TERMINATED;
}
```
除此之外,Thread类还有一些属性是和线程对象有关的:
- long tid:线程序号
- char name[]:线程名称
- int priority:线程优先级
- boolean daemon:是否守护线程
- Runnable target:线程需要执行的方法
介绍一下上面图中讲解到线程的几个重要方法,它们都会导致线程的状态发生一些变化:
- `Thread.sleep(long)`:调用之后,线程进入`TIMED_WAITING`状态,但是不会释放对象锁,到时间苏醒后进入`Runnable`就绪状态
- `Thread.yield()`:线程调用该方法,表示放弃获取的`CPU`时间片,但是不会释放锁资源,同样变成就绪状态,等待重新调度,不会阻塞,但是也不能保证一定会让出`CPU`,很可能又被重新选中。
- `thread.join(long)`:当前线程调用其他线程`thread`的`join()`方法,当前线程不会释放锁,会进入`WAITING`或者`TIMED_WAITING`状态,等待thread执行完毕或者时间到,当前线程进入就绪状态。
- `object.wait(long)`:当前线程调用对象的`wait()`方法,当前线程会释放获得的对象锁,进入等待队列,`WAITING`,等到时间到或者被唤醒。
- `object.notify()`:唤醒在该对象监视器上等待的线程,随机挑一个
- `object.notifyAll()`:唤醒在该对象监视器上等待的所有线程
## 单线程和多线程
单线程,就是只有一条线程在执行任务,串行的执行,而多线程,则是多条线程同时执行任务,所谓同时,并不是一定真的同时,如果在单核的机器上,就是假同时,只是看起来同时,实际上是轮流占据CPU时间片。
下面的每一个格子是一个时间片(每一个时间片实际上超级无敌短),不同的线程其实可以抢占不同的时间片,获得执行权。**时间片分配的单位是线程,而不是进程,进程只是容器**
![image-20210511002923132](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210511002923132.png)
### 如何启动一个线程
其实`Java`的`main()`方法本质上就启动了一个线程,但是**本质上不是只有一个线程**,看结果的 5 就大致知道,其实一共有 5 个线程,主线程是第 5 个,大多是**后台线程**:
```java
public class Test {
public static void main(String[] args) {
System.out.println(Thread.currentThread().toString());
}
}
```
执行结果:
```txt
Thread[main,5,main]
```
可以看出上面的线程是`main`线程,但是要想创建出有别于`main`线程的方式,有四种:
- 自定义类去实现`Runnable`接口
- 继承`Thread`类,重写`run()`方法
- 通过`Callable`和`FutureTask`创建线程
- 线程池直接启动(本质上不算是)
#### 实现Runnable接口
```java
class MyThread implements Runnable{
@Override
public void run(){
System.out.println("Hello world");
}
}
public class Test {
public static void main(String[] args) {
Thread thread = new Thread(new MyThread());
thread.start();
System.out.println("Main Thread");
}
}
```
运行结果:
```txt
Main Thread
Hello world
```
如果看底层就可以看到,构造函数的时候,我们将`Runnable`的实现类对象传递进入,会将`Runnable`实现类对象保存下来:
```java
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}
```
然后再调用`start()`方法的时候,会调用原生的`start0()`方法,原生方法是由`c`或者`c++`写的,这里看不到具体的实现:
```java
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
// 正式的调用native原生方法
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
```
`Start0()`在底层确实调用了`run()`方法,并且不是直接调用的,而是启用了另外一个线程进行调用的,这一点在代码注释里面写得比较清楚,在这里我们就不展开讲,我们将关注点放到`run()`方法上,调用的就是刚刚那个`Runnable`实现类的对象的`run()`方法:
```java
@Override
public void run() {
if (target != null) {
target.run();
}
}
```
#### 继承Thread类
由于`Thread`类本身就实现了`Runnable`接口,所以我们只要继承它就可以了:
```java
class Thread implements Runnable {
}
```
继承之后重写run()方法即可:
```java
class MyThread extends Thread{
@Override
public void run(){
System.out.println("Hello world");
}
}
public class Test {
public static void main(String[] args) {
Thread thread = new Thread(new MyThread());
thread.start();
System.out.println("Main Thread");
}
}
```
执行结果和上面的一样,其实两种方式本质上都是一样的,一个是实现了`Runnable`接口,另外一个是继承了实现了`Runnable`接口的`Thread`类。两种都没有返回值,因为`run()`方法的返回值是`void`。
#### Callable和FutureTask创建线程
要使用该方式,按照以下步骤:
- 创建`Callable`接口的实现类,实现`call()`方法
- 创建`Callable`实现类的对象实例,用`FutureTask`包装Callable的实现类实例,包装成`FutureTask`的实例,`FutureTask`的实例封装了`Callable`对象的`Call()`方法的返回值
- 使用`FutureTask`对象作为`Thread`对象的`target`创建并启动线程,`FutureTask`实现了`RunnableFuture`,`RunnableFuture`继承了`Runnable`
- 调用`FutureTask`对象的`get()`来获取子线程执行结束的返回值
```java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) throws Exception{
Callable<String> callable = new MyCallable<String>();
FutureTask<String> task = new FutureTask<String>(callable);
Thread thread = new Thread(task);
thread.start();
System.out.println(Thread.currentThread().getName());
System.out.println(task.get());
}
}
class MyCallable<String> implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(
Thread.currentThread().getName() +
" Callable Thread");
return (String) "Hello";
}
}
```
执行结果:
```txt
main
Thread-0 Callable Thread
Hello
```
其实这种方式本质上也是`Runnable`接口来实现的,只不过做了一系列的封装,但是不同的是,它可以实现返回值,如果我们期待一件事情可以通过另外一个线程来获取结果,但是可能需要消耗一些时间,比如异步网络请求,其实可以考虑这种方式。
`Callable`和`FutureTask`是后面才加入的功能,是为了适应多种并发场景,`Callable`和`Runnable`的区别如下:
- `Callable` 定义方法是`call()`,`Runnable`定义的方法是`run()`
- `Callable`的`call()`方法有返回值,`Runnable`的`run()`方法没有返回值
- `Callable`的`call()`方法可以抛出异常,`Runnable`的`run()`方法不能抛出异常
#### 线程池启动线程
本质上也是通过实现`Runnable`接口,然后放到线程池中进行执行:
```java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " : hello world");
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
MyThread thread = new MyThread();
executorService.execute(thread);
}
executorService.shutdown();
}
}
```
执行结果如下,可以看到五个核心线程一直在执行,没有规律,循环十次,但是并没有创建出十个线程,这和线程池的设计以及参数有关,后面会讲解:
```txt
pool-1-thread-5 : hello world
pool-1-thread-4 : hello world
pool-1-thread-5 : hello world
pool-1-thread-3 : hello world
pool-1-thread-2 : hello world
pool-1-thread-1 : hello world
pool-1-thread-2 : hello world
pool-1-thread-3 : hello world
pool-1-thread-5 : hello world
pool-1-thread-4 : hello world
```
总结一下,启动一个线程,其实本质上都离不开`Runnable`接口,不管是继承还是实现接口。
### 多线程可能带来的问题
- 消耗资源:上下文切换,或者创建以及销毁线程,都是比较消耗资源的。
- 竞态条件:多线程访问或者修改同一个对象,假设自增操作`num++`,操作分为三步,读取`num`,`num`加1,写回`num`,并非原子操作,那么多个线程之间交叉运行,就会产生不如预期的结果。
- 内存的可见性:每个线程都有自己的内存(缓存),一般修改的值都放在自己线程的缓存上,到刷新至主内存有一定的时间,所以可能一个线程更新了,但是另外一个线程获取到的还是久的值,这就是不可见的问题。
- 执行顺序难预知:线程先`start()`不一定先执行,是由系统决定的,会导致共享的变量或者执行结果错乱
## 并发与并行
并发是指两个或多个事件在同一时间间隔发生,比如在同`1s`中内计算机不仅计算`数据1`,同时也计算了`数据2`。但是两件事情可能在某一个时刻,不是真的同时进行,很可能是抢占时间片就执行,抢不到就别人执行,但是由于时间片很短,所以在1s中内,看似是同时执行完成了。当然前面说的是单核的机器,并发不是真的同时执行,但是多核的机器上,并发也可能是真的在同时执行,只是有可能,这个时候的并发也叫做并行。
![image-20210511012516227](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210511012516227.png)
并行是指在同一时刻,有多条指令在多个处理器上同时执行,真正的在同时执行。
![image-20210511012723433](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210511012723433.png)
如果是单核的机器,最多只能并发,不可能并行处理,只能把CPU运行时间分片,分配给各个线程执行,执行不同的线程任务的时候需要上下文切换。而多核机器,可以做到真的并行,同时在多个核上计算,运行。**并行操作一定是并发的,但是并发的操作不一定是并行的。**
### 关于作者
秦怀,公众号【**秦怀杂货店**】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。
[2020年我写了什么?](http://aphysia.cn/archives/2020)
[开源编程笔记](https://damaer.github.io/Coding/#/)
[150页的剑指Offer PDF领取](https://mp.weixin.qq.com/s?__biz=MzA3NTUwNzk0Mw==&tempkey=MTExNF9zZ2FPelJtWkNCdlZ6dTRuVThBSDdNc01JNFZuSTBrVlZWU0dCRk45dzlLVmx3SWx3NXlHVE5DWkRTSFBnNWVhRFV6RkNKOURjSmhUTExZeVp4QndwbEZ4Q2NfWUlzMzI2bDQzSm51TVJ4SE14QVhsUFIxSWJkcWtGQVhhLVVwZGRPZ0cwRHFDaGJvZ2pPeDM3NXdzcGF5N3A5bFdRaE9JU1Rpbi1Rfn4%3D&chksm=383018090f47911fd2458fe7c2ee89cbde7a7875dcba06d9f2e4daca191c7c0ab6409777f14d#rd)
</p>
【实战问题】-- 布隆过滤器的三种实践:手写,Redission以及Guava(2)
https://segmentfault.com/a/1190000039995197
2021-05-13T16:00:43+08:00
2021-05-13T16:00:43+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
2
<p>前面我们已经讲过布隆过滤器的原理<a href="https://link.segmentfault.com/?enc=1UrDsJOCLTD7WalRPygEjg%3D%3D.ps1xsVaChPdoYle%2Fs7J2mtrMF23eKp9sh7AeKaD1vid9LmqYBf3o6VfY3ahEVsXWxTk8VXGOuC2TyWW0GhifVAo%2BoDHnzq1c1maLCeHHWv0I7c7Y%2B8zT%2F%2FZ6ZAElqSMRmOx6b3OF4Pzzamtb7g0cyjLYg%2FsTWrdw0g%2FsznppmTg9Yorwdi3jGrdgf8x8xKQXvXS0PtJgMJMGMwAG2GUSg10EWnmRsy0Awg26dyHiGwwccey5kVfCP9QrM9dpJSXlVCKbzUeFCdXGlEQPFMgEVVVAosZW9JQ3IZil%2FYZ0WofMd28jIGWAos527%2FkfFbFd8vL2FrWwf%2FbRwM%2Bn%2Fic1fw%3D%3D" rel="nofollow">【实战问题】-- 缓存穿透之布隆过滤器(1)</a>,都理解是这么运行的,那么一般我们使用布隆过滤器,是怎么去使用呢?如果自己去实现,又是怎么实现呢?</p><p>[TOC]</p><h2>布隆过滤器</h2><p><strong>再念一次定义:</strong></p><p>布隆过滤器(<code>Bloom Filter</code>)是由布隆(<code>Burton Howard Bloom</code>)在 1970 年提出的,它实际上是由一个很长的二进制向量和一系列随机<code>hash</code>映射函数组成(说白了,就是用二进制数组存储数据的特征)。</p><p>譬如下面例子:有三个<code>hash</code>函数,那么“陈六”就会被三个<code>hash</code>函数分别<code>hash</code>,并且对位数组的长度,进行取余,分别hash到三个位置。</p><p><img src="/img/remote/1460000039724782" alt="" title=""></p><p>如果对原理还有不理解的地方,可以查看我的上一篇文章。</p><h2>手写布隆过滤器</h2><p>那么我们手写布隆过滤器的时候,首先需要一个位数组,在<code>Java</code>里面有一个封装好的位数组,<code>BitSet</code>。</p><p>简单介绍一下<code>BitSet</code>,也就是位图,里面实现了使用紧凑的存储空间来表示大空间的位数据。使用的时候,我们可以直接指定大小,也就是相当于创建出指定大小的位数组。</p><pre><code class="java">BitSet bits = new BitSet(size);</code></pre><p>同时,<code>BitSet</code>提供了大量的<code>API</code>,基本的操作主要包括:</p><ul><li>清空位数组的数据</li><li>翻转某一位的数据</li><li>设置某一位的数据</li><li>获取某一位的数据</li><li>获取当前的<code>bitSet</code>的位数</li></ul><p>下面就讲一下,写一个简单的布隆过滤器需要考虑的点:</p><ul><li>位数组的大小空间,需要指定,其他相同的时候,位数组的大小越大,<code>hash</code>冲突的可能性越小。</li><li>多个<code>hash</code>函数,我们需要使用<code>hash</code>数组来存,<code>hash</code>函数需要如何设置呢?为了避免冲突,我们应该使用多个不同的质数来当种子。</li><li>方法:主要实现两个方法,一个往布隆过滤器里面添加元素,另一个是判断布隆过滤器是否包含某个元素。</li></ul><p>下面是具体的实现,只是简单的模拟,不可用于生产环境,<code>hash</code>函数较为简单,主要是使用<code>hash</code>值得高低位进行异或,然后乘以种子,再对位数组大小进行取余数:</p><pre><code class="java">import java.util.BitSet;
public class MyBloomFilter {
// 默认大小
private static final int DEFAULT_SIZE = Integer.MAX_VALUE;
// 最小的大小
private static final int MIN_SIZE = 1000;
// 大小为默认大小
private int SIZE = DEFAULT_SIZE;
// hash函数的种子因子
private static final int[] HASH_SEEDS = new int[]{3, 5, 7, 11, 13, 17, 19, 23, 29, 31};
// 位数组,0/1,表示特征
private BitSet bitSet = null;
// hash函数
private HashFunction[] hashFunctions = new HashFunction[HASH_SEEDS.length];
// 无参数初始化
public MyBloomFilter() {
// 按照默认大小
init();
}
// 带参数初始化
public MyBloomFilter(int size) {
// 大小初始化小于最小的大小
if (size >= MIN_SIZE) {
SIZE = size;
}
init();
}
private void init() {
// 初始化位大小
bitSet = new BitSet(SIZE);
// 初始化hash函数
for (int i = 0; i < HASH_SEEDS.length; i++) {
hashFunctions[i] = new HashFunction(SIZE, HASH_SEEDS[i]);
}
}
// 添加元素,相当于把元素的特征添加到位数组
public void add(Object value) {
for (HashFunction f : hashFunctions) {
// 将hash计算出来的位置为true
bitSet.set(f.hash(value), true);
}
}
// 判断元素的特征是否存在于位数组
public boolean contains(Object value) {
boolean result = true;
for (HashFunction f : hashFunctions) {
result = result && bitSet.get(f.hash(value));
// hash函数只要有一个计算出为false,则直接返回
if (!result) {
return result;
}
}
return result;
}
// hash函数
public static class HashFunction {
// 位数组大小
private int size;
// hash种子
private int seed;
public HashFunction(int size, int seed) {
this.size = size;
this.seed = seed;
}
// hash函数
public int hash(Object value) {
if (value == null) {
return 0;
} else {
// hash值
int hash1 = value.hashCode();
// 高位的hash值
int hash2 = hash1 >>> 16;
// 合并hash值(相当于把高低位的特征结合)
int combine = hash1 ^ hash1;
// 相乘再取余
return Math.abs(combine * seed) % size;
}
}
}
public static void main(String[] args) {
Integer num1 = new Integer(12321);
Integer num2 = new Integer(12345);
MyBloomFilter myBloomFilter =new MyBloomFilter();
System.out.println(myBloomFilter.contains(num1));
System.out.println(myBloomFilter.contains(num2));
myBloomFilter.add(num1);
myBloomFilter.add(num2);
System.out.println(myBloomFilter.contains(num1));
System.out.println(myBloomFilter.contains(num2));
}
}</code></pre><p>运行结果,符合预期:</p><pre><code class="txt">false
false
true
true</code></pre><p>但是上面的这种做法是不支持预期的误判率的,只是可以指定位数组的大小。</p><p>当然我们也可以提供数据量,以及期待的大致的误判率来初始化,大致的初始化代码如下:</p><pre><code class="java"> // 带参数初始化
public BloomFilter(int num,double rate) {
// 计算位数组的大小
this.size = (int) (-num * Math.log(rate) / Math.pow(Math.log(2), 2));
// hsah 函数个数
this.hashSize = (int) (this.size * Math.log(2) / num);
// 初始化位数组
this.bitSet = new BitSet(size);
}</code></pre><h2>Redis实现</h2><p>平时我们可以选择使用<code>Redis</code>的特性于布隆过滤器,为什么呢?因为<code>Redis</code>里面有类似于<code>BitSet</code>的指令,比如设置位数组的值:</p><pre><code>setbit key offset value</code></pre><p>上面的<code>key</code>是键,<code>offset</code>是偏移量,<code>value</code>就是<code>1</code>或者<code>0</code>。比如下面的就是将key1 的第7位置为1。</p><p><img src="/img/remote/1460000039995199" alt="" title=""></p><p>而获取某一位的数值可以使用下面这个命令:</p><pre><code class="java">gitbit key offset</code></pre><p>借助<code>redis</code>这个功能我们可以实现优秀的布隆过滤器,但是实际上我们不需要自己去写了,<code>Redisson</code>这个客户端已经有较好的实现。<br>下面就是用法:<br>使用<code>maven</code>构建项目,首先需要导包到<code>pom.xml</code>:</p><pre><code class="xml"> <dependencies>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.2</version>
</dependency>
</dependencies></code></pre><p>代码如下,我使用的<code>docker</code>,启动的时候记得设置密码,运行的时候修改密码不起效果:</p><pre><code class="shell">docker run -d --name redis -p 6379:6379 redis --requirepass "password"</code></pre><p>实现的代码如下,首先需要连接上<code>redis</code>,然后创建<code>redission</code>,使用<code>redission</code>创建布隆过滤器,直接使用即可。(<strong>可以指定预计的数量以及期待的误判率</strong>)</p><pre><code class="java">import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class BloomFilterTest {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
config.useSingleServer().setPassword("password");
// 相当于创建了redis的连接
RedissonClient redisson = Redisson.create(config);
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("myBloomFilter");
//初始化,预计元素数量为100000000,期待的误差率为4%
bloomFilter.tryInit(100000000,0.04);
//将号码10086插入到布隆过滤器中
bloomFilter.add("12345");
System.out.println(bloomFilter.contains("123456"));//false
System.out.println(bloomFilter.contains("12345"));//true
}
}</code></pre><p>运行结果如下:值得注意的是,这是单台<code>redis</code>的情况,如果是<code>redis</code>集群的做法略有不同。<br><img src="/img/remote/1460000039995200" alt="" title=""></p><h2>Google GUAVA实现</h2><p><code>Google</code>提供的<code>guava</code>包里面也提供了布隆过滤器,引入<code>pom</code>文件:</p><pre><code class="xml"> <dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency></code></pre><p>具体的实现调用的代码如下,同样可以指定具体的存储数量以及预计的误判率:</p><pre><code class="java">import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class GuavaBloomFilter {
public static void main(String[] args) {
BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charsets.UTF_8),1000000,0.04);
bloomFilter.put("Sam");
System.out.println(bloomFilter.mightContain("Jane"));
System.out.println(bloomFilter.mightContain("Sam"));
}
}</code></pre><p>执行的结果如下,符合预期<br><img src="/img/remote/1460000039995201" alt="" title=""></p><p>上面三种分别是手写,<code>redis</code>,<code>guava</code>实践了布隆过滤器,只是简单的用法,其实<code>redis</code>和<code>guava</code>里面的实现也可以看看,有兴趣可以了解一下,我先立一个<code>Flag</code>。</p><h3>关于作者</h3><p>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=2MpMojWD9bZ3PoTIh2jNAw%3D%3D.1vWAPybXag6deh5fLx%2BZ7b7e6%2FYZO5eP%2B3DLcciXNg0%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=K2GTmhu5aZWGy%2FA%2FRRdRTw%3D%3D.FoC3sBF4aeZgFMrwdDxOJB3%2FVJ9IbGwSTHN5eGaYn76Axu%2FgIyCa0s1zCfkN53QA" rel="nofollow">开源编程笔记</a></p>
150页的剑指Offer解答PDF,它来了!!!
https://segmentfault.com/a/1190000039890584
2021-04-24T19:44:39+08:00
2021-04-24T19:44:39+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
4
<p>它来了!!!</p><p>终于整理出了第一版<strong>剑指Offer</strong>的PDF,主要以Java语言为主,一共67道题,100多页。</p><p><img src="/img/remote/1460000039890586" alt="" title=""><br><img src="/img/remote/1460000039890587" alt="" title=""><br><img src="/img/remote/1460000039890588" alt="" title=""></p><p><strong>领取方式</strong>如下(无套路直接获取百度网盘的 🔗 链接,如果链接失效可以直接找我):</p><p>【秦怀杂货店】公众号内发送:<strong>剑指Offer</strong></p><p><a href="https://link.segmentfault.com/?enc=ihynYY5a6XSJTha6HbWcXg%3D%3D.dFL8NMdBKUoXmbfThPrUqZl88vkgBwo8ysa%2BE5lbqDKPsv144Niwiov2uGn3thT2VBaKfEi3wXb4G522mVZx9Scdd5iPdO%2BYWeCwoML4uyxGjDEyoUTEdfldAOJ2J2KbKlkyyK%2B6w99LMVfUzXVwd6wRM9Gr%2F6xSQJBxTcpYvB8drJGjCjybbc%2BSQmhubFuqknUU5vah82nO0t4ssIc7bXIIYQI0CBQzZ72iFkBj2VlQ5%2FymfJr37W7IqRCBEWaQz4ClWmcdByxqcpz4X%2BmMxSIkO%2F%2FhQGxE2VfLgug2Tqi1WnHHCjvB1%2BPMiud%2B%2BjX0" rel="nofollow">刷题仓库:CodeSolution</a> ,具体的分类如下:</p><h2>数组</h2><p><a href="https://link.segmentfault.com/?enc=HOGYffjd8BJdQGY%2FZOYFAw%3D%3D.T4YyDkuymVyg%2BCz9EKno560ZyXaEdAQOnuni29yADzxHZdLkKs75Iyr%2BoWAvfg81v2dOwZwnMJeE580t7WUxg7LAvu4d35J6LZXZf9sbNGY9NqNVLDjkypFIwIhJhZ3IHWWPcy7eyRkYCqC9NB2i2zKTmLL5AjYoRup1HOnkwR6pBagjQ%2FI1FRo4%2F1A53r1fsYVoPoSHRI75l4k0jscUGYdpXRkfrC19R8SHER%2BcC5VJ%2BAGoYeF68FKh%2F%2BrFKarsw%2Fk0MvqvIRpQODBFEzKgh0wNgAARfa7Oig6SZyRwGr8fDxcuAeIDaVnp%2BCHp2p1pUqEatUgPRX5DqSNsTO3ozQ%3D%3D" rel="nofollow">剑指Offer(二)--二维数组中的查找</a> <br><a href="https://link.segmentfault.com/?enc=phoTTUkqil3aK5UeUDczeQ%3D%3D.Bf8QXBdEgpzDoXrXs6HqF0a%2FOJvbRJNV2WbYX0slrdJ3QflOshBu%2FeAvIzSkPGvI9CX4KTLXHea0lGCiuS8%2FhNbpUZ258MykwaROTK807g0wQkFZ18gkvrywMPS29JECfiP9Axw8LufKrHvM2Qps0O1S8iUQF41hPCOoTKldZiZrP%2BuLvLPKxeJO%2F5hKu3NTL8ex8e2bg5NEKuHLJxG2a4bJpCxuL0XyME6qaxQNO9B6wIVUHozJLYruZ7c42uOKmOWb9Aeub8lbp8SAnu%2Fr5g%3D%3D" rel="nofollow">剑指Offer(十三)--调整数组顺序使奇数位于偶数前面</a> <br><a href="https://link.segmentfault.com/?enc=%2BHj3XDdpLhR2BzQKyyNDlw%3D%3D.ZJwolrwZqrxgoU9QFHtkf0jPExNHbl%2BSfMey0Oty7tNxEV36z1ZjDueh04OVvT6Htniz%2B6HTs29o3Dh1jzOaIK82JK9G4SeKkE8%2BMeSALu%2FtRRfVMQg7WjKBr4Odg%2BHKbIycQKC2XNocgePo8EZ%2BJWSQbesXZUQOQCnbS4LDRB6azLZldGCNLqGfyZy71AcF9XSq8n1oCqZwl64XNdFsmflYe27x3R0uzgP9uSpBAdZ0Fs7Pmkq%2F0qw53IMY5jMSq4ajH%2BDEHsG5VKAwpW5vsg%3D%3D" rel="nofollow">剑指Offer(十九)-- 顺时针打印矩阵</a> <br><a href="https://link.segmentfault.com/?enc=81XVd3S7VFGvgLmE9D2sFQ%3D%3D.ioVSTqJ84rsYyOi5YW8jPNgzX6r3%2BdXopSYWFR6dI%2BTow9ZF%2BfsDi4hK4OXALK1n14boBsj74B%2BSpEbmkvGViGRTO%2Bp43iflekK9ASQQNXv%2Fpsf6wzobs6%2BQX3y3cG88vjoZJuynIEjELgZRqBgVsfYZZ6sQe32vADsdrxSulHPafT7sUNxJVIRZGMeG7%2FxKDw38EYBk5M%2BFccpvLdGmT%2BHrXdtECrgprxihyWRr0do7ZWIbgXFQ19Fy%2BUpcOt%2Fk2Dn9PlTs4T%2BR%2FSzEkPxxLQ%3D%3D" rel="nofollow">剑指Offer(二十八)-- 数组中次数出现超过一半的数字</a> <br><a href="https://link.segmentfault.com/?enc=XgXiucXfnAG0h4GuUdJyiA%3D%3D.3Jbw6pSGA10RYKlcdzicn3k7HZOrp98fFHCEQOwMftHar75zuBvJo0q7ZgicCsiHdYmlsYkpne%2Bbk8klgJQR8AydNSyn13Nwm9GtHCWTGVee5Evi%2FaqDvUn5osBFlfUV5NL8vEVviXi7k6sMsdG%2FSCeT2ddVDCI0A2G9Tdl6rRMlcaySsfSFq1HyDcyI8DtBvNUwTMpRp%2BxdUnxGCOGxo5TrjX6ccml2PPXQXYNN7pOMKlTPtBpoh1qEbyZP0LH8g2P2%2Bw%2FGr9vG%2FKsQZavNag%3D%3D" rel="nofollow">剑指Offer(三十)-- 连续子数组的最大和</a> <br><a href="https://link.segmentfault.com/?enc=00J8OnkrboyqBaMLeiEC7g%3D%3D.mRfqfpn1tXGAuhYGncMEICy3JXuLPXyjJgH48EsnWmgyUJ%2Bw%2B4zMtZJFWpZz4%2BObsZAMTdaVgyFIo9uAlck95cJssDO3kLQsQj%2FWMXeLwOyxU2m8kP1hYVFtO5Siul33r0UKOc2ivDR06co60fiCZHYt7X2%2F%2Fa90u4n6gIw%2BteutnY%2BcziuoIc2Ie7U1qbxmtSK3Zv208AJr%2FjfU99g45jV6tFj6T%2FDDXoBzD%2BJ2CvDscOAPp6HuRkQ9Ips50wUnO9LvowJ7%2BnH75ZCXHpcFPA%3D%3D" rel="nofollow">剑指Offer(三十五)-- 数组中的逆序对</a> <br><a href="https://link.segmentfault.com/?enc=wSLaEJd1D49XrnboCRk4vA%3D%3D.4%2BX0bK3Dbb2zbQuJPqQml%2FwQgo6BgB4Ms4dYUYRJXahcdTNBEIhsNfSFYEgctg0zeFLDV7jjocAADZbtK9vNXhqIfKF7tvblurGnpIgw4lx6NWnXVdHN2HSH22Fcx3wwsTdLuWwLf5WuNKPmbE1wKGrnCFfe4%2BM1zSJhjbsJy9gLaMNSxUyVqyF%2BPBELYU86eT3FpRj2FoDfkt8KbuCWBOvhOhe1fX1cQDkCFRNckEsQOp49rn6EqW0NMbWkMc2yqj18cQtdkwFOuw28VyA15g%3D%3D" rel="nofollow">剑指Offer(三十七)-- 数字在升序数组中出现的次数</a> <br><a href="https://link.segmentfault.com/?enc=bALQ2tby6KD8bcGrmrUK3w%3D%3D.8DfI4ivW%2FYIpS3IWlipA9N%2FpdWLxjJF5t9%2BhYrxtyxHBhF0z9i2RqedRV%2B9rl92YuCsV7eum1yCDo5wDXf%2BsjyNf1SVb6uWUQZN%2B3rUiY2RaMQpViEk3lL3Nmim9JsFUoW5p5jLp5n%2FUBRsICY9NIWnXagpgeGP0p43VajnAKg3QnnEdtwyt%2FaL%2BNqS10XmX9fkW2dKsgfFTfQ3BodDcL38RObmAYQ8RkJ%2BFUqwzxt8FK%2BaR2IkWOR%2BnDjVK3Fkg21AYMLTXCZ%2BNnt0zBfNrIA%3D%3D" rel="nofollow">剑指Offer(四十)-- 数组中只出现一次的数字</a> <br><a href="https://link.segmentfault.com/?enc=ISnD5ezxUValWUu6EUWNkw%3D%3D.UH6OlGA9ye3Irt3L9HlHz0mu91n6951YjUFbEdZP6qmlAyZhmip3HmxHPTSmc87%2Bl2ucBDJMeGneVm6VcLcfj7ni37t8f2SOZmfPUhWBsqWI5Egm6MmNvHygdecs%2BIXwzNW1EsWO7EOfsQ4joRsDxUpJ3mtNtMy91571APXwUDlX5INByx7ADJkaAEWKAu8AX5OOskNRbu36YwBPmKPX6RbBFtQHCG8vFd17cC9rKnZ9jgsgoIbG%2F0FgMTQIk6W9wj%2BW6jP9D0t8NrzQdN6ejg%3D%3D" rel="nofollow">剑指Offer(五十)-- 数组中重复的数字</a> <br><a href="https://link.segmentfault.com/?enc=mfNxLqKayOBvSn0rJnRUug%3D%3D.VcVnwQ1ihSLK7A%2FJ%2B3d8SHgt%2BFbqhv48rjSrZZshNR1ceHO%2FNPrigOSiNTg%2FGW35e6v4GLzRTjSZdXWSRG26wNYyd01oIPFG74Fdbx9LibOlG1LUthcNMTkKAI22yEmUT4sGFjDgXfyHCRbFqxniwUvag%2BM4tQ%2F8V04n97cZE0Ujxitvv7WC3SLPHEXqv%2FAdw9c2X%2FVApAVH02beGBQAP9%2FdAQy86Jq5O6sASJbDMyzHcXwHs83NcfBEcUhvc%2BRvkBgjJj3enaI4vAIUnUn4Yw%3D%3D" rel="nofollow">剑指Offer(五十一)-- 构建乘积数组</a></p><h2>字符串</h2><p><a href="https://link.segmentfault.com/?enc=OxrtldxkoLI%2B7GLaUJ3R4A%3D%3D.hQwxgbNRynvXDoxUPTQNLXk1ujSyPow4EwMT1yNycUe%2BJqxWtH2b0WLKi%2FfPk3zegf%2FXi3TXdFFtfDcpy5f20Wz%2FLIu57d91GmyNoP807b7BtFL%2B6ImOqelGRimljavkPdMJ3mnep3FFdyZN0jOLF2ywl5iNx%2Bihm0YyAcUZa%2F9xzxdYggWAg3hO%2FvpyQamU%2BLg03VjtsHWZFYJW3j%2B4sroE4vk9GPb%2BOcOsF%2FGJZrzACVQPJGLx35k17vxaaLOOq5HQGDkaZXSAZ0ym36%2FCOg%3D%3D" rel="nofollow">剑指Offer(二)-- 替换空格</a> <br><a href="https://link.segmentfault.com/?enc=HH8P9S%2Fq6OG2CyEO6r8IXg%3D%3D.0XxVhtnTqNxop6N2HEjD0ccT3YWwepdsNlqbPNvTHraT8%2F9vvLWZEhq%2Fb9L1eRfMEY9noj70bMigKxsmfkORQBNLe6jFMPbBqjYkfwOkPFgIz1rqjkUJx599WPzNUIjFsgXm7QHi%2Fo0RjqSOODo%2BqN2S0Nss8Ti8HbUh8FkagVk7iKZQEQncc4IH4lZpsoukbTRl3s3En9JRnphEEVj9XFuQCOpUUKH7UMWvGZcRDmiNyY1xamwU4BmvCrzeOgwm20f1ziDqhxphVa4FNt5IJg%3D%3D" rel="nofollow">剑指Offer(二十七)-- 字符串的排序</a> <br><a href="https://link.segmentfault.com/?enc=3cCZo%2FnXBhQs1NQljDpNuQ%3D%3D.WIOToNdeT2gXeDXWoCoz9IN2dlhXBLoLkWDgb%2FkqcdbF1UcpplsyYa9o1skwIW6zwzytBxY9FGrKH1WU16FbuYsAejtpa8OxSVbMkQb627Slay9pKgkZB5kZPOHItXMGY4wYrvkI5XL7rUe%2FJO%2BW1EBny2P7QiXwxnt8DsOk5zAnHdWZMOsgG0bXEJ%2Bs3KI87WDrJ8hkEgtVPdERR0Rqzq9JB3kBAjEIBSPRSGCwb%2BOIqr2LyQew7DIhAQuB%2FfXvipYrsHkB5To59haqBzDTWQ%3D%3D" rel="nofollow">剑指Offer(三十四)-- 第一个只出现一次的字符</a> <br><a href="https://link.segmentfault.com/?enc=S0RX%2B3qOXZtKcjbc5wKemw%3D%3D.yKUPyOC4NtAvGxmzRnHVqbXViXxblPd3WL9oDXCgezvxYYY7T7vdXCrK9cxiFWdQXJAmALiBGXN663m0GpNrXtmLkHS2OE%2BoygbcxpqQy1Yt5AfiKtmcWE2vf9Ea%2BBoC8P0%2BvBB73FHfoDxenveGFJOws%2FkuJ3tB%2Bzkw829GfybPX6QysdQmCWwt6P5MoM7zbbrPtS7FjtVQayXmvwmJqPxNFgdC1wbvKOr63KPvVOb37V7wn1i%2FAVWBUnTvlod8h0n6LEZ5SIVM%2FV%2Fjsjhdlw%3D%3D" rel="nofollow">剑指Offer(四十三)--左旋转字符串</a> <br><a href="https://link.segmentfault.com/?enc=fgLR1ccB188x5zUs%2FPfwGg%3D%3D.Ii14b0%2B%2B4Y%2Bs5yYhydRUSIeuShMkUIIofmVwZQcH8kNNy8iig5B1VwCM1ibg%2FdOfD5%2BiTtisKaN9Y3vFDHIZ3KCGUpV6b53PxHky3EcZupawBwUdUoHeQKbfNqjThU4ZPHt6gIrVPck8%2FC9Q4PJuNXXLvsHpz17hQRksPOyP6lfX%2BCpTCEEFeG%2BRBKTSuheSJAg1t%2FMGWobIM7PJS42%2FlZ3Q%2BjQrVRRd78uD%2B%2BDNapI6L16pDF%2BW5CcSEK6dNyU8K84d7gyMGF31wyQoQQYeHQ%3D%3D" rel="nofollow">剑指Offer(四十四)-- 翻转单词序列</a> <br><a href="https://link.segmentfault.com/?enc=jAqDiL700yxMfvhesj2Sjg%3D%3D.iLQg%2FAVFOKzndzgAhewwbxL6t%2Bt7%2BfU%2FMXqV2RlgmfMtaRE0hhSThoe7Z9ix6e3BajBpknZ6CcabQ87ZKns3nc9D%2FMnxRGnMWKlnmS%2BXtjTTfClQMS%2F1OricLnO3oUu%2BGT7Liw052uSiIbQ51Q27fZxV%2BaOmUyDo%2FjA22HK55ANQYWV9bKXywFy9iDgxWKD43Y57hdsxQYlZ65LhUpFSnGdh77D2L1%2B%2BNp9m5O1IvOzdz57P%2FZbTtEWpygBb7p%2BKbSBeVdGY2nUjdMdZ2AMsIg%3D%3D" rel="nofollow">剑指Offer(四十四)-- 翻转单词序列(不调用API)</a> <br><a href="https://link.segmentfault.com/?enc=6A7jZ9YTj%2FWpmFm8j2Hc8g%3D%3D.7VA7SnRpbUAcwGUHGrRSJl9Qf1ClFh59TjvGmJU0DxpjtS0ouORIr7p1i7V5WLjYVtMgC37%2F%2Fy1bG5XqddFNAZaxILaB3ZPfXW75neIp6dZUoSJ7%2F6V9z56DF8drF4Rhu9FvC5iG%2BX1WiiV0PVeH5Iz0t6EjwEiDXD6iP33FdMqmZqfkbNn%2BZcesJLcu7u75ndvu1%2FX1Ho5mXc%2FGqTqbuPIv2OqN2ROnCjKiEny6xcj6hnQWptvOd1U9CYSWdVIwKl%2FRv493zj%2FnvSQMBnk5wA%3D%3D" rel="nofollow">剑指Offer(四十九)-- 把字符串转换为整数</a> <br><a href="https://link.segmentfault.com/?enc=1icvHvQhzAkGuyRhhebJEA%3D%3D.%2FuYUm9omENcLrXh8OQ%2FqKgnpo4kd9b7Lz3g6bklZXpKvr1eApRczG%2BILonN1KNce9djlnxSdCsIy9CiSPsoiPk8dA1%2FvBWJRJUbfIAJ9h8W50OB50cFeK93pN685YvUh%2FNQK9zgTXVwllMuivoKQK6AyftTh%2FL8jarh8UltKSvZR3QumqOZyA3sYqDxX6f72HdRizRLLQK3r0aYFelI%2FFfeewyWqqxQPFuLH5SI%2FBNRdXGZiLqT7XvEMTebA2NXWFzQv4wK1WbL%2BtLFKKO9D0w%3D%3D" rel="nofollow">剑指Offer(五十三)-- 表示数值的字符串</a> <br><a href="https://link.segmentfault.com/?enc=xXfGeZamK1pHxe2IrP3A4w%3D%3D.nG89Tyatbz9Fq%2Bc2PICvP24l6f1VmT4ths1RxV%2FHtcKrbh6gYoJdSuEtYFAOIR6TVd%2FgC4tMeuUcQ%2FgHHubVK6cAC844gXd8VMuOp4e6cvYj7YTnL6cALSbQ43hlIj%2FahDx75xzW0gYGVFaAkly2CJG1NxIMmMQQvjVKSSNxEhjtns2cLhpWADfvW%2F0zl0x%2FXqhrwSoAliKxarHQDPSQJZ6T2ZRIw9SKAd1AxOcsGjpCVHRV1MO63mbDawQ0vDcqiYRO4E0IxYVE6O8MdFlt8g%3D%3D" rel="nofollow">剑指Offer(五十四)-- 字符流中第一个不重复的字符</a> <br><a href="https://link.segmentfault.com/?enc=7dPb081to9JV91w4n%2BmSZQ%3D%3D.M1CSaC4nXk1OH3S8IhPHYxZ7PC0tPOlUMhny1g91vTYP%2FkEalSKgUzjzPAgwt7aOk4s6xbYNdEWn8u019cK%2FX5RPCQKvGMw3AViEHN%2BW2RNccZOmphh6gaPTRNzmGtdvDju6IiWpEUaHhG4elrpkMl0WrL31CrS8oOd8fei1P1zXcrdhfDQvj72BwXYMOz11mHhGYb7Nx1hJtbhdstmDDvlVexzgkMCuk47LTD%2FUEsvtJFLemlbXWMUFp%2BiVfy3yvNTcC8JaPVL%2BZYLbUL2%2BDA%3D%3D" rel="nofollow">剑指Offer(六十四)-- 滑动窗口最大值</a></p><h2>链表</h2><p><a href="https://link.segmentfault.com/?enc=hP9PLD3QXHTkth6b7LNbjQ%3D%3D.c318rR0dODcFJlGbDt%2ByRtb7ehcvoc73fBU%2FqeYylvVkxAbM%2Bpp4zaWB%2BFpjbyQjvOU0Ni2E1y9HYRzXypHb%2BbQBDAy15b04%2Fb5I5xlcnc3VaWn%2FnxrvEXYG53qc%2B7w%2BIEl%2BMWKi22ck8AkzDRFN7alYjVWnQ7eLgflmmwzzM17pFTHPDZ5qx%2B2wlpi9hYOuvEnDNvjp5maz1NGJwnWMIJkgwFCS6TDjGco%2Bph8OYeGH8DaB5V0Onw8hNCPi5nRTYqP5h52CQ2lot7H8VRuUXw%3D%3D" rel="nofollow">剑指Offer(三)-- 从尾到头打印链表</a> <br><a href="https://link.segmentfault.com/?enc=g5QDja1oydLL7QhuFLfR4Q%3D%3D.t%2FSo9%2Fh5%2BcrUBOc%2BxgTYyRuuSo6MGuYbRkhPmGDRqYHcf5qn98Yvb30gprGVX6O2dLQsOkNn28OCLFyw6mG5m5qQUOfcscbAMkMSdrS%2Ffj3k%2Fr1F6RQzzsszijXflxRv9YgkJnLx2E8KId6tW8wjidfgzoCz6RY4RPGHrER0YgW%2BFO4uO5v0c9T8p1mh68Q%2FiWYJAn%2BLFc3cqpFeT97htaYoX7A%2FOo2ctHmhgkIfL4id%2Fsjr7c9z0NmHcieUJvyTBrGIZV3DvS38kcxuW7Fvqg%3D%3D" rel="nofollow">剑指Offer(十四)--链表中倒数第k个节点</a> <br><a href="https://link.segmentfault.com/?enc=Glb8b1pND2rGh%2Baf6iS5pw%3D%3D.vURIGbaN5X6QciTTMC%2BalanvniSxTUJzAilZ9PO7jcUMj3Jrt94KDmwlBoNWz3L6wIs2ao3oHRSUe8zvv%2FQk%2FzBZYEc4cnxCfMKoSl7TTEUjoCqQnbixnYYMhcmev9M8m7MNAyIzo%2FGklfjdLwZp4dwBsj3SpBVOxEvf%2BH4pp5%2FvBk6fcbg%2BqaXI68ZdZH45MVEkeNzOPn1wMqQXwgJ5IUsbOyP60feyExKeZubWW76Q%2B2rlkbWNGNiZCbALc8J4UesCskk5Q%2Bw5yznmjM688A%3D%3D" rel="nofollow">剑指Offer(十五)-- 反转链表</a> <br><a href="https://link.segmentfault.com/?enc=rKJaPdT2zjDn2U7mJkNRow%3D%3D.qq04aDSaBEeCAE0wvJYNdvmMio809Nrh9BIneauCqfzIt2PzDa527rhVw4ucrasNn%2FiD3%2Fh%2FwV0sKT%2BP6x3FRSHhYAgbB8%2FJl2T8gOGdTBJT6Y2fo62yBIRHr1FnCnH%2BTOdjq8ugaenNRbP9T9zSusgh3OcP20S%2BlxVOQozrjB%2BOQibdSK%2BXfcIsbEEqVrWqtqO%2BmxAdw7umTMpjDZSvwCd54qf3OoZwQefM%2BTUTO6BGzwzi7WkqSCAsrofSRQacaGdS1ehqCSKlQKB%2FGB%2BAoQ%3D%3D" rel="nofollow">剑指Offer(十六)-- 合并两个排序的链表</a> <br><a href="https://link.segmentfault.com/?enc=XpjGCEwONort%2FWNqCXqrOA%3D%3D.9F2tQCKWFFKd%2FFw5RDzW%2FLfrBijr5eaUwfOzgFxkVPSqbwdHLHiyysvqGpSWEDlwwZYYKnmSyn9MTvqMZhdFb1wukWPYMFvocvSf95Qm%2BsGQEuVcDMfGHqhEhdZNof5k%2Fw3dKruhheCnZjrM0GLkB%2FKhxKPlRr%2BJC3YSn8c%2B2ElCUZ41kwS%2B%2FEzxMNvDouyOb%2FwoTz40yF1xs9iIUYcxmlXp3yeZscARMpcmWnDvxOS%2Budc0JNOfI6GM6gofMsPGd95BKoB%2FRYKvxcNZsjOHeQ%3D%3D" rel="nofollow">剑指Offer(二十五)-- 复杂链表的复制</a> <br><a href="https://link.segmentfault.com/?enc=ZhpLroAbMfgxvlGDzUcDXA%3D%3D.DeSFuKt7soqhUq%2BeoAmIWfOxbS5ri8SGzlqw68I1GJlP0OuZ9MMlwFukDPqpynvH7p0QiJiwmeAGBHKjzMvwzAvptgJJD%2F%2B4hKZm8z03icXkWMOt3ZW8DGHXpUuOXzwj2q%2BwCwpr2RudpATkIumf9EJyGfNyK5pChCFdVTH6%2FZyh0AWiiQ47F76Nzd3UXQlY%2BuQZhhHMq6GS%2BKznGDd4Xr7%2FmZA0s2RixdzeTw9zMhFPSDIod4ycx6OHCnA1Bi5BBr6qB6mhQiOTfELI7nVsiw%3D%3D" rel="nofollow">剑指Offer(三十六)-- 两个链表的第一个公共节点</a> <br><a href="https://link.segmentfault.com/?enc=k%2BO%2ByiOg291nTA6BVRzm2g%3D%3D.tFTLn5SddFoc6%2FYEvOkyK%2BmEl3fnLvWoeew4oCMOvzgvdZSuP6WtbNaZEKZpwqd0hKsj40wPvYx2O0yMU741LA6Zw0YdjZqg9d%2FfOQ3I2s8Ae5rJUnSKRgQNcFbtMY1FLKWCKa70UP711I%2F9NA8nJ8FKDhMigOM%2Fg%2Fa1C8DNf2SGnpo13f39pmzOvzzqObxYcoWps3VOONy2NZKNQoATIw2o6V7pZZB4g5VglRzpF0kIRn0pC4zJ%2BcXDZlIc8E2A1AMMe%2FAJYJJZ03Mw3DRpag%3D%3D" rel="nofollow">剑指Offer(五十五)-- 链表中环的入口节点</a> <br><a href="https://link.segmentfault.com/?enc=2WQ9E8N2QODb0STBFx%2Fabg%3D%3D.VQc9hlHa9VtSRnv%2FVEBW4r33etsoN9aAk7pV4lsHrgMbz0eJoRiAyuwgrxO%2BBOj67SXncVUF6lz8NRLDXS0NzxxbAOLsaVqIiZB2se2cen64gu%2Biv1Qc7Rrc05xWbg0DZD%2BkVoycbM%2FtqKK4%2FYAlmZzUPix6D4DwJR0H9VmIaoTtTzSoqsQVBvLrOoqK4asBIHgvqxWGF0dUKjNZc%2BFLSzpZ4uiaqNoyztF%2BpVXhdZftnpmliLFqqYEW%2FdfIeKZrARr3hictgqZwbt2xTfpBww%3D%3D" rel="nofollow">剑指Offer(五十六)-- 删除链表中重复的元素</a></p><h2>栈和队列</h2><p><a href="https://link.segmentfault.com/?enc=y6vQaIaAVXVnj7yCnmFsBg%3D%3D.3Y361BzBoIptuUrTF9bL5GyXiMp38pVxjg9UdQQiVjBA%2FtQLeebckhI%2BhTZbYIIhtGIMMXplAUaBoAlATtvc9HflNaYbWO%2Bs88R48aC%2BxWQSQiBwE3a0FosF4%2BFaWN8guTJfXyNWt69pa5vy1OI4c3d7qvIe5nsE%2FvSf5qTcOcBcbmFCg6UsB45zezEBLdz9LMLiX6kCsza1ymzwt%2FrF0ZP90ActAEc0PRbG128SEmmJZ8rJk5MeDw8TKMEyxVdFsvIJXM8Tfr8IAK4r%2FcQ91w%3D%3D" rel="nofollow">剑指Offer(八)--用两个栈实现队列</a> <br><a href="https://link.segmentfault.com/?enc=y6%2BS0HSRQMb1ONfMoiDTew%3D%3D.qByXsfJtfUwlj0BgqSBTtYVHEde8l5gNSNirWwbTc716mhoDyLBiA4Lrr7a8JfO5XVdzhTiyI8H3iqJV7bf44owm1ygfjrt5J0oarC5VleGytNhSvsLbKG47wSA5FxubT8Aj5RYL97OtIwDpH2sUWa%2F4jVyBdH8oIL1YaSjJiHabF0ptppZRsAiYRxFNB83UfazarHY8Jt061S6p8oIbh3vtc33NWo7qvwer7Kw4hZ3wpLSMgFsrkqIvRwb3JpxPfuo5Jt2w0Er72l15a5LSXw%3D%3D" rel="nofollow">剑指Offer(二十)-- 包含min函数的栈</a> <br><a href="https://link.segmentfault.com/?enc=RaC6uxz993yp9wkhE85UNQ%3D%3D.T5ZmBAPRlAZTTdxhY8%2ByAJPOykeb7xLp8RDYnDXBGcl8UfmSh4rUiZ24gj5rrqIf8DxmL6PL0675Z6OLN%2F7OTp%2FwTFqwOYRBVtc71CrbUfZP0H%2FAF8Nwy5pWCC1XR6VVb6H7sPa0wWKWyRbRRRyP0twry6urUrP3Xn0TLzZa95HUFvSK95Y1h3PwVLDqNTZi5UtqXcTrLZxxWGVZMXiXJpisMOriy6j8%2Fn19oByV07FPaY3Y8Ghx3QnKGL60nouTj9bDzznv%2BoyJ0N%2FWIVDQ0Q%3D%3D" rel="nofollow">剑指Offer(二十一)-- 栈的压入,弹出序</a></p><h2>堆</h2><p><a href="https://link.segmentfault.com/?enc=S35wBV0XxAxVNmCCRZHanA%3D%3D.Div383JfK8oKkEtyHVFVzCd2JzAf3fGL%2BqmM%2F6nowv%2BMojj6XWhbzSpNF4ppLxZU1KgQbtmgUb5PvbE1Oy7kBml%2FIh7N8DZjY%2FFONSZvDE2p5sZ%2B8X5lv%2B7tKlAaF303JEx3%2FIL9KAJ6VeWl46KqcZRMavuFQASxR7Z9TrdarMb4xweGO88nuH6MNCaXZkPQBGEqI3Y1YdXXiKUWE8fdIcDHYsh%2B8TzSZd%2BHkLXSWXM6I5nVKsRKmYNClcTPW6%2BUdwo1m9bQApcZ4hQR4ARBpQ%3D%3D" rel="nofollow">剑指Offer(二十九)-- 最小的k个数</a></p><h2>搜索算法</h2><p><a href="https://link.segmentfault.com/?enc=tVZWuhvrZPbdKMFfITcMNA%3D%3D.akY40dC3zXBCyUSEkZrP9CfpB4wnqrxY5qLQDvrF1IDr9MRKjnXMbDPJA3Aizr7PQe%2FHNXNO2A%2FU6uFQyuX8CG9ixnILd9Bj4NejLUg4dqdFr3Lkyakre9O0sgmXlcfh8bPYUpjVzZNAiP0cVjLBTNvN9b3%2BC31UtnfQxB36PbDtxjsovDfOWwSTj3VLuhzVCJEkXNGky3BgZo%2B0YdwhuPTEul6p5FPtFpJvvuVFUv0sIghdgkBdARudR5nS1F6nSwHKtt%2FiEzILaep8uCxT2g%3D%3D" rel="nofollow">剑指Offer(六十三)-- 数据流中的中位数</a></p><h2>动态规划</h2><p><a href="https://link.segmentfault.com/?enc=eSgTe5oXf1jL3WWLLmG5gg%3D%3D.pis4Hx9SHnvcU%2BmEjlH8TScxjzuxmVU0IcEM8daLRdHS5%2B5Ueimt5F63o54EeupB%2FMnQ0a%2BtpiI4BHti1P9BSjF9WLC%2BzmhDzOfrD2zVaLq4es2EhWKIaDD6Yn7Rn8vGbm5ytstH2wXFF6XZBgvg9FzzX%2Fu8g6sLccdhh2BC8MGww%2FUQyuw8ELntfm8zA03MYdg81Z905N%2BSjx0AqzYmWsh4n7NK1ishkrNL%2BUux%2F%2FV35Y%2BIPcAIY08Ppl%2BgmYoiI7sT8ARc8INbsEI3prICNQ%3D%3D" rel="nofollow">剑指Offer(六)-- 斐波那契数列</a> <br><a href="https://link.segmentfault.com/?enc=IziTaHE5EnLX%2F6ROQr05gA%3D%3D.UG0xpnqj0D0939KLzcok%2FqMT0WYW64QlRZne2EiKjztK90o3bL3RohLbKQiDQjBJGDMO0Z76wxaUGXh8UGw5yngwUN1pZUoL0a1QItdhSlkkwWiwF8JOpiU5Naj870VLFF1UugF%2F%2B9bXCq%2BQ05Ze5B5gNLkgAWGGz72b7a6dCdO6xIkVDl%2FblT8xt7uDMusDwRWrTuuE334eXct1zjx3BM4Fh80QCyneGhi7WE857SWTxtfTp6HCJLQM9nyZZTgOHicWVXKDvvm%2FhaIl6jccoA%3D%3D" rel="nofollow">剑指Offer(七)-- 跳台阶</a> <br><a href="https://link.segmentfault.com/?enc=5sNa6NvqtFztIX20aQtpDg%3D%3D.URHymChyFQhiswoW0%2FzrEm82Rk9NVS0AjjiSLcyoDhlbuXg7UumsEFu1jI4rq2d3D%2B61nALAmnNh5k%2BwqTyXdotqmVXRKkbcq3%2BjgzBzv1WtkqrNjHYt%2F56LM3%2BdRxZ%2Fpwk2xy9ByM2125%2BPtzOSNUB5mpKsIIyVM7eZSJjUCYOBXf%2BpaYsBoqVen262gtJnydDMWj9DPxbYuLZwvR%2BXer0cq47ore1%2FBPS30MSDG%2F4taVCKsEUKyzPfhxzYNnD7nNAvKBeEQfTCa2Y5o0k0jA%3D%3D" rel="nofollow">剑指Offer(十)-- 矩形覆盖</a> <br><a href="https://link.segmentfault.com/?enc=bizFkblDBc1mbwCltE10cw%3D%3D.9UT2jrvWcNridbOzxpTu9juf83a%2B1%2B%2Ftm8of70Hl5KkDtg8KlYehlaTksLBa7VLzad%2FC7f%2B37WE5cs2vwMlOYdttCTwn53eU6h1%2BV8fnkIv49AO%2FtHZnN4eOVm%2FTfGfhw3nzPFJcDkIeODS0IPc3veNmv%2Fj1kVoBnvb7QXvDXHisv3YvbzlhlIooyNHT5bMS2l0yClz8Gdp74XxDyKB%2BNdb1yxNIu1fkjIQnMfjjTZjplKC3nzuGKBrvnZPLKtuVcl7HDhc%2F3eqgbAwGB%2FIynQ%3D%3D" rel="nofollow">剑指Offer(五十二)-- 正则表达式匹配(动态规划)</a></p><h2>回溯</h2><p><a href="https://link.segmentfault.com/?enc=8vsURZ6BBUUSSBj%2BpGn69Q%3D%3D.EpG1X7XO2s8oeJgpH3hryhPDoYngd0Vpckjsm60rkSDVHb%2Bc0b%2BZ73SOT6AcdR8jph1z3E2Vf63SV6NFw7spKYPOY6BjcTzxLMfq1FmtaNu9TEjT8o6pr9gC%2FcwqLfYQV5azhDvLu292tpUAKcQ7iVZkew0BNm6eYccfQO3vpimbWakh6Zn0QINoMxQZWtqk5YU2Ycu8luNO8kTUZkA7sZM%2F5jI4BXsJ%2BAuWv5I14bHbw%2FdQV0FUhkWfF3Vz%2FyONa2Ui1NLSkiUTiivhXPRqYA%3D%3D" rel="nofollow">剑指Offer(六十五)-- 矩阵中的路径(经典回溯法)</a> <br><a href="https://link.segmentfault.com/?enc=Z9uc%2BNWZk3kwOYepZZHjVQ%3D%3D.B2L3iffz5BxQ5jJBmlFVbm432JU4c%2BRkh%2BgqRC2FE4CJcNEqz%2BttSLyPpOla%2BysNK63Yc14dsqlpF0v%2Fbu4umE%2BZ%2F%2B%2FRQiuWr4kpUAeZ%2FCP33Oi2xrM5vKI%2BGTK0b2NeuCSslQ07bipmOur5T0naeEfPwwpu4nCSwXegdWk8HMXgLKiLFX%2Bhu3MV3sXkiMcM7QLICaLSGKi90fXNFPg9kF%2B8kGSkBOeIKGovnq40eATYL7n5rmaPN%2BGQQubGQ4OGwUaSvissTJT54vn%2FKvS8Iw%3D%3D" rel="nofollow">剑指Offer(六十六)-- 机器人的运动范围</a></p><h2>排序</h2><p><a href="https://link.segmentfault.com/?enc=y3n4sww2aqNZiHDLsnizjw%3D%3D.P%2Bsx0bSbtnsgIkyIaKhno7wb9olwXAr3B0MdfHEYsalyrewYYJRqrgiPxV%2B1%2F%2FeThBOG5rEoqksbVwVr9Z5yMq%2FDKBfRDgsA2ny%2BILWaBPo7al6mf4JIiHEGbHwT%2Ftjw26Sa8pb6FspRhOzvVPH%2FZSWunBjtrwe95x0YEUBzzmZzjA5ioNTM30py5wpOtw28%2BuD37ShmABbonYIcSUI9y4KM89AdhYm6YFWjU8U%2FzudLyWwossvaxQke5i%2FF6iou%2FTXld1KAm%2Fvlm12peW3xZA%3D%3D" rel="nofollow">剑指Offer(三十二)-- 将数组排成最小的数</a></p><h2>位运算</h2><p><a href="https://link.segmentfault.com/?enc=%2FQ8l02PwMtEywm53ogXQgQ%3D%3D.%2FgYuXg1K9jSMBfIPSST6IXkviRfIv9GWyb4whuTG46LQXKaA1gcyaYpWSP%2F0wrGosR6BNtnRkr4MTyec1tPvmalOaPbM%2Fq0kXrJ9UxdC8WvkFIsGxAOovlZGsDEwEBs12MEeMAE9c3NXeNtGac08xc%2B0rwm2LSDZO6AiGJozv0250WJuHWyuX3dRYInCgqCSVxNDVtXTQrRRHEjLSLH8bCBucPaYNWjdCbJNdSyrCwq7OuoW28I8Lv2Ltej1DTgq0Assn6rLv3qYq9bN3QkL%2FA%3D%3D" rel="nofollow">剑指Offer(十一)-- 二进制中1的个数</a> <br><a href="https://link.segmentfault.com/?enc=KkDMAM%2B8YMT4paVEB0LLnA%3D%3D.JznyibWiN7zDZqAfbeE3jd2%2FGmGy6aEtg%2FOTAcP8APmUpI6%2FjxX0wlWx2N1pr5S9wDnAy7oJ01YENbtpBmUJ80cELNakQGoh%2BUWidWfCcNBtIMtG7wxSAAKXEq5W1sqSvHjWoHqAPnHi8teelptfxGJJbd9blqGANZzpLOzU9jkbqEKSkIkQpIHOFkUNIslWJk6FGEedJTzF6SspmQa8bzBF%2F%2FY88WMdB9Mo%2BweqV%2B%2BX0vGqozix4HGFukj05dp6TSP0LKF4qZMUOA0EwyHAsQ%3D%3D" rel="nofollow">剑指Offer(三十一)-- 整数中1出现的次数</a> <br><a href="https://link.segmentfault.com/?enc=hQ6MTHZEbJ7wCaR9U1t29Q%3D%3D.KvZ5Wb1TF0A7i4XNYhLmjbIvv%2BOoG6gwkmTrIvrcst07QIN4w28VslMjVW%2FHxJNjQ9wEgvsshPAtE%2FOdWuQXCn8VWun1FOJrC0PR98iLdCaTopBQ9yamh1CC4n4PCuWgDpcCFotja3U9jlPiLZwpDTjIxNlsDXrWXbXPZ8%2FToUvVkB8wcm5J%2BZm1AZabV5i7XCiQpdmeGZeH%2FbV5iFbea5s8zisQKSSNwgyk91O%2F0%2Brz9fmMMvmTkwqq2fAtF1VwIyo1PaAnuLpdfoswTwk32w%3D%3D" rel="nofollow">剑指Offer(四十八)-- 不使用加减乘除实现加法</a></p><h2>二叉树</h2><p><a href="https://link.segmentfault.com/?enc=TCUJJsk3uALs9%2BIOktWKBQ%3D%3D.B6YGtE6ZccpdnKaUIC84e9sI1O0RmQ%2FJpBKgdcf7Ad24LoDDlPevoLAUHYfJ4eV22HQ73f3JdIP7wjxpj0KmEFj8nZrK3wSIgCQhPvGWaM7hYOie4oGXeZq4FIudrx7mQ420DA22PPkrb0X3XQShCDiH4l2qKsQEVBrqQ0I5hPZobLkqBMlg4fMJrWPiYEtt8Y782StjcJwc4iLGw0k3klH%2Frj3UVmXwCEK7lYBjHG24kPZsITIybBp9nrjoNqQtjdXmxw5%2B88qVkjfuOm9aEA%3D%3D" rel="nofollow">剑指Offer(四)-- 重建二叉树</a> <br><a href="https://link.segmentfault.com/?enc=ofQaxtOd2cgrbpvXEH6EBw%3D%3D.x%2BKuzfP6vdJ5iPE%2FjkC3l1rEC10qCCnUHmJygrtNWcs7RiBgiXrRqR%2B8eSb9rEkmmRi4QEClxz9UIw%2F%2BsXGDpSe%2F2lAkWfd1lMvYV%2BVon14NRvNqHMiIcZ9XYm8i6j6nYzH7edDZhLEzxbleOPCa5hxdDsLvx01jy6j%2BupcIBNqGCHvDR56d14BDXdFEbeLqxbH%2BJtqz4ObebOjuiovaNwl2%2FfmCd%2FCUE3F%2ByDerlAxNLT5kYwoVm%2FiG4fcfg2Ejw%2B5meE2ouJUTXgl6%2FSsVfw%3D%3D" rel="nofollow">剑指Offer(十七)-- 树的子结构</a> <br><a href="https://link.segmentfault.com/?enc=9DZWuVsKH2JwmaWrDlZrNA%3D%3D.I7Rtqe%2FRemaOhX2mZrxk0DGtnaY00dVfsg2gjqgHUkhq7E6hdlgVVWj1QxJKQTXnvKMzI5kjqgYN0ujCqPCJdujdny6XDcBRyJjJB2HMXkPirv85k5sJMJTsbTMsD1qQ1TaX8TCVXXIqUtTHH%2FQ8zt9zc%2FVxYraMqS1dcgrFZ%2FiZX%2BUvx1fp84Z1igxrp8zHtWeqd1kp4VgivA22Wr2UwAuKcmbA22JCLjQlmVw3%2FTHXSqdnv%2FzipG36Uz1ARRvcvK%2BN%2FddXBKP%2FFjk%2BssbpRQ%3D%3D" rel="nofollow">剑指Offer(十八)-- 二叉树的镜像</a> <br><a href="https://link.segmentfault.com/?enc=XutNCj4Y7pt4tl3Y%2Bu60rg%3D%3D.pWWzvbyT%2FX1c7MSvPIut9pqPKhIR9sflW%2BHJXmqHxtUGWC1riLXpmUXlgJIGvtIDrT05H6hJuFUngGXeLWu0zb3pWEstlxjrkWDcZN%2FaD2uRvjAMlUfLil3SN4iNwV2rOpeFPgFUhhjab6HQDGa2UJWw6wrfmOCTqphU18lFvP18qP4yiQyb0UKCjSes87%2B0ArduHmEgNzhIvNnUnWcuNp%2BrtJYF3FF24P3k%2F3fMkfCZ3SV4Qb5MMUfHI6mZCV8qimBSZ78OFpc8wJWhLesT%2Bg%3D%3D" rel="nofollow">剑指Offer(二十二)-- 从上往下打印二叉树</a> <br><a href="https://link.segmentfault.com/?enc=cWXIqmQcm%2BirrAcURrPYIw%3D%3D.tVm41uj20ubvA8o%2F6P9o%2F%2BRnBqtT8lyztKj%2F9nkF7Q%2FvLaPtRj%2Bf6eIiFgABzPOPrfzEBF4oTio%2BSaggnrXYuE5zgkaSDb8UC6wZ5EOsaXuIuSWXBGHhu3dCtjr2CaKizsvbjcvU2eVI1dZ9uyn1AZSCFhYnrHZY4bVSSWt2g9APlYog8ZxStBjjBzzFtkegfgAan0TvCzI7r%2BjF2UwQm4R8QB2Lf9r42TMJrD75i3nXlaUHfhSx00%2FKq5AHuXGkhvDAB%2FnTPc%2Bt%2FEtpwEF%2BpA%3D%3D" rel="nofollow">剑指Offer(二十三)-- 二叉树搜索树的后序遍历序列</a> <br><a href="https://link.segmentfault.com/?enc=G7IhEI%2BXvShnjLsv9roupQ%3D%3D.g8Mh%2BMn90YXJSHX30EClNhofAyDOafT3P35aQC3ChgjuC0oT2lzfAtArzS5U%2Bn5mfd5ZWzbF5U0joSnfx%2B9DP8jAPRfFMxfGLrbVZbH0%2FkltCOle2jVU4P2utECJiNPuTp75amZ6uRDq3lp%2FPelz3kdNE%2F1UYuiPCQHvKMQLP9dfdpc4WBlO1MtXvh2ppoI9vBdrzzetnmuPaOV5A2RGzVUoqEQMInEhlqUunCtLwDi3PmsbMOsR7cWGuIj8zEABVWwHcZQiFMbEeCpW8UcxENerUjfMAc44EO1k8TNEFsTJOoxni%2BpauGoZxRAre4KT5g8ezczISi2fsdWRCfNfpA%3D%3D" rel="nofollow">剑指Offer(二十四)-- 二叉树中和为某一值的路径</a> <br><a href="https://link.segmentfault.com/?enc=sVg1Icjz%2BJE6TeapRWtoMA%3D%3D.Ht4fENGri0dOY9ZfcXOmcA%2F5EYREUYD3JQfGz2UMdlXZsnLgASsc0PezgVnPBp7XaldihgQ%2BFpsv29TOkKqOqNdAcKMrrerQmDSG%2FcGTzlkOUsdlU%2FLNREh181HFzu%2BY9S4yOlSJEzOsrpWpWUoFy9wvlmdshKGfdCGkWpHAoGztEz7vs8Nb6vtLkRcYVaoi%2BMoj2AVRIyBKg9mDixPpgdgkmBC9XeyP2VbBqTfEeZaJq1XEuIO0h3pv90ymdU59cHmAnW2bGwPMl2IMZu8KQw%3D%3D" rel="nofollow">剑指Offer(二十六)-- 二叉搜索树和双向链表</a> <br><a href="https://link.segmentfault.com/?enc=XSqPQcIp0VDwy4fTPYivSw%3D%3D.yeVzyGT8X9HUbpu8cSkR1382WbnGyFVLjQNvawTNR2%2BcJRSowM0MuryA%2F8OpU9I%2BchMbFLmg4JtxGY%2Bxx8zi5lPOmkJKuY%2FoUZr5Ijzk82B259aHeLuZOElyXmb9JeET81AHppb8d7u3VYp5l6%2BzV0mk7SzEjN%2FtAOD5FWLRSSaX6dfUHtiy5zRQrJm0JirPMKg5zQfmkM51IC3LTp5tEUPAeG9BXP8BUvmH21ckMpcJ6GXdKM8I7qj98k%2BL6VHTtbTaGroYcDkcWux3uEiALg%3D%3D" rel="nofollow">剑指Offer(三十八)-- 树的深度</a> <br><a href="https://link.segmentfault.com/?enc=Rqw4mj1sviicFKmJr2veug%3D%3D.mHmcdfs3tASzVIJutVdOEpUuy%2BWW7ZoqS5nbcFWIvPWKgkGzz20M%2BP9et7r8L8v70RSMxXQbPQ6QvmNQP7WkXVGHjOCj5A%2BKwYiE%2BNTq%2FNvCuL0JLd3%2BUndMagS565phKjtyDg9uthQSW0IaCRx4kjgJ2L7tvkuIybAuUZMit8aiDqhuxyuXQodS7tj%2Fer477xp6w0RX2%2B82YYKVn79UuGRLaRdXJdqITi%2FpBmDuwy9OJ33REJQUi3%2FMKr9vmG5kxhdmm0mtI9ndxHlOWrZNMg%3D%3D" rel="nofollow">剑指Offer(三十九)-- 平衡二叉树</a> <br><a href="https://link.segmentfault.com/?enc=kbZ76B5kd3v4DseY3yfqbw%3D%3D.szJ91IJL6ETQd6EwxCkB6jGOscCROCtWHrd5lUp0TJbtxb6jShkwlJ3%2Fh5XIO5OkeTuV5aC6COLp14LQTuiBmlkikvE127KtWVEX8w5VVd4xy0SW%2B%2FC9vAVQrAPpYjPGUfcyppNTCHo71Ryfy7dGtXK0KpsvTWqFrtGFOu%2Bv7xmQDvIEwsiXqSLZKOJ19XnW3uNfS8lkzrX4WAOaJuyCDg%2BLb8bx5dGNTTq9dSVHwue4oL8DMF0tMHVwTthk%2Btzk5NXzClk5Cu%2BAJ6rpi6BAV3zxMjutv71AxY1BgKu9BgKOA3HCmR6df%2BzxdMfY2lI4ON2NBLQ%2BAu0nRKlj8rW%2FYg%3D%3D" rel="nofollow">剑指Offer(五十七)-- 二叉树的下一个节点</a> <br><a href="https://link.segmentfault.com/?enc=1GpIYeSBG3MRksg2qxCoEg%3D%3D.rYYlgwtJZxbmVvp7a9IyGQ2jxuXrsP5gsjQN2Edk%2BLNslBn%2F4a8AcZrnGyJfSMuiZM81Thfww1BDvV57sr33WSSTOuxV0xDuZIV8QNI%2FnBmJShzSJtpugepbx0uJD43AnTHsw23eIhtwvJbZY4wm7BOeadlo%2BcXjDv39t%2BXO2VtUPUUgrvXDmAol4BA7sIS8NRNLmekZS5yK0aZM%2Bqmoq0ayYX7mhfIAQSvcPNrBHuyFuE2MAHgVscOfwtakNU0%2BaqiRxwnZaI459qj59FdlDA%3D%3D" rel="nofollow">剑指Offer(五十八)-- 对称二叉树</a> <br><a href="https://link.segmentfault.com/?enc=NUngXC3fEOJLebO2Iun3lA%3D%3D.SqPfbx09F%2BtCW0D%2BxDtnSN1JeqdkowIqDeR7Oxsp4HTMcIxhtzbkZXUSlS5Vmc2G4oEIlWlenMk2PWugogFWmsEWXYsigcCtiREBbNV6vF74OaQDFefZCVLCtw2ACArzZ7U3v4TxijIRhCxJX0ZQNsxLL39MH2fLMUOh0TOE7HZPXlLNdgLZXcL5v5RcvgmKss2uRDmXx01qz2MKHQSlccKp967olg5HhMNiIjbzcEeDw7y4HIUxOqGI%2Bc%2FplKg9olFvJtkNVpH2FSvVbhbw0g%3D%3D" rel="nofollow">剑指Offer(五十九)-- 按之字形顺序打印二叉树</a> <br><a href="https://link.segmentfault.com/?enc=q%2BlK%2BxxEgYyZtxQP4SM45Q%3D%3D.NU6escsoPta2Y4SQ7Yw3shYtlEROwpZa0bOtCag%2B4sD%2BLh7ALP32ODpe2DEIjgIXrYZVjK0WtO2OXYe60121S0crTJ6KejWqjurMz%2FKekK8EphFJnl4jklbrVRdaMw%2FlLxUs49UCx0c%2FLQ3OEKanM3fLewMIHi4s4H1Rjn7byden3AAeZy0LtVE3e53FV7seiPnVGUub7jZNICeSSn7a6Vf%2FEjlb5qd09LUKz4YbvhCDb6JmYR50tgeElOeIpSzqf3L5Tj2mFheaAMzqJiOJRA%3D%3D" rel="nofollow">剑指Offer(六十)-- 将二叉树打印成多行</a> <br><a href="https://link.segmentfault.com/?enc=%2FByqsL%2FI5cRZXi9%2Bw9%2BKFg%3D%3D.JzStlPnmttjujjtxzCXn4VurF2cKJGQw7lT28EDoCuNDmjchPXwW3XzNeBFDnBNLAviTCFUPtp%2BpA30rpLu8qy7gsJraWryTmgaJ6dENqsJPDbA2xnOiaPitly6m8wYOWHeK2jBxbvEH3FijzxD5kj05K2HaPdBBIrPquvRbzEvujCUaGnGJQ8InPz47GFGjrtkgr51XRA%2Fm7ltTbON2Js1LOp3iFIu8h2NyvMFQyfjGhwu2aG4IF0ec%2BCtIko2zfLl1iiRdm79FjjG2b2YCig%3D%3D" rel="nofollow">剑指Offer(六十一)-- 序列化二叉树</a> <br><a href="https://link.segmentfault.com/?enc=o%2Fk%2FjXKrYAnafo442PvhMw%3D%3D.bKjecEs7qPVoGaVNR0THxhPZis%2B%2FJN3HwRBdufgH7gzqR%2BHq5eezd1IjTmzjjHgT2ZkBEdlY1Yjc1Lr%2BEScp4U%2BFgQMxZzW5X7dtxjo5lMC%2FuYzPnG%2FUtLeCE%2FVuHi066HN5qGhTa%2FJ52ffYV%2FIAyKvcFTR%2BRlxIVnK9HlxUjL2xDA212G7UuI0I2c4vbFdYHp%2FlfinHvokhYD%2BE3MYzEo3%2BAXuG54SLs8ynQQR412BbATihDJwWZioEMeWTISKVrdkD3%2Fe23ZCfWO8lvJV8WQ%3D%3D" rel="nofollow">剑指Offer(六十二)-- 二叉搜索树的第k个节点</a></p><h2>其他算法</h2><p><a href="https://link.segmentfault.com/?enc=8TIrcH3xp8t1SlSsCv9Rcw%3D%3D.XcspWWDwOMzavw7HRe6Lp7quEiNZJueWiFSU%2B0ETvlOoM8rbTidx8CFO7O6%2BW9OGNy37mNwZCgdNgnbHfEIJmETU2jf1%2FUzERDpVaNZPzbQDM7Kj5OIa0gD0Tfi4WgtSZG3INLiV04UCAToO3fMDtyKw5AtC9%2Btt7UyoeVDveHhkfOh1ZJirw9ofHlf48TpZP1ueE8VeUlau7mz4P2zxrI2MJpKRId6qgbSxZJsx0WXB4bLpyLGULLECl9PkCQC57XbxcpNl8e0URci1X%2FlcqA%3D%3D" rel="nofollow">剑指Offer(九)--跳台阶变态版</a> <br><a href="https://link.segmentfault.com/?enc=ZVrZhVQtzJL8Ginnc3hNQg%3D%3D.qPQaga8BmhNSrsC4Qr3J%2FRQB1gZ5QfzCfjopK7IJcGjsu0OhFWJxWyiPZFfQBJNG8R%2F%2BM0yfQG3IbAvtZ%2FdljkvxXdL0vItkt6Uszs2NUGXYDLHME9HNkahRVW1Gd4jzKXlHZJVML%2BiCr695Fe0%2BPbh3EGMNIIp7gGfd5P3VwJY2mPHvvBueXuuECTQBAlT8V%2FMu4ad%2BylS%2FeodGEaMvYrcl8ujutTsSOLv8p%2FZqYkKVMs4gX%2BHkQw5LV1r%2BUQdihQr9IEVbZc%2Fi5tw92w1HGg%3D%3D" rel="nofollow">剑指Offer(十二)--数值的整数次方</a> <br><a href="https://link.segmentfault.com/?enc=7p93ZJCwsVfsyEr8%2ByqSTg%3D%3D.TOCkjf22NvFBDUwCfzKV2aFVyR%2Fnm1hzKiO1pj6xefcmqqo0ZVvmFt8LyHg8PmceRBb15rUMKh28NrxvBTMSi%2FqeRbleXUwgpNgB%2Bjs3nPtlSqG44nRBC8rnHBPdRb1cwsmIlDLuRxz3ZwEQSPsQsUEgFCVArEofV9Af%2F%2Ba1uC9rERQ1QfGatzCzJAODkPreTvxihU%2B2T6xxGLpxx8pw2VU1xyJhZc6hcF5sDnCl%2FZh2olaTetzU0OgfMKCJ%2FCk5HqNVpZZ25uzI5%2FgTgcnonQ%3D%3D" rel="nofollow">剑指Offer(三十三)-- 丑数</a> <br><a href="https://link.segmentfault.com/?enc=OtBvOyc1KlK%2FR07MlDWO0g%3D%3D.cgfFoZrEQPBAwa%2BxLXfmMW1iBrb3hrO3I%2Bj36Wdt4ad0Q53DEhlV5Caerd0LqgLopqh8hMw54nivoI1n5uIFGjAGf%2FY29I%2BsFPvx6sgYRp5m3%2FZXUbbQhJHjYUbDCRQk5JxjqV%2F1UDKVo2ErPbv5u1zWAhNpCQk4nZj6mPFDRodMNIwuqHOC4CgrZxflkg9MqN9%2Fs4WF5A8cx7y4MN26B1gdYheefBtpCeIQpNV31YCfZkgUMpjhL6fPzOO7Im7KD%2BFCiAbYybUqB%2FVD4oZYKw%3D%3D" rel="nofollow">剑指Offer(四十一)-- 和为S的连续正数序列</a> <br><a href="https://link.segmentfault.com/?enc=Z70rLKINeogzAQLyM%2BYygg%3D%3D.F2%2BfYy3bBXQyM32vXLxFjEMxVDH3wzfGthCYbGjX4XtUifNfEcWfh07e4lJxunyf%2Bysf5HE8QPGlAbt%2B8HMiB%2BPGIMaE6FEBzaTw7g2adGqKcko13ab9f7%2Ft0I%2BXwsg82JQxi5MPrJzxkmTg5jKUrIcLvdhFR4BCghAABtSNkEzO3AyR65dPGYFHhYnZCrWoTdhshDiIM1%2Fsmt%2FOqAeA7tQuDfshBOQqvhSDUmaeHqQ0jq27FE5YyEtcZH5%2FRj8vWeuXsCVvBcF09SeoBdY2%2BA%3D%3D" rel="nofollow">剑指Offer(四十二)-- 和为S的两个数字</a> <br><a href="https://link.segmentfault.com/?enc=%2F0EcRUUHO%2BY7ZpjfeOUFpw%3D%3D.tO3g7odbQ8igws7FqZ0GEB3wat2cIHK0AJYxezP6PZIC5zps4cBey4%2Bri%2FUl8GcHi8pZAzjqxIm0YtKey6F1QYppGb9Ubl9LsQMfJDyR3bc8qj24kNfmZvLlngpeT2QPbHogzIegYjJqNqQgevkiOQ%2FoH%2F%2BZtleTjd84JUWUPw4gkthcPzrlZYGfiUkPoFh0%2FtmXzIpUBhailDr95x7u3NDnuRw6oNW8bW74pPJo%2BAbIrkeO6Hj7D8zWRsUJWw39zhJuZRO8rc1K0aXqTOAY7w%3D%3D" rel="nofollow">剑指Offer(四十五)-- 扑克牌顺子</a> <br><a href="https://link.segmentfault.com/?enc=sUDPpzatQXKuV86OAxz9Pw%3D%3D.BziOKgbNq8ogTFHuky6z5ogtrdGvRd%2FmNs9tpJufWQweaq9zGDXrI8v%2FG4sEr9LgdBQitCrkK%2B14areqiukDtXFINe2ETBgm2ybReLjg%2FsyUtyO6AAZZ78HyZo11M1VQMImAlOJnq6OYAFeW1DUuBXwtYKfgivVWYeJm%2FXM4xQtnFC0QDZKdqjzvYn8Ip99I8GdAirEi6ZJU3WxzPdJ17jW681PaJ%2BJ973XS33uPFCKhU5DRAPUG5ZWhnq%2Fz0HjgZ%2BMSQJxM5sKUS1EBlYzW9Q%3D%3D" rel="nofollow">剑指Offer(四十六)-- 最后出圈的士兵(约瑟夫)</a> <br><a href="https://link.segmentfault.com/?enc=zIah5sjM2FDOspjxZqsKSA%3D%3D.1exRsE0xYsqVC%2Bjmx%2BLMS7USej%2FwUepA%2F9DdodWlbNFrG2J2LbYm0bAfzPGDTk1NsOmXmG1HfV5SWs8PtVc6Vcs%2B3BB2OkGtcwHA9CetZNgLihBUn0BhRxjppLT3epkt%2FXONd4XXuXvYGp8ne2wSI2vHK7tN4HrLUKZx8bKHqZvXBALlUFid1EZaeUvTLNTHsygX0dWtWHjtxzbsIfkg0U4kdJjKdSRb9RjWBdHYuaxlSY39ICjevKe9s4XVxmMT2GETgTOXwUXQdbNQLoja2g%3D%3D" rel="nofollow">剑指Offer(四十七)-- 1+2+...+n的求和(不使用循环或者乘法)</a> <br><a href="https://link.segmentfault.com/?enc=l1lmRbQRyqayP0cV33ZWhw%3D%3D.tZsnHtnThuuyugx7GBmIvRAtQAeT%2B%2B1DFL9nJjGzfow6miRNy5PdIXU0PfydM%2BlEDZirVsFgoEaKASq1QTtP0s1m%2F8nta4JSrSdUryFVwfq3f74T7yor6DiBrohqVjW2sgA02pF5j10lzhgFjIzEInYD2vuQQ8uc8%2BkZ8WXIL3RB%2BnWmBzvQZ3XwIJw%2B%2B3mYT6vRlA9oIGQg54L59IlaRVl9ctWjAw1qPeYfOsQZJ10we55rjrze8XsAPG3mFiwYBI0K%2BQxAE2KIxaHmsutpzQ%3D%3D" rel="nofollow">剑指Offer(六十七)-- 剪绳子</a></p><h3>为什么要做这个刷题的仓库?</h3><p>算法题已经变成各个厂面试的标配,而算法题不是一朝一夕的事情,傻傻的每天或者每两天,刷一道题,或者学习一种思路,只要坚持住,后面不那么畏惧算法了。这是一只拦路虎,但是只要在心理上战胜它,就已经赢了一半。而不断地训练,能够让人不那么恐惧。</p><p>平时业务代码写得多,好像没有怎么用上算法。其实不然,譬如算法就隐藏在我们调用sort()函数的时候。里面的实现也是经过作者一版一版的优化的。一个能解决复杂算法的人,一般代码写得都比较优美。算法在无形中锻炼了,处理复杂问题的能力,写业务代码的时候就不太容易自己把自己绕晕。</p><p>自己对算法比较感兴趣,每次看到一些神奇的算法,总会想到,这些人怎么这么牛,amazing...奇怪的知识又增加了。这种快乐,是在你突然间想清楚一道算法题,或者看到别人更优美的解答并且理解了的时候,突然产生的。算是一种简单的快乐。</p><p>当然,我们并不追求,花很多时间,要把某个题目,把效率从 0.9999 提高到 1 ,对于每个人来说,时间都是宝贵的。在时间和某个知识点面前,我想每个人都有自己平衡的策略,if you happy,you do. 但是我们做的目的是把某个题目解决,至少在限定的条件下把它解决,大部分人能想到的优化,也能够提出来。</p><h3>关于作者</h3><p>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=Im0EDEOAolhyUKbueHRj%2Bg%3D%3D.nPs%2BDHCmw15o6MJ74FcFxTP%2FA4N6iD3ascigEnJEXoo%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=walzxldjqLEy8KCYN%2FQM0A%3D%3D.H7d1M57bwcdinivxCesqodM8UA5o%2BeYdfFzZ8TDxqhCURoZkHukaqXIcPp0QUZhM" rel="nofollow">开源编程笔记</a></p>
Java学习之路 -- Java怎么学?
https://segmentfault.com/a/1190000039734532
2021-03-29T15:10:22+08:00
2021-03-29T15:10:22+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
5
<p>@[toc]</p><h2>java基础怎么学?</h2><p>当时,作为懵懂的小白,大一学习了<code>c</code>和<code>c++</code>,还有数据结构,后来才自学的<code>java</code>。有了<code>c++</code>的基础,其实学<code>java</code>确实感觉挺容易上手。如果没有<code>c</code>或者<code>c++</code>的基础,建议开始需要先把<code>java</code>的基础打好,基础是指什么?基础的语法,能用!至于源码,不建议在刚刚开始学就看源码,绝对劝退!!!</p><p>推荐几本可以入门的书籍:</p><ul><li><p><a href="https://link.segmentfault.com/?enc=f5kY5EHCF2ppUy7zvzTOfg%3D%3D.KzOj%2FYWyyl3cEfWKImnYanssiDcx6xM1fk7L7sHNwj5IAQAC2Z4LoQMuJWACEIWl" rel="nofollow">Head First Java</a></p><ul><li>简单易懂,可以教你如何像一个对象开发者一样去思考,图文并茂学习方式能让你快速地在脑海中掌握住知识</li></ul></li><li><p><a href="https://link.segmentfault.com/?enc=SxH3eyT8hcaMVLtYp7P9Aw%3D%3D.2m6HUFDMzBsoadPbRB2GWbDfvq5XMeL4ofqKQDx8hlCBg6fAOmBxBssR%2FwQbmW4a" rel="nofollow">疯狂Java讲义</a></p><ul><li>很全面,很厚,覆盖了Java的基本语法结构、Java的面向对象特征、Java集合框架体系、Java泛型、异常处理、Java GUI编程、JDBC数据库编程、Java注释、Java的IO流体系、Java多线程编程、Java网络通信编程和Java反射机制。</li></ul></li><li><p><a href="https://link.segmentfault.com/?enc=n9EgUhvVMMiqgsZFRBGyNw%3D%3D.uDbTTQn2el54AjwWhjifJoVZzlEiuNCu8xIJUYpuce6%2FqJbDBlbMQYPqXY4PCqdc" rel="nofollow">Java核心技术·卷 I(原书第10版)</a></p><ul><li>特别经典的书籍,内容比较实在,但是没有疯狂Java讲义那么接地气,很简洁,上手难度也不是很大。</li></ul></li></ul><p>进阶书籍:</p><ul><li><p><a href="https://link.segmentfault.com/?enc=ng6vikLtjp%2B42LggPc85pA%3D%3D.57PbvJh94i9Sgm1bMz6qvU%2F6fRf8a9ciq5IDWFZiTkGToqj3OwqRdjKIHmvmfcEP" rel="nofollow">Java 编程思想第四版</a></p><ul><li>圣经段位,绝对的好书,但是不适合刚刚入门的小白,如果你觉得想啃下来,也可以阐释,里面讲得东西,很详细,时常透露出:Java语言为什么这样设计,如果想进阶,这绝对是本好书。</li></ul></li><li><p><a href="https://link.segmentfault.com/?enc=dDozlcETon8cOyJLJRcWJw%3D%3D.drwxVcD0bOI6zCyS6fsF5RPHuW%2FW13n4tn7NglQ1PTPdswdOww9sEtvJK3tD5Skj" rel="nofollow">Java网络编程(中文版 第三版)(O'Reilly Java系列)</a></p><ul><li>这本书是我大学时一门选修课的课本,主要是讲解Java里面的网络应用,可以考虑看看,但是优先级不是很高。</li></ul></li></ul><p><strong>看视频还是看书?</strong><br>前期新手绝对会有的一个疑惑,看书还是看视频?看书感觉很枯燥,坚持不下去,看视频感觉很爽,但是看完好像不是很能记住。</p><p>个人觉得,前期看书会快点,但是确实很枯燥,可以在b站(小破站牛逼)上找一些全集的视频来看,记得,边看边敲,基本记不住!!!建议,看完一节或者一章,凭借记忆把代码敲出来,一开始肯定很慢,但是长期来看,帮助很大,基本可以记住并掌握。</p><p>如果视频的话,推荐以下两个:</p><ul><li><p><a href="https://www.bilibili.com/video/BV1Kb411W75N?from=search&seid=12627698240394885697">Java零基础教程</a></p><ul><li>宋红康老师的视频,七百多集了,真的可以看很久,内容真的很全,讲的也很细致,不会很枯燥,老师挺有趣的,无利益相关,我在b站看过老师的Jvm视频。</li></ul></li><li><p><a href="https://www.bilibili.com/video/BV1Rx411876f?from=search&seid=12627698240394885697">Java零基础教程视频(适合Java 0基础,Java初学入门)</a></p><ul><li>也是七八百集,挺详细</li></ul></li></ul><p>总结:视频不在于多,书籍也是,越想要全面,越不太可能,前期有一本书,一个视频就可以了,抓大放小,要不,很容易就放弃,真的是从入门到放弃。</p><p><strong>多打,多练习,熟能生巧!代码量上去才能发生质变!</strong></p><p>在这个过程中,主要学习的东西(每一个都可以分得很细,下面只是大概,想到再补充):</p><ul><li>基本数据类型</li><li>常用关键字</li><li>接口</li><li>抽象类</li><li>集合</li><li>继承(子类和父类)</li><li>反射</li><li>序列化</li><li>动态代理</li><li>注解</li><li>锁与多线程</li><li>IO编程</li><li>JDBC</li><li>Java网路编程</li></ul><h2>学完基础学什么?</h2><p>我当时学完Java的基础之后,JDBC学了,知道怎么连接数据库了,就想着搞网站,我想大部分人也是,学了东西,就想做个东西出来!!这个很正常的心态,不断地有反馈才能不断往前~</p><p>我当时搞作业搞了一段时间的前端以及jsp之类的,如果学习Java,可以把前端知识放在一个低一点优先级的级别,前期基本理解和会用就可以,不要忘记自己真正的目标。</p><p>我的前端是在<a href="https://link.segmentfault.com/?enc=0e3DfU8ovE4mTIHkSI6LOQ%3D%3D.ObrN5ewq9n9uzZyQeC3t390jarXPCYV79V%2F%2B3NB4IW0%3D" rel="nofollow">w3School</a>学习的,你们也可以去学习,当时还在慕课网学习了视频,不过这些都不重要,主要是会点html和css,js就可以。</p><p>学完前端的大致知识,可以考虑学Servlet和jsp,也有一部分人说其实不需要再学习这个东西,现在直接上框架,就可以。我想说的是,如果你的时间很紧急,确实可以这么做。如果你的时间比较充裕,在大学,可以考虑一下把这一块也学一下,因为以后你不会再回来学了,而所谓的框架,也是建立在这上面的。</p><p>有时候,走慢一点,是为了走得更远。<br>推荐JavaWeb的书籍两本:</p><ul><li><p><a href="https://link.segmentfault.com/?enc=Nv04%2Fwdg6F3lX7R62qlefA%3D%3D.OFF2xZY4SwS%2BzP62ieRbWKyE96JbaWtTO1gash%2BS%2BZby51oKwCrnBF7gpFHkSJ33" rel="nofollow">深入分析Java Web技术内幕(修订版)</a></p><ul><li>这本书主要围绕Java Web 相关技术从三方面全面、深入地进行了阐述。首先介绍前端知识,即在JavaWeb 开发中涉及的一些基本知识,包括Web 请求过程、HTTP、DNS 技术和CDN 技术。其次深入介绍了Java 技术,包括I/O 技术、中文编码问题、Javac 编译原理、class 文件结构解析、ClassLoader 工作机制及JVM 的内存管理等。</li></ul></li><li><p><a href="https://link.segmentfault.com/?enc=NgOHpaoEp5lVxPso73WVaQ%3D%3D.p8CYDYZUCrjDX%2BDvgyV4D7GZeFJ0xth1%2BztTWtEEqFlGBmKmhH2fY8h7KWJ%2FUB7a" rel="nofollow">Tomcat与Java Web开发技术详解</a></p><ul><li>这本书主要是讲解了JavaWeb和tomcat相关的知识点,算是为了之后学习JavaWeb做准备。</li></ul></li></ul><p>再推荐一个博客地址,主要是JavaWeb的笔记,讲得挺好的:<a href="https://link.segmentfault.com/?enc=Ph6ynqxMtxYN79KGk3jhZA%3D%3D.kwBy%2B4417uyMDez%2FWhI%2Bd4Zs31Hh7cN2gEAQo6EPu%2F5qjeLHXje8STOxhVufhAEqWduwqbUwiDsyib0%2BCObz6Q%3D%3D" rel="nofollow">JavaWeb</a></p><p>学完这些可以学习框架了,框架里面用得最多的是反射,动态代理!!!一定要熟悉。</p><p>当时我学习的框架不是主流的,是<code>Jfinal</code>,可能大家不怎么听过,那个框架比较简单,但是现在不建议大家去学习,可以了解。</p><p>接下来大家需要学习的是SSM(Spring+SpringMVC+Mybatis),为啥学习Spring,不是SpringBoot,因为SpringBoot是建立在Spring的基础上的,学了Spring,SpringBoot很快上手。</p><p>建议学习的话,可以先学Mybatis,前面有JDBC的基础,可以很快上手。推荐自己的博客:<a href="https://link.segmentfault.com/?enc=pwAmAiq9vAIbegVMh0FLrg%3D%3D.SjHJNPL1jfvL7bd8HJVR5AwqSxdaUte%2FJQYlMeE6bcldgFHoKXXGMkoG0X1fdro%2BhfWopuQ8b2LrAh5WbT6Ht10TNVzCy1seuDlDxUwuKVE%3D" rel="nofollow">Mybatis学习笔记</a></p><p>如果学习视频的话,我建议去b站搜索一下,例如:<a href="https://www.bilibili.com/video/BV1mE411X7yp?from=search&seid=2275609375584527505">SSM框架合集</a></p><p>关于SSM的学习视频很多,大家各自判断自己适合什么类型,有一本书籍:<a href="https://link.segmentfault.com/?enc=rZ0TMkO49VOHNkBQROcvVQ%3D%3D.%2F39aHYPieVyavMfdfnFqEsM3CgDuof%2BHdesbZWuXGOht2d6A6CCEMQ%2Fw%2B9ddk1hn" rel="nofollow">Java EE互联网轻量级框架整合开发 : SSM框架(Spring MVC+Spring+MyBatis)和Redis实现</a>,是对SSM的整合以及讲解,还不错,可以考虑一下。</p><p>在这个过程中,你也了解了如何处理日志,异常等等。</p><p>Spring的学习中,主要掌握AOP和IOC的原理以及应用,学习的过程,首先是需要会用,然后再去挖源码,要是直接上来看源码,又是一次劝退。</p><p>学习完Spring之后,可以上手SpringBoot,这个网上也有很多教程,推荐这个网站:<br><a href="https://link.segmentfault.com/?enc=E6D5fi7KF0CpW1kDuIX6bA%3D%3D.pXlCSUNm2Idlm%2B6GdIEbp%2BU4w7efuHv08RIFIxEcnlmxyxJYUgdHToQlI%2F96vxSa4kdA6CJMAV6OY1LdfI10lA%3D%3D" rel="nofollow">how2j</a><br>自己上b站按照播放量查找也是可以的,重要的是行动,不是资源!!!</p><p>Spring有两本本比较好的书籍推荐:</p><ul><li><a href="https://link.segmentfault.com/?enc=cr06KaJVzuLp97BCT9gzEg%3D%3D.Ha%2BUR%2BEjSxvcINGhhBLFoRNKxy37%2B8XaQVBrf6OdsjNMNPPvqpIulvzV7MhxnbOJ" rel="nofollow">Spring实战(第4版)</a>:实战的知识</li><li><a href="https://link.segmentfault.com/?enc=IBsFlZsMdSxnop4x7oJHBQ%3D%3D.zRkZaveDSoiJBxm9zUDB%2Fw0SPM3TtIapNqlwh%2BhR%2F%2BcJb3nWREjTKmidq2DilSbd" rel="nofollow">Spring源码深度解析</a>:主要是深挖源码,其实也可以b站上找一些优质的课程</li></ul><p>前面讲解的有数据库以及redis相关的,如果关系型数据库,建议在网上搜索教程学习即可,譬如:<a href="https://link.segmentfault.com/?enc=Vu78wfQYQI77NExbHeQOFg%3D%3D.Eo3hpBbzNeXgMnu6byUrpd2XklFw6gfeKwt7Pj9vfpSWnSqpGoNjO3PfMoI4Zi4YoUdZ0aQpT4%2FDudAx8Kbp0Q%3D%3D" rel="nofollow">菜鸟教程</a></p><p>关于入门书籍:<a href="https://link.segmentfault.com/?enc=wXc4hlkoOVdIcQYsYH7Xpw%3D%3D.VKbHSV05evqCNlmIzm94pMmjI2iVK2XfeBVuDNu86ngRNix1q6nhA5oK90L%2FdEAZ" rel="nofollow">Mysql必知必会</a></p><p>如果是深入书籍,推荐:<a href="https://link.segmentfault.com/?enc=IM2LtA2cNUH8AAFgpMRdMQ%3D%3D.%2BGiV7fdMviHM18G48RtQWE3QRkBusIKnKMj7jiMTEEs1cVpFcxJrgBJP4JYVlX77" rel="nofollow">MySQL技术内幕 : InnoDB存储引擎(第2版)</a></p><p>redis的学习,基本的数据类型,操作,这些学习直接网上菜鸟教程也有:<a href="https://link.segmentfault.com/?enc=SY4Qf10HaGdPIH%2BqlbfDmA%3D%3D.6AqjHlMRqqo%2FkUOvx2BgliiBSEfBM8LMwEAs%2FR8vRr4lCuuPzgDKInFCSnd9n%2Bv6" rel="nofollow">redis教程</a></p><p>如果需要学习如何使用?推荐这本书:<a href="https://link.segmentfault.com/?enc=LxI793bF0KrLVKc2wCULgA%3D%3D.0TPc5RIsTvlPVYqzCAGFkZ%2F4FbgH39Kxr2cICYbp%2FWeMqNycEzLCnZ70xb9MzEeh" rel="nofollow">Redis 深度历险:核心原理与应用实践</a></p><p>了解了使用之后,再去做redis的源码分析?牛人可以直接下载redis的源码,第一版只有几万行代码,当然,需要c语言或者c++基础。书籍:<a href="https://link.segmentfault.com/?enc=6qSJLi4AESJ3hEogj8mQcQ%3D%3D.vQ8lyVdhPT6woIsoblwNyCga7ISalMiK6ybWD%2B4i35BWgZ%2BgmWzXkZdoBp7aM%2Fdx" rel="nofollow">Redis5设计与源码分析</a></p><h2>几个常用框架学完学什么?</h2><p>其实这个时候,你已经可以进去开发的阶段了,后面的路需要自己摸索了。</p><h2>MQ</h2><p>也就是消息队列,挑一个比较常用的大型的进行学习就可以了,不要贪图多,比如Kafka或者RabbitMQ,系统做异步解耦合的时候经常遇到。</p><p>先学会如何使用,然后学习里面的原理,架构。</p><h2>JVM的知识跑不掉</h2><p>JVM怎么学?<br>肯定的推荐周志明老师的 <a href="https://link.segmentfault.com/?enc=QrrI5rDH34oy1OsLBUtFEA%3D%3D.6nRcMp34Y08wRU8pmVgbOH1doBSfIjjDNu%2F%2F1REXqTdVtfUodho2OJCTPNJb1hIl" rel="nofollow">深入理解Java虚拟机</a>,这本书推荐多看几遍。</p><p>除此之外,b站宋红康老师的视频也强烈推荐:<a href="https://www.bilibili.com/video/BV1PJ411n7xZ">JVM全套教程</a></p><p>搞定这两个之后,再找一些JDK11的新特定的书籍,来看看,基本问题不大了。</p><h2>微服务等等</h2><p>前面学习的,肯定是单体的应用,也就是一台服务器,一个应用。当用户量到达一定数量,需要做应用拆分,得学习的知识有:分库分表,RPC框架,微服务,注册中心,监控等等。</p><p>一开始推荐看两本书,了解分布式的一些知识,知道架构大致的演变过程,为什么要这样做,以前怎么样,怎么样变化的。</p><ul><li><a href="https://link.segmentfault.com/?enc=R3kAXVJ%2FpEfm5X%2FBzC5fWA%3D%3D.n2iweK874uE4FjavYmY1GKimGUgCcOAU78%2FrdKqHirvHiUY2RHdl9kBiE5hbg%2Fj5" rel="nofollow">大型网站技术架构 : 核心原理与案例分析</a></li><li><a href="https://link.segmentfault.com/?enc=lbwd9JHV0H47nd1ZzBWr6Q%3D%3D.FfzuZl9WtAIqY%2BtOPO5nYS8N8Fiv2pIfvs5C0nHebA%2BI1RwFqXRMV%2BIaPEZAFulc" rel="nofollow">大型网站系统与Java中间件开发实践</a></li></ul><p>前面两本书看着挺爽的,拓展知识面,但是不回特别深入,算是一个概述以及全面了解。要想两本书吃透,不可能的!!!一口怎么吃成一个胖子呢?是吧。</p><p>然后再看看大型分布式的架构相关知识:<br><a href="https://link.segmentfault.com/?enc=vr5tede23TrtlSRB0B0Qug%3D%3D.kNgwJnGJNc1cKoH8zGtnvkD2hlG6eex0QSx2fymbw7NvgEdaR6Yl0CXD4Aj5UMQH" rel="nofollow">大型分布式网站架构设计与实践 : 一线工作经验总结,囊括大型分布式网站所需技术的全貌、架构设计的核</a></p><p>然后就是挨个知识点各个击破(下面知识部分相关的例子):</p><ul><li>RCP原理</li><li>Netty</li><li>DUBBO</li><li>Zookeeper</li></ul><h2>其他</h2><h3>数据结构和算法</h3><p>如果你是科班的,数据结构和算法基本掌握,那下面这一部分仅供参考:</p><ul><li><p><a href="https://link.segmentfault.com/?enc=q9z%2Bvhrtsn3j4wlXwfQmJg%3D%3D.zT5mcM4dDb%2FkY4Uczur55Z4LT0Im16bHL8Wgj20BiVDYbON1FWYcGefRwtoZxUNJ" rel="nofollow">算法(第四版)</a></p><ul><li>俗称红书,最推荐的Java程序员学习的算法书籍,入门很友好,视频貌似b站也可以找到。</li></ul></li><li><p><a href="https://link.segmentfault.com/?enc=OBSlRa%2BUM9pivvKdOcIehw%3D%3D.0I4f2qioIPhSKDwtKDoDyRLuYGWVaSmGxGE6JUhyd5hOSiIbEsiAkk%2FxpbBCRdp2" rel="nofollow">数据结构与算法分析</a></p><ul><li>国外数据结构与算法分析方面的经典教材,使用卓越的Java编程语言作为实现工具讨论了数据结构(组织大量数据的方法)和算法分析(对算法运行时间的估计)。本书把算法分析与有效率的Java程序的开发有机地结合起来,深入分析每种算法,内容全面、缜密严格,并细致讲解精心构造程序的方法。</li></ul></li><li><p><a href="https://link.segmentfault.com/?enc=Qq6F9srPGJX5sBE6Howacg%3D%3D.DakUdOpBwXmWvuRdphixisQ5x%2FPXatgnM40TJyRDY5N8aUI5qYhooKSfyuNr4Dgr" rel="nofollow">Java常用算法手册(第3版)</a></p><ul><li>比较简单的算法数据,挺有意思,难度不大,挺薄。</li></ul></li><li><p><a href="https://link.segmentfault.com/?enc=cVqM0HMFtWjPexTlV%2FTbaQ%3D%3D.oilbz5P%2BstC%2FLtX8MNS9MK0Ak4pAglnhVasxjAdphelF%2FA0benUKv%2BQqmAZ99vU8" rel="nofollow">算法导论</a></p><ul><li>绝对劝退书籍,谨慎!很多数学公式推导,以及伪代码,建议作为程序员道路上相伴的书籍🤣🤣🤣(反正我是没有看完,太变态了)</li></ul></li></ul><p>如果刷题的话,推荐先刷剑指Offer,然后LeetCode每日/每周(时间自己定)一题,推荐一下自己的刷题笔记仓库:<a href="https://link.segmentfault.com/?enc=TNPNMIrvJ1oVtlFE7L7Qqg%3D%3D.JJLYZFocQs3%2B5y6H1slXps%2BJFUT3g2SlG7lOArn4%2BgOMmddt9Ub4pRQ1j4AjIVDE" rel="nofollow">codeSolution</a></p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,本人就职于国内某知名在线旅游公司,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p>平日时间宝贵,只能使用晚上以及周末时间学习写作,<strong>关注我</strong>,我们一起成长吧~</p><p><a href="https://link.segmentfault.com/?enc=FWP1EPfxb0ognRmanfvmiQ%3D%3D.I9d5hxec%2B9ReDtYTOQyk%2FCEj8uekZsm38eiV7sRR8EI%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=F%2Fukcg2rXg3YPcWGGU5ytA%3D%3D.JD27m%2F6k6TFCfIMtpkr%2BYpir%2B5Ps%2BURIsGYJ7UYYoSAGTzLY2fCYA2PNGSeWnztZ" rel="nofollow">开源编程笔记</a></p>
【实战问题】-- 缓存穿透之布隆过滤器(1)
https://segmentfault.com/a/1190000039724775
2021-03-27T15:11:10+08:00
2021-03-27T15:11:10+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
1
<p>前面我们提到,在防止缓存穿透的情况(缓存穿透是指,<strong>缓存和数据库都没有的数据</strong>,被大量请求,比如订单号不可能为<code>-1</code>,但是用户请求了大量订单号为<code>-1</code>的数据,由于数据不存在,缓存就也不会存在该数据,所有的请求都会直接穿透到数据库。),我们可以考虑使用布隆过滤器,来过滤掉绝对不存于集合中的元素。</p><h2>布隆过滤器是什么呢?</h2><p>布隆过滤器(Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的,它实际上是由一个很长的二进制向量和一系列随机hash映射函数组成(说白了,就是用二进制数组存储数据的特征)。可以使用它来判断一个元素是否存在于集合中,它的优点在于查询效率高,空间小,缺点是存在一定的误差,以及我们想要剔除元素的时候,可能会相互影响。</p><p>也就是当一个元素被加入集合的时候,通过多个hash函数,将元素映射到位数组中的k个点,置为1。</p><h2>为什么需要布隆过滤器?</h2><p>一般情况下,我们想要判断是否存在某个元素,一开始考虑肯定是使用数组,但是使用数组的情况,查找的时候效率比较慢,要判断一个元素不存在于数组中,需要每次遍历完所有的元素。删除完一个元素后,还得把后面的其他元素往前面移动。</p><p><img src="/img/remote/1460000039724777" alt="" title=""></p><p>其实可以考虑使用<code>hash</code>表,如果有<code>hash</code>表来存储,将是以下的结构:<br><img src="/img/remote/1460000039724778" alt="" title=""></p><p>但是这种结构,虽然满足了大部分的需求,可能存在两点缺陷:</p><ul><li>只有一个hash函数,其实两个元素hash到一块,也就是产生hash冲突的可能性,还是比较高的。虽然可以用拉链法(后面跟着一个链表)的方式解决,但是操作时间复杂度可能有所升高。</li><li>存储的时候,我们需要把元素引用给存储进去,要是上亿的数据,我们要将上亿的数据存储到一个hash表里面,不太建议这样操作。</li></ul><p>对于上面存在的缺陷,其实我们可以考虑,用多个hash函数来减少冲突(注意:冲突时不可以避免的,只能减少),用位来存储每一个hash值。这样既可以减少hash冲突,还可以减少存储空间。</p><p>假设有三个hash函数,那么不同的元素,都会使用三个hash函数,hash到三个位置上。<br><img src="/img/remote/1460000039724780" alt="" title=""></p><p>假设后面又来了一个张三,那么在hash的时候,同样会hash到以下位置,所有位都是1,我们就可以说张三已经存在在里面了。</p><p><img src="/img/remote/1460000039724781" alt="" title=""></p><p>那么有没有可能出现误判的情况呢?这是有可能的,比如现在只有张三,李四,王五,蔡八,hash映射值如下:<br><img src="/img/remote/1460000039724779" alt="" title=""></p><p>后面来了陈六,但是不凑巧的是,它hash的三个函数hash出来的位,刚刚好就是被别的元素hash之后,改成1了,判断它已经存在了,但是实际上,陈六之前是不存在的。</p><p><img src="/img/remote/1460000039724782" alt="" title=""></p><p>上面的情况,就是误判,布隆过滤器都会不可避免的出现误判。但是它有一个好处是,<strong>布隆过滤器,判断存在的元素,可能不存在,但是判断不存在的元素,一定不存在。</strong>,因为判断不存在说明至少有一位hash出来是对不上的。</p><p>也是由于会出现多个元素可能hash到一起,但有一个数据被踢出了集合,我们想把它映射的位,置为0,相当于删除该数据。这个时候,就会影响到其他的元素,可能会把别的元素映射的位,置为了0。这也就是为什么布隆过滤器不能删除的原因。</p><h2>具体步骤</h2><p>添加元素:</p><ul><li><ol><li>使用多个hash函数对元素item进行hash运算,得到多个hash值。</li></ol></li><li><ol><li>每一个hash值对bit位数组取模,得到位数组中的位置索引index。</li></ol></li><li><ol><li>如果index的位置不为1,那么就将该位置为1。</li></ol></li></ul><p>判断元素是否存在:</p><ul><li><ol><li>使用多个hash函数对元素item进行hash运算,得到多个hash值。</li></ol></li><li><ol><li>每一个hash值对bit位数组取模,得到位数组中的位置索引index。</li></ol></li><li><ol><li>如果index所处的位置都为1,说明元素可能已经存在了。</li></ol></li></ul><h2>误判率推导</h2><p>庆幸的是,布隆过滤器的误判率是可以预测的,由上面的分析,也可以得知,其实是与位数组的大小,以及hash函数的个数等,这些都是息息相关的。</p><p>假设位数组的大小是m,我们一共有k个hash函数,那么每一个hash函数,进行hash的时候,只能hash到m位中的一个位置,所以没有被hash到的概率是:<br>$$1-\frac{1}{m}$$</p><p>k个hash函数都hash之后,该位还是没有被hash到1的概率是:<br>$$(1-\frac{1}{m})^k$$</p><p>如果我们插入了n个元素,也就是hash了n*k次,该位还是没有被hash成1的概率是:<br>$$(1-\frac{1}{m})^{kn}$$</p><p>那该位为1的概率就是:<br>$$1-(1-\frac{1}{m})^{kn}$$</p><p>如果需要检测某一个元素是不是在集合中,也就是该元素对应的k个hash元素hash出来的值,都需要设置为1。也就是该元素不存在,但是该元素对应的所有位都被hash成为1的概率是:<br>$${(1-(1-\frac{1}{m})^{kn})}^{k}\approx {(1-e^{-kn/m})}^k $$</p><p>可以大致看出,随着位数组大小m和hash函数个数的增加,其实概率会下降,随着插入的元素n的增加,概率会有所上升。</p><p>最后也可以通过自己期待的误判率P和期待添加的个数n,来大致计算出布隆过滤器的位数组的长度:<br>$$m=-(\frac{nInP}{(In2)^2})$$</p><p>上面就是误判率的大致计算方式,同时也提示我们,可以根据自己业务的数据量以及误判率,来调整我们的数组的大小。</p><h2>布隆过滤器的作用</h2><p>除了我们前面说的过滤爬虫恶意请求,还可以对一些URL进行去重,过滤海量数据里面的重复数据,过滤数据库里面不存在的id等等。</p><p>但是,即使有布隆过滤器,我们也不可能完全避免,或者彻底解决缓存穿透这个问题。只是相当于做了优化,将准确率提高。</p><p>很多的key-value数据库也会使用布隆过滤器来加快查询效率,因为全部挨个判断一遍,这个效率太低了。</p><blockquote><strong>【刷题笔记】</strong><br>Github仓库地址:<a href="https://link.segmentfault.com/?enc=9u%2Fe0W7NNdH0Nuj0IeD%2Feg%3D%3D.6mb%2FdX6Dk1D5ch9QcIPeGkqi6ys1NYysZ5lybBTZd5azcGF1FB5cjKCzgxc2zj3G" rel="nofollow">https://github.com/Damaer/cod...</a> <br>笔记地址:<a href="https://link.segmentfault.com/?enc=tkn0f7m1fLF7yLRBDr0UNA%3D%3D.OOxJoXjozuNjP8iaJoqnJSSlu4YO6HrPGmpojI1j39lQ31XrZP2xTUU7KP%2FZggE5" rel="nofollow">https://damaer.github.io/code...</a></blockquote><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=rNX3wWS95UQOnweEzn42Sg%3D%3D.Z4JZkFuvhCeYrKnTRsRJEnVKxLy97Tgv0PiGHr9Ae0A%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=r61FWVrWXkeTaZChq4JHQw%3D%3D.iJmRQJ%2FO8hAVYm3LLSF%2FpsO0x1sPZZOqqmAnZCDIynViAmSPY7JyJOrTTS1fsRwq" rel="nofollow">开源刷题笔记</a></p><p>平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~</p>
java集合【12】——— ArrayList,LinkedList,Vector的相同点与区别是什么?
https://segmentfault.com/a/1190000039720465
2021-03-26T16:49:27+08:00
2021-03-26T16:49:27+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>[TOC]<br>要想回答这个问题,可以先把各种都讲特性,然后再从底层存储结构,线程安全,默认大小,扩容机制,迭代器,增删改查效率这几个方向入手。</p><h2>特性列举</h2><p><img src="/img/remote/1460000039720467" alt="" title=""></p><ul><li><p><code>ArrayList</code>:动态数组,使用的时候,只需要操作即可,内部已经实现扩容机制。</p><ul><li>线程不安全</li><li>有顺序,会按照添加进去的顺序排好</li><li>基于数组实现,随机访问速度快,插入和删除较慢一点</li><li>可以插入<code>null</code>元素,且可以重复</li></ul></li><li><p><code>Vector</code>和前面说的<code>ArrayList</code>很是类似,这里说的也是1.8版本,它是一个队列,但是本质上底层也是数组实现的。同样继承<code>AbstractList</code>,实现了<code>List</code>,<code>RandomAcess</code>,<code>Cloneable</code>, <code>java.io.Serializable</code>接口。具有以下特点:</p><ul><li>提供随机访问的功能:实现<code>RandomAcess</code>接口,这个接口主要是为<code>List</code>提供快速访问的功能,也就是通过元素的索引,可以快速访问到。</li><li>可克隆:实现了<code>Cloneable</code>接口</li><li>是一个支持新增,删除,修改,查询,遍历等功能。</li><li>可序列化和反序列化</li><li>容量不够,可以触发自动扩容</li><li><em>*最大的特点是:线程安全的</em>,相当于线程安全的<code>ArrayList</code>。</li></ul></li><li><p>LinkedList:链表结构,继承了<code>AbstractSequentialList</code>,实现了<code>List</code>,<code>Queue</code>,<code>Cloneable</code>,<code>Serializable</code>,既可以当成列表使用,也可以当成队列,堆栈使用。主要特点有:</p><ul><li>线程不安全,不同步,如果需要同步需要使用<code>List list = Collections.synchronizedList(new LinkedList());</code></li><li>实现<code>List</code>接口,可以对它进行队列操作</li><li>实现<code>Queue</code>接口,可以当成堆栈或者双向队列使用</li><li>实现Cloneable接口,可以被克隆,浅拷贝</li><li>实现<code>Serializable</code>,可以被序列化和反序列化</li></ul></li></ul><h2>底层存储结构不同</h2><p><code>ArrayList</code>和<code>Vector</code>底层都是数组结构,而<code>LinkedList</code>在底层是双向链表结构。</p><p><img src="/img/remote/1460000039720470" alt="" title=""></p><p><img src="/img/remote/1460000039720469" alt="" title=""></p><p><img src="/img/remote/1460000039720468" alt="" title=""></p><h2>线程安全性不同</h2><p>ArrayList和LinkedList都不是线程安全的,但是Vector是线程安全的,其底层是用了大量的synchronized关键字,效率不是很高。</p><p>如果需要ArrayList和LinkedList是线程安全的,可以使用Collections类中的静态方法synchronizedList(),获取线程安全的容器。</p><h2>默认的大小不同</h2><p>ArrayList如果我们创建的时候不指定大小,那么就会初始化一个默认大小为10,<code>DEFAULT_CAPACITY</code>就是默认大小。</p><pre><code class="java">private static final int DEFAULT_CAPACITY = 10;</code></pre><p>Vector也一样,如果我们初始化,不传递容量大小,什么都不指定,默认给的容量是10:</p><pre><code class="java"> public Vector() {
this(10);
}</code></pre><p>而LinkedList底层是链表结构,是不连续的存储空间,没有默认的大小的说法。</p><h2>扩容机制</h2><p>ArrayList和Vector底层都是使用数组<code>Object[]</code>来存储,当向集合中添加元素的时候,容量不够了,会触发扩容机制,ArrayList扩容后的容量是按照1.5倍扩容,而Vector默认是扩容2倍。两种扩容都是申请新的数组空间,然后调用数组复制的native函数,将数组复制过去。</p><p>Vector可以设置每次扩容的增加容量,但是ArrayList不可以。Vector有一个参数capacityIncrement,如果capacityIncrement大于0,那么扩容后的容量,是以前的容量加上扩展系数,如果扩展系数小于等于0,那么,就是以前的容量的两倍。</p><h2>迭代器</h2><p><code>LinkedList</code>源码中一共定义了三个迭代器:</p><ul><li><code>Itr</code>:实现了<code>Iterator</code>接口,是<code>AbstractList.Itr</code>的优化版本。</li><li><code>ListItr</code>:继承了<code>Itr</code>,实现了<code>ListIterator</code>,是<code>AbstractList.ListItr</code>优化版本。</li><li><code>ArrayListSpliterator</code>:继承于<code>Spliterator</code>,Java 8 新增的迭代器,基于索引,二分的,懒加载器。</li></ul><p><code>Vector</code>和<code>ArrayList</code>基本差不多,都是定义了三个迭代器:</p><ul><li><code>Itr</code>:实现接口<code>Iterator</code>,有简单的功能:判断是否有下一个元素,获取下一个元素,删除,遍历剩下的元素</li><li><code>ListItr</code>:继承<code>Itr</code>,实现<code>ListIterator</code>,在<code>Itr</code>的基础上有了更加丰富的功能。</li><li><code>VectorSpliterator</code>:可以分割的迭代器,主要是为了分割以适应并行处理。和<code>ArrayList</code>里面的<code>ArrayListSpliterator</code>类似。</li></ul><p><code>LinkedList</code>里面定义了三种迭代器,都是以内部类的方式实现,分别是:</p><ul><li><code>ListItr</code>:列表的经典迭代器</li><li><code>DescendingIterator</code>:倒序迭代器</li><li><code>LLSpliterator</code>:可分割迭代器</li></ul><h2>增删改查的效率</h2><p><strong>理论上</strong>,<code>ArrayList</code>和<code>Vector</code>检索元素,由于是数组,时间复杂度是<code>O(1)</code>,在集合的尾部插入或者删除是<code>O(1)</code>,但是其他的地方增加,删除,都是<code>O(n)</code>,因为涉及到了数组元素的移动。但是<code>LinkedList</code>不一样,<code>LinkedList</code>不管在任何位置,插入,删除都是<code>O(1)</code>的时间复杂度,但是<code>LinkedList</code>在查找的时候,是<code>O(n)</code>的复杂度,即使底层做了优化,可以从头部/尾部开始索引(根据下标在前一半还是后面一半)。</p><p>如果插入删除比较多,那么建议使用<code>LinkedList</code>,但是它并不是线程安全的,如果查找比较多,那么建议使用<code>ArrayList</code>,如果需要线程安全,先考虑使用<code>Collections</code>的<code>api</code>获取线程安全的容器,再考虑使用<code>Vector</code>。</p><p>测试三种结构在头部不断添加元素的结果:</p><pre><code class="java">
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
public class Test {
public static void main(String[] args) {
addArrayList();
addLinkedList();
addVector();
}
public static void addArrayList(){
List list = new ArrayList();
long startTime = System.nanoTime();
for(int i=0;i<100000;i++){
list.add(0,i);
}
long endTime = System.nanoTime();
System.out.println((endTime-startTime)/1000/60);
}
public static void addLinkedList(){
List list = new LinkedList();
long startTime = System.nanoTime();
for(int i=0;i<100000;i++){
list.add(0,i);
}
long endTime = System.nanoTime();
System.out.println((endTime-startTime)/1000/60);
}
public static void addVector(){
List list = new Vector();
long startTime = System.nanoTime();
for(int i=0;i<100000;i++){
list.add(0,i);
}
long endTime = System.nanoTime();
System.out.println((endTime-startTime)/1000/60);
}
}
</code></pre><p>测出来的结果,LinkedList最小,Vector费时最多,基本验证了结果:</p><pre><code class="txt">ArrayList:7715
LinkedList:111
Vector:8106</code></pre><p>测试get的时间性能,往每一个里面初始化10w个数据,然后每次get出来:</p><pre><code class="java">
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
public class Test {
public static void main(String[] args) {
getArrayList();
getLinkedList();
getVector();
}
public static void getArrayList(){
List list = new ArrayList();
for(int i=0;i<100000;i++){
list.add(0,i);
}
long startTime = System.nanoTime();
for(int i=0;i<100000;i++){
list.get(i);
}
long endTime = System.nanoTime();
System.out.println((endTime-startTime)/1000/60);
}
public static void getLinkedList(){
List list = new LinkedList();
for(int i=0;i<100000;i++){
list.add(0,i);
}
long startTime = System.nanoTime();
for(int i=0;i<100000;i++){
list.get(i);
}
long endTime = System.nanoTime();
System.out.println((endTime-startTime)/1000/60);
}
public static void getVector(){
List list = new Vector();
for(int i=0;i<100000;i++){
list.add(0,i);
}
long startTime = System.nanoTime();
for(int i=0;i<100000;i++){
list.get(i);
}
long endTime = System.nanoTime();
System.out.println((endTime-startTime)/1000/60);
}
}</code></pre><p>测出来的时间如下,<code>LinkedList</code> 执行<code>get</code>操作确实耗时巨大,<code>Vector</code>和<code>ArrayList</code>在单线程环境其实差不多,多线程环境会比较明显,这里就不测试了:</p><pre><code class="txt">ArrayList : 18
LinkedList : 61480
Vector : 21</code></pre><p>测试删除操作的代码如下,删除的时候我们是不断删除第0个元素:</p><pre><code class="java">import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
public class Test {
public static void main(String[] args) {
removeArrayList();
removeLinkedList();
removeVector();
}
public static void removeArrayList(){
List list = new ArrayList();
for(int i=0;i<100000;i++){
list.add(0,i);
}
long startTime = System.nanoTime();
for(int i=0;i<100000;i++){
list.remove(0);
}
long endTime = System.nanoTime();
System.out.println((endTime-startTime)/1000/60);
}
public static void removeLinkedList(){
List list = new LinkedList();
for(int i=0;i<100000;i++){
list.add(0,i);
}
long startTime = System.nanoTime();
for(int i=0;i<100000;i++){
list.remove(0);
}
long endTime = System.nanoTime();
System.out.println((endTime-startTime)/1000/60);
}
public static void removeVector(){
List list = new Vector();
for(int i=0;i<100000;i++){
list.add(i);
}
long startTime = System.nanoTime();
for(int i=0;i<100000;i++){
list.remove(0);
}
long endTime = System.nanoTime();
System.out.println((endTime-startTime)/1000/60);
}
}</code></pre><p>测试结果,LinkedList确实效率最高,但是<code>Vector</code>比<code>ArrayList</code>效率还要高。因为是单线程的环境,没有触发竞争的关系。</p><pre><code class="txt">ArrayList: 7177
LinkedList: 34
Vector: 6713</code></pre><p>下面来测试一下,vector多线程的环境,首先两个线程,每个删除5w元素:</p><pre><code class="java">package com.aphysia.offer;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
public class Test {
public static void main(String[] args) {
removeVector();
}
public static void removeVector() {
List list = new Vector();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
list.remove(0);
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
list.remove(0);
}
}
});
long startTime = System.nanoTime();
thread1.start();
thread2.start();
while (!list.isEmpty()) {
}
long endTime = System.nanoTime();
System.out.println((endTime - startTime) / 1000 / 60);
}
}</code></pre><p>测试时间为:12668</p><p>如果只使用一个线程,测试的时间是:8216,这也从结果说明了确实<code>Vector</code>在多线程的环境下,会竞争锁,导致执行时间变长。</p><h2>总结一下</h2><ul><li><p>ArrayList</p><ul><li>底层是数组,扩容就是申请新的数组空间,复制</li><li>线程不安全</li><li>默认初始化容量是10,扩容是变成之前的1.5倍</li><li>查询比较快</li></ul></li><li><p>LinkedList</p><ul><li>底层是双向链表,可以往前或者往后遍历</li><li>没有扩容的说法,可以当成双向队列使用</li><li>增删比较快</li><li>查找做了优化,index如果在前面一半,从前面开始遍历,index在后面一半,从后往前遍历。</li></ul></li><li><p>Vector</p><ul><li>底层是数组,几乎所有方法都加了Synchronize</li><li>线程安全</li><li>有个扩容增长系数,如果不设置,默认是增加原来长度的一倍,设置则增长的大小为增长系数的大小。</li></ul></li></ul><blockquote><strong>【刷题笔记】</strong><br>Github仓库地址:<a href="https://link.segmentfault.com/?enc=KH5GAVVqWUHy88Y50BDkcA%3D%3D.bs5uXQZfByI%2B7MGMLzdH0UaICgcGxVqwPaR83MHpNDcMBOT9BIat%2B7B4Xta%2BlFjN" rel="nofollow">https://github.com/Damaer/cod...</a> <br>笔记地址:<a href="https://link.segmentfault.com/?enc=gM%2FBOZzeFbcwf9%2B6ab01Fg%3D%3D.qh1j7FgRSQ9PUKMn90B4KtCFN%2BnvZ%2FKwUai6J2%2FlnVOqcAWm74jMEkInhFbYgXKS" rel="nofollow">https://damaer.github.io/code...</a></blockquote><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=WJJUpI7LcoAbMbIN68ESAg%3D%3D.ZuRF6T15y%2F%2BIv7TiiSDBZiH6rUTYZjq8c3ntVz4hlc4%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=ljTrONwBHmBIWbwm7i7VPw%3D%3D.IxUo0PcaG4BD3NGq4VnRMiwPLGkvBMNB1QOi4%2BrZ19gL8zLtRO7kPPaAzv%2BSP1wd" rel="nofollow">开源刷题笔记</a></p><p>平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~</p>
从解决Github TimeOut到经典面试题:从输入URL到浏览器显示页面发生了什么?
https://segmentfault.com/a/1190000039710945
2021-03-25T14:05:18+08:00
2021-03-25T14:05:18+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p><strong>问题描述</strong></p><p>在<code>Windows</code> 操作系统上,<code>push</code>代码到<code>git</code>的时候,出现了<code>Failed to connect to github.com port 443: Timed out</code>的错误。一脸懵逼,浏览器网页也访问不了。</p><p><strong>思路以及解决方案</strong><br>一开始,我以为自己代理网络出现了问题,关掉之后,还是一样的问题。首先我们可以使用以下的命令,删除代理配置:</p><pre><code class="shell">git config --global --unset http.proxy</code></pre><p>然后打开<code>ipaddress.com</code>,查询以下的域名,记录其ip:</p><ul><li>github.com</li><li>github.global.ssl.fastly.net</li></ul><p><img src="/img/remote/1460000039710949" alt="" title=""><br><img src="/img/remote/1460000039710948" alt="" title=""></p><p>然后打开<code>C:\Windows\System32\drivers\etc\hosts</code>文件,把两个ip配置进去:<br><img src="/img/remote/1460000039710950" alt="" title=""></p><p>保存之后,打开<code>CMD</code>,刷新<code>DNS</code>,重新<code>push</code>:</p><pre><code class="shell">ipconfig /flushdns</code></pre><p><img src="/img/remote/1460000039710947" alt="" title=""><br>以上做法可以解决部分连接<code>github</code>慢的问题,主要是超时的问题,如果不是超时的问题,上面的做法是不会起作用的,这个只是把对应的域名和<code>ip</code>的对应关系直接映射在<code>DNS</code>配置上,不用去查找了,直接找到<code>ip</code>地址。</p><p><strong>为什么刷新DNS就生效了呢</strong>? <br>这就涉及到一个面试经常问的一个问题了,先把问题变成:在浏览器输入一个<code>www.baidu.com</code>,会发生什么?间不固定。</p><p>为什么刷新<code>DNS</code>就生效了呢?这就涉及到一个面试经常问的一个问题了,先把问题变成:在浏览器输入一个<code>www.baidu.com</code>,会发生什么?</p><ol><li>解析域名:首先需要根据域名去查找该域名的<code>ip</code>地址,解析前会先查看浏览器的缓存,浏览器会保存一段时间访问的网址的<code>DNS</code>地址,根据浏览器不同时间不固定(在<code>chrome</code>浏览器中输入<code>:chrome://dns/</code>,可以看到<code>chrome</code>浏览器的<code>DNS</code>缓存。)。</li><li>如果浏览器的缓存没有这个记录,那么就回去查找系统的缓存,系统缓存没有的情况会去查找 <code>hosts </code>文件里面的<code> ip</code> 地址(如果存在的话)。</li><li>如果本地的<code>hosts</code>文件里面没有该域名对应的<code>ip</code>地址,那么就会发送一个<code>DNS</code>请求到本地<code>DNS</code>服务器,一般是由网络接入服务器商提供(譬如中国移动)。</li><li>请求到达本地<code>DNS</code>服务器之后,也会先查询缓存,缓存有则直接返回,没有则递归查询,本地<code>DNS</code>服务器需要向根服务器查询。</li><li>根服务器不记录具体的域名和<code>ip</code>对应关系,会告诉<code>DNS</code>服务器,到域服务器(给出地址)上查询。</li><li>继续往域服务器查询,譬如<code>“baidu.com”</code>,<code>.</code> -> <code>.com </code>-> <code>baidu.com. </code>-> <code>www.baidu.com.</code>,查询到之后,写入缓存并且返回ip。</li><li>拿到<code>ip</code>之后,会建立<code>TCP</code>链接,也就是三次握手。</li><li>三次握手成功之后,浏览器发起<code>HTTP</code>请求,请求包括三部分:请求方法<code>URI</code>协议/版本,请求头,正文。</li><li>服务器处理请求,返回。</li><li>关闭<code>TCP</code>链接,四次握手(或称四次挥手)。</li><li>浏览器解析报文或者资源,渲染。</li></ol><p>上述只是一个概述,具体的细节很多,这个下次具体聊聊,但是我们可以看出,在这个过程中确实涉及到了<code>DNS</code>的服务器以及缓存,所以我们刷新缓存之后,访问<code>github</code>就可以请求到对应的ip上去。</p><blockquote><strong>【刷题笔记】</strong><br>Github仓库地址:<a href="https://link.segmentfault.com/?enc=XUfiLxiqgQs4hMUHFTiNSg%3D%3D.vTFWxaQGM4JguuMA0M5aPCpDzRUT9xuDIxb7MU%2BYPAeubm2EkRtqXGWBvez6BxMb" rel="nofollow">https://github.com/Damaer/cod...</a> <br>笔记地址:<a href="https://link.segmentfault.com/?enc=qeoEm2eZscX7qQyiYGgyEA%3D%3D.nd9t0LOdNlkLagx%2BioCCqILGrhr%2B6efpo%2BQDEUt8RSmgZdZvM8nY9FX0nfygOiHj" rel="nofollow">https://damaer.github.io/code...</a></blockquote><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=8vA3WFbWqCDqhbal7vmcPA%3D%3D.OVVH%2BNF4kNS0ge8dpz%2BS8yvDuKX9Kq3abw7NEwG7tes%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=%2FwFQGgjPaufMIQf0NHf%2Bbw%3D%3D.4WanGPlXORzq%2BXESDfQUsTcamelYp0X0kjBpPpWioNjd1R8y3Un7hjrHch3YYNry" rel="nofollow">开源刷题笔记</a></p><p>平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~</p>
【实战问题】-- 缓存穿透,缓存击穿和缓存雪崩的区别以及解决方案
https://segmentfault.com/a/1190000039688578
2021-03-22T16:57:47+08:00
2021-03-22T16:57:47+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
11
<p>平时我们使用缓存的方案,一般是在数据库中存储一份,在缓存中同步存储一份。当请求过来的视乎,可以先从缓存中取数据,如果有数据,直接返回缓存中的结果。如果缓存中没有数据,那么去数据库中取出数据,同时更新到缓存中,返回结果。如果数据库中也没有数据,可以直接返回空。</p><p>关于缓存,一般会有以下几个常见的问题</p><h2>缓存穿透</h2><p>缓存穿透是指,<strong>缓存和数据库都没有的数据</strong>,被大量请求,比如订单号不可能为<code>-1</code>,但是用户请求了大量订单号为<code>-1</code>的数据,由于数据不存在,缓存就也不会存在该数据,所有的请求都会直接穿透到数据库。<br>如果被恶意用户利用,疯狂请求不存在的数据,就会导致数据库压力过大,甚至垮掉。</p><p>注意:穿透的意思是,都没有,直接一路打到数据库。</p><p><strong>那对于这种情况,我们该如何解决呢?</strong></p><ol><li>接口增加业务层级的<code>Filter</code>,进行合法校验,这可以有效拦截大部分不合法的请求。</li><li>作为第一点的补充,最常见的是使用布隆过滤器,针对一个或者多个维度,把可能存在的数据值hash到bitmap中,bitmap证明该数据不存在则该数据一定不存在,但是bitmap证明该数据存在也只能是可能存在,因为不同的数值hash到的bit位很有可能是一样的,hash冲突会导致误判,多个hash方法也只能是降低冲突的概率,无法做到避免。</li><li>另外一个常见的方法,则是针对数据库与缓存都没有的数据,对空的结果进行缓存,但是过期时间设置得较短,一般五分钟内。而这种数据,如果数据库有写入,或者更新,必须同时刷新缓存,否则会导致不一致的问题存在。</li></ol><h2>缓存击穿</h2><p>缓存击穿是指数据库原本有得数据,但是缓存中没有,一般是缓存突然失效了,这时候如果有大量用户请求该数据,缓存没有则会去数据库请求,会引发数据库压力增大,可能会瞬间打垮。</p><p>针对这类问题,一般有以下做法:</p><ol><li>如果是热点数据,那么可以考虑设置永远不过期。</li><li>如果数据一定会过期,那么就需要在数据为空的时候,设置一个互斥的锁,只让一个请求通过,只有一个请求去数据库拉取数据,取完数据,不管如何都需要释放锁,异常的时候也需要释放锁,要不其他线程会一直拿不到锁。</li></ol><p>下面是缓存击穿的时候互斥锁的写法,注意:获取锁之后操作,不管成功或者失败,都应该释放锁,而其他的请求,如果没有获取到锁,应该等待,再重试。当然,如果是需要更加全面一点,应该加上一个等待次数,比如1s中,那么也就是睡眠五次,达到这个阈值,则直接返回空,不应该过度消耗机器,以免当个不可用的场景把整个应用的服务器带挂了。</p><pre><code class="java"> public static String getProductDescById(String id) {
String desc = redis.get(id);
// 缓存为空,过期了
if (desc == null) {
// 互斥锁,只有一个请求可以成功
if (redis.setnx(lock_id, 1, 60) == 1) {
try {
// 从数据库取出数据
desc = getFromDB(id);
redis.set(id, desc, 60 * 60 * 24);
} catch (Exception ex) {
LogHelper.error(ex);
} finally {
// 确保最后删除,释放锁
redis.del(lock_id);
return desc;
}
} else {
// 否则睡眠200ms,接着获取锁
Thread.sleep(200);
return getProductDescById(id);
}
}
}</code></pre><h2>缓存雪崩</h2><p>缓存雪崩是指缓存中有大量的数据,在同一个时间点,或者较短的时间段内,全部过期了,这个时候请求过来,缓存没有数据,都会请求数据库,则数据库的压力就会突增,扛不住就会宕机。</p><p>针对这种情况,一般我们都是使用以下方案:</p><ol><li>如果是热点数据,那么可以考虑设置永远不过期。</li><li>缓存的过期时间除非比较严格,要不考虑设置一个波动随机值,比如理论十分钟,那这类key的缓存时间都加上一个1~3分钟,过期时间在7~13分钟内波动,有效防止都在同一个时间点上大量过期。</li><li>方法1避免了有效过期的情况,但是要是所有的热点数据在一台redis服务器上,也是极其危险的,如果网络有问题,或者redis服务器挂了,那么所有的热点数据也会雪崩(查询不到),因此将热点数据打散分不到不同的机房中,也可以有效减少这种情况。</li><li>也可以考虑双缓存的方式,数据库数据同步到缓存A和B,A设置过期时间,B不设置过期时间,如果A为空的时候去读B,同时异步去更新缓存,但是更新的时候需要同时更新两个缓存。</li></ol><p>比如设置产品的缓存时间:</p><pre><code class="java">redis.set(id,value,60*60 + Math.random()*1000);</code></pre><h2>小结</h2><p>缓存穿透是指数据库原本就没有的数据,请求如入无人之境,直奔数据库,而缓存击穿,则是指数据库有数据,缓存也本应该有数据,但是突然缓存过期了,这层保护屏障被击穿了,请求直奔数据库,缓存雪崩则是指很多缓存同一个时间失效了,流量全部涌入数据库,造成数据库极大的压力。</p><blockquote><strong>【刷题笔记】</strong><br>Github仓库地址:<a href="https://link.segmentfault.com/?enc=8YM1CRImLXQ8IYsODcEA7Q%3D%3D.JaM6hYIj8uBxDgm3W%2FITA9mF%2BxqOXx05zmsHzmZD5C1SrhnfUQJEFjfXs61lmcOn" rel="nofollow">https://github.com/Damaer/cod...</a> <br>笔记地址:<a href="https://link.segmentfault.com/?enc=WqtddHq5dGLuRG0c9MhotA%3D%3D.XcQnihf6qOoD7ueZ0XP%2BBtSbzEihDex%2FYQEBo50qm220QkxLUHnBuoz15pc0acJf" rel="nofollow">https://damaer.github.io/code...</a></blockquote><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=8OF49olwvOfVmlr8b5kpbQ%3D%3D.AO%2Fof%2FUY7FMwdVRANLpjSOpIQMIKoaeiTxWQF8Hcyjs%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=FHI8XLtiVr6bY5%2FqCc4Qpg%3D%3D.d13R4v5SkBjyJ5l%2B7JiBpL8YWiX%2Fvo%2BBfs%2BBSh4Zd0igxznQLMuHpuiwuOpPcd61" rel="nofollow">开源刷题笔记</a></p><p>平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~</p>
【实战问题】-- 并发的时候分布式锁setnx细节
https://segmentfault.com/a/1190000039670844
2021-03-19T11:11:15+08:00
2021-03-19T11:11:15+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>前面讲解到<a href="https://link.segmentfault.com/?enc=pxZotG4bwdOTvu2UeNGSJQ%3D%3D.zDjyVYcdQBBn%2BnvKPLhBbV55nt5RPXOAN38cGlPXcdpGUpcb4L%2BDpJrNtE%2B%2Bk41JMW94Q35KxlEm%2BCSTJw%2FcVwvQCKZVEi2u%2FP6DroXx2H7py1diZjhv2DSNIOuLrw%2Bv1y0bP1bB7EhXk84uue32X8k%2BEy0hUITQPDP4b1QJBlSU%2Fl5Y5XaSaEfe%2Fyd2SvcNdD2CrqvZiKlt5glH9KTWV86bp4CrnvnHAFL96%2BahOn%2BEpduRjoD9%2BkEOwxLZSpN4A3C1Ps663vm%2BZug30v9J8vdpvo%2FGnKa%2BmqXoC%2BIxjmZPfqqoBQV4GVre66Cpt5gDAw3bqCRLcHO6Ootn295fMg%3D%3D" rel="nofollow">实战问题】-- 设计礼品领取的架构设计以及多次领取现象解决?</a>,如果出现网络延迟的情况下,多个请求阻塞,那么恶意攻击就可以全部请求领取接口成功,而针对这种做法,我们使用<code>setnx</code>来解决,确保只有一个请求可以进入接口请求。</p><p><img src="/img/remote/1460000039663958" alt="" title=""></p><pre><code class="java"> public String receiveGitf(int activityId,int giftId,String uid){
// isExist判断活动是否存在,内部包括redis和数据库请求,省略
if(isActivityExist(activityId,giftId)){
// 活动和礼品有效,判断是否领取过
if(!userReceived(uid,activityId,giftId)){
// 没有领取过,调用C系统
try {
// setnx
if(redis.setnx("uid_activityId_giftId")){
boolean receivedResult = Http.getMethod(C_Client.class, "distributeGift");
if(receivedResult){
// 领取成功更新mysql
updateMysql(uid,activityId,giftId);
}else{
// 领取成功更新redis
deleteRedis(uid,activityId,giftId);
return "已经领过/领取失败";
}
}else{
return "已经领过/领取失败";
}
}catch (Exception e){
// 记录日志
logHelper.log(e);
return "调用领券系统失败,请重试";
}
}
}
return "领取失败,活动不存在";
}</code></pre><p>下面,我们就专门讲解一下<code>setnx</code>,<code>setnx</code>可以用作分布式锁,但是<strong>这个场景并不是分布式锁的一个较好的实践,因为每个用户的key都是不一样的,我们主要是防止同一个用户恶意领取</strong>,<code>setnx</code>本身是一个原子操作,可以保证多个线程只有一个能拿到锁,能返回<code>true</code>,其他的都会返回<code>false</code>。</p><p>但是上面的做法,没有设置过期时间,在生产上一般是不可以这么使用。<strong>不设置过期时间的key多了之后,redis服务器很容易内存打满,这时候不知道哪些是强制依赖的,只能扩容,从代码层面去清理,如果直接清理不常用的,也很难保证不出事。</strong>(基本不允许这么干,除非是基础数据,跟着服务器启动,写入<code>redis</code>的,不会变更的,比如城市数据,国家数据等等,当然,这些也可以考虑在本地内存中实现)</p><p>如果在上面的代码中,加入超时时间,假设是一个月或者半年,流程变成这样:<br><img src="/img/remote/1460000039670846" alt="" title=""></p><p>设置key的超时时间使用<code>expire</code>,但是这样还有缺陷么?</p><p>在<code>redis 2.6.12</code>之前,<code>setnx</code>和<code>expire</code>都不是原子操作,也就是很有可能在<code>setnx</code>成功之后,redis当季,expire设置失败,也就不会有超时时间了。虽然这个影响在当前业务不是很大,但是还是一个小缺陷。</p><p><code>Redis2.6.12</code>以上版本,可以用<code>set</code>获取锁,set包含<code>setnx</code>和<code>expire</code>,实现了原子操作。也就是两步要么一起成功,要么一起失败。</p><p>除此之外,上面的流程可能还存在的一个问题,是请求<code>C</code>服务的时候出现超时,然后删除key,恰好这个时候<code>redis</code>有问题,删除失败了,这个<code>key</code>就永远存在了。表现在业务上,就是<code>A</code>用户点击了领取,领取失败了,但是后面再怎么点,都是已经领取的状态了。</p><p><strong>那这种现象怎么优化呢?</strong></p><p>这种情况,其实已经是很少见的情况,按照我们当前的业务场景也看,就是当前的用户,<code>redis</code>记录了它已经领取过了,但是由于接口的失败,成功之后还没将<code>mysql/其他数据库</code>更新,两个数据库不一致了。</p><p>我能想到的一个方法,就是再删除失败的时候,告警,并且将业务相关的数据记录下来,比如<code>key</code>,<code>uid</code>等等,针对这部分数据,做一次补发,或者手动删除key。</p><p>或者,启动一个定时任务或者<code>lua</code>脚本,去判定<code>redis</code>和数据库不一致的情况,但是切记不要全部查询,应该是隔一段时间,查询最后增加的部分,做一个校验以及相应的处理。枚举<code>key</code>是十分耗时的操作!!!</p><p><code>setnx</code> 除了解决上面的问题,还可以应用在解决<strong>缓存击穿</strong>的问题上。</p><p>譬如现在有热点数据,不仅在<code>mysql</code>数据库存储了,还在<code>redis</code>中存了一份缓存,那么如果有一个时间点,缓存失效了,这时候,大量的请求打过来,同时到达,缓存拿不到数据,都去数据库取数据,假设数据库操作比较耗时,那么压力全都在数据库服务器上了。</p><p>这个时候所有的请求都去更新数据,明显是不合适的,应该是使用分布式锁,让一个线程去请求<code>mysql</code>一次即可。但是为了避免死锁的情况,如果超时,得及时额外释放锁,要不可能请求<code>mysql</code>都失败了,其他线程又拿不到锁,那么数据就会一直为<code>null</code>了。</p><p>可以使用以下的命令:</p><pre><code class="shell">SETNX lock.foo <current Unix time + lock timeout + 1></code></pre><p>关于这个场景下的<code>setnx</code>先讲到这里,后面再讲讲分布式锁相关的知识。</p><blockquote><strong>【刷题笔记】</strong><br>Github仓库地址:<a href="https://link.segmentfault.com/?enc=kvOcwH48Lkbd4nyGWRCBHw%3D%3D.XNWUwf6UiiVjcUUMTC1DQyT5%2FATtmhiyBGifOjVenYtUE2MWRoZGKib2ROZWhdOF" rel="nofollow">https://github.com/Damaer/cod...</a> <br>笔记地址:<a href="https://link.segmentfault.com/?enc=Hy6%2BI0oXTpla9ZnEUukUHg%3D%3D.3web5BZzTnIYmNp9KVcHms59OHE5PEXsC8BSK7fHOWFCnU6Vop4NfQUXNIFlsgFN" rel="nofollow">https://damaer.github.io/code...</a></blockquote><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=jPzUIeSfhy1OC2doh3ofvg%3D%3D.xKQ6yb4PQoMoiGoAMQiCSS3mW0cH83zwCYffw4Ie7Do%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=UYqu%2FOkQa3PzUBNMnTxHVg%3D%3D.bD1Lmgc1mozAHNxa0BhjIQ0Y%2F%2BiJ%2F%2B1yQiYpRJN5VC%2FjD6E9fz3LAsmiAHI1svqU" rel="nofollow">开源刷题笔记</a></p><p>平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~</p>
【实战问题】-- 高并发架构设计以及超领现象解决?
https://segmentfault.com/a/1190000039663955
2021-03-18T14:06:01+08:00
2021-03-18T14:06:01+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>现在 有一个场景,领取礼品,每个用户有次数限制,用户通过前端点击,调用了应用A的接口,里面调用了服务B,服务B里面去调用了服务C,注意服务C是其他部门的服务。服务C负责真正的发放礼品。(假设这个服务C我们是不可修改的,A,B是自己团队负责的,并且可能出现高并发的情况)</p><p><img src="/img/remote/1460000039663960" alt="" title=""></p><p>我们应该如何做这个次数限制呢?</p><p>假设每次领取礼品的活动有一个<code>activityId</code>,一个用户一个活动可以领取一件礼品,礼品有<code>giftId</code>,不可以多领,每个用户对应一个<code>uid</code>。</p><h2>查询是否可以领取</h2><p>首先对于前端而言,进入系统,首先需要获取用户是否已经领取过,而这个是否已经领取过,具体的实现我们应该写在B服务中,用户通过应用A,请求到服务B,返回用户是否已经领取的结果。</p><p>查询是否领取的流程大致如下:<br>用户进入页面,前端如果有缓存的话,可以为他展示之前缓存的结果,假设没有缓存,就会请求A应用,A应用会去请求B服务,B服务首先需要判断礼品或者活动是否存在。</p><p>去redis里面取活动或者礼品是否存在,如果redis没有查询到,那么就查询数据库,返回结果,如果数据库都没有,说明这个前端请求很可能是捏造的,直接返回结果“活动或者礼品不存在”,如果此时查询出来,确实存在,那么就需要去查询是否领取过,同样是查询redis,不存在的情况下,查询数据库,再返回结果。,如果领取过,则会有领取结果,前端将按键置灰,否者用户按键可以领取。</p><p><img src="/img/remote/1460000039663959" alt="" title=""></p><p>上面的redis肯定是需要我们维护的,这里不展开讲。比如增加活动的时候,除了改数据库,同时需要<code>redis</code>里面写一份数据,key可以是<code>activityId_giftId</code>,记录已经有的活动,用户成功领取的时候,同样是不仅增加数据库记录,也需要往<code>redis</code>写一份数据,key可以是<code>activityId_giftId_uid</code>,记录该用户已经领取过该活动的奖品。</p><p>但是上面的系统,有一个问题,就是活动/礼品不存在的时候,请求会每一次都直接打到数据库,如果是恶意攻击,数据库就挂了。这里当然可以考虑使用布隆过滤器,对请求参数中的活动/礼品做过滤,同时也可以考虑其他的防爬虫手段,比如滑动窗口统计请求数,根据<code>ip</code>,客户端<code>id</code>,<code>uid</code>等等。</p><p>当然,如果可以保证<code>redis</code>数据可靠,稳定,可以不请求数据库,<code>redis</code>不包含则说明不存在,直接返回。但是这种做法需要在增加活动/修改商品的时候,同时将<code>redis</code>一同修改同步。如果redis挂掉的情况,或者请求<code>redis</code>异常,再去查询数据库。如果能接受修改数据库活动信息不立马更新,也可以考虑更新完数据库,用消息队列发一条消息,收到再做<code>redis</code>更新。当然,这个不是一种好的做法,解耦合之后,增加了复杂度。前面说的做法,只要<code>redis</code>挂了,数据库理论上也支撑不了多久(极端情况)。</p><p>(当然,上面不是完美的方案,是个大致流程)</p><h2>领取礼品接口怎么处理?</h2><p>首先流程上与上面的查询是否领取过有些类似,,但是在查询是否领取过这一步之后,有所不同。如果已经领取过,则直接返回,但是如果没有领取过,需要调用C服务进行领取,如果调用C接口失败,或者返回领取失败,B服务需要做的事,就是记录日志或者告警,同时返回失败。<br>如果C服务返回领取成功,那么需要记录领取记录到数据库,并且更新缓存,表示已经领取过该礼品,这也是上面为什么一般能直接查询缓存就可以知道用户是否领取过的原因。<br><img src="/img/remote/1460000039663958" alt="" title=""></p><p>这个设计中,其实C服务才是真正实现方法奖品的服务,我们做的A和B相当于调用别人的服务,做了中间服务,这种情况更需要记录日志,控制爬虫,恶意攻击等等,同时做好异常处理。</p><p>上面的设计,如果我们来写段伪代码,来看看有什么问题?</p><pre><code class="java"> public String receiveGitf(int activityId,int giftId,String uid){
// isExist判断活动是否存在,内部包括redis和数据库请求,省略
if(isActivityExist(activityId,giftId)){
// 活动和礼品有效,判断是否领取过
if(!userReceived(uid,activityId,giftId)){
// 没有领取过,调用C系统
try {
boolean receivedResult = Http.getMethod(C_Client.class, "distributeGift");
if(receivedResult){
// 领取成功更新mysql
updateMysql(uid,activityId,giftId);
// 领取成功更新redis
updateRedis(uid,activityId,giftId);
}else{
return "已经领过/领取失败";
}
}catch (Exception e){
// 记录日志
logHelper.log(e);
return "调用领券系统失败,请重试";
}
}
}
return "领取失败,活动不存在";
}</code></pre><p>看起来好像没有什么问题,领取成功写<code>redis</code>,之后读到就不会再领取。但是高并发环境下呢?高并发环境下,很有可能出现领取多次的情况,因为网络请求不是瞬时可以返回的,如果有很多个同一个uid的请求,同时进来,C服务的处理或者延迟比较高。所有的请求都会堵塞在请求C服务这里。(网络请求需要时间!!!)</p><p><img src="/img/remote/1460000039663957" alt="" title=""></p><p>这时候还没有任何请求成功,所以<code>redis</code>根本不会更新,数据库也不会,所以的请求都会打到C服务,假设别人的服务是不可靠的,可以多次领取,那么所有的请求都会成功,并且会有多条成功的记录!!!</p><p>那如何来改进这个问题呢?<br>我们可以使用<code>setnx</code>来处理,先请求<code>setnx</code>,更新缓存,然后只有一个可以成功进来,如果真的成功,再写数据库,如果异常或者请求失败,将缓存删除。</p><pre><code class="java"> public String receiveGitf(int activityId,int giftId,String uid){
// isExist判断活动是否存在,内部包括redis和数据库请求,省略
if(isActivityExist(activityId,giftId)){
// 活动和礼品有效,判断是否领取过
if(!userReceived(uid,activityId,giftId)){
// 没有领取过,调用C系统
try {
// setnx
if(redis.setnx("uid_activityId_giftId")){
boolean receivedResult = Http.getMethod(C_Client.class, "distributeGift");
if(receivedResult){
// 领取成功更新mysql
updateMysql(uid,activityId,giftId);
}else{
// 领取成功更新redis
deleteRedis(uid,activityId,giftId);
return "已经领过/领取失败";
}
}else{
return "已经领过/领取失败";
}
}catch (Exception e){
// 记录日志
logHelper.log(e);
return "调用领券系统失败,请重试";
}
}
}
return "领取失败,活动不存在";
}</code></pre><p>在 <code>Redis</code> 里,所谓 <code>SETNX</code>,是<code>「SET if Not eXists」</code>缩写,也就是只有<code>key</code>不存在的时候才设置,可以利用它来实现锁的效果。这样只有一个请求可以进入。</p><pre><code class="shell">
redis> EXISTS id # id 不存在
redis> SETNX id "1" # id 设置成功1
redis> SETNX id "2" # 尝试覆盖 id ,返回失败 0
redis> GET job # 没有被覆盖"2"</code></pre><p>这个场景下的问题已经得到初步的解决,那这个<code>setnx</code>有没有坑呢?下次我们聊一下...</p><blockquote><strong>【刷题笔记】</strong><br>Github仓库地址:<a href="https://link.segmentfault.com/?enc=8jeLJQDwe51%2BUqfb8O8ovQ%3D%3D.DoCIPyIFO70OyYtL9geVabuQZN5MMysSqsOtj9t2UW3mNCsHDBeVHLUpliXfxXaU" rel="nofollow">https://github.com/Damaer/cod...</a> <br>笔记地址:<a href="https://link.segmentfault.com/?enc=7XK6lLPtPKghH2yqOw08qw%3D%3D.cP1dsMPD%2FNzquXBnx4vT2gDiMPTHc1vgWErAqBB2KzloRAqWo22ltg8JfWJsNpMM" rel="nofollow">https://damaer.github.io/code...</a></blockquote><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=0a0UyBhjmomnWZuwldijVQ%3D%3D.m6NyP42VN54yw8MHZtJ5ztEdenIRbkIvdw%2BgLtGBHzQ%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=xXexJlIP7f2S51yQyHZFHg%3D%3D.4AaYkhEh3YFDXTycLVsAMS3aDhWgreUWsS08J1WLYhwVS4z0V%2FqU9e6gd7hyYwuV" rel="nofollow">开源刷题笔记</a></p><p>平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~</p>
JVM笔记 -- 来,教你类加载子系统
https://segmentfault.com/a/1190000039651113
2021-03-17T10:23:36+08:00
2021-03-17T10:23:36+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<h2>类加载子系统</h2><p><img src="/img/remote/1460000039424890" alt="" title=""></p><p>类文件首先需要经过类加载子系统,进行加载,进类信息等加载到运行时数据区,生成Klass的实例。</p><p>在类加载子系统中有以下3个阶段操作(广义上的加载):</p><ul><li><p>加载阶段</p><ul><li>Bootstrap ClassLoader:引导类加载器,主要加载JDK里面的核心类</li><li>Extension ClassLoader:拓展类加载器</li><li>Application ClassLoader:应用加载器</li></ul></li><li><p>链接阶段</p><ul><li>验证</li><li>链接</li><li>解析</li></ul></li><li>初始化阶段</li></ul><p><img src="/img/remote/1460000039651115" alt="" title=""><br>如果加载的时候失败了,则不会执行后面的链接等操作。</p><p>类加载子系统的作用:</p><ul><li>类加载器子系统可以从<strong>本地文件</strong>或者<strong>网络</strong>中加载Class文件,Class文件开头有特定标识“CAFEBABY”(魔数)。</li><li>类加载器只负责将文件加载到运行时数据区,但<strong>是否可以运行,是执行引擎管的</strong>。</li><li>加载的类信息存放在方法区中,除了类信息以外,方法区还存放了运行时产量池信息,可能HIA包括字符串字面量和数字常量(这部分常量是Class文件中常量池部分的内存映射)。</li></ul><p>譬如反编译后,会产生常量信息,里面包括常量以及符号引用等:<br><img src="/img/remote/1460000039651117" alt="" title=""></p><p>类加载器ClassLoader的角色,以下面的People.class为例:<br><img src="/img/remote/1460000039651119" alt="" title=""></p><p>通过类信息实例,可以通过new 实例化对象,也可以通过getClassLoader()获取类加载器,也可以通过实例getClass()获取类信息实例。</p><ol><li>People.class 存在本地硬盘上,相当于一个模板,最终可以实例化出n个同一个类但是属性不同的实例。</li><li>People.class加载到JVM中,被称为DNA元数据模板,存放在方法区,也就是类信息。类信息也是对象。</li><li>从.class文件,到加载到JVM中,称为元数据模板,这个过程需要一个转换工具,这个工具就是类加载器(Class Loader)。</li></ol><h2>加载(Loading)</h2><p>此处的加载,指的是类加载过程中的第一个阶段(环节),主要工作包括:</p><ul><li>1.通过类的全限定名获取定义此类的二进制字节流。</li><li>2.将这个二进制字节流所代表的静态存储结构转化为方法区(JDK7以及之前叫永久代,JDK8之后成为元空间)的运行时数据结构。</li><li>3.在内存中生成一个该类的<code>java.lang.Class</code>对象,作为方法区该类的各种数据的访问入口,也就是类信息对象。</li></ul><p>类的.class文件来源方式包括以下:</p><ul><li>本地系统直接加载</li><li>网络传输获取</li><li>从zip压缩包读取</li><li>运行的时候计算生成,譬如动态代理技术</li><li>由其他文件生成,譬如场景:JSP</li><li>从加密文件中解密获得</li></ul><h2>链接</h2><p>链接阶段又分为3个阶段:</p><ul><li><p>验证:</p><ul><li>目的是校验安全和法,确保Class文件的字节流中包含信息符合当前虚拟机要求,保证加载的类的正确性,不会危害到虚拟机的安全。</li><li><p>主要包括4种验证:</p><ul><li>文件格式验证(譬如文件开头是"CAFEBABY")</li><li>元数据验证</li><li>字节码验证</li><li>符号引用验证</li></ul></li></ul></li><li><p>准备:</p><ul><li>为类变量(static)分配内存并且设置该变量的默认初始值,即零值</li><li>不包含final修饰的static,因为final在编译的时候已经分配了,准备阶段会显示初始化。</li><li>不会为实例变量分配初始化,类变量会分配在方法区,但是实例变量是跟随对象一起分配在Java堆里面(一般情况)</li></ul></li><li><p>解析:</p><ul><li>将常量池的符号引用转化成为直接引用的过程</li><li>事实上,解析操作往往会伴随JVM在执行完<strong>初始化之后再执行</strong></li><li>符号引用就是一组符号来描述所引用的目标,《Java虚拟机规范》的Class文件格式中,直接引用就是直接指向目标的指针,相对偏移量或者一个间接定位到目标的句柄。</li><li>解析这个阶段,主要是针对类或者接口,字段,类方法,接口方法,方法类型等等,对应的常量池中的CONSTANT_Class_info,CONSTANT_Fieldred_info,CONSTANT_Methodref_info等。</li></ul></li></ul><h2>初始化</h2><p>初始化,就是执行类的构造器<code><clinit>()</code>的过程,注意<code><clinit>()</code>是类的构造器,不是对象的。<code><clinit>()</code>是初始化类的,就是把类装到JVM里的初始化,不是运行时对象的初始化。</p><p><code><clinit>()</code>这个方法不需要显式定义,而是<code>javac</code>编译器自动收集类中的所有变量的赋值动作,加上静态代码块,合并成的一个方法。</p><p><code><clinit>()</code>中代码的顺序和我们在类文件写的顺序一致。</p><p>执行子类的<code><clinit>()</code>方法之前,JVM会保证先执行其父类的<code><clinit>()</code>,默认父类是<code>Object</code>。</p><p><img src="/img/remote/1460000039651118" alt="" title=""></p><p>仔细观察上面的代码,会发现,final的属性,即使是static修饰的,在<code><clinit>()</code>里面都不会存在,这是为什么呢?</p><p>这是因为<strong>final修饰的是常量,常量不会在初始化的时候执行赋值!!!</strong>常量在编译的时候已经分配了,准备阶段会显示初始化。</p><p>如果我们将final去掉,就可以发现,去掉final修饰,字节码就会加上该字段的赋值:(下面的ldc是指常量池的意思,从常量池编号为#6的地方,加载该常量)<br><img src="/img/remote/1460000039651116" alt="" title=""></p><p>虚拟机在初始化的时候,已经保证了类的<code><clinit>()</code>方法,即使在多线程的环境下,也只会执行一次,其底层的逻辑就是默认同步加锁了。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=x0Ujg9xaOWUkYGvO8q8CnQ%3D%3D.Pvbo5AlLEyXNDBDlyAunTeUreIDrapcnFQWIGXOcrGY%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=QR4kAolxeCYXVFqzPfik8w%3D%3D.bZiTXRO1k25tnICLxO0YpA89QXt9UYRlYOJ3Hn0iXVja77evHKqegAUIoQ39GxmA" rel="nofollow">开源刷题笔记</a></p><p>平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~</p>
JVM笔记--如果你写JVM,最需要考虑的重要结构是什么?
https://segmentfault.com/a/1190000039424887
2021-03-16T10:43:30+08:00
2021-03-16T10:43:30+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<p>开局一张图,前面已经从每一部分解析过JVM的内存结构了,现在按照顺序来分析:<br><img src="/img/remote/1460000039424889" alt="" title=""></p><p>整体上来看:类文件从类加载子系统,加载完成之后,主要存放在方法区(JRockit和H9没有方法区,这里指的是HotSpot)。运行时的数据主要是存放在运行时数据区,代码的解释编译优化以及垃圾收集,都是在执行引擎中。本地方法是指Native方法,也就是C/C++编写的方法。</p><h2>类加载子系统</h2><p>类文件首先需要经过类加载子系统,进行加载,进类信息等加载到运行时数据区。</p><p>在类加载子系统中有以下三个阶段操作:</p><ul><li>加载</li><li>链接</li><li>初始化</li></ul><p><img src="/img/remote/1460000039424890" alt="" title=""></p><p>其中加载的时候,有三种类加载器:</p><ul><li>Bootstrap ClassLoader:引导类加载器,主要加载JDK里面的核心类</li><li>Extension ClassLoader:拓展类加载器</li><li>Application ClassLoader:应用加载器</li></ul><p>而链接也分为3个阶段,主要是:</p><ul><li>验证</li><li>链接</li><li>解析</li></ul><h2>运行时数据区</h2><p>经过类加载子系统加载之后,进入运行时数据区,运行时区域主要分为:</p><ul><li><p>线程私有:</p><ul><li>程序计数器:<code>Program Count Register</code>,线程私有,没有垃圾回收</li><li>虚拟机栈:<code>VM Stack</code>,线程私有,没有垃圾回收</li><li>本地方法栈:<code>Native Method Stack</code>,线程私有,没有垃圾回收</li></ul></li><li><p>线程共享:</p><ul><li>方法区:<code>Method Area</code>,以<code>HotSpot</code>为例,<code>JDK1.8</code>后元空间取代方法区,有垃圾回收。</li><li>堆:<code>Heap</code>,垃圾回收最重要的地方。</li></ul></li></ul><p><img src="/img/remote/1460000039424891" alt="" title=""></p><p>虚拟机栈,每一个线程有一份,每一个线程的虚拟机栈里面,存放的是一个个栈帧,每一个栈帧表示一个方法调用。</p><p>PC寄存器,同样是每一个线程有一份,不同线程之间执行到何处,互不干扰。</p><h2>执行引擎</h2><p>执行引擎里面可以逐行解释执行,也可以编译成机器指令直接执行,主要包括:</p><ul><li>解释器</li><li>即时编译器:即时编译器中包括了中间代码生成器,代码优化器,目标代码生成器等。</li><li>垃圾收集器</li></ul><p>解释器,需要逐行解释执行,效率低下。譬如:如果循环两千次,循环体很大,每次执行都需要解释执行。</p><p><code>JIT</code> 编译器,除了可以直接全部即时编译,还可以统计出那些代码执行频率比较高,这部分代码就是<strong>热点代码</strong>,这种技术叫做<strong>热点代码探测技术</strong>,<code>JIT</code> 编译器会将热点代码,提前编译成为机器指令,放在方法区缓存起来,下次执行到的时候,不需要解释执行,而是直接运行机器指令。</p><p><img src="/img/remote/1460000039391968" alt="" title=""></p><p><strong>即时编译器的执行效率很高,为什么不将它全部提前编译好缓存起来呢?</strong></p><ul><li>全部提前编译,首次启动响应速度慢,会有卡顿的感觉,因为编译需要大量时间。(主要原因)</li><li>缓存代码,需要放在方法区,占用内存空间,容易溢出。</li><li>翻译成为机器指令,则这部分缓存的 <code>CodeCache</code> 是不能够直接跨平台,因为不同环境的机器指令是不大一样的,只能每次运行前就全部编译。</li></ul><p>如果需要写一个虚拟机,那么需要考虑的重要两部分是:<strong>类加载子系统</strong>和<strong>执行引擎</strong>。<strong>类加载子系统</strong>负责将类信息按照规定,加载到运行时数据区,而执行引擎主要负责对代码解释执行或者编译成二进制缓存起来,进行执行。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=WshHMmKZtK5rYS%2BR1TT0Yw%3D%3D.2F7ToS0tqueSxIlHTn1sJnkXMS65lca%2BhlZh43pk4KE%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=1JYgsFvoUhqz%2BS11iSovhg%3D%3D.MWkhdaUNs9Unyjx6SpShGEYLY9k6Jt47Bc9jXGx9mQ1%2FNaEsQNihWceH1xn3XB2r" rel="nofollow">开源刷题笔记</a></p><p>平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~</p>
JVM笔记 -- JVM经历了什么?
https://segmentfault.com/a/1190000039391966
2021-03-11T12:58:11+08:00
2021-03-11T12:58:11+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<h2>Sun Classic VM</h2><ul><li>世界上第一款商用 <code>Java</code> 虚拟机,<code>JDK1.4</code> 已经淘汰。</li><li>内部只有解释器,可以自己外挂<code>JIT</code>编译器,但是二者只能使用其一,不能配合工作。</li><li><code>hotspot</code> 内置了该虚拟机。</li></ul><p>解释器,需要逐行解释执行,效率低下。譬如:如果循环两千次,循环体很大,每次执行都需要解释执行。</p><p><code>JIT</code> 编译器,除了可以直接全部即时编译,还可以统计出那些代码执行频率比较高,这部分代码就是热点代码,<code>JIT</code> 编译器会将热点代码,提前编译成为机器指令,放在方法区缓存起来,下次执行到的时候,不需要解释执行,而是直接运行机器指令。(<strong>此时的 Classic VM 还不具备热点代码探测的功能,只会全部提前编译</strong>)</p><p><img src="/img/remote/1460000039391968" alt="" title=""></p><p><strong>即时编译器的执行效率很高,为什么不将它全部提前编译好缓存起来呢?</strong></p><ul><li>全部提前编译,首次启动响应速度慢,会有卡顿的感觉,因为编译需要大量时间。(主要原因)</li><li>缓存代码,需要放在方法区,占用内存空间,容易溢出。</li><li>翻译成为机器指令,则这部分缓存的 <code>CodeCache</code> 是不能够直接跨平台,因为不同环境的机器指令是不大一样的,只能每次运行前就全部编译。</li></ul><h2>Exact VM</h2><p>为解决上一个虚拟机 <code>Classic VM</code> 的问题(解释器和即时编译器只能二选一),<code>JDK 1.2</code> 的时候,提出来的虚拟机。</p><p>准确内存管理:<code>Exact Memory Management</code>,虚拟机可以知道内存中的某一个位置的数据具体是什么类型。</p><p>该虚拟机已经初步具备了现在高性能虚拟机的雏形:</p><ul><li>热点代码探测</li><li>编译器和解释器混合工作</li></ul><p>遗憾的是,<code>Exact VM</code> 只在<code>Solaris</code>短暂使用,后面就被 <code>Hotspot</code> 代替了。</p><h2>HotSpot VM</h2><p>三大商用虚拟机之一。<br>由小公司 <code>“Longview Technologies”</code> 设计,该公司 1997 年被 <code>Sun</code> 收购,<code>Sun</code> 2009 年被甲骨文收购。<br><code>JDK 1.3 HotSpot</code> 成为默认虚拟机,目前仍是,(<code>JRockit</code>和<code>J9</code>都没有方法区),<code>Hotspot</code>在服务器,桌面,移动端,嵌入式等都有应用。</p><p><code>HotSpot</code> 名称来源主要是<strong>热点代码探测技术</strong>:</p><ul><li>通过计数器找到最具有编译价值的代码,触发即时编译和栈上替换。</li><li>编译器和解释器协同工作,可以在响应时间和最佳执行性能中取得平衡。解释器负责是启动时间,而编译器主要是针对执行效率。</li></ul><h2>JRockit</h2><p>三大商用虚拟机之一。<br><code>BEA</code> 公司研发的,2008年,<code>BEA</code> 公司被 <code>Oracle</code> 收购,<code>Oracle</code> 在<code>JDK8</code> 中,在 Hotspot 的基础上,整合了 <code>JRockit</code> 的优秀特性。</p><ul><li>专注于服务端应用,不太关注启动速度,<strong>内部不包含解释器实现</strong>,全部靠即时编译器编译后执行。</li><li>号称世界上最快的虚拟机,执行性能强劲。</li><li>针对延迟敏感的应用也有解决方案 <code>“JRockit Real Time”</code>。</li></ul><h2>J9</h2><p><code>J9</code>是三大商用虚拟机之一,全称<code>IBM Technology for Java Virtual Machine</code>,简称 <code>IT4J</code>,内部称<code>“J9”</code>。</p><p>定位和 <code>HotSpot</code> 差不多,号称世界上最快(在自己<code>IBM</code>的机器上最快)。</p><p><code>2007</code> 年,<code>IBM</code> 发布了 <code>J9 VM</code>,命名<code>OpenJ9</code>,交给 <code>Eclipse</code> 基金会管理。</p><h2>KVM和CDC/CLDC Hotspot</h2><ul><li><code>Oracle</code> 在 <code>Java ME</code> 产品线上的两款虚拟机:<code>CDC/CLDC Hotspot Implementation VM</code></li><li><code>KVM</code> 是 <code>CLDC-HI</code> 早期产品</li><li><p>主要是低端的移动端,简单,轻量,高度可移植</p><ul><li>智能控制器,传感器</li><li>老人手机,功能机</li></ul></li></ul><h2>Azul VM</h2><p>是与特定的硬件平台绑定,软硬件结合的专用的虚拟机,高性能<code>Java</code>虚拟机中的战斗机。</p><p><code>Azul VM</code> 是 <code>Azul System</code> 公司在 <code>Hotspot</code> 基础上进行大量改进,运行在自家专用硬件 <code>Vega</code> 系统上的 Java 虚拟机。 <br>每一个 <code>Azul VM</code> 可以管理至少数十个 <code>CPU</code> 和数百 <code>GB</code> 的内存,而且可以在<strong>巨大内存范围内实现可控的GC时间</strong>的垃圾收集器。</p><p>2010 年后,<code>Azul System</code> 发布了通用平台的 <code>Zing</code> 虚拟机。</p><h2>BEA Liquid VM</h2><p>高性能 <code>Java</code> 虚拟机中的战斗机,<code>BEA</code>公司开发,运行在自己的<code>Hypervisor</code>系统上。</p><p><code>Liquid VM</code> 不需要操作系统的支持,可以说本身已经实现了一个专用的操作系统的必要功能,比如线程调度,文件系统,网络支持等。<code>JRockit</code>停止开发,<code>Liquid VM</code> 研发也停止了。</p><h2>Apache Harmony</h2><p><code>Apache</code> 曾经推出过 <code>JDK 1.5</code>, <code>1.6</code> 兼容的 <code>Java</code> 运行平台 <code>Apache Harmony</code>。</p><p>由 <code>IBM</code> 和 <code>Intel</code> 联合开发,但是 <code>OpenJDK</code> 压制,并且 <code>Sun</code> 拒绝给予 <code>JCP</code> 认证,2011 年退役,其中 <code>Java</code> 类库代码吸纳进入 <code>Android SDK</code>中。</p><h2>Microsoft VM</h2><p>微软推出的,在 <code>IE3</code> 中支持 <code>Java Applets</code>,但是 <code>Sun</code>公司 <code>1997</code>年指控微软侵权,后续微软抹去了 <code>Microsoft VM</code>。</p><h2>Taobao JVM</h2><p>由阿里推出,基于<code>OpenJDK Hotspot Vm</code>,改造,深度定制一款高性能虚拟机。</p><ul><li>创新的 <code>GCIH(GC invisible heap)</code>技术,实现了 <code>off-heap</code>,将生命周期较长的 <code>Java</code>对象从<code>heap</code>中移动到 <code>heap</code> 之外,并且<code>GC</code>不能管理 <code>GCIH</code> 内部的 <code>Java</code> 对象,降低了 <code>GC</code> 的回收频率和提高<code>GC</code>的回收效率。</li><li><code>GCIH</code> 中的对象可以多个<code>Java</code>虚拟机进程之间共享。</li><li>使用<code>crc32</code>指令实现<code>JVM intrinsic</code> 降低<code>JNI</code>的调用开销。</li><li><code>PMU hardware</code> 的<code>Java profiling tool</code> 和诊断协助功能</li><li>针对大数据场景的<code>ZenGC</code></li></ul><p>缺点:硬件严重依赖<code>Intel</code>的<code>cpu</code>,损失兼容性。</p><h2>Dalvik VM</h2><ul><li>谷歌开发,应用于<code>Android</code>系统,并且在<code>Android 2.2</code>中提供了<code>JIT</code>。只能称虚拟机,而不是<code>“Java虚拟机”</code>,没有遵循<code>Java</code>虚拟机规范。</li><li>不能直接执行<code>Java</code>的<code>class</code>文件。</li><li>基于寄存器架构,而不是栈的架构。</li><li>执行的是编译以后的<code>dex(dalvik Executale)</code>文件,执行效率比较高。<code>dex</code>文件可以通过<code>Class</code>文件转化而来,使用<code>Java</code>语法编写应用程序,可以直接使用大部分<code>Java API</code>。</li><li><code>Android 5.0</code> 使用提前编译(<code>Ahead of Time Compilation</code>,<code>AOT</code>)的<code>ART VM</code> 替换<code>Dalvik VM</code>。</li></ul><p>PS:<code>Android</code>文件<code>.apk</code>修改文件后缀为<code>.zip</code>,解压之后就是很多文件,当然也包括<code>.dex</code>文件。</p><h2>Graal VM</h2><p>理念:<code>“Run Program Faster Anywhere”</code>。</p><ul><li>在<code>Hotspot VM</code>基础上增强,跨语言全栈虚拟机,可以作为任何语言的运行平台。</li><li>支持不同语言混用接口和对象</li><li>原理是将这些语言的源代码或者中间格式,通过解释器转化成为一种<code>Graal VM</code>接受的中间格式。</li><li>在运行时能够进行即时编译优化,获得更优秀的执行效率。</li></ul><p>最后:具体<code>JVM</code>的内存结构,取决于其实现,不同产商或者同一个产商的不同版本,都可能存在一定的差异。一般我们说的,是指<code>Hotspot</code>虚拟机。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=fghcCV6en7insOlodoenDA%3D%3D.XePmv06jWes3qCrRgsRnQpqd6R4Ic9NUAJ0HZNN%2BS%2FA%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=Ok3Pe1X002KlUyn0U5OU6w%3D%3D.rUa9T9ndgOMnJqDDXA78jmHJQ%2F6oD04dFRsfOBaGz1BUnGsZhgRkmAhCVjTMZwO8" rel="nofollow">开源编程笔记</a></p><p>平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~</p>
JVM笔记 -- JVM的生命周期介绍
https://segmentfault.com/a/1190000039372185
2021-03-09T11:42:52+08:00
2021-03-09T11:42:52+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<blockquote>Github仓库地址:<a href="https://link.segmentfault.com/?enc=tVciVtXZZsq99K%2FRVjgkAg%3D%3D.ZcaL5cQuF9nUGoioR38ipl6VOUd5NTsMPBbJeiswjyRb9vSgT61CovTElagwYdnk" rel="nofollow">https://github.com/Damaer/Jvm...</a> <br>文档地址:<a href="https://link.segmentfault.com/?enc=DKss0w7OMzuFk%2FZuiajA%2Bg%3D%3D.qEE8v2RhC5nzaVzxdNPfh2XB55shGsLz%2FGUJe%2BYYBndpnfUd6g1l3Ix4pBCv61l5" rel="nofollow">https://damaer.github.io/JvmN...</a></blockquote><p><strong>JVM生命周期</strong></p><ul><li>启动</li><li>执行</li><li>退出</li></ul><h2>启动</h2><p>Java虚拟机的启动时通过引导加载器(<code>bootstrap class loader</code>)创建一个初始类(<code>initial class</code>)来完成的,这个类是由Java虚拟机的具体实现指定的。</p><p>自定义的类是由系统类加载器加载的。自定义类的顶级父类都是<code>Object</code>,<code>Object</code>作为核心<code>api</code>中的类,是需要被引导加载器(<code>bootstrap class loader</code>)加载的。父类的加载是优先于子类加载的,所以要加载自定义的之前,会就加载<code>Object</code>类。</p><h2>执行</h2><ul><li><code>Java</code>虚拟机执行的时候有一个清晰的任务:执行<code>Java</code>程序。</li><li>真正执行程序的是一个叫<code>Java虚拟机</code>的进程。</li></ul><h2>退出</h2><p>虚拟机的退出有以下几种情况:</p><ul><li>程序正常执行结束</li><li>程序执行过程中遇到了异常或者错误而异常终止</li><li>由于操作系统出现错误而导致Java虚拟机进程终止</li><li>某线程调用<code>Runtime</code>类或者<code>System</code>类的<code>exit</code>方法,或者<code>Runtime</code>类的<code>halt()</code>方法,并且<code>Java</code>安全管理器也允许这次操作的条件下。</li><li><code>JNI</code>(<code>java native Interface</code>):用<code>JNI</code> 的<code>api</code>加载或者卸载<code>Java</code>虚拟机的时候,<code>Java</code>虚拟机可能异常退出。</li></ul><h2>System.exit()和Runtime.halt()</h2><p><strong>下面分析System.exit()和Runtime.halt():</strong></p><p><code>System.exit()</code>其实调用的是<code>Runtime</code>对象的<code>exit()</code>方法,<code>Runtime.getRuntime()</code>获取的是当前的运行时状态,也就是<code>Runtime</code>对象。</p><pre><code class="java">public static void exit(int status) {
Runtime.getRuntime().exit(status);
}</code></pre><p>看<code>Runtime</code>的<code>exit()</code>方法,里面调用的是<code>Shutdown.exit(status)</code>。</p><pre><code class="java"> public void exit(int status) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkExit(status);
}
Shutdown.exit(status);
}</code></pre><p>我们看<code>Shutdown</code>的<code>exit()</code>方法,当status不为0的时候,调用的是<code>halt(status)</code>。</p><pre><code class="java"> static void exit(int status) {
boolean runMoreFinalizers = false;
synchronized (lock) {
if (status != 0) runFinalizersOnExit = false;
switch (state) {
case RUNNING: /* Initiate shutdown */
state = HOOKS;
break;
case HOOKS: /* Stall and halt */
break;
case FINALIZERS:
if (status != 0) {
/* Halt immediately on nonzero status */
halt(status);
} else {
/* Compatibility with old behavior:
* Run more finalizers and then halt
*/
runMoreFinalizers = runFinalizersOnExit;
}
break;
}
}
if (runMoreFinalizers) {
runAllFinalizers();
halt(status);
}
synchronized (Shutdown.class) {
/* Synchronize on the class object, causing any other thread
* that attempts to initiate shutdown to stall indefinitely
*/
sequence();
halt(status);
}
}</code></pre><p>而<code>halt(int status)</code>本质上调用的是一个本地方法<code>halt0(int status)</code>,暂停虚拟机进程,退出。</p><pre><code class="java"> static void halt(int status) {
synchronized (haltLock) {
halt0(status);
}
}
static native void halt0(int status);</code></pre><p><img src="/img/remote/1460000038672207" alt="image-20201222221827719" title="image-20201222221827719"></p><p><code>Runtime</code>是运行时数据的对象,<strong>全局单例</strong>的,可以理解为它代表了运行时数据区。是一个饿汉式单例模式。从 JDK1.0 开始就,可以看出,这就是虚拟机的核心类!</p><p><img src="/img/remote/1460000039372187" alt="" title=""></p><p>下面可以测试一下<code>Runtime</code>的属性:</p><pre><code class="java">public class RuntimeTest {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
System.out.println(runtime.getClass().getName());
System.out.println("maxMemory: "+runtime.maxMemory()/1024/1024);
System.out.println("totalMemory: "+runtime.totalMemory()/1024/1024);
System.out.println("freeMemory: "+runtime.freeMemory()/1024/1024);
}
}</code></pre><p>运行结果:表示最大的内存是2713M,总的内存是184M,可以使用内存是180M。</p><pre><code class="java">java.lang.Runtime
maxMemory: 2713
totalMemory: 184
freeMemory: 180</code></pre><p>PS:本笔记是在宋红康老师的JVM视频中学习的笔记,均经过实践,加上自己的理解。地址:<a href="https://www.bilibili.com/video/BV1PJ411n7xZ">https://www.bilibili.com/vide...</a> ,强烈推荐!!!</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。</p>
JVM笔记 -- JVM的发展以及基于栈的指令集架构
https://segmentfault.com/a/1190000039358208
2021-03-07T14:46:55+08:00
2021-03-07T14:46:55+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<ul><li><ol><li>2011年,<code>JDK7</code>发布,1.7u4中,开始启用新的垃圾回收器<code>G1</code>(但是不是默认)。</li></ol></li><li><ol><li>2017年,发布<code>JDK9</code>,<code>G1</code>成为默认<code>GC</code>,代替<code>CMS</code>。(一般公司使用<code>jdk8</code>的时候,会通过参数,指定<code>GC</code>为<code>G1</code>)</li></ol></li><li><ol><li>2018年,发布<code>JDK11</code>,带来了革命性<code>ZGC</code>,性能比较强。</li></ol></li></ul><h2>虚拟机介绍</h2><p>虚拟机,就是虚拟的计算机,可以执行一系列虚拟计算机指令,大体上可以分为系统虚拟机和程序虚拟机。它们运行时,都会受到虚拟机提供的资源的限制。</p><ul><li>系统虚拟机:仿真模拟系统的,比如<code>Visual Box</code>,<code>VMware</code>。</li><li>程序虚拟机:为执行单个计算机程序设计的,比如<code>Java</code>虚拟机。</li></ul><h2>JAVA虚拟机</h2><p><code>Java</code>虚拟机是一台执行字节码的虚拟机计算机,但是字节码不一定是由<code>Java</code>语言编译而成。但是只要使用这一套虚拟机规则的语言,就可以享受到跨平台,垃圾收集以及可靠的即时编译器。<code>JVM</code>和硬件之间没有直接的交互。</p><ul><li>一次编译,到处运行。</li><li>自动内存管理</li><li>自动垃圾回收</li></ul><p>下面是ava平台文档中Java概念图的描述,可以看出<code>javac</code>命令在<code>JDK</code>中,也就是将<code>.java</code>文件编译成为<code>.class</code>文件,这个就是前端编译器,将源文件编译成为字节码。这个编译器不在<code>JRE</code>中,也说明了<code>JRE</code>不包括编译环境。</p><p>JRE和JDK都包括了JVM虚拟机。JRE是运行时环境,而JDK包含了开发环境。</p><p>JDK7 中java家族的结构组成 : <a href="https://link.segmentfault.com/?enc=oPEsup88befky7%2BNLWDq9A%3D%3D.0MmCllocRXbSKJZ981rQ4ZlsobJXQy2qQtjbE%2BjXWj2web15RI%2FuhNN9nzT5ZIVC" rel="nofollow">https://docs.oracle.com/javas...</a><br><img src="/img/remote/1460000039358215" alt="" title=""></p><p>JDK7 中java家族的结构组成 : <a href="https://link.segmentfault.com/?enc=iafAbk0%2BAiuSnTm2FBwe0Q%3D%3D.Enn62KwrtMSuwVdp2DyN11VrRN%2BAXEDR%2BDER7Zcryvc9puny85jwlW22TuTQo%2BSt" rel="nofollow">https://docs.oracle.com/javas...</a><br><img src="/img/remote/1460000039358210" alt="" title=""></p><h2>JVM结构</h2><p><img src="/img/remote/1460000039341403" alt="" title=""></p><p>上面的图主要包括三部分:类加载器,运行时数据区,执行引擎。</p><p>类加载器,主要是将Class文件(已经经过前端编译器编译后的字节码文件),加载到运行时数据区,生成Class对象,这个过程会设计加载,链接,初始化等过程。</p><p>运行时区域主要分为:</p><ul><li><p>线程私有(每个线程有一份):</p><ul><li>程序计数器:<code>Program Count Register</code>,线程私有,没有垃圾回收</li><li>虚拟机栈:<code>VM Stack</code>,线程私有,没有垃圾回收</li><li>本地方法栈:<code>Native Method Stack</code>,线程私有,没有垃圾回收</li></ul></li><li><p>线程共享:</p><ul><li>方法区:<code>Method Area</code>,以<code>HotSpot</code>为例,<code>JDK1.8</code>后元空间取代方法区,有垃圾回收。</li><li>堆:<code>Heap</code>,垃圾回收最重要的地方。</li></ul></li></ul><p>执行引擎主要包括解释器和即时编译器(热点代码提前编译好,这是后端编译器),垃圾回收器。字节码文件不能直接被机器识别,所以需要执行引擎来做转换。</p><h2>Java代码执行流程</h2><p>Java代码变成字节码文件的过程中,其实包含了词法分析,语法分析,语法树,语义分析等一系列操作。</p><p><img src="/img/remote/1460000039358212" alt="" title=""></p><p>在执行引擎中,有JIT编译器,也就是第二次编译的过程会发生在这里,会将热点代码编译成为机器指令,是按照方法的维度,缓存起来(放在方法区),也称之为<code>CodeCache</code>。</p><h2>JVM架构模型</h2><p>Java编译器主要是基于栈的指令集架构,个人觉得主要原因是可移植性决定的,JVM需要跨平台。指令集架构主要有两种:</p><ul><li>基于栈的指令集架构:一个方法相当于一个入栈的操作,执行完相当于出栈操作。</li><li>基于寄存器的指令集架构</li></ul><h3>基于栈的指令集架构的特点</h3><p>主要特点:</p><ul><li>设计实现简单,适用于资源受限的系统,比如机顶盒,小玩具上。</li><li>避开寄存器分配难题:使用零地址指令方式分配。</li><li>指令流中大部分都是零地址指令,执行过程依赖操作栈,指令集更小(零地址),编译器容易实现。</li><li>不需要硬件支持,可移植性强,容易实现跨平台。</li></ul><h3>基于寄存器架构的特点</h3><ul><li>典型应用是x86的二进制指令集</li><li>依赖于硬件,可移植性差</li><li>性能好,执行效率高</li><li>更少指令执行一项操作</li><li>大部分情况下,寄存器的架构,一,二,三地址指令为主,而基于栈的指令集却是以零地址指令为主。</li></ul><p><strong>说明:什么叫零地址指令,一地址指令,二地址指令?</strong><br>零地址指令只有操作码,没有操作数。这种指令有两种情况:一是无需操作数,另一种是操作数为默认的(隐含的),默认为操作数在寄存器中,指令可直接访问寄存器。</p><ul><li>三地址指令:一般地址域中A1、A2分别确定第一、第二操作数地址,A3确定结果地址。下一条指令的地址通常由程序计数器按顺序给出。</li><li>二地址指令:地址域中A1确定第一操作数地址,A2同时确定第二操作数地址和结果地址。</li><li>单地址指令:地址域中A 确定第一操作数地址。固定使用某个寄存器存放第二操作数和操作结果。因而在指令中隐含了它们的地址。</li><li>零地址指令:在堆栈型计算机中,操作数一般存放在下推堆栈顶的两个单元中,结果又放入栈顶,地址均被隐含,因而大多数指令只有操作码而没有地址域。</li></ul><p>栈数据结构,一般只有入栈和出栈,所以操作的地方只有栈顶元素,所以位置是确定的,不需要地址。</p><p><strong>例子</strong><br>执行2+3的操作,如果是基于栈的计算流程:</p><pre><code class="txt">iconst_2 // 常量2入栈
istore_1
iconst_3 // 常量3入栈
istore_2
iload_1
iload_2
iadd // 常量2,3出栈,执行相加
istore_0 // 结果5入栈</code></pre><p>基于寄存器的计算流程:</p><pre><code class="txt">mov eax,2 //将eax寄存器的值设置为2
add eax,3 // 将eax寄存器的值加3</code></pre><p>从上面的例子可以看出来,基于栈的寄存器的指令更小,但是基于寄存器的指令更少。</p><p>我们可以通过一个简单程序看一下:</p><pre><code class="java">public class StackStructTest {
public static void main(String[] args) {
int i = 2 + 3;
}
}</code></pre><p>编译后,切换到<code>class</code>目录下,使用命令反编译:</p><pre><code class="shell">java -v StackStructTest.class</code></pre><p><img src="/img/remote/1460000039358213" alt="" title=""></p><p>看到字节码的模块,可以看到前面有<code>iconst_5</code>,其实<code>5</code>就是<code>2+3</code>的结果,也就是编译期间就会直接把<code>2+3</code>变成<code>5</code>,不会在运行的时候才去计算,这个是因为<code>2</code>和<code>3</code>都是常量。</p><p>这个现象称之为<strong>编译期的常量折叠</strong>。<br><img src="/img/remote/1460000039358211" alt="" title=""></p><p>但是如果我们把代码成下面这种情况呢?</p><pre><code class="java"> int i = 2;
int j = 3;
int k = i + j;</code></pre><p>反编译出来的指令:<br><img src="/img/remote/1460000039358214" alt="" title=""></p><p><code>const</code>意思是<code>constant</code>(常量),<code>store</code>是<code>storeage</code>寄存器。</p><pre><code class="txt"> stack=2, locals=4, args_size=1
0: iconst_2 // 2是个常量
1: istore_1 // 2加载到1号操作数栈
2: iconst_3 // 3是一个产量
3: istore_2 // 3加载到2号操作数栈
4: iload_1 // 将1号操作数栈取出,加载进来
5: iload_2 // 将2号操作数栈取出,加载进来
6: iadd // 两者相加
7: istore_3 // 结果存储到索引为3号操作数栈中
8: return</code></pre><p>也就是栈架构的<code>JVM</code>,需要 8 条指令才能完成上面的变量相加计算。</p><p><strong>栈架构总结</strong> </p><p>由于跨平台特性,Java指令基于栈来设计,因为不同的CPU架构不同,优点是跨平台,指令集小,编译器容易实现。缺点是性能下降,实现同样功能需要更多指令。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=bLq0bXSNnhLJUpfiB293wg%3D%3D.1FfznOSqDjMt8Ad2SlbiU4lXoDwWLFHhW7hemZq%2B5F4%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=OxmMtPH%2FwXZkkjgGmxOWlg%3D%3D.AJed8%2BCCHdo%2BrMaXoj4xPgs7XUH5J6KJW%2BGaPgAciw5A9jKgWy70cbl%2FfSnwdO%2BO" rel="nofollow">开源编程笔记</a></p><p>平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~</p>
从JVM底层原理分析数值交换那些事
https://segmentfault.com/a/1190000039341392
2021-03-05T01:10:29+08:00
2021-03-05T01:10:29+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<h2>基础数据类型交换</h2><p>这个话题,需要从最最基础的一道题目说起,看题目:以下代码a和b的值会交换么:</p><pre><code class="java"> public static void main(String[] args) {
int a = 1, b = 2;
swapInt(a, b);
System.out.println("a=" + a + " , b=" + b);
}
private static void swapInt(int a, int b) {
int temp = a;
a = b;
b = temp;
} </code></pre><p>结果估计大家都知道,a和b并没有交换:</p><pre><code class="shell">integerA=1 , integerB=2</code></pre><p>但是原因呢?先看这张图,先来说说Java虚拟机的结构:<br><img src="/img/remote/1460000039341403" alt="" title=""></p><p>运行时区域主要分为:</p><ul><li><p>线程私有:</p><ul><li>程序计数器:<code>Program Count Register</code>,线程私有,没有垃圾回收</li><li>虚拟机栈:<code>VM Stack</code>,线程私有,没有垃圾回收</li><li>本地方法栈:<code>Native Method Stack</code>,线程私有,没有垃圾回收</li></ul></li><li><p>线程共享:</p><ul><li>方法区:<code>Method Area</code>,以<code>HotSpot</code>为例,<code>JDK1.8</code>后元空间取代方法区,有垃圾回收。</li><li>堆:<code>Heap</code>,垃圾回收最重要的地方。</li></ul></li></ul><p>和这个代码相关的主要是虚拟机栈,也叫方法栈,是每一个线程私有的。<br>生命周期和线程一样,主要是记录该线程Java方法执行的内存模型。虚拟机栈里面放着好多<strong>栈帧</strong>。<strong>注意虚拟机栈,对应是Java方法,不包括本地方法。</strong></p><p><strong>一个Java方法执行会创建一个栈帧</strong>,一个栈帧主要存储:</p><ul><li>局部变量表</li><li>操作数栈</li><li>动态链接</li><li>方法出口</li></ul><p>每一个方法调用的时候,就相当于将一个<strong>栈帧</strong>放到虚拟机栈中(入栈),方法执行完成的时候,就是对应着将该栈帧从虚拟机栈中弹出(出栈)。</p><p>每一个线程有一个自己的虚拟机栈,这样就不会混起来,如果不是线程独立的话,会造成调用混乱。</p><p>大家平时说的java内存分为堆和栈,其实就是为了简便的不太严谨的说法,他们说的栈一般是指虚拟机栈,或者虚拟机栈里面的局部变量表。</p><p>局部变量表一般存放着以下数据:</p><ul><li>基本数据类型(<code>boolean</code>,<code>byte</code>,<code>char</code>,<code>short</code>,<code>int</code>,<code>float</code>,<code>long</code>,<code>double</code>)</li><li>对象引用(reference类型,不一定是对象本身,可能是一个对象起始地址的引用指针,或者一个代表对象的句柄,或者与对象相关的位置)</li><li>returAddress(指向了一条字节码指令的地址)</li></ul><p>局部变量表内存大小编译期间确定,运行期间不会变化。空间衡量我们叫Slot(局部变量空间)。64位的long和double会占用2个Slot,其他的数据类型占用1个Slot。</p><p>上面的方法调用的时候,实际上栈帧是这样的,调用main()函数的时候,会往虚拟机栈里面放一个栈帧,栈帧里面我们主要关注局部变量表,传入的参数也会当成局部变量,所以第一个局部变量就是参数<code>args</code>,由于这个是<code>static</code>方法,也就是类方法,所以不会有当前对象的指针。</p><p><img src="/img/remote/1460000039341396" alt="" title=""></p><p>如果是普通方法,那么局部变量表里面会多出一个局部变量<code>this</code>。</p><p>如何证明这个东西真的存在呢?我们大概看看字节码,因为局部变量在编译的时候就确定了,运行期不会变化的。下面是<code>IDEA</code>插件<code>jclasslib</code>查看的:<br><img src="/img/remote/1460000039341401" alt="" title=""></p><p>上面的图,我们在<code>main()</code>方法的局部变量表中,确实看到了三个变量:<code>args</code>,<code>a</code>,<code>b</code>。</p><p><strong>那在main()方法里面调用了swapInt(a, b)呢?</strong></p><p>那堆栈里面就会放入<code>swapInt(a,b)</code>的栈帧,<strong>相当于把a和b局部变量复制了一份</strong>,变成下面这样,由于里面一共有三个局部变量:</p><ul><li>a:参数</li><li>b:参数</li><li>temp:函数内临时变量</li></ul><p>a和b交换之后,其实<code>swapInt(a,b)</code>的栈帧变了,a变为2,b变为1,但是<code>main()</code>栈帧的a和b并没有变。<br><img src="/img/remote/1460000039341402" alt="" title=""></p><p>那同样来从字节码看,会发现确实有3个局部变量在局部变量表内,并且他们的数值都是int类型。<br><img src="/img/remote/1460000039341400" alt="" title=""></p><p>而<code>swap(a,b)</code>执行结束之后,该方法的堆栈会被弹出虚拟机栈,此时虚拟机栈又剩下<code>main()</code>方法的栈帧,由于基础数据类型的数值相当于存在局部变量中,<code>swap(a,b)</code>栈帧中的局部变量不会影响<code>main()</code>方法的栈帧中的局部变量,所以,就算你在<code>swap(a,b)</code>中交换了,也不会变。</p><p><img src="/img/remote/1460000039341395" alt="" title=""></p><h2>基础包装数据类型交换</h2><p>将上面的数据类型换成包装类型,也就是<code>Integer</code>对象,结果会如何呢?</p><pre><code class="java"> public static void main(String[] args) {
Integer a = 1, b = 2;
swapInteger(a, b);
System.out.println("a=" + a + " , b=" + b);
}
private static void swapInteger(Integer a, Integer b) {
Integer temp = a;
a = b;
b = temp;
}</code></pre><p>结果还是一样,交换无效:</p><pre><code class="shell">a=1 , b=2</code></pre><p>这个怎么解释呢?</p><p>对象类型已经不是基础数据类型了,局部变量表里面的变量存的不是数值,而是对象的引用了。先用<code>jclasslib</code>查看一下字节码里面的局部变量表,发现其实和上面差不多,只是描述符变了,从<code>int</code>变成<code>Integer</code>。<br><img src="/img/remote/1460000039341397" alt="" title=""></p><p><img src="/img/remote/1460000039341404" alt="" title=""></p><p>但是和基础数据类型不同的是,局部变量里面存在的其实是堆里面真实的对象的引用地址,通过这个地址可以找到对象,比如,执行<code>main()</code>函数的时候,虚拟机栈如下:</p><p>假设 a 里面记录的是 1001 ,去堆里面找地址为 1001 的对象,对象里面存了数值1。b 里面记录的是 1002 ,去堆里面找地址为 1002 的对象,对象里面存了数值2。</p><p><img src="/img/remote/1460000039341399" alt="" title=""></p><p>而执行<code>swapInteger(a,b)</code>的时候,但是还没有交换的时候,相当于把 局部变量复制了一份:<br><img src="/img/remote/1460000039341398" alt="" title=""></p><p>而两者交换之后,其实是<code>SwapInteger(a,b)</code>栈帧中的a里面存的地址引用变了,指向了b,但是b里面的,指向了a。</p><p><img src="/img/remote/1460000039341407" alt="" title=""></p><p>而<code>swapInteger()</code>执行结束之后,其实<code>swapInteger(a,b)</code>的栈帧会退出虚拟机栈,只留下<code>main()</code>的栈帧。<br><img src="/img/remote/1460000039341399" alt="" title=""></p><p>这时候,a其实还是指向1,b还是指向2,因此,交换是没有起效果的。</p><h2>String,StringBuffer,自定义对象交换</h2><p>一开始,我以为<code>String</code>不会变是因为<code>final</code>修饰的,但是实际上,不变是对的,但是不是这个原因。原因和上面的差不多。</p><p><code>String</code>是不可变的,只是说堆/常量池内的数据本身不可变。但是引用还是一样的,和上面分析的<code>Integer</code>一样。<br><img src="/img/remote/1460000039341405" alt="" title=""></p><p>其实<code>StringBuffer</code>和自定义对象都一样,局部变量表内存在的都是引用,所以交换是不会变化的,因为<code>swap()</code>函数内的栈帧不会影响调用它的函数的栈帧。</p><p>不行我们来测试一下,用事实说话:</p><pre><code class="java"> public static void main(String[] args) {
String a = new String("1"), b = new String("2");
swapString(a, b);
System.out.println("a=" + a + " , b=" + b);
StringBuffer stringBuffer1 = new StringBuffer("1"), stringBuffer2 = new StringBuffer("2");
swapStringBuffer(stringBuffer1, stringBuffer2);
System.out.println("stringBuffer1=" + stringBuffer1 + " , stringBuffer2=" + stringBuffer2);
Person person1 = new Person("person1");
Person person2 = new Person("person2");
swapObject(person1,person2);
System.out.println("person1=" + person1 + " , person2=" + person2);
}
private static void swapString(String s1,String s2){
String temp = s1;
s1 = s2;
s2 = temp;
}
private static void swapStringBuffer(StringBuffer s1,StringBuffer s2){
StringBuffer temp = s1;
s1 = s2;
s2 = temp;
}
private static void swapObject(Person p1,Person p2){
Person temp = p1;
p1 = p2;
p2 = temp;
}
class Person{
String name;
public Person(String name){
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}</code></pre><p>执行结果,证明交换确实没有起效果。</p><pre><code class="java">a=1 , b=2
stringBuffer1=1 , stringBuffer2=2
person1=Person{name='person1'} , person2=Person{name='person2'}</code></pre><h2>总结</h2><p>基础数据类型交换,栈帧里面存的是局部变量的数值,交换的时候,两个栈帧不会干扰,<code>swap(a,b)</code>执行完成退出栈帧后,<code>main()</code>的局部变量表还是以前的,所以不会变。</p><p>对象类型交换,栈帧里面存的是对象的地址引用,交换的时候,只是<code>swap(a,b)</code>的局部变量表的局部变量里面存的引用地址变化了,同样<code>swap(a,b)</code>执行完成退出栈帧后,<code>main()</code>的局部变量表还是以前的,所以不会变。</p><p>所以不管怎么交换都是不会变的。</p><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=8xeem1xHncgEbep0lghp5Q%3D%3D.b6s01dNunK817ZU13jUcKC%2BTXi2vCayuLENX6KBYVUE%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=mVfU1vCfjBwbI2JVSpNxRQ%3D%3D.EbICaOwR58pU%2BqYT%2BwnumOHczHI5qEEKy3Htn56phg1vQ5N5SRnhFtmV5NhRony0" rel="nofollow">开源编程笔记</a></p><p>平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~</p>
设计模式【3.1】-- 浅谈代理模式之静态、动态、cglib代理
https://segmentfault.com/a/1190000039322050
2021-03-03T00:07:43+08:00
2021-03-03T00:07:43+08:00
秦怀杂货店
https://segmentfault.com/u/aphysia
0
<blockquote><ul><li>代理模式:为其他对象提供一种代理以控制对这个对象的访问,在某种情况下,一个对象不适合或者不能够直接引用另一个对象,而代理对象可以在客户类和目标对象之间起到中介的作用。</li><li>可以这么理解:使用代理对象,是为了在不修改目标对象的基础上,增强主业务的逻辑。就相当于某个普通人(目标对象),他现在需要打官司,那么他可以自己学习法律,为自己辩护(相当于把业务代码逻辑自己来实现),这就是修改了目标对象,那么当然有一种更好的方法啦,那就是请律师(也相当于代理对象),业务代码(为自己辩护)可以由律师来实现。</li></ul></blockquote><p>代理一般可以分为三种:<strong>静态代理</strong>,<strong>动态代理</strong>,<strong>cglib代理</strong>;</p><h4>1.静态代理</h4><p>静态代理使用的时候,一般是定义接口或者父类,目标对象(被代理的对象)与代理对象都要实现相同的接口或者继承同样的父类。<br>下面实现静态代理<br><br>代码结构:<br><br><img src="/img/remote/1460000039322055" alt="" title=""><br><br>创建一个接口类 (IBuyDao.calss)买东西:</p><pre><code class="java">public interface IBuyDao {
public void buySomething();
}</code></pre><p>然后创建一个实现了接口的目标类(BuyDao.calss )即要买东西的客户:</p><pre><code class="java">public class BuyDao implements IBuyDao {
@Override
public void buySomething() {
System.out.println("我是客户,我想买东西");
}
}</code></pre><p>代理类(BuyDaoProxy):将目标对象当成属性传进去,对目标对象进行增强</p><pre><code class="java">public class BuyDaoProxy implements IBuyDao{
private IBuyDao target;
public BuyDaoProxy(IBuyDao target){
this.target = target;
}
@Override
public void buySomething() {
System.out.println("开始代理方法(购物)");
target.buySomething();
System.out.println("结束代理方法");
}
}
</code></pre><p>测试方法(Test.class):</p><pre><code class="java">public class Test {
public static void main(String [] args){
IBuyDao target = new BuyDao();
//应该写成这样
IBuyDao proxy = new BuyDaoProxy(target);
//下面的这样写就不算严格意义的代理了,代理应该是返回目标对象或接口对象(java一切皆对象)
//BuyDaoProxy proxy = new BuyDaoProxy(target);
proxy.buySomething();
}
}
</code></pre><p>结果如下:<br><br><img src="/img/remote/1460000039322054" alt="" title=""><br><br></p><blockquote><ul><li>在这里有一个疑惑,就是如果BuyDaoProxy.class 没有实现接口的话,也是可以跑起来,而且结果一样。假如改成这样子:</li></ul></blockquote><pre><code class="java">public class BuyDaoProxy {
private IBuyDao target;
public BuyDaoProxy(IBuyDao target){
this.target = target;
}
public void buySomething() {
System.out.println("开始代理方法(购物)");
target.buySomething();
System.out.println("结束代理方法");
}
}</code></pre><p>个人理解:如果没有实现接口的话,也是可以实现的,这就相当于接口调用,但是一般我们使用代理都会是相同方法名字,使用接口的话,可以强制性使用相同的方法名,而不是随意起一个名字,不使用接口时使用相同方法名也是没有问题的,只是容易写错名字,特别是同一个代理有很多方法的时候。但是这样写就不能用 <strong>IBuyDao proxy = new BuyDaoProxy(target);</strong> ,那么这意义也就不能算是代理了,代理应该返回<strong>接口或目标对象</strong><br><br>实现多个接口的例子(新增加了一个学生买书的接口,以及实现类):<br><br><img src="/img/remote/1460000039322057" alt="" title=""><br><br>接口:</p><pre><code class="java">public interface IStudent {
public void Buybook();
}
</code></pre><p>接口实现类:</p><pre><code class="java">public class Student implements IStudent{
@Override
public void Buybook() {
System.out.println("我是学生,我想买书");
}
}
</code></pre><p>代理类:</p><pre><code class="java">public class BuyDaoProxy implements IStudent,IBuyDao{
private IBuyDao target;
public Student student;
public BuyDaoProxy(IBuyDao target){
this.target = target;
}
public BuyDaoProxy(Student student){
this.student =student;
}
@Override
public void buySomething() {
System.out.println("开始代理方法(购物)");
target.buySomething();
System.out.println("结束代理方法");
}
@Override
public void Buybook() {
System.out.println("开始代理方法(买书)");
student.Buybook();
System.out.println("结束代理方法");
}
}
</code></pre><p>测试类:</p><pre><code class="java">public class Test {
public static void main(String [] args){
Student target = new Student();
BuyDaoProxy proxy = new BuyDaoProxy(target);
proxy.Buybook();
}
}
</code></pre><p>结果:<br><br><img src="/img/remote/1460000039322059" alt="" title=""><br><br>个人理解:实现多个接口的时候,要是没有去实现多个接口,就很容易把名字写错,所以强制性使用接口,实现一致的名字,对目标类进行功能增强(在目标类方法之前或者之后处理)。<br>缺点:代理对象需要和目标对象实现一样的接口,所以目标类多了,或者接口增加方法,目标类以及代理的类都要维护。</p><h4>2.动态代理(即JDK代理,接口代理)</h4><blockquote><ul><li>代理对象不需要实现接口,但是目标对象一定要实现接口</li><li>使用的是jdk的API,动态的创建代理对象</li></ul></blockquote><p>我们来看代理的方法源码:</p><pre><code class="java">public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, intfs);
/*
* Invoke its constructor with the designated invocation handler.
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}</code></pre><blockquote><p><strong>从return cons.newInstance(new Object[]{h});</strong> 这句我们可以知道,我们需要去操作h,h其实就是 <strong>InvocationHandler h</strong> 这个参数,那么我们需要重新定义 InvocationHandler h</p><ul><li><strong>ClassLoader loader</strong>:是一个类加载器,这个获取类加载器的方法是固定的,我们不能坐任何改变</li><li><strong>Class<?>[] interfaces</strong> :这是接口类的数组,使用泛型确认接口类型,这时候接口参数就只能是目标对象所实现的接口</li><li><strong>InvocationHandler h</strong>:重要的是这个参数,重写它的invoke()方法,就可以实现对目标对象的<strong>接口</strong>的增强。</li></ul><p><br>类的结构如下(之所以实现两个接口,是因为多接口的时候更容易分清):<br><br><br><img src="/img/remote/1460000039322058" alt="" title=""><br><br>代码如下:<br><br>IBuyDao.java(买东西的接口)</p></blockquote><pre><code class="java">public interface IBuyDao {
public void buySomething();
}
</code></pre><p>IPlayDao.java(玩的接口)</p><pre><code class="java">public interface IPlayDao {
void play();
}</code></pre><p>StudentDao.java(实现了买东西,玩的接口的学生类)</p><pre><code class="java">public class StudentDao implements IBuyDao,IPlayDao {
@Override
public void buySomething() {
System.out.println("我是学生,我想买东西");
}
@Override
public void play() {
System.out.println("我是学生,我想出去玩");
}
}
</code></pre><p>MyProxy.java 代理类:</p><pre><code class="java">import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class MyProxy {
private Object target;
public MyProxy(Object target){
this.target=target;
}
public Object getProxyInstance(){
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
//一个接口可能很多方法,要是需要针对某一个方法,那么需要在函数里判断method
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始事务2");
//执行目标对象方法
Object returnValue = method.invoke(target, args);
System.out.println("提交事务2");
return returnValue;
}
}
);
}
}</code></pre><p>测试类(Test.java)</p><pre><code class="java">public class Test {
public static void main(String [] args){
StudentDao studentDao =new StudentDao();
IBuyDao target = studentDao;
System.out.println(target.getClass());
IBuyDao proxy = (IBuyDao) new MyProxy(target).getProxyInstance();
System.out.println(proxy.getClass());
// 执行方法 【代理对象】
proxy.buySomething();
System.out.print("=========================================================");
IPlayDao target2 = studentDao;
System.out.println(target2.getClass());
IPlayDao proxy2 = (IPlayDao) new MyProxy(target2).getProxyInstance();
System.out.println(proxy2.getClass());
// 执行方法 【代理对象】
proxy2.play();
}
}
</code></pre><p>结果如下:<br><br><img src="/img/remote/1460000039322053" alt="" title=""><br><br>个人理解:代理对象类不需要实现接口,通过对象的增强返回一个接口类对象(实际上是代理后产生的),然后再调用接口方法即可。缺点:目标对象一定要实现接口,否则就无法使用动态代理,因为方法参数有一个是接口名。</p><h4>3.cglib代理</h4><p>Student.class:</p><pre><code class="java">package test;
public class Student {
public void buy() {
System.out.println("我是学生,我想买东西");
}
}</code></pre><p>MyProxy.class(代理类)</p><pre><code class="java">
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class MyProxy implements MethodInterceptor {
public Object target;
public Object getInstance(Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// TODO Auto-generated method stub
System.out.println("代理前-------");
proxy.invokeSuper(obj, args);
System.out.println("代理后-------");
return null;
}
}
</code></pre><p>测试类(Test.class)</p><pre><code class="java">public class Test {
public static void main(String[] args){
MyProxy myProxy =new MyProxy();
Student student = (Student)myProxy.getInstance(new Student());
student.buy();
}
}
</code></pre><p>结构结果:<br><img src="/img/remote/1460000039322056" alt="" title=""><br></p><ul><li>cgilib 可以实现没有接口的目标类的增强,它的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,采用的是继承,所以不能对final修饰的类进行代理。</li></ul><p><strong>【作者简介】</strong>: <br>秦怀,公众号【<strong>秦怀杂货店</strong>】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。</p><p><a href="https://link.segmentfault.com/?enc=RpbQiuIlD5r157SjZKeNnw%3D%3D.TX4JzulAZM1EfjuCskAjEV3aH2Wo10PXOhWAI909ov4%3D" rel="nofollow">2020年我写了什么?</a></p><p><a href="https://link.segmentfault.com/?enc=jbwy4woXmiU%2BGwpEOa4Ahg%3D%3D.5%2BwyoxQ8%2Bz%2BbZvSUKx20AIqOsMokqnjWN69GUOc0v9xdi9NqLHbenjIRdpDhRkUF" rel="nofollow">开源编程笔记</a></p><p>平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~</p>