说明:Java & Go 并发编程序列的文章,根据每篇文章的主题或细分标题,分别演示 Java 和 Go 语言当中的相关实现。更多该系列文章请查看:Java & Go 并发编程系列
上一篇从以下维度介绍了 Java 中的阻塞队列和 Go 语言通道。
零容量 | 有限容量 | |
---|---|---|
Go | unbuffered channel | buffered channel |
Java | SynchronousQueue | LinkedBlockingQueue |
本篇将进一步介绍有限容量的 buffered channel 和 LinkedBlockingQueue 的基本操作。然后再单独说明零容量的工具与有限容量在操作上的主要差异。
从尾部添加 VS 发送
- 「Java」往 LinkedBlockingDeque 尾部添加一个元素,如果队列已满,则阻塞
// 创建一个容量为1的阻塞队列
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>(1);
queue.put(1);// 添加成功
queue.put(2);// 阻塞在这里,直到队列有可用的空间
- 「Go」往通道发送一个元素值,如果通道已满,则阻塞
// 创建容量为1,元素类型为int的通道
ch := make(chan int, 1)
ch <- 1 // 发送成功
ch <- 2 // 阻塞在这里,直到通道有可用的空间
如果不希望一直阻塞,我们可以给以上操作设置一个超时时间
- 「Java」使用 offer 操作(注意区别前面的 put),并设置超时时间
// 创建一个容量为1的阻塞队列
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>(1);
System.out.printf("是否添加成功:%b\n", queue.offer(1));// 是否添加成功:true
System.out.printf("是否添加成功:%b\n", queue.offer(2, 1, TimeUnit.SECONDS));// 1s后返回false
- 「Go」结合 select 语句实现超时不执行操作
// 创建容量为1,元素类型为int的通道
ch := make(chan int, 1)
ch <- 1 // 发送成功
// select 语句是专门针对通道而设计的语句
// 通常包含 case 表达式和一个可选的 defalt 分支
// case 表达式中只能包含操作通道的表达式,如发送或接收表达式
// 如果没有 default 分支,则会阻塞到其中的一个 case 语句被选中
// 这里是1s后第二个分支被选中(从接收通道接收到一个值)
select {
case ch <- 2: // 该分支不会被选中执行
fmt.Printf("写入成功\n")
case <-time.After(time.Second * 1):
fmt.Printf("1s后超时退出\n")
}
从头部移除并返回 VS 接收
- 「Java」从 LinkedBlockingDeque 头部移除并返回一个元素,如果队列为空,则阻塞
// 创建一个容量为1的阻塞队列
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>(1);
// 新起一个线程,500ms 之后往队列尾部添加元素
Thread thread = new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(500);
queue.put(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
// 阻塞直到获取到元素,如果获取不到则永远阻塞在这里
System.out.printf("阻塞片刻之后返回结果: %d\n", queue.take()); // 阻塞片刻之后返回结果:1
- 「Go」从通道接收元素值,如果通道为空,则阻塞
// 创建容量为1,元素类型为int的通道
ch:= make(chan int, 1)
// 从另一个 Goroutine 500ms 后往通道发送一个元素值
go func() {
time.Sleep(time.Millisecond * 500)
ch <- 1
}()
// 阻塞直到接收到元素值
fmt.Printf("阻塞片刻之后返回结果: %d\n", <-ch) // 阻塞片刻之后返回结果: 1
同样我们可以给以上操作设置超时时间
- 「Java」使用 poll 操作(注意区别前面的 take),并设置超时时间
// 创建一个容量为1的阻塞队列
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>(1);
// 新起一个线程,500ms 之后往队列尾部添加元素
Thread thread = new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(500);
queue.put(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
// 如果超时获取不到则返回 null
System.out.printf("设置1s超时返回结果: %d\n", queue.poll(1, TimeUnit.SECONDS));// 设置1s超时返回结果: 1
- 「Go」结合 select 语句实现超时不执行操作
// 创建容量为1,元素类型为int的通道
ch := make(chan int, 1)
go func() {
// 从另一个 Goroutine 500ms 后往通道发送一个元素值
time.Sleep(time.Millisecond * 500)
ch <- 1
}()
select {
// 由于该 case 先从通道接收到值所以被选中
case number := <-ch:
fmt.Printf("结果: %d\n", number) // 结果: 1
case <-time.After(time.Second * 1):
fmt.Printf("1s后超时退出\n")
}
零容量的工具与有限容量在操作上的主要差异
Java 中的 SynchonousQueue 的以下操作与 LinkedBlockingQueue 保持一致:
put()
take()
offer(E e, long timeout, TimeUnit unit)
poll(long timeout, TimeUnit unit)
主要差异在于 SynchonousQueue 内部没有容量,所以获取剩余容量的时候总是返回0。
/**
* Always returns zero.
* A {@code SynchronousQueue} has no internal capacity.
*
* @return zero
*/
public int remainingCapacity() {
return 0;
}
另外元素的传递也是直接由发送方直接传递给接收方,不像 LinkedBlockingQueue 一定会经过内部的队列。
同样的,对于 Go 语言的非缓冲通道,其容量也总是为0
unbufChan := make(chan int) // 创建一个非缓冲通道
fmt.Printf("容量为%d\n", cap(unbufChan)) // 容量为0
fmt.Printf("长度为%d\n", len(unbufChan)) // 长度为0
bufChan := make(chan int, 8) // 创建一个缓冲通道
fmt.Printf("容量为%d\n", cap(bufChan)) // 容量为8
fmt.Printf("长度为%d\n", len(bufChan)) // 长度为0
bufChan <- 1
fmt.Printf("容量为%d\n", cap(bufChan)) // 容量为8
fmt.Printf("长度为%d\n", len(bufChan)) // 长度为1
关于队列和通道的容量和获取方式总结如下:
其中队列(或通道)的长度代表它当前包含的元素值的个数。当队列(或通道)已满时,其长度与容量相同。
容量 | 长度 | 剩余容量 | |
---|---|---|---|
SynchonousQueue | 0 | 0 | 0 |
LinkedBlockingQueue | 构造函数指定的capacity | size() | remainingCapacity() |
unbuffered channel | 0 | 0 | 0 |
buffered channel | cap(ch) | len(ch) | cap(ch) - len(ch) |
ch 表示通道。
更多该系列文章请查看:Java & Go 并发编程系列