作业帮面试题汇总

1. rwmutex与Mutex 的区别

sync.RWMutex(读写互斥锁)和sync.Mutex(互斥锁)都是Go语言标准库中用于并发控制的数据结构,但它们在功能上有显著的区别:

  1. 互斥性

    • sync.Mutex:提供了一种独占访问机制。一次只有一个goroutine可以持有Mutex的锁。当一个goroutine获取了Mutex后,其他任何尝试获取该锁的goroutine都会被阻塞,直到持有锁的goroutine释放它。
    • sync.RWMutex:提供了更精细的并发控制。它允许同时有多个读取者(goroutine)持有读锁进行读操作;但是,在任何时候如果有写入者试图获取写锁,所有读取者必须等待写锁释放,并且新的读取者也会被阻止,直到写入完成并释放写锁。
  2. 读写操作处理

    • sync.Mutex:无论是读操作还是写操作,都使用同一把锁来保护资源。这意味着只要有任意一个goroutine持有锁执行写操作,其他所有goroutine(包括读操作)都需要等待。
    • sync.RWMutex:支持独立的读锁和写锁。在没有写入发生时,多个读取者可以通过调用RLock()方法并发地进行读操作;而写入操作通过调用Lock()方法获取写锁,此时会阻塞所有的读取者和写入者。
  3. 性能考量

    • 在读多写少的场景下,RWMutex能够提高系统吞吐量,因为它允许多个读取者同时访问共享资源。
    • 如果写操作频繁或者读写比例相近,则RWMutex可能不如Mutex高效,因为每次写操作都需要确保没有任何读或写操作正在进行,这可能会增加上下文切换的成本。

总结来说,Mutex适合于任何对共享资源的访问都需要互斥控制的情况,而RWMutex则特别适用于读操作远大于写操作的场合,以实现更高的并发性和性能。

2 map 为何不是线程安全的 go

在Go语言中,map不是线程安全的主要原因在于其内部实现机制和内存管理。Go语言的map底层采用哈希表结构来存储键值对,并且为了提高性能,当插入或删除元素时可能会重新调整哈希表的大小。这个过程中涉及到如下几个与并发不兼容的操作:

  1. 动态扩容: 当map的元素数量增长到一定程度时,会触发map进行扩容操作,这包括分配新的更大的内存空间、将旧数据迁移到新空间并释放旧空间。这个过程中的多个步骤如果在没有同步的情况下被不同的goroutine同时执行,会导致数据丢失或损坏。

  2. 散列冲突处理: 在解决哈希冲突时,同一个桶(bucket)下的链表可能需要插入或删除节点,这些操作也不是线程安全的。

  3. 内部指针: map的数据结构内部包含指向内存块的指针,在并发环境下,如果没有适当的保护措施,一个goroutine在修改map的同时,另一个goroutine可能正在读取该map,这样可能导致读取到未初始化的或者已经被删除的数据,甚至导致程序崩溃。

  4. 不确定性: 由于上述原因,Go编译器和运行时系统无法保证在并发情况下map操作的一致性,因此默认情况下map是线程不安全的。

为了避免这些问题,在并发环境中访问map时,必须采取同步措施,例如使用sync.RWMutex或其他并发安全的数据结构(如sync.Map)。对于读多写少的场景,sync.Map提供了比直接使用mutex更高效的并发访问方式。

3 sync.map 的源码

由于当前时间点为2024年,Go语言的源码可能会有所更新。不过,我可以提供一个基于历史版本(截至我知识更新的时间点)的sync.Map源码简要结构和关键部分说明。

 
  

Go

1// sync.Map在Go标准库中的实现非常复杂,这里仅展示简化后的概念性结构和核心操作。
2package sync
3
4// Map 结构体,实现了并发安全的键值存储
5type Map struct {
6    mu Mutex        // 互斥锁,用于写入操作或整个map的同步
7    read atomic.Value // 原子值类型,用于存储只读映射read中最新的数据快照
8
9    dirty map[interface{}]*entry   // 存储实际的键值对,并且在有写入时使用
10    misses int64                     // 记录从read.m缓存中未找到项的次数
11}
12
13// entry 是dirty map内部的键值对结构
14type entry struct {
15    p unsafe.Pointer // 指向实际的key-value pair
16}
17
18// Store 方法将键值对存入sync.Map
19func (m *Map) Store(key, value interface{}) {
20    m.mu.Lock()
21    if race.Enabled {
22        // 运行时检测竞争条件
23        race.Write(&m.read)
24    }
25    if e, ok := m.dirty[key]; ok { // 更新已存在的键
26        e.store(value)
27    } else {
28        // 如果不存在,则创建新的entry并加入到dirty map中
29        if m.read.Load() == nil && m.dirty != nil {
30            // 初始状态下,将dirty的数据复制到read中
31            m.read.Store(new readOnlyMap(m.dirty))
32        }
33        m.dirty[key] = newEntry(value)
34    }
35    m.mu.Unlock()
36}
37
38// Load 方法从sync.Map中获取指定键对应的值
39func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
40    read, _ := m.read.Load().(readOnlyMap) // 先尝试从read缓存加载
41    e, ok := read.m[key]
42    if !ok && read.amended { // 如果没找到且read已经过修改,需要重新检查dirty map
43        m.mu.Lock()
44        e, ok = m.dirty[key]
45        m.misses++
46        m.mu.Unlock()
47    }
48    if ok {
49        return e.load()
50    }
51    return nil, false
52}
53
54// ... 省略其他方法,如Delete、Range等
55
56// readOnlyMap 和 entry 的具体实现细节在此省略

