java转go语言入门基础篇(二)

在入门篇一的并发安全场景中,我们初步接触了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并发的核心认知,关键要点可梳理为:

  1. 协程(goroutine) :Go的轻量级并发载体,比Java Thread更轻量、高效,启动仅需go关键字,是实现百万级并发的基础;
  2. 信道(channel):协程间的安全通信桥梁,遵循"通信共享内存"理念,天然支持同步和并发安全,可替代Java的"共享内存+锁"通信模式;
  3. 锁(sync包):并发安全的兜底保障,Mutex(互斥锁)和RWMutex(读写锁)分别对标Java的synchronized和ReentrantReadWriteLock,适合共享变量修改场景;
  4. 上下文(context):协程生命周期的管家,支持取消信号、超时信号传递和少量数据共享,比Java的Thread.interrupt()更优雅地实现协程统一控制。

后续学习建议:结合"协程+信道"实现简单的并发任务调度(如生产者-消费者模型),再逐步探索sync包的其他工具(如sync.WaitGroup),深化对Go并发的理解。

相关推荐
我不会写代码njdjnssj2 小时前
基于SpringBoot+SSM的外卖平台Day1-6
java·spring boot·后端
崎岖Qiu2 小时前
【设计模式笔记26】:深入浅出「观察者模式」
java·笔记·观察者模式·设计模式
会算数的⑨2 小时前
Java场景化面经分享(一)—— JVM有关
java·开发语言·jvm·后端·面试
lpfasd1232 小时前
Spring Boot 4.0 新特性全解析 + 实操指南
java·spring boot·后端
葵花楹2 小时前
【JAVA期末复习】
java·开发语言·排序算法
3824278272 小时前
Edge开发者工具:保留日志与禁用缓存详解
java·前端·javascript·python·selenium
m0_598177232 小时前
SQL(5)- 事务
java·数据库·sql
C++chaofan3 小时前
JUC 并发编程从入门到精通(超详细笔记 + 实战案例)
java·jvm·spring boot·redis·后端·并发·juc