Go与Java(14) Go的并发编程

300 阅读8分钟

四.专门详解Go并发编程相关知识

1.Go为什么天然支持高并发,纤程比线程的优势是什么?

Go语言在设计的时候就考虑了充分利用计算机的多核处理器,具体表现为,Go中开启一个并发的任务以操作系统的线程资源调度为单位的,而是Go的创造者们自己写了一套管理多个任务的机制,在这个机制下,每一个并发的任务线程叫做纤程,这个纤程的作用等同一个线程,也是并发执行的,只不过纤程是在应用程序管理的,懂底层的可以讲是在用户态的一个线程,而Java中调度的线程是属于操作系统,也就是操作系统内核态的线程。

用户态的纤程归属于用户编写的软件管理和调度,优点是可以根据情况灵活实现堆栈的内存分配,最优化其中的运行资源配置。

内核态的线程归属于操作系统调度和管理,他底层是有windows或者linux操作系统底层的代码管理的,那么他就不灵活,每个线程分配的资源可能造成浪费,创建的线程数肯定也有一定的限制。  

Go的创造可以为自己的语言和任务灵活配置资源,Linux和windows操作系统的代码是通用的,总不能为你这个语言修改源代码把。

在实际程序运行中,一个操作系统的内核态线程可能管理着好几个甚至数十个纤程(根据实际情况和设置不同而不同),所以省去了线程时间片上下文切换的时间。

同时因为内部机制灵活,所以执行效率高,占用内存也少。

这就是Go语言的并发优势的核心所在。

 

2.并发和并行的区别?

并发是指的一个角色在一段时间内通过来回切换处理了多个任务。

并行是指两个或者多个角色同时处理自己的任务。

举例:

并发:在一个小时内,你写了10分钟语文作业,又写了10分钟数学,之后又写了10分中英语作业,然后再从语文10分钟,数学10分钟,英文10分钟又来一次。  

这个叫做你并发的写语文数学英语作业。  

你一个一段时间(一个小时内)通过切换(一会写数学,一会写语文。。。),处理了多个任务(写了三门课的作业)

并行:你和小明同时写自己的作业。你们俩同时运行的状态叫做并行运作状态,强调的是你们两个人同时在处理任务(做作业)。  

你和小明(两个以上的角色)同时写作业(处理自己的任务)。

在计算机中,比如有4个cpu,4个cpu同时工作,叫做这4个cpu并行执行任务,每个cpu通过时间片机制上下文切换处理100个小任务,叫做每个cpu并发的处理100个任务。

3.Go是如何用Channel进行协程间数据通信数据同步的?

go中的线程相关的概念是Goroutines(并发),是使用go关键字开启。

  Java中的线程是通过Thread类开启的。

在go语言中,一个线程就是一个Goroutines,主函数就是(主) main Goroutines。

使用go语句来开启一个新的Goroutines

 

比如:

  普通方法执行  

​ myFunction()

 

开启一个Goroutines来执行方法

  ​ go myFunction()

 

java中是

​ new Thread(()->{

  ​ //新线程逻辑代码

​ }).start();

参考下面的代码示例:

package main


import (
   "fmt"
)


//并发开启新线程goroutine测试


//我的方法
func myFunction() {
   fmt.Println("Hello!!!")
}
//并发执行方法
func goroutineTestFunc() {
   fmt.Println("Hello!!! Start Goroutine!!!")
}




func main() {
   /*
   myFunction()
   //go goroutineTestFunc()
   //此时因为主线程有时候结束的快,goroutineTestFunc方法得不到输出,由此可以看出是开启了新的线程。
   */
   //打开第二段执行
   /*
   go goroutineTestFunc()
   time.Sleep(10*time.Second)//睡一段时间  10秒
   myFunction()
    */
}

  线程间的通信:

  java线程间通信有很多种方式:

比如最原始的 wait/notify

到使用juc下高并发线程同步容器,同步队列

到CountDownLatch等一系列工具类

......

甚至是分布式系统不同机器之间的消息中间件,单机的disruptor等等。  

Go语言不同,线程间主要的通信方式是Channel。  

Channel是实现go语言多个线程(goroutines)之间通信的一个机制。  

