Go协程揭秘:轻量、并发与性能的完美结合

目录

    • 1. Go协程简介
      • 什么是Go协程?
      • Go协程与线程的比较
      • Go协程的核心优势
    • 2. Go协程的基本使用
      • 创建并启动Go协程
      • 使用匿名函数创建Go协程
      • Go协程与主函数
    • 3. Go协程的同步机制
      • 1. 通道 (Channels)
      • 2. `sync.WaitGroup`
      • 3. 互斥锁 (`sync.Mutex`)
    • 4. Go协程的高级用法
      • 1. 选择器 (`select`)
      • 2. 超时处理
      • 3. 使用`context`进行协程控制
    • 5. Go协程的性能与最佳实践
      • 1. 限制并发数
      • 2. 避免竞态条件
      • 3. 使用工作池模式
    • 6.总结

Go协程为并发编程提供了强大的工具,结合轻量级、高效的特点,为开发者带来了独特的编程体验。本文深入探讨了Go协程的基本原理、同步机制、高级用法及其性能与最佳实践,旨在为读者提供全面、深入的理解和应用指导。

关注【TechLead】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。

file

1. Go协程简介

Go协程(goroutine)是Go语言中的并发执行单元,它比传统的线程轻量得多,并且是Go语言并发模型中的核心组成部分。在Go中,你可以同时运行成千上万的goroutine,而不用担心常规操作系统线程带来的开销。

什么是Go协程?

Go协程是与其他函数或方法并行运行的函数或方法。你可以认为它类似于轻量级的线程。其主要优势在于它的启动和停止开销非常小,相比于传统的线程来说,可以更有效地实现并发。

package mainimport ("fmt""time"
)func sayHello() {for i := 0; i < 5; i++ {time.Sleep(100 * time.Millisecond)fmt.Println("Hello!")}
}func main() {go sayHello() // 启动一个Go协程for i := 0; i < 5; i++ {time.Sleep(150 * time.Millisecond)fmt.Println("Hi!")}
}

输出:

Hi!
Hello!
Hi!
Hello!
Hello!
Hi!
Hello!
Hi!
Hello!

处理过程:
在上面的代码中,我们定义了一个sayHello函数,它在一个循环中打印“Hello!”五次。在main函数中,我们使用go关键字启动了sayHello作为一个goroutine。此后,我们又在main中打印“Hi!”五次。因为sayHello是一个goroutine,所以它会与main中的循环并行执行。因此,输出中“Hello!”和“Hi!”的打印顺序可能会变化。

Go协程与线程的比较

  1. 启动开销:Go协程的启动开销远小于线程。因此,你可以轻松启动成千上万个goroutine。
  2. 内存占用:每个Go协程的堆栈大小开始时很小(通常在几KB),并且可以根据需要增长和缩小,而线程通常需要固定的、较大的堆栈内存(通常为1MB或更多)。
  3. 调度:Go协程是由Go运行时系统而不是操作系统调度的。这意味着Go协程之间的上下文切换开销更小。
  4. 安全性:Go协程为开发者提供了简化的并发模型,配合通道(channels)等同步机制,减少了并发程序中常见的错误。

示例代码:

