goroutine

   在其他的编程语言中,线程调度是交由os来进行处理的。

   但是在Go语言中,会对此做一层封装,Go语言中的并发由goroutine来实现,它类似于用户态的线程,更类似于其他语言中的协程。它是交由Go语言中的runtime运行时来进行调度处理,这使得Go语言中的并发性能非常之高。

   一个Go进程,可以启动多个goroutine

   一个普通的机器运行几十个线程负载已经很高了,然而Go可以轻松创建百万goroutine

   Go标准库的net包,写出的go web server性能直接媲美Nginx

   比如在java/c++里,开发者通常要去自己维护一个线程池,并且需要包装多个线程任务,同时还要由开发者手动调度线程执行任务并且维护上下文切换,这非常的耗费心智,故在Go语言中出现了goroutine,它的概念类似于线程与协程,Go语言内置的就有调度与上下文切换机制,所以不用开发人员再去注意这些,并且goroutine的使用也非常的简单,它相较于其他语言的多并发编程更加轻松。

goroutine与线程

动态栈

   操作系统中的线程都有固定的栈内存(一般为2MB),这使得开启大量的线程会面临性能下降的问题。

   但是goroutine在生命周期之处的栈内存一般只有2KB,并且它会按需进行增大和缩小。最大的栈限制可达到1GB,所以在Go语言中一次创建上万级别的goroutine是没有任何问题的。

goroutine调度

   GPMGo语言运行时runtime层面的实现,这是Go语言自己实现的一套调度系统,区别于操作系统来调度os线程。

  • G很好理解,就是单个goroutine的信息,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
  • P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
  • M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;

  

   P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。

   P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。

   单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

   点我了解更多

   上面这么多专业术语看起来比较头痛,这边用一幅图来明确的进行表示。

goroutine使用

   在调用函数前加上go关键字,就可以为函数创建一个goroutine

   一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

   每个Go语言都有一个goroutine,类似于主线程的概念。

   goroutine的启动是随机进行调度的,这个无法手动控制。

基本使用

   下面是创建单个goroutine与主goroutine进行并发执行任务。

package main

import (
"fmt"
"sync"
) func main() {
var wg sync.WaitGroup wg.Add(1)
go func(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println(i)
}
fmt.Println("子goroutine执行完毕") }() // 立即执行函数,一个goroutine任务
wg.Wait()
fmt.Println("主goroutine执行完毕") }

   下面是创建多个goroutine与主goroutine进行并发执行任务。

package main

import (
"fmt"
"sync"
) var wg sync.WaitGroup func f1(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
}
fmt.Println("子goroutine1执行完毕")
} func f2(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务2",i)
}
fmt.Println("子goroutine2执行完毕")
} func main() {
wg.Add(2)
go f1()
go f2()
wg.Wait()
fmt.Println("主goroutine执行完毕") }

sync.WaitGroup

   该属性类似于一把全局锁,只有当子goroutine任务结束后,主goroutine任务才能结束。

   类似于守护线程。

package main

import (
"fmt"
"sync"
) var wg sync.WaitGroup // 当前有任务 0 个 func f1(){
defer wg.Done() // 执行完成后,任务减 1
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
}
fmt.Println("子goroutine执行完毕")
} func main() {
wg.Add(1) // 任务加 1 注意,一定要放外面,不能放函数中
go f1()
wg.Wait() // 任务必须为0时才继续向下执行
fmt.Println("主goroutine执行完毕")
}

GOMAXPROCS

   该函数可设定开启多少os线程来运行子goroutine任务。

   默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

   Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

   Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

   如下示例,两个子goroutine任务在一个线程上运行,会通过时间片轮询等策略来抢占执行权。

package main

import (
"fmt"
"sync"
"runtime"
) var wg sync.WaitGroup func f1(){
wg.Add(1)
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
}
fmt.Println("子goroutine1执行完毕")
} func f2(){
wg.Add(1)
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务2",i)
}
fmt.Println("子goroutine2执行完毕")
} func main() {
runtime.GOMAXPROCS(1) // 设置最多开启1个子线程
f1()
f2()
wg.Wait()
fmt.Println("主goroutine执行完毕")
}