Channel是一个线程间传输数据的管道,创建Channel必须声明管道内的数据类型是什么  

下面我们创建一个传输int类型数据的Channel  

代码示例:

package main


import "fmt"


func main() {
   ch := make(chan int)
   fmt.Println(ch)
}

channel是引用类型,函数传参数时是引用传递而不是值拷贝的传递。  

channel的空值和别的应用类型一样是nil。  

==可以比较两个Channel之间传输的数据类型是否相等。  

channel是一个管道,他可以收数据和发数据。  

具体参照下面代码示例:  

package main


import (
   "fmt"
   "time"
)
//channel发送数据和接受数据用 <-表示,是发送还是接受取决于chan在  <-左边还是右边
//创建一个传输字符串数据类型的管道
var  chanStr  = make(chan string)
func main() {
   fmt.Println("main goroutine print Hello ")
   //默认channel是没有缓存的,阻塞的,也就是说,发送端发送后直到接受端接受到才会施放阻塞往下面走。
   //同样接收端如果先开启,直到接收到数据才会停止阻塞往下走
   //开启新线程发送数据
   go startNewGoroutineOne()
   //从管道中接收读取数据
   go startNewGoroutineTwo()
   //主线程等待,要不直接结束了
   time.Sleep(100*time.Second)
}


func startNewGoroutineOne() {
   fmt.Println("send channel print Hello ")
   //管道发送数据
   chanStr <- "Hello!!!"
}


func startNewGoroutineTwo(){
   fmt.Println("receive channel print Hello ")
   strVar := <-chanStr
   fmt.Println(strVar)
}

无缓存的channel可以起到一个多线程间线程数据同步锁安全的作用。  

缓存的channel创建方式是  

make(chan string,缓存个数)  

缓存个数是指直到多个数据没有消费或者接受后才进行阻塞。  

类似于java中的synchronized和lock  

可以保证多线程并发下的数据一致性问题。  

首先我们看一个线程不安全的代码示例:  

package main


import (
   "fmt"
   "time"
)


//多线程并发下的不安全问题
//金额
var moneyA int =1000
//添加金额
func subtractMoney(subMoney int) {
   time.Sleep(3*time.Second)
   moneyA-=subMoney
}


//查询金额
func getMoney() int {
   return moneyA;
}




func main() {


   //添加查询金额
   go func() {
      if(getMoney()>200) {
         subtractMoney(200)
         fmt.Printf("200元扣款成功,剩下:%d元\n",getMoney())
      }
   }()


   //添加查询金额
   go func() {
      if(getMoney()>900) {
         subtractMoney(900)
         fmt.Printf("900元扣款成功,剩下:%d元\n",getMoney())
      }
   }()
   //正常逻辑,只够扣款一单,可以多线程环境下结果钱扣多了
   time.Sleep(5*time.Second)
   fmt.Println(getMoney())
}

缓存为1的channel可以作为锁使用:

示例代码如下:

package main


import (
   "fmt"
   "time"
)


//多线程并发下使用channel改造
//金额
var moneyA  = 1000
//减少金额管道
var synchLock = make(chan int,1)


//添加金额
func subtractMoney(subMoney int) {
   time.Sleep(3*time.Second)
   moneyA-=subMoney
}


//查询金额
func getMoney() int {
   return moneyA;
}




func main() {


   //添加查询金额
   go func() {
      synchLock<-10
      if(getMoney()>200) {
         subtractMoney(200)
         fmt.Printf("200元扣款成功,剩下:%d元\n",getMoney())
      }
      <-synchLock
   }()


   //添加查询金额
   go func() {
      synchLock<-10
      if(getMoney()>900) {
         subtractMoney(900)
         fmt.Printf("900元扣款成功,剩下:%d元\n",getMoney())
      }
      synchLock<-10
   }()
   //这样类似于java中的Lock锁,不会扣多
   time.Sleep(5*time.Second)
   fmt.Println(getMoney())
}

4.Go中的Goroutine使用和GMP模型?

Go中的线程(实际是纤程)goroutine的底层管理和调度是在runtime包中自己实现的,其中遵循了GMP模型。  

G就是一个goroutine,包括它自身的一些元信息。  