package mainimport ("fmt""time"
)func worker(id int, ch chan int) {for {fmt.Printf("Worker %d received data: %d\n", id, <-ch)}
}func main() {ch := make(chan int)for i := 0; i < 3; i++ {go worker(i, ch) // 启动三个Go协程}for i := 0; i < 10; i++ {ch <- itime.Sleep(100 * time.Millisecond)}
}

输出:

Worker 0 received data: 0
Worker 1 received data: 1
Worker 2 received data: 2
Worker 0 received data: 3
...

处理过程:
在这个示例中,我们启动了三个工作goroutine来从同一个通道接收数据。在main函数中,我们发送数据到通道。每当通道中有数据时,其中一个工作goroutine会接收并处理它。由于goroutines是并发运行的,所以哪个goroutine接收数据是不确定的。

Go协程的核心优势

  1. 轻量级:如前所述,Go协程的启动开销和内存使用都远远小于传统线程。
  2. 灵活的调度:Go协程是协同调度的,允许用户在适当的时机进行任务切换。
  3. 简化的并发模型:Go提供了多种原语(如通道和锁),使并发编程变得更加简单和安全。

总的来说,Go协程为开发者提供了一个高效、灵活且安全的并发模型。与此同时,Go的标准库提供了丰富的工具和包,进一步简化了并发程序的开发过程。


2. Go协程的基本使用

在Go中,协程是构建并发程序的基础。创建协程非常简单,并且使用go关键字就可以启动。让我们探索一些基本用法和与之相关的示例。

创建并启动Go协程

启动一个Go协程只需使用go关键字,后跟一个函数调用。这个函数即可以是匿名的,也可以是预定义的。

示例代码:

package mainimport ("fmt""time"
)func printNumbers() {for i := 1; i <= 5; i++ {time.Sleep(200 * time.Millisecond)fmt.Println(i)}
}func main() {go printNumbers()  // 启动一个Go协程time.Sleep(1 * time.Second)fmt.Println("End of main function")
}

输出:

1
2
3
4
5
End of main function

处理过程:
在这个示例中,我们定义了一个printNumbers函数,它会简单地打印数字1到5。在main函数中,我们使用go关键字启动了这个函数作为一个新的Go协程。主函数与Go协程并行执行。为确保主函数等待Go协程执行完成,我们使主函数休眠了1秒钟。

使用匿名函数创建Go协程

除了启动预定义的函数,你还可以使用匿名函数直接启动Go协程。

示例代码:

package mainimport ("fmt""time"
)func main() {go func() {fmt.Println("This is a goroutine!")time.Sleep(500 * time.Millisecond)}()fmt.Println("This is the main function!")time.Sleep(1 * time.Second)
}

输出:

This is the main function!
This is a goroutine!

处理过程:
在这个示例中,我们在main函数中直接使用了一个匿名函数来创建Go协程。在匿名函数中,我们简单地打印了一条消息并使其休眠了500毫秒。主函数先打印其消息,然后等待1秒来确保Go协程有足够的时间完成执行。

Go协程与主函数

值得注意的是,如果主函数(main)结束,所有的Go协程都会被立即终止,不论它们的执行状态如何。

示例代码:

package mainimport ("fmt""time"
)func main() {go func() {time.Sleep(500 * time.Millisecond)fmt.Println("This will not print!")}()
}

处理过程:
在上面的代码中,Go协程在打印消息前休眠了500毫秒。但由于主函数在此期间已经结束,所以Go协程也被终止,因此我们不会看到任何输出。

总结,Go协程的基本使用非常简单和直观,但需要注意确保主函数在所有Go协程执行完毕之前不会结束。


3. Go协程的同步机制

在并发编程中,同步是确保多个协程能够有效、安全地共享资源或协同工作的关键。Go提供了几种原语,帮助我们实现这一目标。

1. 通道 (Channels)

通道是Go中用于在协程之间传递数据和同步执行的主要方式。它们提供了一种在一个协程中发送数据,并在另一个协程中接收数据的机制。

示例代码:

package mainimport "fmt"func sendData(ch chan string) {ch <- "Hello from goroutine!"
}func main() {messageChannel := make(chan string)go sendData(messageChannel) // 启动一个Go协程发送数据message := <-messageChannelfmt.Println(message)
}

输出:

Hello from goroutine!

处理过程:
我们创建了一个名为messageChannel的通道。然后启动了一个Go协程sendData,将字符串"Hello from goroutine!"发送到这个通道。在主函数中,我们从通道接收这个消息并打印它。

2. sync.WaitGroup

sync.WaitGroup是一个等待一组协程完成的结构。你可以增加一个计数来表示应等待的协程数量,并在每个协程完成时减少计数。

示例代码:

package mainimport ("fmt""sync""time"
)func worker(id int, wg *sync.WaitGroup) {defer wg.Done()fmt.Printf("Worker %d starting\n", id)time.Sleep(time.Second)fmt.Printf("Worker %d done\n", id)
}func main() {var wg sync.WaitGroupfor i := 1; i <= 5; i++ {wg.Add(1)go worker(i, &wg)}wg.Wait()fmt.Println("All workers completed.")
}

输出:

Worker 1 starting
Worker 2 starting
Worker 3 starting
Worker 4 starting
Worker 5 starting
Worker 1 done
Worker 2 done
Worker 3 done
Worker 4 done
Worker 5 done
All workers completed.

处理过程:
我们定义了一个名为worker的函数,它模拟一个需要一秒钟才能完成的工作任务。在这个函数中,我们使用defer wg.Done()来确保在函数退出时减少WaitGroup的计数。在main函数中,我们启动了5个这样的工作协程,每启动一个,我们就使用wg.Add(1)来增加计数。wg.Wait()则会阻塞,直到所有工作协程都通知WaitGroup它们已完成。

3. 互斥锁 (sync.Mutex)

当多个协程需要访问共享资源时(例如,更新一个共享变量),使用互斥锁可以确保同时只有一个协程能访问资源,防止数据竞态。

示例代码:

package mainimport ("fmt""sync"
)var counter int
var lock sync.Mutexfunc increment() {lock.Lock()counter++lock.Unlock()
}func main() {var wg sync.WaitGroupfor i := 0; i < 1000; i++ {wg.Add(1)go func() {defer wg.Done()increment()}()}wg.Wait()fmt.Println("Final Counter:", counter)
}

输出:

Final Counter: 1000

处理过程:
我们有一个全局变量counter,我们希望在多个Go协程中并发地增加它。为了确保每次只有一个Go协程能够更新counter,我们使用了互斥锁lock来同步访问。

这些是Go协程同步机制的一些基本方法。正确地使用它们可以帮助你编写更安全、更高效的并发程序。


4. Go协程的高级用法

Go协程的高级用法涉及更复杂的并发模式、错误处理和协程控制。我们将探索一些常见的高级用法和它们的具体应用示例。

1. 选择器 (select)

select语句是Go中处理多个通道的方法。它允许你等待多个通道操作,执行其中一个可以进行的操作。

示例代码:

package mainimport ("fmt""time"
)func main() {ch1 := make(chan string)ch2 := make(chan string)go func() {time.Sleep(1 * time.Second)ch1 <- "Data from channel 1"}()go func() {time.Sleep(2 * time.Second)ch2 <- "Data from channel 2"}()for i := 0; i < 2; i++ {select {case msg1 := <-ch1:fmt.Println(msg1)case msg2 := <-ch2:fmt.Println(msg2)}}
}

输出:

Data from channel 1
Data from channel 2

处理过程:
我们创建了两个通道ch1ch2。两个Go协程分别向这两个通道发送数据,但它们的休眠时间不同。在select语句中,我们等待两个通道中的任何一个准备好数据,然后进行处理。由于ch1的数据先到达,因此它的消息首先被打印。

2. 超时处理

使用select,我们可以轻松实现对通道操作的超时处理。

示例代码:

package mainimport ("fmt""time"
)func main() {ch := make(chan string)go func() {time.Sleep(3 * time.Second)ch <- "Data from goroutine"}()select {case data := <-ch:fmt.Println(data)case <-time.After(2 * time.Second):fmt.Println("Timeout after 2 seconds")}
}

输出:

Timeout after 2 seconds

处理过程:
Go协程会休眠3秒钟后再向ch发送数据。在select语句中,我们等待这个通道的数据或2秒的超时。由于Go协程在超时之前没有发送数据,因此超时的消息被打印。

3. 使用context进行协程控制

context包允许我们共享跨多个协程的取消信号、超时和其他设置。

示例代码:

package mainimport ("context""fmt""time"
)func work(ctx context.Context) {for {select {case <-ctx.Done():fmt.Println("Received cancel signal, stopping the work")returndefault:fmt.Println("Still working...")time.Sleep(1 * time.Second)}}
}func main() {ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)defer cancel()go work(ctx)time.Sleep(5 * time.Second)
}

