在入门篇一的并发安全场景中,我们初步接触了Go的并发相关操作。Go的并发优势是其核心竞争力,而协程(goroutine)、信道(channel)、锁(sync包)、上下文(context)则是支撑Go并发编程的四大核心基础。本篇将围绕这四部分展开,通过与Java并发机制的对比,结合可运行代码示例,快速掌握Go并发编程
一、协程(goroutine):Go的轻量级"线程"
对Java程序员而言,协程(goroutine)是理解Go并发的起点,可将其类比为"轻量级线程",但在资源占用和调度效率上远超Java的Thread。它是Go运行时调度的用户级线程,也是Go实现百万级并发的核心载体。
//java 的 线程 是通过操作系统的线程实现的,jdk21使用了虚拟线程,也就是 go 的协程,因此 go 语言的高性能之处就在于这里,大厂很多核心业务系统也转型为go语言就是因为这个,阿里保持 Java 生态的核心还是因为历史问题,通过 java 强大生态实现快速组合。比如淘宝在短短几天内将飞猪、饿了么等等直接融合进去淘宝app中
1.1 协程与Java Thread的核心差异
|----------|---------------------------------------------|-------------------------------|
| 对比维度 | Go goroutine | Java Thread |
| 资源占用 | 初始栈仅2KB,支持动态扩容(最大可达1GB) | 初始栈约1MB,资源占用高 |
| 调度机制 | 由Go运行时(runtime)调度(用户级调度),切换开销极小(千分之一线程切换成本) | 由操作系统内核调度(内核级调度),切换开销大 |
| 并发上限 | 一个Go进程可轻松支撑百万级goroutine | 一个JVM进程通常仅能支撑千级线程(受内存限制) |
| 启动方式 | 仅需在函数调用前加go关键字,语法简洁 | 需创建Thread对象或实现Runnable接口,代码繁琐 |
1.2 协程的基本使用(代码示例)
注意:main函数本身也是一个goroutine,若main goroutine退出,所有子goroutine会被强制终止。因此需要通过同步机制确保子goroutine执行完成(后续会讲解信道和锁的同步方式,此处先用time.Sleep简单演示)。
package main
import (
"fmt"
"time"
)
// 模拟并发任务:打印任务名称和序号
func concurrentTask(name string) {
for i := 0; i < 3; i++ {
fmt.Printf("任务[%s]执行:%d\n", name, i)
time.Sleep(100 * time.Millisecond) // 模拟任务耗时,触发goroutine切换
}
}
func main() {
// 启动2个协程(仅需加go关键字)
go concurrentTask("goroutine-1")
go concurrentTask("goroutine-2")
// 关键:main goroutine等待子协程执行完成(否则main退出,子协程终止)
// 此处用time.Sleep临时模拟,后续会用更优雅的同步方式替代
time.Sleep(500 * time.Millisecond)
fmt.Println("main goroutine退出")
}
输出结果(顺序可能因调度略有差异):
任务[goroutine-1]执行:0
任务[goroutine-2]执行:0
任务[goroutine-1]执行:1
任务[goroutine-2]执行:1
任务[goroutine-1]执行:2
任务[goroutine-2]执行:2
main goroutine退出
对比Java实现(需创建Thread对象):
public class ConcurrentDemo {
public static void main(String[] args) throws InterruptedException {
// 启动2个线程
new Thread(() -> concurrentTask("Thread-1")).start();
new Thread(() -> concurrentTask("Thread-2")).start();
Thread.sleep(500);
System.out.println("main线程退出");
}
private static void concurrentTask(String name) {
for (int i = 0; i < 3; i++) {
System.out.printf("任务[%s]执行:%d%n", name, i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
二、信道(channel):协程间的"安全通信桥梁"
在Java中,线程间通信需通过共享内存(如volatile变量、ConcurrentHashMap)+ 锁机制实现,容易出现数据竞争问题。而Go的设计理念是"不要通过共享内存来通信,要通过通信来共享内存",这里的"通信"核心就是信道(channel)。信道可理解为协程间传递数据的"管道",天然支持同步,能安全实现协程间的数据交互。
2.1 信道的核心特性与Java通信方式对比
- 同步特性:无缓冲信道(默认)的发送和接收操作是同步的,发送方会阻塞直到接收方接收数据,反之亦然(类似Java的SynchronousQueue);
- 类型安全:信道在声明时需指定数据类型,仅能传递该类型数据(Java的阻塞队列也支持类型安全,但需显式声明泛型);
- 并发安全:信道本身是并发安全的,无需额外加锁(Java的普通队列需配合锁才能实现并发安全)。
核心对比:Java线程间通信依赖"共享内存+锁",需手动处理线程安全;Go协程间通信依赖信道,通过"数据传递"替代"内存共享",天然避免数据竞争。
2.2 信道的基本使用(代码示例)
信道的声明语法:chan 数据类型,通过make函数初始化,支持无缓冲和有缓冲两种类型。
package main
import "fmt"
// 子协程:向信道发送数据
func sendData(ch chan string) {
ch <- "Hello from goroutine" // 发送数据到信道,无缓冲信道会阻塞直到接收方接收
}
func main() {
// 1. 初始化无缓冲信道(默认)
ch := make(chan string)
// 2. 启动子协程发送数据
go sendData(ch)
// 3. 接收信道数据(main goroutine会阻塞直到收到数据)
data := <-ch
fmt.Println("收到数据:", data)
// 4. 关闭信道(可选,关闭后无法再发送数据,接收方仍可接收剩余数据)
close(ch)
// 补充:有缓冲信道的使用
bufferedCh := make(chan int, 2) // 容量为2的有缓冲信道
bufferedCh <- 1 // 发送数据,容量未满,不阻塞
bufferedCh <- 2
fmt.Println("有缓冲信道接收1:", <-bufferedCh)
fmt.Println("有缓冲信道接收2:", <-bufferedCh)
}
输出结果:
收到数据: Hello from goroutine
有缓冲信道接收1: 1
有缓冲信道接收2: 2
对比Java:无缓冲信道类似Java的SynchronousQueue,有缓冲信道类似Java的ArrayBlockingQueue。上述Go代码的Java等效实现需使用SynchronousQueue:
import java.util.concurrent.SynchronousQueue;
public class ChannelDemo {
public static void main(String[] args) throws InterruptedException {
// 类似Go的无缓冲信道
SynchronousQueue<String> queue = new SynchronousQueue<>();
// 启动线程发送数据
new Thread(() -> {
try {
queue.put("Hello from Thread");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 接收数据(阻塞)
String data = queue.take();
System.out.println("收到数据:" + data);
}
}
三、锁(sync包):并发安全的"兜底保障"
尽管Go推荐通过信道实现协程通信,但在某些场景下(如多个协程共享变量),仍需通过锁机制保证并发安全。Go的sync包提供了多种锁实现,核心常用的有互斥锁(Mutex)和读写锁(RWMutex),可类比Java的synchronized和ReentrantReadWriteLock。
3.1 核心锁类型与Java对比
|-------------------|------------------------------|--------------------------------------|
| Go锁类型 | Java对应类型 | 核心作用 |
| sync.Mutex(互斥锁) | synchronized / ReentrantLock | 保证同一时间只有一个协程进入临界区,实现互斥访问 |
| sync.RWMutex(读写锁) | ReentrantReadWriteLock | 读锁可共享(多个协程同时读),写锁排他(仅一个协程写),适合读多写少场景 |
3.2 锁的基本使用(代码示例)
以"多个协程并发修改共享变量"为例,演示锁的使用(若不加锁,会出现数据竞争,导致结果错误)。
package main
import (
"fmt"
"sync"
"time"
)
var (
count = 0
mu sync.Mutex // 互斥锁
rwMu sync.RWMutex // 读写锁
)
// 用互斥锁保护写操作
func increment() {
mu.Lock() // 加锁,进入临界区
defer mu.Unlock() // 延迟解锁,确保函数退出时释放锁(避免 panic 导致锁未释放)
count++
}
// 用读写锁保护读操作
func readCount() int {
rwMu.RLock() // 加读锁
defer rwMu.RUnlock() // 延迟释放读锁
return count
}
func main() {
// 启动10个协程并发修改count
for i := 0; i < 10; i++ {
go func() {
for j := 0; j< 1000; j++ {
increment()
}
}()
}
time.Sleep(100 * time.Millisecond) // 等待所有写协程完成
// 启动5个协程并发读取count
for i := 0; i < 5; i++ {
go func(idx int) {
fmt.Printf("协程%d读取count:%d\n", idx, readCount())
}(i)
}
time.Sleep(100 * time.Millisecond)
fmt.Println("最终count值:", count)
}
输出结果(最终count值稳定为10000,无数据竞争):
协程0读取count:10000
协程1读取count:10000
协程2读取count:10000
协程3读取count:10000
协程4读取count:10000
最终count值: 10000
对比Java实现(使用ReentrantLock和ReentrantReadWriteLock):
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LockDemo {
private static int count = 0;
private static final ReentrantLock lock = new ReentrantLock();
private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private static void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 必须在finally中释放锁
}
}
private static int readCount() {
rwLock.readLock().lock();
try {
return count;
} finally {
rwLock.readLock().unlock();
}
}
public static void main(String[] args) throws InterruptedException {
// 10个线程并发写
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
increment();
}
}).start();
}
Thread.sleep(100);
// 5个线程并发读
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
System.out.printf("线程%d读取count:%d%n", finalI, readCount());
}).start();
}
Thread.sleep(100);
System.out.println("最终count值:" + count);
}
}
四、上下文(context):协程生命周期的"管家"
在Java中,若需终止线程,通常需通过"中断标记"(如Thread.interrupt())配合手动判断实现。而在Go中,当启动多个嵌套协程时,若需统一控制协程的退出(如超时、取消),可通过上下文(context.Context)实现。context就像协程的"管家",负责传递取消信号、超时信号,统一管理协程生命周期。
4.1 上下文的核心作用与Java对比
- 取消信号传递:父协程可通过context向子协程传递取消信号,实现协程的优雅退出(类似Java的Thread.interrupt(),但支持嵌套传递);
- 超时控制:可设置context超时时间,到期后自动发送取消信号(Java需配合ScheduledExecutorService实现);
- 数据传递:可在context中存储少量共享数据(类似Java的ThreadLocal,但更适合协程间临时数据传递)。
4.2 上下文的基本使用(代码示例)
Go的context包提供了context.Background()(空上下文,作为根上下文)、context.WithCancel()(创建可取消上下文)、context.WithTimeout()(创建超时上下文)等常用方法。
package main
import (
"context"
"fmt"
"time"
)
// 子协程:监听context的取消信号
func task(ctx context.Context, name string) {
for {
select {
case <-ctx.Done(): // 接收取消信号(ctx被取消或超时)
fmt.Printf("任务[%s]收到取消信号,退出\n", name)
return
default:
fmt.Printf("任务[%s]正在执行...\n", name)
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
// 1. 创建可取消上下文(根上下文为context.Background())
ctx, cancel := context.WithCancel(context.Background())
// 启动2个子协程
go task(ctx, "goroutine-1")
go task(ctx, "goroutine-2")
// 3秒后取消所有子协程
time.Sleep(3 * time.Second)
cancel() // 发送取消信号
// 等待子协程退出
time.Sleep(500 * time.Millisecond)
fmt.Println("main goroutine退出")
// 补充:超时上下文的使用
fmt.Println("\n--- 超时上下文演示 ---")
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 2*time.Second)
defer timeoutCancel() // 确保超时后释放资源
go task(timeoutCtx, "timeout-goroutine")
time.Sleep(3 * time.Second)
fmt.Println("超时演示结束")
}
输出结果:
任务[goroutine-1]正在执行...
任务[goroutine-2]正在执行...
...(持续3秒)
任务[goroutine-1]收到取消信号,退出
任务[goroutine-2]收到取消信号,退出
main goroutine退出
--- 超时上下文演示 ---
任务[timeout-goroutine]正在执行...
...(持续2秒)
任务[timeout-goroutine]收到取消信号,退出
超时演示结束
对比Java实现(需手动管理中断标记):
public class ContextDemo {
public static void main(String[] args) throws InterruptedException {
// 模拟取消信号(中断标记)
Thread t1 = new Thread(() -> task("Thread-1"));
Thread t2 = new Thread(() -> task("Thread-2"));
t1.start();
t2.start();
// 3秒后中断线程
Thread.sleep(3000);
t1.interrupt();
t2.interrupt();
t1.join();
t2.join();
System.out.println("main线程退出");
}
private static void task(String name) {
while (!Thread.currentThread().isInterrupted()) {
System.out.printf("任务[%s]正在执行...%n", name);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// 捕获中断异常,重置中断标记
Thread.currentThread().interrupt();
}
}
System.out.printf("任务[%s]收到中断信号,退出%n", name);
}
}
五、全文总结
本篇聚焦Go并发编程的四大核心基础,通过与Java并发机制的对比,帮你建立Go并发的核心认知,关键要点可梳理为:
- 协程(goroutine) :Go的轻量级并发载体,比Java Thread更轻量、高效,启动仅需
go关键字,是实现百万级并发的基础; - 信道(channel):协程间的安全通信桥梁,遵循"通信共享内存"理念,天然支持同步和并发安全,可替代Java的"共享内存+锁"通信模式;
- 锁(sync包):并发安全的兜底保障,Mutex(互斥锁)和RWMutex(读写锁)分别对标Java的synchronized和ReentrantReadWriteLock,适合共享变量修改场景;
- 上下文(context):协程生命周期的管家,支持取消信号、超时信号传递和少量数据共享,比Java的Thread.interrupt()更优雅地实现协程统一控制。
后续学习建议:结合"协程+信道"实现简单的并发任务调度(如生产者-消费者模型),再逐步探索sync包的其他工具(如sync.WaitGroup),深化对Go并发的理解。