实际上,sync.Map的源码更加复杂,包括了诸如lazy初始化、清理无用entry等机制以提高性能。上述代码仅为简化示意,实际源码会包含更详尽的错误处理、内存管理以及针对race condition的检测等内容。要查看最新或特定版本的sync.Map源码,请查阅官方发布的Go标准库源代码。

4 count(1) count(*)  count(name) 区别

COUNT() 函数在SQL中用于计算表中的行数或满足特定条件的行数。以下是对 COUNT(1)COUNT(*), 和 COUNT(column_name) 的区别解释:

  1. COUNT(*)

    • 这是最常用的计数形式,它会统计表中的所有行,不管这些行的列值是否为NULL。
    • 它不关心具体的列内容,仅仅统计行数。
  2. COUNT(1)

    • 在大多数SQL数据库系统中(例如MySQL、PostgreSQL、Oracle等),COUNT(1) 效果上与 COUNT(*) 相同。
    • 它并不是真的去数第一列的非空值,而是利用了任何常数值(这里为1)作为占位符来指示对所有行进行计数,无论这些行的其他列是否有NULL值。
    • 这种写法有时候会被用作优化上的提示,虽然实际上很多数据库会对 COUNT(*) 自动进行优化。
  3. COUNT(column_name)

    • 当你使用 COUNT(column_name) 时,它会计算指定列中非NULL值的数量。
    • 如果该列存在NULL值,则不会计入总数。
    • 如果你想知道某列具体有多少个非空记录,这种写法就非常有用。

总结来说:

  • COUNT(*):返回表中所有行的总数,包括NULL值的行。
  • COUNT(1):通常情况下与 COUNT(*) 效果相同,计数时不考虑列值,只关注行数。
  • COUNT(column_name):返回指定列中非NULL值的行数。

收起

COUNT(*) 与COUNT(1) 那个性能好

在MySQL等数据库中,COUNT(*) 和 COUNT(1) 在大多数情况下性能是相同的,尤其是在没有 WHERE 条件或者索引可用的情况下。这是因为无论你计数的是星号(*)还是常数1,数据库引擎实际上并不会去读取和检查列的具体值,而是直接统计表的行数。

当查询条件存在且涉及到索引时,性能差异主要取决于查询优化器如何使用索引以及是否有索引下推优化。但即便如此,对于仅需计算满足条件的行数的情况,COUNT(*) 和 COUNT(1) 的执行计划通常也会很接近,因为两者都不会真正去访问实际的数据行,而是依赖于索引来估算或快速获取行数。

所以,在大部分实际应用中,二者在性能上可以认为是相等的,并无明显优劣之分。但在某些特定场景下,根据查询条件、索引结构以及数据库内部实现的细节,可能会出现微小的差异,但这通常不是选择哪一个的主要考虑因素,更应关注SQL语句的整体逻辑和索引设计。

5 mysql 的锁分类