输出:

Still working...
Still working...
Still working...
Received cancel signal, stopping the work

处理过程:
在这个示例中,我们创建了一个带有3秒超时的context。Go协程work会持续工作,直到接收到取消信号或超时。经过3秒后,context的超时被触发,Go协程接收到了取消信号并停止工作。

这些高级用法为Go协程提供了强大的功能,使得复杂的并发模式和控制成为可能。掌握这些高级技巧可以帮助你编写更健壮、更高效的Go并发程序。


5. Go协程的性能与最佳实践

Go协程为并发编程提供了轻量级的解决方案。但为了充分利用其性能优势并避免常见的陷阱,了解一些最佳实践和性能考虑因素是很有必要的。

1. 限制并发数

虽然Go协程是轻量级的,但无节制地创建大量的Go协程可能会导致内存耗尽或调度开销增大。

示例代码:

package mainimport ("fmt""sync"
)func worker(id int, wg *sync.WaitGroup) {defer wg.Done()fmt.Printf("Worker %d started\n", id)
}func main() {var wg sync.WaitGroupnumWorkers := 1000for i := 1; i <= numWorkers; i++ {wg.Add(1)go worker(i, &wg)}wg.Wait()fmt.Println("All workers done")
}

输出:

Worker 1 started
Worker 2 started
...
Worker 1000 started
All workers done