M是指操作系统内核态的线程的一个虚拟表示,一个M就是操作系统内核态的一个线程。  

P是一个组列表,P管理着多个goroutines,P还有一些用于组管理的元数据信息。

5.Go的select怎么用?

Go中的select是专门用于支持更好的使用管道(channel)的。  

我们之前虽然讲了能从管道中读取数据,但是这有一个缺陷,就是我们在一个Goroutine中不能同时处理读取多个channel,因为在一个Goroutine中,一个channel阻塞后就无法继续运行了,所以无法在一个Goroutine处理多个channel,而select很好的解决了这个问题。  

select相当于Java中Netty框架的多路复用器的功能。

举例代码示例:

package main


import "fmt"


func main() {
   //创建一个缓存为1的chan
   myChan := make(chan int,1)
   for i:=1;i<=100;i++{
      //select 的用法是,从上到下依次判断case 是否可执行,如果可执行,则执行完毕跳出select,如果不能执行,尝试下一个执行
      //这里的可执行是指的不阻塞,也就是说,select从上到下开始挑选一个不阻塞的case执行,执行完毕后跳出,
      //如果所有case都阻塞,则执行default
      //如下输出结果,i=奇数的时候走case   myChan<-i:,把奇数放入mychan
      //走偶数的时候因为myChan中有数据了,则把上一个奇数打印出来。
      //所以结果是 1  3  5  7  ...
      select {
            case  data := <-myChan:
               fmt.Println(data)
            case   myChan<-i:
            default:
               fmt.Println("default !!!")
      }
   }


}

6.Go中的互斥锁(类似于Java中的ReentrantLock)

先按线程不安全的数据错误的代码示例:

package main


import (
	"fmt"
	"sync"
)


//全局变量
var num int


var wait sync.WaitGroup


func main() {
	wait.Add(5)
	go myAdd()
	go myAdd()
	go myAdd()
	go myAdd()
	go myAdd()
	wait.Wait()
	//预期值等于5万,可是因为线程不安全错误,小于5万
	fmt.Printf("num = %d\n",num)
}




func  myAdd() {
	defer wait.Done()
	for i:=0 ;i<10000;i++ {
		num+=1
	}
}

打印输出结果:

num = 38626

 

互斥锁示例代码如下:

 

package main


import (
   "fmt"
   "sync"
)


//全局变量
var num int


var wait sync.WaitGroup


var lock sync.Mutex
func main() {
   wait.Add(5)
   go myAdd()
   go myAdd()
   go myAdd()
   go myAdd()
   go myAdd()
   wait.Wait()
   //预期值等于5万,可是因为线程不安全错误,小于5万
   fmt.Printf("num = %d\n",num)
}




func  myAdd() {
   defer wait.Done()
   for i:=0 ;i<10000;i++ {
      lock.Lock()
      num+=1
      lock.Unlock()
   }
}

 

7.Go中的读写锁(类似于Java中的ReentrantReadWriteLock)

读写锁用于读多写少的情况,多个线程并发读不上锁,写的时候才上锁互斥  

读写锁示例代码如下:  

package main


import (
   "fmt"
   "sync"
   "time"
)




//金额
var moneyA  = 1000
//读写锁
var rwLock sync.RWMutex;
var wait sync.WaitGroup
//添加金额
func subtractMoney(subMoney int) {
   rwLock.Lock()
   time.Sleep(3*time.Second)
   moneyA-=subMoney
   rwLock.Unlock()
}


//查询金额
func getMoney() int {
   rwLock.RLock()
   result := moneyA
   rwLock.RUnlock()
   return result;
}




func main() {
   wait.Add(2)
   //添加查询金额
   go func() {
      defer wait.Done()
      if(getMoney()>200) {
         subtractMoney(200)
         fmt.Printf("200元扣款成功,剩下:%d元\n",getMoney())
      }else {
         fmt.Println("余额不足,无法扣款")
      }
   }()


   //添加查询金额
   go func() {
      defer wait.Done()
      if(getMoney()>900) {
         subtractMoney(900)
         fmt.Printf("900元扣款成功,剩下:%d元\n",getMoney())
      }else {
         fmt.Println("余额不足,无法扣款")
      }
   }()
   wait.Wait()
   fmt.Println(getMoney())
}