MySQL中的锁根据不同的分类标准可以划分为多种类型,以下是主要的几种分类:

  1. 按锁的粒度分类:

    • 表级锁(Table-level Locks):锁定整个数据表,在锁定期间其他客户端不能对该表进行写操作。MyISAM存储引擎仅支持表级锁。
    • 行级锁(Row-level Locks):只锁定一行记录,不影响其他行的操作。InnoDB存储引擎支持行级锁,提高了并发性能。
    • 页级锁(Page-level Locks):锁定一个数据库页内的所有记录,BDB存储引擎支持页级锁。
  2. 按锁模式分类:

    • 共享锁(Shared Locks/S Locks):多个事务可以同时获取同一数据项上的共享锁以进行读取操作,不会阻塞其他事务的共享锁请求,但会阻塞排他锁请求。
    • 排他锁(Exclusive Locks/X Locks):如果一个事务获取了某数据项的排他锁,则其他事务既不能获取该数据项的共享锁也不能获取排他锁,直到持有排他锁的事务释放它。
    • 意向锁(Intention Locks)
      • 意向共享锁(IS Locks):在事务准备对行加共享锁之前,先在表级别加意向共享锁,表示事务意图在某个范围内获取共享锁。
      • 意向排他锁(IX Locks):同理,表示事务意图在某个范围内获取排他锁。
    • 插入意向锁(Insert Intention Locks):在插入新行时自动获得,用于防止死锁,表明事务打算插入新的行到已经被锁定的索引区间中。
  3. 间隙锁(Gap Locks):针对键值范围而不是具体的行,阻止其他事务在这个范围内插入新行,主要用于处理唯一性约束和避免幻读问题。

  4. 临键锁(Next-Key Locks):是行锁和间隙锁的组合,即锁定一个行和这个行前面的间隙。

  5. 自增锁(Auto-Increment Locks):在执行INSERT语句并使用自增列时,MySQL会获取自增锁来确保自增值的唯一性和一致性。

  6. 全局锁(Global Locks):如FLUSH TABLES WITH READ LOCK 或者 GET_LOCK() 函数可以对整个MySQL实例进行锁定,这种锁很少使用,通常在全库备份等特定场景下需要。

注意,不同存储引擎可能支持不同的锁类型和机制,以上内容以InnoDB存储引擎为主进行了介绍。

6 如果您是想询问 B 树(B-Tree)与 B+树(B+Tree)的区别:

B-Tree (B 树):

  1. 每个节点可以存储多个键值对以及对应的数据项。
  2. 非叶子节点除了包含键值外,还存储了指向子节点的指针以及与其关联的数据。
  3. 查询可以在非叶子节点结束,因为每个节点都存储了数据,所以搜索性能相对较高。
  4. 叶子节点并不一定连续排列,且不直接链接。

B+Tree:

  1. 非叶子节点仅存储键值和指向子节点的指针,不存储实际数据;所有实际数据只存储在叶子节点上。
  2. 所有叶子节点构成一个链表,通过指针相连,使得范围查询非常高效,只需要遍历叶子节点即可。
  3. 叶子节点通常按照键值排序,并且相邻叶子节点之间的键值是有序连接的。
  4. 相对于B树,B+树由于数据只在叶子节点存放,因此相同大小的节点可以容纳更多的键,索引深度更小,降低了磁盘I/O次数,更适合于大规模数据存储和检索。

总结起来,B+树相比B树的主要优点在于:

  • 更适合磁盘存储,因为它减少了随机访问的次数,提高了大范围扫描的效率。
  • 叶子节点形成了连续的有序链表,方便进行顺序查找和范围查询。

7 mysql 的update 执行都发生了什么

MySQL在执行UPDATE语句时,会经历以下几个步骤:

  1. 连接与验证

    • 客户端发送更新请求到MySQL服务器。
    • 连接器首先验证客户端的身份和权限,并建立连接。
  2. 分析与优化

    • 分析器解析SQL文本,确定UPDATE语句的目标表、列名以及操作符等元素。
    • 优化器根据统计信息和其他因素决定使用哪个索引(如果有可用索引的话),以及执行更新的顺序(如果涉及到多表更新)以获得最优性能。
  3. 预处理与权限检查

    • 执行预处理阶段,进行引用消解,确保列存在且用户具有更新这些列的权限。
    • 检查是否有足够的资源(如行锁或事务所需的空间)来完成操作。
  4. 锁定与事务管理

    • 在InnoDB存储引擎中,为满足事务ACID特性,UPDATE操作会隐式地开始一个事务或者加入到已存在的事务中。
    • InnoDB采用行级锁定,在执行UPDATE时会对需要修改的记录加锁,防止并发访问导致的数据不一致。对于包含范围条件的更新,可能还会涉及间隙锁或临键锁。
  5. 重做日志与数据更改

    • 更新前,InnoDB会将变更写入重做日志(Redo Log)。即使在更新过程中出现系统崩溃,也可以通过重做日志恢复未提交的更改。
    • 修改缓冲池中的页面,并将新的行版本存放到合适的位置,旧版本可能会被标记为删除并放入undo日志,以便支持MVCC和回滚操作。
  6. 提交与解锁

    • 当所有更改完成后,事务被提交,此时会触发flush机制,将缓冲池中的脏页刷新到磁盘,并释放之前获取的所有行锁。
    • 如果事务被回滚,则撤销更改并释放所持的锁。
  7. 清理缓存

    • 查询缓存(如果开启)会失效相关查询结果,因为表数据已经发生变化。

整个过程保证了数据的一致性、完整性以及并发控制,同时也体现了MySQL对事务安全性的重视。在实际执行过程中,不同场景下具体的细节会有微小差异,比如是否使用了索引、是否是批量更新等都会影响到具体的操作流程。

