Go与java基础
一.go的简介
Go语言是Google出品的近年来快速崛起的编程语言.简单高效是它的标签. 最近抽空从零开始学一下go.不罗嗦, 咱们从 java的学习路线来 看看 go的实现吧~
二.Go的基础语法
1.编译与运行
##java用jvm做到程序与操作系统的隔离. 一次性编译生成的class文件,处处可以运行
## 简单来说 windows上编译的出class文件,拿到安装了jvm的linux上可直接可以执行
javac Hello.java
java Hello
## go 在不同的操作系统build出对应的可执行文件
go build hello.go
## 当然直接编译+执行亦可,慢一些
go run hello.go
2.main方法与测试类
// java 源码 "一等公民"是 类,源码文件 通过类来区分关联
public class Hello { // 一个 类下只有一个启动方法
public static void main(String[] args) {
System.out.println("hello java"); // jdk提供的内部类不需要导入
}
}
// jdk本身无测试类,需要引入第三方依赖包 junit4
public class Test {
@org.junit.Test // 外部依赖的导入
public void test() {
System.out.println("this is test");
}
}
``
```go
// 1. go "一等公民" 是函数, 源码 通过包名 区分关联
// 一个文件夹下,可以有多个源码文件,但必须 统一 包名,不过 包名 ≠ 文件夹名
package main // 定义包名
import "fmt" // 导入内部 "包"
func main() { // 每个包 下只允许一个 启动方法
fmt.Println("hello world")
}
// 2. 自带内部包 有test 功能
// 源码文件 后缀 _test 标注
import (
"fmt"
"testing" // 导入内部测试包
)
func TestFirstTry(t *testing.T) { // 函数 Test 开头
fmt.Println("dadayu try")
}
3.变量与常量
// 每一个变量都有属于自己的类型
public static final int MONDAY = 1; // 常量
public static final int TUESDAY = 2;
public static void main(String[] args) {
// 案例: 交换 数值型 a= 1 b=2 的值
int a = 1;
int b = 2;
int tmp = a; // 中间变量
a = b;
b = tmp;
}
const (
Monday = iota + 1 // 常量可自增定义
Tuesday
Wednesday
)
func TestChange(t *testing.T) {
// 1. var 声明
//var a int = 1
//var b int = 2
// 2. var 类型推断
//var (
// a = 1
// b = 2
//)
// 3. 类型自动推断
a := 1
b := 2
a,b = b,a // 值可以直接交换
t.Log(a,b)
}
4. 类型 type
func TestImplicit(t *testing.T) {
var a int= 1
var b int64
// 1. 不支持 隐式 类型转换 即使是同一种 变量
// 必须使用显式类型转换
b = int64(a)
// 2.go 可以 获得指针
aPrt := &a // & 取址符
var str string // 3. string 是值类型 , 初始化是 ""
t.Log(str == "") // true
}
5.条件判断 condition
// java 多条件判断
if (n % 2 == 0) {
return "Even";
} else if (n % 2 == 1) {
return "Odd";
} else {
return "Unkonw";
}
//1. go的 switch 也可以实现 if -else
func SwitchCaseCondition(i int) (string, bool) {
switch {
case i % 2 == 0: // 留意 java的 switch 需要break嗷!
return "Even", true // go的函数 支持多返回值 特色之一
case i % 2 == 1:
return "Odd", true
default:
return "Unkonw", false
}
}
func TestSwitch(t *testing.T) {
//2. go的 if 可以边变量声明, 边进行判断
if str,flag := SwitchCaseCondition(5); flag {
t.Log(str)
}
}
6.循环语句 loop
// go 关键字 很少 循环只有 for
func TestWhileLoop(t *testing.T) {
n := 0
// 1. go 实现 while
// 等同于 while(n < 5) 不需要 () , 关键字 只有for
for n <5 {
t.Log(n)
n++
}
// 2. 常规的 for循环 // 去掉 () 没啥 区别
for i:= 0; i < 5; i++ {
t.Log(i)
}
}
7.数组 array
// 初始化 定长的数组, 默认为0
int[] arr= new int[5];
arr[1] = 2;
int[] arr1 = new int[]{1,2,3,4}; // 初始化 也可赋值
for (int i : arr1) { // 遍历 数组
System.out.println(i);
}
var arr [3]int //1. 初始化, 默认为 0
arr[2] = 3
arr1 := [3] int{1,2,3} // 声明 并初始化
arr2 := [...] int {1,2,3} // 自动长度推断
t.Log(arr1 == arr2) //2. 结果 : true 数组是值类型
for i, v := range arr1 {
t.Log(i,v) // 3.注意 变量声明后必须使用, 如果不行用咋办
}
for _, v := range arr1 {
t.Log(v) // 严格 编程约束 下, 可以使用 占位符 _ 来代替
}
// 4. 数组的切分
// a[开始索引(包含), 结束索引(不包含)]
arr3 := arr1[1,len(arr1)] // 得到索引1,往后的值 即 2,3
8. 切片 slice
go的引用类型之一,简单说 切片就是可变长的数组引用 , 数组通过len函数可以得到长度. 而切片除了真实的长度 ,还有 用cap函数 获得其容量, 我们会初始化一个容量, 如果数据真实长度超过这个容量.切片就会发生扩容,这时候会发生值copy. 这里联想一下java, 这不就是 ArrayList…
ArrayList<Integer> list = new ArrayList<>(8);// 初始化 容量为 8
list.add(1);
System.out.println(list.size()); // 数据真实长度
var sli []int // 初始化
sli = append(sli, 1) // 添加元素
t.Log(len(sli), cap(sli)) // 1,1
// 初始化 并赋值
sli1 := [] int{1,2,3,4}
t.Log(len(sli1), cap(sli1)) //4 , 4
// 初始化 容量大小
sli2 := make([]int, 3, 5)
//make 是专门用来创建 slice、map、channel 的值的。
// 它返回的是被创建的值,并且立即可用。
t.Log(sli2 ,len(sli2), cap(sli2)) // [0 0 0] 3 5
// 如何扩容
sli3 := [] int{1,2,3,4}
fmt.Printf("%p\n",sli3) // 0xc00009c140
sli3 = append(sli3, 5)
fmt.Printf("%p\n",sli3) //0xc0000ba240 引用的内存地址改变 copy了新的引用
t.Log(sli3 ,len(sli3), cap(sli3)) // 与 ArrayList的扩容机制 完全相同
9.字典 map
map亦是go的基本引用类型之一, go比较有特色的一点是,java里面很多基本的集合(链表,堆),在go里面都是作为自定义类型,"自举"实现的(即用 Go 语言编写程序来实现 Go 语言自身), 可以直接阅读go的源码包学习,里面甚至有测试的案例
HashMap<Integer, Integer> map = new HashMap<>(); //约定类型
map.put(1,1); // 添加
boolean b = map.containsKey(3); // 判断 有没有
map.remove(1); // 移除
// 遍历
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + entry.getValue());
}
// 初始化 并赋值
map2 := map[int]int{1: 1, 2: 2, 3: 9}
map2[2] = 4 // 修改
map2[4] = 16 // 添加
t.Log(map2,len(map2)) // map[1:1 2:4 3:9 4:16] 4
map3 := make(map[int]int,10)
t.Log(map3,len(map3)) // map[] 0
// 遍历
for k, v := range map2 {
t.Log(k,v)
}
// 案例1 :map 实现 set
mySet := map[int] bool{}
mySet[1] = true
n := 3
// 判断值是否存在
if mySet[n] {
t.Logf("%d is exiting", n)
}else {
t.Logf("%d is not exiting", n) // 3 is not exiting
}
// 案例2 :map的value 可以放函数 , java 中一般放对象
// 很容易的实现了 工厂模式
funcMap := map[int] func(op int) int{}
funcMap[1] = func(op int) int { return op}
funcMap[2] = func(op int) int { return op * op}
funcMap[3] = func(op int) int { return op * op * op}
t.Log(funcMap[1](2),funcMap[2](2),funcMap[3](2)) //2 4 8
10.通道 channel
chan基本的引用类型之一,Go 语言最有特色的数据类型,通道(channel)完全可以与 goroutine( 协程)并驾齐驱,共同代表 Go 语言独有的并发编程模式和编程哲学。
channe类型的值本身就是并发安全的,这也是 Go 语言自带的、唯一一个可以满足并发安全性的类型。
一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。元素值的发送和接收都需要用到操作符**<-**。我们也可以叫它接送操作符。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向。
// 这里 简单演示一下 channel ,并发时, channel 大有用途
func TestChan(t *testing.T) {
ch1 := make(chan int, 1) // buffer chan ,队列的大小
ch1 <- 1 // 输入
elem1 := <-ch1 // 输出
fmt.Printf("The first element r: %v\n", elem1)
delay(ch1)
fmt.Println("put elem to channel")
ch1 <- 2
time.Sleep(time.Second * 1) //守护线程
}
func delay(ch chan int) {
go func() { // 开启协程
fmt.Printf("receive elem from chanel %d", <-ch) // 阻塞式等待
}()
}
###################结果打印#####################
The first element r: 1
put elem to channel
receive elem from chanel 2
11.函数 function
在 Go 语言中,函数可是一等的(first-class)公民 ,函数类型也是一等的数据类型.相较于java , 函数基本的语法差异在于 可以多返回值. 更关键的是 天然实现 了java中的 函数式编程, 1.接受其他的函数作为参数传入 2.把其他的函数作为结果返回.
// 案例1 声明函数类型
static Predicate<Integer> judgeEven = e -> e % 2 == 0; //函数式编程 搭配 lambda更香..
static Consumer<Integer> printlnInt = System.out::println;
// 案例 2 计算函数执行的耗时的切面通用方法 装饰者模式, 不得不说, 学习成本 有点高(手动笑哭)
static Function<Integer, Integer> timeSpent(Function<Integer, Integer> inner) {
return (Function<Integer, Integer>) integer -> {
Instant first = Instant.now();
Integer ret = inner.apply(integer); // 执行传入的方法
Instant second = Instant.now();
System.out.println("time spent:"+Duration.between(first, second).toMillis());
return ret;
};
}
public static int simpleFunc(int n) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
}
return n;
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
list.stream().filter(judgeEven).forEach(printlnInt); // 2,4,6
Function<Integer, Integer> func = timeSpent(funcInterface::simpleFunc);
func.apply(10); // time spent:1000
}
// 案例一 go 实现 装饰者模式
// 函数类型 (接口) 自定义函数的签名
type myFunc func(op int) int
func timeSpent(inner myFunc) myFunc { //
return func(n int) int{
start := time.Now()
ret := inner(n)
// 计算日期 since 从什么时候开始
fmt.Println("time spent:", time.Since(start).Seconds())
return ret
}
}
func slowFun(op int) int{
time.Sleep(time.Second*1)
return op
}
// 案例2 延迟执行 方法
func Clear() {
fmt.Println("Clear resources.")
}
func TestFn(t *testing.T) {
// 直接 拿到增强后的方法
tsSF := timeSpent(slowFun)
t.Log(tsSF(10))
defer Clear() // 类似于 java的 finally
fmt.Println("Start")
// 异常报错, 也会有延迟执行
panic("err")
}
###############结果展示##############
Start
Clear resources.
12. 面向对象 (封装)
Go 严格上不能算是一个面向对象的语言,但是通过go 基本可以模拟出类似java的 面向对象的效果.
type Employee struct { // struct 结构体封装了 java的属性
Id string
Name string
Age int
}
// 给 (e Employee) 结构体的指针 绑定行为 类似java的行为
func (e *Employee) String() string{
return fmt.Sprintf("ID:%s/Name:%s/Age:%d", e.Id, e.Name, e.Age)
}
func TestCreateEmployeeObj(t *testing.T) {
e := Employee{"0", "Bob", 20}
e1 := Employee{Name: "Mike", Age: 30}
e2 := &Employee{Name: "Mike", Age: 30} // 返回对象的 指针
e3 := new(Employee) // 返回 对象的 指针
e3.Id = "2"
e3.Name = "Rose"
e3.Age = 22
t.Log(e1) // { Mike 30}
t.Log(e2) // ID:/Name:Mike/Age:30
// %T 代表类型 encap_test 是包名
t.Logf("e is %T", e) //e is encap_test.Employee
t.Logf("e2 is %T",e2) //e2 is *encap_test.Employee
}
13.面向对象 (继承)
java中子类继承父类属性与行为的,通过实现子类的方法,重写父类方法, 实现了LSP 里氏替换原则.往往作为策略模式在代码中体现.Go 无法做到继承. 只能做到复用 父类方法. 再Go的语言哲学里. 组合 > 继承. 其实这也是 java的spring 框架中 依赖注入的思想.
type Pet struct { // 父结构体
}
func (p *Pet)Speak() { // 行为
fmt.Print("...")
}
func (p *Pet)SpeakTo(host string) { // 主流程
p.Speak()
fmt.Println("", host)
}
type Dog struct {
Pet // 匿名嵌套类型 , 类似继承的作用, 使用了父类的 方法
}
func (d *Dog) Speak() { //重写 父类方法
fmt.Print("wang!!")
}
func TestDog(t *testing.T) {
// 不支持隐式类型转换
dog := new(Dog)
dog.Speak() // 可以覆盖
dog.SpeakTo("chao") // 但是无法重写父结构体的方法
}
#############执行结果##############
wang!!
... chao
14.面向对象 (多态)
// 类型别名
type Code string
type Programmer interface {
WriterHelloWorld() Code
}
// 无显式的 implement 关键字
type GoProgrammer struct { // 第一个接口实现
}
func (g *GoProgrammer)WriterHelloWorld() Code {
return "fmt.Println(\"Hello World\")"
}
type JavaProgrammer struct { // 第二个接口实现
}
func (j *JavaProgrammer) WriterHelloWorld() Code {
return "system.out.println(\"Hello World\")"
}
func writeFirstProgram(p Programmer) { // 将接口传入方法
fmt.Printf("%T, %v\n", p, p.WriterHelloWorld()) // 接口的 多态
}
func TestPolymorphic(t *testing.T) {
//var p Programmer = new(JavaProgrammer)
// 接口 参数必须有指针 引用 & 取址符
p := &JavaProgrammer{}
writeFirstProgram(p) //
goPro := new(GoProgrammer)
writeFirstProgram(goPro)
}
###################执行结果#####################
*polym__test.JavaProgrammer, system.out.println("Hello World")
*polym__test.GoProgrammer, fmt.Println("Hello World")
15. 并发 concurrent
Java的并发线程模型
线程- Thread 是比 进程 - Progress 更轻量的调度单位. 众所周知, 操作系统会把内存分为内核空间和用户空间, 内核空间的指令代码具备直接调度计算机底层资源的能力.用户空间的代码没有访问计算底层资源的能力,需要通过系统调用等方式切换为内核态来实现对计算机底层资源的申请和调度. 线程作为操作系统能够调度的最小单位,也分为用户线程和内核线程. 常用的java的线程模型属于一对一线程模型. 下图中反映到Java中, Progress是jvm虚拟机. LWP就是程序开启的Thread, 1:1 对应上内核线程,最后通过操作系统的线程调度器, 操作底层资源.
进程内每创建一个新的线程都会调用操作系统的线程库在内核创建一个新的内核线程对应,线程的管理和调度由操作系统负责,这将导致每次线程切换上下文时都会从用户态切换到内核态,会有不小的资源消耗。好处是多线程能够充分利用 CPU 的多核并行计算能力,因为每个线程可以独立被操作系统调度分配到 CPU 上执行指令,同时某个线程的阻塞并不会影响到进程内其他线程工作的执行。以上就是 java并发的特色. 我们用代码测试一下.
//java 案例 1 开始多线程来打印,中间线程休眠,让出cpu的执行资源
class PrintIntTask implements Runnable{ // 任务类的封装
int num; // 传入的 参数
CountDownLatch cnt; // 并发协同 线程计数器
public PrintIntTask(int num, CountDownLatch cnt) {
this.num = num;
this.cnt = cnt;
}
@Override
public void run() {
TimeUnit.SECONDS.sleep(1); // 休眠 1s
System.out.println(num); // 打印
cnt.countDown(); // 计数锁 -1
}
}
public static void main(String[] args) throws InterruptedException {
Instant first = Instant.now();
int num = 10000;
CountDownLatch countDownLatch = new CountDownLatch(num);// 计数锁
for (int i = 0; i < num; i++) {
new Thread(new PrintIntTask(i,countDownLatch)).start(); // 任务分配,开启线程
}
countDownLatch.await(); //守护线程 ,子线程改造时,切记阻塞住 主线程
Instant second = Instant.now();
System.out.println("spent time:" + Duration.between(first, second).toMillis());
}
java线程池的作用
############# 上一页程序 测试结果 ###################
n = 1000, spent time:1106 ms // 资源竞争 不明显
n = 10000, spent time:5970 ms // 资源竞争 明显
虽然每个线程之间是独立的,但是处于就绪状态的线程,需要被cpu调度才能进入运行态**, 当线程休眠时,会进入等待状态,让出cpu资源,执行其他线程**. 而开辟的线程数上升后,竞争也愈发明显.同时开启大量的线程,对于系统的内存资源也会有很大的负担,在Jvm内存模型中,线程主要有
1.程序计数器
线程数超过CPU内核数量时,线程之间就要根据时间片轮询抢夺CPU时间资源,线程等待让出cpu资源时,它就需要记录正在执行字节码指令的地址.
2.虚拟机栈
每个方法执行时,会开辟一个栈帧,存储局部变量表,操作数栈等.调用方法时,生成一个栈帧,压入栈中,退出方法时,将栈帧弹出栈. 通过配置jvm参数 -Xss可以设置栈的大小,一般为1M.这就是递归方法过多后StackOverFlowError 的原因.
所以在java 编程中,需要开启大量的线程时一定要控制, 这就是常说的 线程池, 配置同时执行的核心线程数, 多余的任务在内部队列中排队执行.
//IO密集型 一般是 cpu 核心数 * 2 , 计算密集型 一般是 cpu核心数
int processors = Runtime.getRuntime().availableProcessors() * 2 ;
// 初始化线程池,阿里巴巴手册推荐手动new,设置相应的 拒绝策略,任务队列的参数 ,巴拉巴拉 之类的东西
Execu torService fixedThreadPool = Executors.newFixedThreadPool(processors);
for (int i = 0; i < num; i++) {
fixedThreadPool.submit(new PrintIntTask(i));// 将任务塞入线程池,线程自己调度开启
}
// 线程池 会自动守护线程 不会中断子线程的运行
Go的并发线程模型
Go在一对一线程模型的基础上,做了一些改进,提出了MPG模型 ,让线程之间也可以灵活的调度.
machine,一个 machine 对应一个内核线程,相当于内核线程在 Golang 进程中的映射
processor,一个 prcessor 表示执行 Go 代码片段的所必需的上下文环境,理解为代码逻辑的处理器
goroutine,是对 Golang 中代码片段的封装,其实是一种轻量级的用户线程,我们叫协程
下左图,每个M都会和一个内核线程绑定,M与P 也是一对一关系,而P与G时一对多的关系.M在生命周期内跟内核线程是至死不渝的.而 M 与 P ,P 与 G.那是自由恋爱.
下右图,M与P的组合为G 提供了运行环境, 可执行的G的挂载在P下,等待调度与执行为GO. 这里不得不提 java的ForkJoinPool的工作窃取算法(感兴趣的2021.5.4 朋友圈有篇). 这里把G看做任务,当P空闲时,首先去全局的执行队列中获取一些G.如果没有则去"窃取"其他P最后的G,保证每一个P都不摸鱼.
//go 案例 1 开始多协程来打印,中间协程休眠,让出cpu的执行资源
func TestGroutine(t *testing.T) {
start := time.Now()
num := 10000
wg := sync.WaitGroup{}
wg.Add(num)
for i := 0; i < num; i++ {
go func(i int) { // 协程 可以直接传参进去...
time.Sleep(time.Second * 1)
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
t.Logf("spent time :%d",time.Since(start).Milliseconds()) // 10000个协程
}
#################测试结果######################
n = 1000, spent time:1005 ms // java spent time:1106 ms
n = 10000, spent time:1262 ms // java spent time:5970 ms
协程其实就是更轻量的线程,go将cpu的调度转为用户空间协程的调度,避免了内核态和用户态的切换导致的成本,通过队列的形式挂载协程, 这些协程共享一个P获得cpu资源.这样我们可以大量的开启协程,而不用太担心协程之间的竞争压力,同时G的栈空间只有2k,无压力创建出大量的实例,高效的利用了系统资源.
资源锁
单纯的并发还蛮简单,但是事情总不是一帆风顺. 并发在做任务时,不可避免的会出现共享资源,当多个线程同时操作同一资源时,就会出现并发安全问题(感兴趣的详见 2021.3.8的有篇). 我们使用资源锁,拿到锁的线程才可以操作资源, 拿不到锁的线程,等待锁的释放,再去抢锁.
public class Count { // 案例1 : 1000 个并发操作计数 9:1 读写比
// 可重入锁
final ReentrantLock lock = new ReentrantLock(); //参数true表示公平锁,性能会低
CountDownLatch cdl;
int count = 0;
public Count(CountDownLatch cdl) { this.cdl = cdl;}
public void read(){
lock.lock();
try {
Thread.sleep(1); // 模拟查询消耗的时间
System.out.println("查询现在计数为:"+count);
} finally {
lock.unlock(); // 一定要记得解锁嗷
cdl.countDown();
}
}
public void count(){
lock.lock();
try {
count++;
Thread.sleep(5); // 模拟修改消耗的时间
System.out.println("计数后现在计数为:"+count);
} finally {
lock.unlock(); // 一定要记得解锁嗷
cdl.countDown();
}
}
}
int num = 1000;
CountDownLatch cdl = new CountDownLatch(num);
Instant start = Instant.now();
// Count count = new Count(cdl);
Count1 count = new Count1(cdl);
for (int i = 0; i < num; i++) {
if (i % 10 == 0){
new Thread(count::count).start();
} else {
new Thread(count::read).start();
}
}
cdl.await();
Instant end = Instant.now();
System.out.println("spent time:" + Duration.between(start, end).toMillis());
####################测试结果#####################
spent time : 1624
锁的优化
加锁之后,并发的性能明显下降,所以使用锁时一定要慎重, 从优化的来看有两个维度:
其一,缩小锁的粒度. 在分布式事务中,如果多个系统事务保证强一致性,并发能力必然很差.,若把一个大事务,分割几个独立的小事务, 只要能保证最终一致性,就能大大提示并发能力.
★其二,读写锁,第一个案例里面,提到一个读写比的概念,这里联想一下数据库的隔离级别,读,写操作都互斥时,不就是性能最低的串行化SERIALIZABLE.把读和写 设计成两钟锁,存在写锁时,才互斥,只有读锁时,不互斥,再读写比较大场景下, 并发性能得到提升.
final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); // 案例2 读写锁
CountDownLatch cdl;
int count = 0;
public Count1(CountDownLatch cdl) { this.cdl = cdl;}
public void read(){
lock.readLock().lock(); // 读锁 加锁
try {
Thread.sleep(1);
System.out.println("查询现在计数为:"+count);
} finally {
lock.readLock().unlock(); // 读锁解锁
cdl.countDown();
}
}
public void count(){
lock.writeLock().lock(); // 写锁 加锁
try {
count++;
Thread.sleep(5);
System.out.println("计数后现在计数为:"+count);
} finally {
lock.writeLock().unlock(); //写锁 解锁
cdl.countDown();
}
}
################测试结果#################
spent time : 699 // 并发的性能得到明显提升
go实现读写锁
num := 1000
wg := sync.WaitGroup{}
wg.Add(num)
mutex := sync.RWMutex{}
count := 0
for i := 0; i < num; i++ {
if i % 10 ==0 {
go func() {
defer func() { // 延迟执行方法, 类似于finally
mutex.Unlock()
}()
mutex.Lock() // 写锁
count = count + 1
fmt.Printf("write num:%d\n",count)
wg.Done()
}()
}else {
go func() {
defer func() {
mutex.RUnlock() // 读锁
}()
mutex.RLock() // 读锁
fmt.Printf("read num:%d\n",count)
wg.Done()
}()
}
}
wg.Wait()
go的CSP并发通信模型
go在并发上的突破不光在线程模型上, 传统的并发通信模型,是以共享内存的方式,通过原子类和管程进行同步控制,比如上述的锁,有锁就有竞争,go 还提供了另一种方案, 支持协程之间以消息传递(Message-Passing)的方式通信, Go有句格言, “不要以共享内存方式通信,要以通道方式共享内存”.
通道即channel. 有点类似消息队列. 分非缓冲通道和 缓冲通道, 前者协程之间建立通信后,同步收发消息, 后者初始化通道容量, 异步收发消息,通道满了或空了,阻塞等待. (2021.11.5 朋友圈有篇)
num := 1000
wg := sync.WaitGroup{}
wg.Add(num)
start := time.Now()
ch := make(chan int, 1) // 容量为1的缓冲通道 数据类型为 int
ch <- 0 // 放入通道初始值
for i := 0; i < num; i++ {
if i % 10 ==0 {
go func() {
count := <- ch // 从通道拿数据
time.Sleep(time.Millisecond * 5)
fmt.Printf("write num:%d\n",count)
ch <- count+1 // +1 后再放入通道
wg.Done()
}()
}else {
go func() {
count := <- ch // 从通道拿数据
ch <- count // 获得数据之后,直接放回即可
time.Sleep(time.Millisecond * 1)
fmt.Printf("read num:%d\n",count)
wg.Done()
}()
}
}
wg.Wait()
t.Logf("spent time :%d",time.Since(start).Milliseconds()) //spent time :550
线程协同 (任务并行)
并发中的关键场景之一.我们调用两个耗时却又毫无关联的两个组件时,不妨试试任务并行,就是烧水煮茶, 时间统筹,同时执行.
Instant start = Instant.now(); // 案例 1 java异步编程类 FutureTask
FutureTask<String> futureTask = new FutureTask<String>(()->{
TimeUnit.SECONDS.sleep(1); // FutureTask java特色的异步编程任务类
return "haha";
});
new Thread(futureTask).start(); // 异步执行
TimeUnit.SECONDS.sleep(1);
String s = futureTask.get(2, TimeUnit.SECONDS); //阻塞等待, 且限定超时时间
Instant end = Instant.now();
System.out.println("spend time :" + Duration.between(start, end).toMillis() + "ms");
#####################执行结果######################
spend time :1050ms // 执行时间 取决于 最长的时间
//go 简单的异步有案例,这里加上 通道传递 + 多路选择 (有点 nio 的味道)
select { // 多路选择
case ret:= <- syncTask(): // 等待通道返回
t.Log(ret)
case <-time.After(time.Second *3): // 超时 返回
t.Error("timeout")
}
func syncTask() chan string{
ret := make(chan string,1)
go func() {
// 异步往通道写
time.Sleep(time.Second * 1)
ret <- "dada"
}()
return ret
}
☆ 拓展: FutureTask之(任务只执行一次)
// 案例2 多个线程创建连接时,只有一个线程可以创建,并建立连接的缓存,优先从缓存连接中拿
// 在 netty中 ,也是使用这种方式,保存建立好的channel,优化资源利用
private ConcurrentHashMap<String,FutureTask<Connection>>connectionPool = new ConcurrentHashMap<String, FutureTask<Connection>>();
public Connection getConnection(String key) throws Exception{
FutureTask<Connection>connectionTask=connectionPool.get(key);
if(connectionTask!=null){
return connectionTask.get();
}
else{
FutureTask<Connection> newTask = new FutureTask<Connection>(this::createConnection);
connectionTask = connectionPool.putIfAbsent(key, newTask);//保证一个线程创建
if(connectionTask==null){
connectionTask = newTask;
connectionTask.run();0
}
return connectionTask.get();// 本来要创建连接的线程,转为阻塞等待
}
}
//创建Connection
private Connection createConnection(){
return null;
}
```