There are many ways to implement a multi-threaded production/consumption model in Kotlin (most of them also apply to Java)
- Synchronized
- Lock
- BlockingQueue
- Semaphore
- RxJava
- Coroutine
Synchronized
val buffer = LinkedList<Data>()
val MAX = 5 //buffer最大size
val lock = Object()
fun produce(data: Data) {
sleep(2000) // mock produce
synchronized(lock) {
while (buffer.size >= MAX) {
// 当buffer满时,停止生产
// 注意此处使用while不能使用if,因为有可能是被另一个生产线程而非消费线程唤醒,所以要再次检查buffer状态
// 如果生产消费两把锁,则不必担心此问题
lock.wait()
}
buffer.push(data)
// notify方法只唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。
// notifyAll会唤醒所有等待中线程,哪一个线程将会第一个处理取决于操作系统的实现,但是都有机会处理。
// 此处使用notify有可能唤醒的是另一个生产线程从而造成死锁,所以必须使用notifyAll
lock.notifyAll()
}
}
fun consume() {
synchronized(lock) {
while (buffer.isEmpty())
lock.wait() // 暂停消费
buffer.removeFirst()
lock.notifyAll()
}
sleep(2000) // mock consume
}
// 同时启动多个生产、消费线程
repeat(10) {
Thread {
produce(Data()) }.start()
}
repeat(10) {
Thread {
consume() }.start()
}
Lock
The advantage of Lock over Synchronized is that when there are multiple production lines/consumption threads, we can precisely specify which one to wake up by defining multiple conditions. The following example is not so complicated, just replace the above Synchronized wording
val buffer = LinkedList<Data>()
val MAX = 5 //buffer最大size
val lock = ReentrantLock()
val condition = lock.newCondition()
fun produce(data: Data) {
sleep(2000) // mock produce
lock.lock()
while (buffer.size >= 5)
condition.await()
buffer.push(data)
condition.signalAll()
lock.unlock()
}
fun consume() {
lock.lock()
while (buffer.isEmpty())
condition.await()
buffer.removeFirst()
condition.singleAll()
lock.unlock()
sleep(2000) // mock consume
}
BlockingQueue
When BlockingQueue reaches a critical condition, reading and writing will automatically block the current thread waiting for the release of the lock, which is naturally suitable for this production/consumption scenario
val buffer = LinkedBlockingQueue<Data>(5)
fun produce(data: Data) {
sleep(2000) // mock produce
buffer.put(data) //buffer满时自动阻塞
}
fun consume() {
buffer.take() // buffer空时自动阻塞
sleep(2000) // mock consume
}
Semaphore
Semaphore is a shared lock mechanism provided by JUC, which can control congestion. This feature can be used to control the size of the buffer.
// canProduce: 可以生产数量(即buffer可用的数量),生产者调用acquire,减少permit数目
val canProduce = Semaphore(5)
// canConsumer:可以消费数量,生产者调用release,增加permit数目
val canConsume = Semaphore(5)
// 控制buffer访问互斥
val mutex = Semaphore(0)
val buffer = LinkedList<Data>()
fun produce(data: Data) {
if (canProduce.tryAcquire()) {
sleep(2000) // mock produce
mutex.acquire()
buffer.push(data)
mutex.release()
canConsume.release() //通知消费端新增加了一个产品
}
}
fun consume() {
if (canConsume.tryAcquire()) {
sleep(2000) // mock consume
mutex.acquire()
buffer.removeFirst()
mutex.release()
canProduce.release() //通知生产端可以再追加生产
}
}
RxJava
RxJava is not suitable for multi-producer and multi-consumer scenarios, but it can be used in single-producer/consumer scenarios.
Flowable's compression mechanism can be used to control the number of buffers
class Producer : Flowable<Data>() {
override fun subscribeActual(subscriber: org.reactivestreams.Subscriber<in Data>) {
subscriber.onSubscribe(object : Subscription {
override fun cancel() {
//...
}
private val outStandingRequests = AtomicLong(0)
override fun request(n: Long) {
//收到下游通知,开始生产
outStandingRequests.addAndGet(n)
while (outStandingRequests.get() > 0) {
sleep(2000)
subscriber.onNext(Data())
outStandingRequests.decrementAndGet()
}
}
})
}
}
class Consumer : DefaultSubscriber<Data>() {
override fun onStart() {
request(1)
}
override fun onNext(i: Data?) {
sleep(2000) //mock consume
request(1) //通知上游可以增加生产
}
override fun onError(throwable: Throwable) {
//...
}
override fun onComplete() {
//...
}
}
@Test
fun test_rxjava() {
try {
val testProducer = Producer)
val testConsumer = Consumer()
testProducer
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.single())
.blockingSubscribe(testConsumer)
} catch (t: Throwable) {
t.printStackTrace()
}
}
Continue/produce
The Channel in the coroutine has a congestion control mechanism, which can realize communication between producers and consumers.
The official encapsulation of the produce method based on Channel, specifically to deal with such scenarios
val produce = produce(Dispatchers.IO, 5) {
repeat(10) {
sleep(2000) //mock produce
val data = Data()
send(data) // 超过5个以上没有消费完时,协程挂起
}
}
produce.consumeEach {
sleep(3000) //mock consume
}
Summary
The producer-consumer problem of the thread is nothing more than dealing with the communication between the two ends. Locks, semaphores, RxJava backpressure, and coroutine channels are all alternative communication methods.