处理过程:
这个示例创建了1000个工作Go协程。尽管这个数字可能不会导致问题,但如果不加限制地创建更多的Go协程,可能会导致问题。

2. 避免竞态条件

多个Go协程可能会同时访问共享资源,导致不确定的结果。使用互斥锁(Mutex)或其他同步机制来确保数据的一致性。

示例代码:

package mainimport ("fmt""sync"
)var (counter intmu      sync.Mutex
)func increment(wg *sync.WaitGroup) {defer wg.Done()mu.Lock()counter++mu.Unlock()
}func main() {var wg sync.WaitGroupfor i := 0; i < 1000; i++ {wg.Add(1)go increment(&wg)}wg.Wait()fmt.Println("Final counter value:", counter)
}

输出:

Final counter value: 1000

处理过程:
我们使用sync.Mutex确保在增加计数器时的互斥访问。这确保了并发访问时的数据一致性。

3. 使用工作池模式

工作池模式是创建固定数量的Go协程来执行任务的方法,避免过度创建Go协程。任务通过通道发送。

示例代码:

package mainimport ("fmt""sync"
)func worker(tasks <-chan int, wg *sync.WaitGroup) {defer wg.Done()for task := range tasks {fmt.Printf("Worker processed task %d\n", task)}
}func main() {var wg sync.WaitGrouptasks := make(chan int, 100)// Start 5 workers.for i := 0; i < 5; i++ {wg.Add(1)go worker(tasks, &wg)}// Send 100 tasks.for i := 1; i <= 100; i++ {tasks <- i}close(tasks)wg.Wait()
}

输出:

Worker processed task 1
Worker processed task 2
...
Worker processed task 100

处理过程:
我们创建了5个工作Go协程,它们从tasks通道中接收任务。这种模式可以控制并发数并重复使用Go协程。

遵循这些最佳实践不仅可以使你的Go协程代码更加健壮,而且还可以更有效地利用系统资源,提高程序的整体性能。


6.总结

随着计算技术的进步,并发和并行成为了现代软件开发中的关键元素。Go语言作为一个现代编程语言,通过其内置的goroutine为开发者提供了一种简洁而强大的并发编程模式。但正如我们在前面的章节中所看到的,理解其工作原理、同步机制、高级用法及性能与最佳实践是至关重要的。

从本文中,我们不仅了解了Go协程的基础知识和工作原理,还探讨了一些关于如何最大限度地发挥其性能的高级主题。关键的洞察包括:

  1. 轻量与高效:Go协程是轻量级的线程,但它们在实现上的特点使其在大量并发场景下更为高效。
  2. 同步与通信:Go的哲学是“不通过共享内存来通信,而是通过通信来共享内存”。这反映在其强大的channel机制中,这也是避免许多并发问题的关键。
  3. 性能与最佳实践:理解并遵循最佳实践不仅可以确保代码的健壮性,而且还可以显著提高性能。

最后,虽然Go提供了强大的工具和机制来处理并发,但真正的艺术在于如何正确地使用它们。正如我们在软件工程中经常看到的那样,工具只是手段,真正的力量在于了解它们的工作原理并正确地应用它们。

希望本文为您提供了关于Go协程的深入、全面的认识,并为您的并发编程之旅提供了有价值的洞见和指导。正如在云服务、互联网服务架构和其他复杂的系统中经常可以看到的那样,真正掌握并发是提高性能、扩展性和响应速度的关键。

关注【TechLead】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/666395.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