时间轮询

   由于底层的os线程切换机制是依照时间轮询进行切换,所以goroutine的切换时机也是由时间片轮询来决定的。

   使用runtime.Gosched()可让当前任务让出线程占用,交由其他任务进行执行。

package main

import (
"fmt"
"sync"
"runtime"
) var wg sync.WaitGroup func f1(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
if i == 300 {
runtime.Gosched() // 让出线程占用
}
}
fmt.Println("子goroutine1执行完毕")
} func f2(){
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务2",i)
}
fmt.Println("子goroutine2执行完毕")
} func main() {
runtime.GOMAXPROCS(1)
wg.Add(2)
go f1()
go f2()
wg.Wait()
fmt.Println("主goroutine执行完毕")
}

终止任务

   runtime.Goexit()终止当前任务。

package main

import (
"fmt"
"sync"
"runtime"
) var wg sync.WaitGroup func f1(){
wg.Add(1)
defer wg.Done()
for i := 0 ; i < 1000 ; i++ {
fmt.Println("任务1",i)
if i == 300 {
runtime.Goexit() // 终止任务
fmt.Println("子goroutine任务被终止")
}
}
fmt.Println("子goroutine执行完毕")
} func main() {
go f1()
wg.Wait()
fmt.Println("主goroutine执行完毕")
}

通道使用

   多个goroutine中必须要有某种安全的机制来进行数据共享,这就出现了channel通道。

   它类似于管道或者队列,作用在于保证多goroutine访问同一资源时达到数据安全的目的。

类型声明

   channel是引用类型,这就代表必须要使用make()进行内存分配。

   初始值为nil

   下面是进行声明的示例:

var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道

channel使用

   使用前要进行内存分配,并且它还可选缓冲区。

   代表该通道最多可容纳多少数据。当然,缓冲区大小是可选的,它具有动态扩容的特性。

make(chan 元素类型, [缓冲大小])

   示例如下:

ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

channel操作

   以下是channel的操作:

方法 说明
ch <- 数据 将数据放入通道中
数据 <- ch 将数据从通道取出
close() 关闭通道

   现在我们先使用以下语句定义一个通道:

ch := make(chan int)

   将一个值发送到通道中。

ch <- 10 // 把10发送到ch中

   从一个通道中接收值。

x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果

   我们通过调用内置的close()函数来关闭通道。

close(ch)

   关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

   关闭后的通道有以下特点:

  1. 对一个关闭的通道再发送值就会导致panic。
  2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  4. 关闭一个已经关闭的通道会导致panic。

阻塞通道

   当一个通道无缓冲区时,将被称为阻塞通道。

   通道中存放一个值,但该值并没有被取出时将会引发异常。

   必须先收,后发。因为发送后会产生阻塞,如果没有接收者则会导致死锁异常

   必须将通道中的值取尽,否则会发生死锁异常,也就是说放了几次就要取几次

package main

import (
"fmt"
"sync"
) var wg sync.WaitGroup func f1(ch chan string){
defer wg.Done()
rose := <- ch // 等待取玫瑰花
lily := <- ch // 等待取百合花
fmt.Println(rose)
fmt.Println(lily)
} func main(){
wg.Add(1)
ch := make(chan string)
go f1(ch) // 必须先有接收者
ch <- "玫瑰花" // 开始放入玫瑰花
ch <- "百合花" // 开始放入百合花
wg.Wait()
fmt.Println("主goroutine运行完毕")
}

非阻塞通道

   非阻塞通道即为有缓冲区的通道。

   只要通道的容量大于零,则代表该缓冲区中能够去存放值。

   非阻塞通道相较于阻塞通道,它的使用其实更加符合人类逻辑

   阻塞通道必须要先接收再存入

   非阻塞通道可以先存入再接收

   并且,非阻塞通道中的值可以不必取尽

package main