8.Go中的并发安全Map(类似于CurrentHashMap)

Go中自己通过make创建的map不是线程安全的,具体体现在多线程添加值和修改值下会报如下错误:  

fatal error : concurrent map writes

  这个错类似于java中多线程读写线程不安全的容器时报的错。

Go为了解决这个问题,专门给我们提供了一个并发安全的map,这个并发安全的map不用通过make创建,拿来即可用,并且他提供了一些不同于普通map的操作方法。

参考如下代码示例:

package main


import (
   "fmt"
   "sync"
)


//创建一个sync包下的线程安全map对象
var myConcurrentMap = sync.Map{}
//遍历数据用的
var myRangeMap = sync.Map{}


func main() {
   //存储数据
   myConcurrentMap.Store(1,"li_ming")
   //取出数据
   name,ok := myConcurrentMap.Load(1)
   if(!ok) {
      fmt.Println("不存在")
      return
   }
   //打印值  li_ming
   fmt.Println(name)
   //该key有值,则ok为true,返回它原来存在的值,不做任何操作;该key无值,则执行添加操作,ok为false,返回新添加的值
   name2, ok2 := myConcurrentMap.LoadOrStore(1,"xiao_hong")
   //因为key=1存在,所以打印是   li_ming true
   fmt.Println(name2,ok2)
   name3, ok3 := myConcurrentMap.LoadOrStore(2,"xiao_hong")
   //因为key=2不存在,所以打印是   xiao_hong false
   fmt.Println(name3,ok3)
   //标记删除值
   myConcurrentMap.Delete(1)
   //取出数据
   //name4,ok4 := myConcurrentMap.Load(1)
   //if(!ok4) {
   // fmt.Println("name4=不存在")
   // return
   //}
   //fmt.Println(name4)


   //遍历数据
   rangeFunc()
}
//遍历
func rangeFunc(){
   myRangeMap.Store(1,"xiao_ming")
   myRangeMap.Store(2,"xiao_li")
   myRangeMap.Store(3,"xiao_ke")
   myRangeMap.Store(4,"xiao_lei")


   myRangeMap.Range(func(k, v interface{}) bool {
      fmt.Println("data_key_value = :",k,v)
      //return true代表继续遍历下一个,return false代表结束遍历操作
      return true
   })


}

9.Go中的AtomicXXX原子操作类(类似于Java中的AtocmicInteger之类的)

 

Go中的atomic包里面的功能和Java中的Atomic一样,原子操作类,原理也是cas,甚至提供了cas的api函数,这里不做过多讲解,  

简单举一个代码示例,因为方法太多,详细的请参考api文档中的atomic包:

package main


import "sync/atomic"


func main() {
   //简单举例
   var num int64 = 20
   atomic.AddInt64(&num,1)
}

10.Go中的WaitGroup(类似于Java中的CountDownLatch)

现在让我们看一个需求,比如我们开启三个并发任务,然后三个并发任务执行处理完毕后我们才让主线程继续往下面走。  

这时候肯定不能用睡眠了,因为不知道睡眠多长时间。

  这是Go中的sync包提供了一个WaitGroup的工具,他基本上和Java中的CountDownLatch的功能一致。

接下来让我们看代码示例:

package main


import (
   "fmt"
   "sync"
   "time"
)


//获取类似于CountDownLatch的对象
var wait sync.WaitGroup


func main() {
   //设置计数器任务为3,当3个任务全部done后,wait.Wait()才会松开阻塞
   wait.Add(3)
   go myFun1()
   go myFun2()
   go myFun3()
   //阻塞
   wait.Wait()
}




func  myFun1() {
   //计数器减1
   defer wait.Done()
   //睡眠五秒
   time.Sleep(time.Second*5)
   fmt.Println("fun1执行完毕")
}


func myFun2() {
   //计数器减1
   defer wait.Done()
   //睡眠五秒
   time.Sleep(time.Second*5)
   fmt.Println("fun2执行完毕")


}
func myFun3() {
   //计数器减1
   defer wait.Done()
   //睡眠五秒
   time.Sleep(time.Second*5)
   fmt.Println("fun3执行完毕")
}