每日一题——LeetCode1394.找出数组中的幸运数

方法一 桶数组计数法 又要保存整数的数值和他出现的频次&#xff0c;那么碰到一个整数num就让res[num]&#xff0c;那么循环res数组&#xff0c;如果res[i]0则代表i没有在arr中出现过&#xff0c;res[i]n则代表i在arr中出现n次 因为题目要求只返回最大的幸运数&#xff0c;所…

计算机软件能力认证考试CCF-202312-1 仓库规划

#自己跑的测试没问题&#xff0c;不知道为啥就是不能满分 原理比较绕&#xff0c;就是让数组中一行不断地与其他行进行比较&#xff0c;最终得到各自的索引 #include <iostream> using namespace std; int main() {int n;int m;cin>>n>>m; int array[n][m];…

【C/C++ 08】简单计算器

一、题目 输入算术表达式&#xff0c;可包含空格&#xff0c;检查算术表达式的合法性&#xff0c;若算术表达式不合法&#xff0c;打印错误类型&#xff0c;若合法&#xff0c;则进行运算&#xff0c;打印计算结果。 二、算法 1. 将输入的算术表达式字符串去除空格。 2. 检查…

电源模块欠压保护点测试方法分享 纳米软件

电源欠压保护原理 欠压保护是指当电源电压低于一定值时&#xff0c;电源的保护功能会及时断开电路&#xff0c;避免设备受到损坏。电源欠压保护一般是通过一个或多个传感器来检测电压&#xff0c;当电压低于设定值时就会触发电源的保护功能&#xff0c;断开电路&#xff0c;保护…

猫用空气净化器真的能除菌吗?除毛可以用宠物空气净化器吗?

猫咪给我们带来了无尽的欢乐&#xff0c;但它们换毛时家里到处都是猫毛。我们会在地板、沙发上发现一大堆&#xff0c;甚至衣服也难逃其影响。这些浮毛中可能携带着微生物和尘螨等。对于免疫力较低的老年人、孩子和孕妇来说&#xff0c;他们更容易感染这些微生物。而对于鼻炎患…

[Tomcat问题]--使用Tomcat 10.x部署项目时,出现实例化Servlet类[xxx]异常

[Tomcat问题]–使用Tomcat 10.x部署项目时&#xff0c;出现实例化Servlet类[xxx]异常 本片博文在知乎同步更新 环境 OS: Windows 11 23H2Java Version: java 21.0.1 2023-10-17 LTSIDE: IntelliJ IDEA 2023.3.3Maven: Apache Maven 3.9.6Tomcat: Tomcat 10.1.18 ReleasedSer…

vulhub中Adminer远程文件读取漏洞复现(CVE-2021-43008)

Adminer是一个PHP编写的开源数据库管理工具&#xff0c;支持MySQL、MariaDB、PostgreSQL、SQLite、MS SQL、Oracle、Elasticsearch、MongoDB等数据库。 在其版本1.12.0到4.6.2之间存在一处因为MySQL LOAD DATA LOCAL导致的文件读取漏洞。 参考链接&#xff1a; https://gith…

JAVA Studynote(7-8)

JAVA Studynote(7-8) 1.DOS系统 ​ *接受指令 *解析指令 *执行指令 2.相对路径和绝对路径 *相对路径 ​ *从当前目录开始定位&#xff0c;形成的一个路径 *绝对路径 ​ *从顶级目录d&#xff0c;开始定位&#xff0c;形成的路径 ​ *示例&#xff1a; 3.常用DOS指令 …

12种算法优化CNN-BiLSTM-Attention多特征输入单步预测,机器学习预测全家桶,持续更新,MATLAB代码...

截止到本期&#xff0c;一共发了12篇关于机器学习预测全家桶MATLAB代码的文章。参考文章如下&#xff1a; 1.五花八门的机器学习预测&#xff1f;一篇搞定不行吗&#xff1f; 2.机器学习预测全家桶&#xff0c;多步预测之BiGRU、BiLSTM、GRU、LSTM&#xff0c;LSSVM、TCN、CNN&…

【lesson8】高并发内存池Central Cache层释放内存的实现

