死锁是怎么产生的?如何解决?
两个或两个以上的进程(或线程,goroutine)在执行过程中,因争夺共享资源而处于一种互相等待的状态,如果没有外部干涉,它们都将无法推进下去,此时,我们称之为处于死锁状态或系统产生了死锁。
产生死锁的四个必要条件
产生死锁通常需要满足以下四个条件,这些条件也被称为死锁的四个必要条件:
- 互斥条件:资源不能被多个任务共享,即一次只能由一个任务使用。
- 请求与保持条件:一个任务因请求资源而阻塞时,对已获得的资源保持不放。
- 不可剥夺条件:已获得的资源在未使用完之前,不能被其他任务强行夺走,只能由占有资源的任务主动释放。
- 循环等待条件:存在一种任务资源的循环等待关系,即任务集合{P1, P2, ..., Pn}中,P1等待P2占有的资源,P2等待P3占有的资源,...,Pn等待P1占有的资源。
如何解决死锁
解决死锁的方法通常有以下几种:
- 预防死锁:通过破坏产生死锁的四个必要条件中的一个或多个来预防死锁的发生。例如,可以破坏互斥条件(使用非独占资源),破坏请求与保持条件(一次性请求所有资源),破坏不可剥夺条件(允许剥夺资源),或者破坏循环等待条件(对资源进行排序,强制按顺序请求资源)。
- 避免死锁:在资源分配时,使用某种策略避免进入不安全状态,从而避免死锁。例如,银行家算法是一种避免死锁的算法,它通过预先计算资源分配的安全性来避免死锁。
- 检测死锁:允许死锁发生,但通过某种机制定期检测系统是否发生了死锁。一旦检测到死锁,就采取措施来解除死锁。
- 解除死锁:当检测到死锁发生时,采取措施来打破死锁。例如,可以终止一个或多个任务,或者剥夺一个或多个任务的资源。
你用过iota吗?
itoa是go语言的常量计数器,只能在常量表达式中使用。
iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。
const (
n1 = iota //0
n2 //1
n3 //2
)
const (
n1 = iota //0
n2 //1
_
n4 //3
)
const (
n1 = iota //0
n2 = 100 //100
n3 = iota //2
n4 //3
)
const n5 = iota //0
定义数量级 (这里的 << 表示左移操作,1<<10表示将1的二进制表示向左移10位,也就是由1变成了10000000000(二进制),也就是十进制的1024。同理2 << 2表示将2的二进制表示向左移2位,也就是由10变成了1000,也就是十进制的8。)
多个iota定义在一行
const (
a, b = iota + 1, iota + 2 //1,2
c, d //2,3
e, f //3,4
)
数据类型复数
complex64
和complex128
var c1 complex64c1 = 1 + 2i
var c2 complex128c2 = 2 + 3i
fmt.Println(c1)fmt.Println(c2)
复数有实部和虚部,complex64
的实部和虚部为32,complex128
的实部和虚部为64位。
byte和rune类型
组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号
(’)包裹起来,如:
var a = '中'
var b = 'x'
Go 语言的字符有以下两种:
- uint8类型,或者叫 byte 型,代表一个ASCII码字符。
- rune类型,代表一个 UTF-8字符。
当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32。
Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可以使用 byte
型进行默认字符串处理,性能和扩展性都有照顾。
遍历字符串
func traversalString() {
s := "hello沙河"
for i := 0; i < len(s); i++ { //byte
fmt.Printf("%v(%c) ", s[i], s[i])
}
fmt.Println()
for _, r := range s { //rune
fmt.Printf("%v(%c) ", r, r)
}
}
输出:
104(h) 101(e) 108(l) 108(l) 111(o) 230(æ) 178(²) 153() 230(æ) 178(²) 179(³)
104(h) 101(e) 108(l) 108(l) 111(o) 27801(沙) 27827(河)
因为UTF8编码下一个中文汉字由3~4个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的
字符串,否则就会出现上面输出中第一行的结果。
字符串底层是一个byte数组,所以可以和[]byte类型相互转换。字符串是不能修改的 字符串是由byte字
节组成,所以字符串的长度是byte字节的长度。 rune类型用来表示utf8字符,一个rune字符由一个或多
个byte组成。
修改字符串
要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。无论哪种转换,都会重新分
配内存,并复制字节数组。
func changeString() {
s1 := "big"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1))
s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
fmt.Println(string(runeS2))
}
copy() 函数(切片的深拷贝)
切片的拷贝分为**2**种,一种是浅拷贝,一种是深拷贝。
浅拷贝:源切片和目的切片共享同一底层数组空间,源切片修改,目的切片同样被修改。(赋值符实现)
深拷贝:源切片和目的切片各自都有彼此独立的底层数组空间,各自的修改,彼此不受影响。(使用内置函数copy()函数实现)
以下通过具体实例来说明:
浅拷贝:源切片和目的切片共享同一底层数组空间
func main(){
slice1 := make([]int, 5, 5)
slice2 := slice1
slice1[1] = 1
fmt.Println(slice1) //[0 1 0 0 0]
fmt.Println(slice2) //[0 1 0 0 0]
}
深拷贝:源切片和目的切片各自都有彼此独立的底层数组空间
func main() {
slice1 := make([]int, 5, 5)
slice1[0] = 9
fmt.Println(slice1)
slice2 := make([]int, 4, 4)
slice3 := make([]int, 5, 5)
fmt.Println(slice2)
fmt.Println(slice3)
//拷贝
fmt.Println(copy(slice2, slice1)) //4
fmt.Println(copy(slice3, slice1)) //5
//独立修改
slice2[1] = 2
slice3[1] = 3
fmt.Println(slice1) //[9 0 0 0 0 0]
fmt.Println(slice2) //[9 2 0 0]
fmt.Println(slice3) //[9 3 0 0 0]
}
copy 函数的原理:copy 函数在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。
恭喜你看到了这里!加油!!!
下面就是后端面试中经常问的数据库和缓存了,后续会持续更新!!!当然后续也会有es搜索引擎和消息中间件等,和微服务中的链路追踪,监控告警等!一起加油!
如有错误请留言!
😂