import (
"fmt"
"sync"
) var wg sync.WaitGroup func f1(ch chan string){
defer wg.Done()
rose := <- ch // 等待取玫瑰花
fmt.Println(len(ch)) // 获取元素数量 1 代表还剩下一个没取
fmt.Println(cap(ch)) // 获取容量 10 代表最多可以放10个
fmt.Println(rose)
} func main(){
wg.Add(1)
ch := make(chan string,10)
ch <- "玫瑰花" // 放入玫瑰花
ch <- "百合花" // 放入百合花
go f1(ch)
wg.Wait()
fmt.Println("主goroutine运行完毕")
}

单向通道

   单向通道即是只能取,或者只能发。

   上面的通道都是双向通道,可能造成阅读不明确的问题,故此Go还提供了单向通道。

   在函数传参中,可以将双向通道转换为单项通道,这也是最常用的方式。

通道标识 说明
ch <- string 代表只能写入string类型的值
<- ch string 代表只能取出string类型的值
package main

import (
"sync"
"fmt"
) var wg sync.WaitGroup func recv(ch <-chan string) { // 只能取
defer wg.Done()
rose := <- ch
fmt.Println(rose)
} func send(ch chan<- string) { // 只能放
defer wg.Done()
ch <- "玫瑰花"
} func main() {
wg.Add(2)
ch := make(chan string, 10)
go send(ch)
go recv(ch)
wg.Wait()
fmt.Println("主goroutine运行完毕")
}

常见情况

   以下是通道的使用常见情况。

   关闭已经关闭的channel也会引发panic。

  

任务池

   多个goroutine的切换会带来性能损耗问题。

   所以我们可以通过做一个goroutine的池来解决这种问题,当一个goroutine的任务结束后,它不会kill掉该goroutine,而是让它继续的取下一个任务。

   所以我们需要与chan结合进行构造一个简单的任务池。

   如下示例,构建了一个简单的任务池并且开启了3个goroutine,并且放了6个任务在task这个chen中交由run进行处理。

   处理结果放在result这个chen中。

package main

import (
"fmt"
"sync"
"time"
) var wg sync.WaitGroup func run(id int, task <-chan string, result chan<- string) {
defer wg.Done()
for {
t, ok := <-task
if !ok {
fmt.Println("处理完了所有任务")
break
}
time.Sleep(time.Second * 2)
t += fmt.Sprintf(":已由%d处理", id)
result <- t
}
} func main() { task := make(chan string, 10)
result := make(chan string, 10) wg.Add(3)
for i := 0; i < 3; i++ {
go run(i, task, result) // 开三个goroutine来处理
} urlRequeste := []string{
"www.baidu.com",
"www.google.com",
"www.cnblog.com",
"www.xinlang.com",
"www.csdn.com",
"www.taobao.com",
} for _, url := range urlRequeste {
task <- url // 开启了六个任务
} close(task) for i := 0; i < len(urlRequeste); i++ {
fmt.Println(<-result)
} close(result) wg.Wait()
fmt.Println("主goroutine运行完毕") } // www.google.com:已由2处理
// www.cnblog.com:已由1处理
// www.baidu.com:已由0处理
// 处理完了所有任务
// 处理完了所有任务
// 处理完了所有任务
// www.taobao.com:已由0处理
// www.xinlang.com:已由2处理
// www.csdn.com:已由1处理
// 主goroutine运行完毕

select多路复用

   类似于事件循环,我们来监听多个通道。

   当一个通道可用时就来操纵该通道。

select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
默认操作
}

   这个示例还是要在具体的应用场景中比较常见,并且一般的库都已经写好了。

   只要知道其中理论就行,没必要白手写select,除非你要做开源框架或公司框架等。

   可处理一个或多个channel的发送/接收操作。

   如果多个case同时满足,select会随机选择一个。

   对于没有case的select{}会一直等待,可用于阻塞main函数。

   小例子:

package main

import (
"fmt"
) func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch: // 允许赋值
fmt.Println("可以读了,已经读出了:", x) // 可读
case ch <- i: // 可写
fmt.Println("可以写了,已经写入了:", i)
}
}
}