文章目录 Central Cache层释放内存的流程Central Cache层释放内存的实现 Central Cache层释放内存的流程 当thread_cache过长或者线程销毁&#xff0c;则会将内存释放回central cache中的&#xff0c;释放回来时–use_count。当use_count减到0时则表示所有对象都回到了span&am…

备战蓝桥杯---数据结构与STL应用(进阶4)

今天主要围绕并查集的一些今典题目展开&#xff1a; 在这里&#xff0c;我们把逻辑真的组合&#xff0c;用并查集即可。一开始&#xff0c;我觉得把a,b,c等价&#xff0c;把第一个赋a,接下来推即可&#xff0c;但这样在判断矛盾时还需要选择合适的点find&#xff0c;于是我们把…

深度学习(10)-Keras项目详解(递归神经网络)

一.递归神经网络基础概念 递归神经网络(Recursive Neural Network, RNN)可以解决有时间序列的问题&#xff0c;处理诸如树、图这样的递归结构。 CNN主要应用在计算机视觉CV中&#xff0c;RNN主要应用在自然语言处理NLP中。 1.h0&#xff0c;h1.....ht对应的是不同输入得到的中…

debian12 解决 github 访问难的问题

可以在 /etc/hosts 文件中添加几个域名与IP对应关系&#xff0c;从而提高 github.com 的访问速度。 据搜索了解&#xff08;不太确定&#xff09;&#xff0c;可以添加这几个域名&#xff1a;github.com&#xff0c;github.global.ssl.fastly.net&#xff0c;github.global.fa…

银河麒麟 aarch64 Mysql环境安装

一、操作系统版本信息 组件版本操作系统Kylin V10 (SP3) /(Lance)-aarch64-Build23/20230324Kernel4.19.90-52.22.v2207.ky10.aarch64MySQLmysql-8.3.0JDK1.8.0_312 二、MySQL下载 官网下载地址&#xff1a;https://dev.mysql.com/downloads/mysql/ 三、MySQL 安装 3.1 删…

年终奖,还得是腾讯。。。

腾讯年终奖 什么是真正的好公司&#xff1f; 一年到头&#xff0c;出不了几次裁员等劳务纠纷的吃瓜新闻。 只有到年底了&#xff0c;才因为年终奖远高于行业水平&#xff0c;实在没法低调了&#xff0c;"被迫"上热搜。 最近网友爆料了腾讯头牌部门的年终奖&#xff1…

JavaSE——流程控制-循环结构(for循环、while循环、小案例、do-while循环、死循环、循环嵌套)

目录 for循环 while循环 小案例 do-while循环 死循环 循环嵌套 for循环 for(int i 0; i < 5; i) {System.out.println("Hello world"); } 执行的流程&#xff1a; 循环一开始&#xff0c;执行int i0 一次。此时 i 0&#xff0c;接着计算机执行循环条件…

下载RTSP播放器

1.网站1 2.海康播放器 在海康官网上选择【服务支持】【工具软件】&#xff0c;往下滚动&#xff0c;打开VSPlayer_x64

陪女朋友学习计算机二级之栈和队列

栈 栈(堆栈)的定义 堆栈又名栈(stack),它是一种线性表。限定仅在表尾进行插入和删除操作的线性表。是一种后进先出的线性表. 空栈:不含任何元素的空表。 栈顶和栈底 进行插入和删除的这一端(表尾)被称为栈顶&#xff0c;相对地&#xff0c;把另一端称为栈底。 入栈和出栈 …

大小姐驾到!高德地图联合《王者荣耀》推出孙尚香导航语音包

“大小姐驾到&#xff01;统统闪开&#xff01;”如果你是一个手游爱好者&#xff0c;多半会对这句话耳熟能详&#xff0c;来自于国内手游界顶流《王者荣耀》中的高人气角色——孙尚香&#xff0c;并成为一代玩家们的记忆。 如今&#xff0c;随着高德地图与《王者荣耀》达成合…

java之ReentrantLock

在讲RentrantLock之前需要先讲一下AQS和LockSupport&#xff0c;因为rentrantLock底层是用AQS实现的&#xff0c;而AQS中获取阻塞和唤醒底使用LockSupport实现的。 1、LockSupport实现 下面代码中&#xff0c;LockSupport.park方法是当前线程等待&#xff0c;直到获得许可&am…