编程题

1 输入一个数,输出小于它的所有素数 go实现

以下是一个用Go语言实现的程序,它会接收一个用户输入的数,并输出所有小于该数的素数:

 
  

Go

1package main
2
3import (
4	"fmt"
5	"strconv"
6)
7
8func isPrime(num int) bool {
9	if num <= 1 {
10		return false
11	}
12	for i := 2; i*i <= num; i++ {
13		if num%i == 0 {
14			return false
15		}
16	}
17	return true
18}
19
20func primesLessThan(n int) []int {
21	primes := make([]int, 0)
22	for i := 2; i < n; i++ {
23		if isPrime(i) {
24			primes = append(primes, i)
25		}
26	}
27	return primes
28}
29
30func main() {
31	var inputString string
32	fmt.Print("请输入一个数:")
33	fmt.Scanln(&inputString)
34	number, err := strconv.Atoi(inputString)
35	if err != nil {
36		fmt.Println("输入错误,请输入一个整数。")
37		return
38	}
39
40	result := primesLessThan(number)
41	fmt.Printf("小于 %d 的所有素数为:%v\n", number, result)
42}

运行此程序后,您将看到提示“请输入一个数:”,然后您可以输入一个整数,程序会计算并显示所有小于该数的素数。

2 开根号 go 实现 不使用系统函数

在Go语言中,如果你想不使用math.Sqrt系统函数实现开平方根,可以采用牛顿法或其他数值方法。以下是一个基于牛顿法的简单实现:

 
  

Go

1package main
2
3import (
4	"fmt"
5	"math"
6)
7
8const epsilon = 1e-6 // 定义一个误差范围,用于判断是否达到足够的精度
9
10// sqrtNewtonRaphson 使用牛顿法计算平方根
11func sqrtNewtonRaphson(x float64, guess float64) float64 {
12	for math.Abs(guess*guess-x) > epsilon { // 当猜测值的平方与目标数之间的差大于阈值时继续迭代
13		guess = (guess + x/guess) / 2.0 // 牛顿法更新公式
14	}
15	return guess
16}
17
18func main() {
19	num := 16.0
20	sqrtNum := sqrtNewtonRaphson(num, num/2.0) // 初始猜测值设为num的一半
21	fmt.Printf("The square root of %.2f is approximately %.6f\n", num, sqrtNum)
22}
23

这个代码示例中,我们定义了一个sqrtNewtonRaphson函数来利用牛顿法求解平方根问题。请注意,在实际应用中,可能需要对输入值进行检查以确保其非负,并且对于某些特殊情况(如x=0)应直接返回正确结果。此外,初始猜测值的选择也可能影响到收敛速度和效果。

3 go 实现等待10个协程完成任务,并获取每个协程的结果

在Go语言中,你可以使用sync.WaitGroup和通道(channel)来等待多个协程完成任务,并获取每个协程的结果。下面是一个示例:

 
  

Go

1package main
2
3import (
4	"fmt"
5	"sync"
6)
7
8// 假设这是你的工作函数,它返回一个结果
9func doWork(id int) (string, error) {
10	// 这里模拟执行一些耗时操作并返回结果或错误
11	// 实际情况下可能是网络请求、数据库查询等操作
12	result := fmt.Sprintf("Result from worker %d", id)
13	return result, nil
14}
15
16func main() {
17	var wg sync.WaitGroup
18	results := make(chan string, 10) // 创建一个缓冲通道以存储结果,容量为10
19
20	for i := 1; i <= 10; i++ {
21		wg.Add(1) // 每启动一个协程就增加计数器
22
23		go func(workerID int) {
24			defer wg.Done() // 协程结束前调用Done减少计数器
25
26			// 执行工作
27			result, err := doWork(workerID)
28			if err != nil {
29				fmt.Printf("Worker %d encountered an error: %v\n", workerID, err)
30				return
31			}
32
33			// 将结果发送到结果通道
34			results <- result
35		}(i)
36	}
37
38	// 等待所有协程完成
39	wg.Wait()
40
41	// 从通道中读取并打印结果
42	for i := 1; i <= 10; i++ {
43		result := <-results
44		fmt.Printf("Received result from worker %d: %s\n", i, result)
45	}
46}

在这个示例中:

  • 使用sync.WaitGroup确保所有的协程都完成了任务。
  • 创建了一个带缓冲的通道results用来传递各个协程的工作结果。
  • 在每个协程完成后通过wg.Done()减少WaitGroup的计数器。
  • 当所有协程都完成时,主goroutine会通过wg.Wait()阻塞直到计数器归零。
  • 主goroutine最后从results通道中读取每个协程的结果并进行处理

你可能感兴趣的:(java,数据库,开发语言)