锁相关

   锁是为了解决资源同步的问题。

   但是对于多个goroutine通信应该是去使用channel,而不是用锁进行解决。

互斥锁

   如下代码,会产生资源竞争问题。致使结果不正确:

package main

import (
"fmt"
"sync"
) var wg sync.WaitGroup func main(){
num := 10000
wg.Add(2)
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
num ++
}
}()
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
num --
}
}()
wg.Wait()
fmt.Println(num)
} // 13966
// 7578
// 9475

   此时添加互斥锁即可,让其变为串行执行:

package main

import (
"fmt"
"sync"
) var wg sync.WaitGroup
var lock sync.Mutex func main(){
num := 10000
wg.Add(2)
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
lock.Lock() // 加锁
num ++
lock.Unlock() // 解锁
}
}()
go func(){
defer wg.Done()
for i:=0; i<10000; i++{
lock.Lock() // 加锁
num --
lock.Unlock() // 解锁
}
}()
wg.Wait()
fmt.Println(num)
}

读写互斥锁

   互斥锁是完全互斥,将并发执行转变为串行执行,性能损耗比较大。

   但是在更多的场景中,我们则不需要完全互斥。

   比如多个人访问统一资源但是并未对资源本身做修改时可以不加锁,但是当有人对资源做修改时其他人将无法访问。

   以上场景使用读写锁更加合适,读写锁在读多写少的场景下非常高效。

   读锁:我获取了读锁你不能去修改,必须等我释放

   写锁:我获取了写锁你不能去读,必须等我释放

   如下,写入200次,读取2000次的用时为1s左右。

package main

import (
"fmt"
"time"
"sync"
) var wg sync.WaitGroup
var rwlock sync.RWMutex // 读写锁
var variety = 10 func read() {
defer wg.Done()
rwlock.RLock() // 加读锁
fmt.Println(variety)
rwlock.RUnlock() // 释放读锁 } func write() {
defer wg.Done()
rwlock.Lock() // 加写锁
variety ++
fmt.Println(variety)
rwlock.Unlock() // 释放写锁
} func main() {
start := time.Now()
for i := 0; i < 200; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 2000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println("运行时间:",end.Sub(start)) // 1s左右
}

   如果单纯使用互斥锁,时间会更长:

package main

import (
"fmt"
"time"
"sync"
) var wg sync.WaitGroup
var lock sync.Mutex // 互斥锁
var variety = 10 func read() {
defer wg.Done()
lock.Lock() // 加互斥锁
fmt.Println(variety)
lock.Unlock() // 释放互斥锁 } func write() {
defer wg.Done()
lock.Lock() // 加互斥锁
variety ++
fmt.Println(variety)
lock.Unlock() // 释放互斥锁
} func main() {
start := time.Now()
for i := 0; i < 200; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 2000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println("运行时间:",end.Sub(start)) // 2s左右
}

sync.Once

   只执行一次,如果一个配置文件体积过于巨大,在初始化时进行加载会拖慢启动速度。

   所以我们可以在要使用时进行加载(懒惰加载),如下示例,有10个goroutine都需要用到配置文件。

   该配置文件只会加载一次,之后便不会重复加载。

package main

import (
"fmt"
"sync"
) var wg sync.WaitGroup
var once sync.Once func load() {
fmt.Println("加载配置文件...")
} func main() {
fmt.Println("运行代码逻辑...发现很多地方都要用配置文件了")
for i := 0; i < 10; i++ {
fmt.Printf("%v需要用到配置文件,开始加载\n", i)
wg.Add(1)
go func() {
defer wg.Done()
once.Do(load) // 只加载一次,并且该函数的格式必须是不能有参数与返回值
}()
}
wg.Wait()
}

sync.Map

   Go语言中内置的map不是并发安全的。不要使用内置的map进行数据传递,你应该使用channel或者sync给你提供的map。该map不用进行make初始化内存。

   sync提供的map有以下功能:

方法 描述
Store(k,v) 设置一组键值对
Load(k) 根据k取出v
LoadorStore(k,v) 根据k取出v,如果没有该k则创建v
Delete(k) 删除一组键值对
Range 循环遍历出k和v
package main

import (
"fmt"
"sync"
) var wg sync.WaitGroup
var once sync.Once
var m = sync.Map{} // g安全的map func out() {
defer wg.Done()
gift, _ := m.Load("礼物")
fmt.Println(gift)
} func put() {
m.Store("礼物", "玫瑰花")
defer wg.Done()
} func main() {
wg.Add(2)
go put()
go out()
wg.Wait()
}

原子操作

功能概述

   对于多个goroutine访问同一资源造成的并发安全问题,可以通过加锁来进行解决。

   但是加锁会使性能降低,所以这里Go语言中sync/atomic包提供了原子操作来代替加锁。

常用方法

   主要对数字类型的数据的加减乘除等。

方法 描述
func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr
*unsafe.Pointer) (val unsafe.Pointer)
读取操作
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr
*unsafe.Pointer, val unsafe.Pointer)
写入操作
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr
*uintptr, delta uintptr) (new uintptr)
修改操作
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr
*uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
交换操作
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr
*uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
比较并交换操作

示例演示

   使用原子操作,速度较快。

package main

import (
"fmt"
"sync"
"sync/atomic"
"time"
) var wg sync.WaitGroup func main() {
var num int64 = 10000
start := time.Now().UnixNano()
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
atomic.AddInt64(&num, 1)
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
atomic.AddInt64(&num, -1)
}
}()
wg.Wait()
end := time.Now().UnixNano()
fmt.Println("运行时间:", end - start) // 981600
fmt.Println(num)
}

   加锁操作,速度会慢一些:

package main

import (
"fmt"
"sync"
"time"
) var wg sync.WaitGroup
var lock sync.Mutex func main() {
var num int64 = 10000
start := time.Now().UnixNano()
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
lock.Lock()
num++
lock.Unlock()
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
lock.Lock()
num--
lock.Unlock()
}
}()
wg.Wait()
end := time.Now().UnixNano()
fmt.Println("运行时间:", end - start) // 1000300
fmt.Println(num)
}

最新文章

  1. 【转】算法杂货铺——k均值聚类(K-means)
  2. linux---------------centos6.4安装完了以后敲ifconfig,没有局域网ip。解决如下
  3. 动态设置和访问cxgrid列的Properties(转)
  4. 关于oracle 10g creating datafile with zero offset for aix
  5. .NET Nancy 详解(一) 初识
  6. Mysql通信协议
  7. Spring MVC实例(增删改查)
  8. 终于完成了Josephus的C语言实现啦~~
  9. windows mobile仿真器内存调整
  10. 机器学习简易入门(四)- logistic回归
  11. linux中cat、more、less、tail、head命令
  12. FIR滤波器设计
  13. /-- Encapsulated exception ------------\ java.lang.NoSuchMethodException: com.sjzdaj.po.Class.setN_id([Ljava.lang.String;)
  14. Android - 分享内容 - 给其他APP发送内容
  15. 使用jQuery操作DOM
  16. 201521123024 《Java程序设计》第4周学习总结
  17. DDD实战进阶第一波(十一):开发一般业务的大健康行业直销系统(实现经销商代注册用例与登录令牌分发)
  18. Git使用:Linux(Ubuntu 14.04 x64)下安装Git并配置连接GitHub
  19. saltstack returners
  20. [01-01]oracle数据库汉化

热门文章

  1. Agumaster页面样式就绪
  2. 使用Flashback救回被误drop掉的表
  3. 有关Sql中时间范围的问题
  4. JSTL日期格式化用法
  5. 双向绑定数据的实现(new Proxy 版本)
  6. [LeetCode]11. 盛最多水的容器(双指针)
  7. 软件工程与UML作业1
  8. 注解在Spring中的运用(对象获取、对象单例/多例、值的注入、初始化/销毁方法、获取容器)
  9. (一)jenkins+git+docker
  10. 万字长文 | 23 个问题 TCP 疑难杂症全解析