你好,这里是codetrend专栏“高并发编程基础”。
引言
与网络通信等进程间通信方式不同,线程间通信是指在同一个进程内的多个线程之间进行的通信。
在多线程编程中,当多个线程需要互斥地访问共享资源时,它们会相互之间发送信号或等待信号的通知。
这些通信方式包括线程等待数据到达的通知、线程收到变量改变的信号等。
本文将探讨Java提供的原生通信API,以及这些通信机制背后的原理和实现细节。
同步阻塞与异步非阻塞
同步阻塞消息处理机制:
优点:
- 简单易用:同步阻塞模型更容易理解和实现。
- 顺序性:消息的处理顺序是确定的,可以确保消息按照预期的顺序处理。
缺点:
- 阻塞等待:在消息处理过程中,线程会被阻塞,无法同时进行其他任务,可能导致资源浪费和系统响应速度变慢。
- 低效性:当某个消息处理时间较长时,其他消息需要等待,可能导致整体处理效率下降。
- 不适合大规模并发:同步模型在高并发场景下,需要创建大量线程来处理请求,而线程的创建和切换开销较大。
异步非阻塞消息处理机制:
优点:
- 高并发能力:异步非阻塞模型通过少量的线程处理大量的请求,提高了系统的并发能力。
- 高效性:由于不需要等待消息处理完成,线程可以继续执行其他任务,提高了系统的响应速度和资源利用率。
- 容错性:异步模型可以通过回调函数或者Future等机制捕获异常,提高了系统的容错性。
缺点:
- 复杂性:异步非阻塞模型相对复杂,需要处理回调函数、事件循环等机制,对开发人员的要求较高。
- 代码可读性:异步代码通常会使用回调函数,导致代码逻辑分散,可读性较差。
- 调试困难:由于异步模型涉及到多个线程之间的交互,调试和排查问题可能更加困难。
同步阻塞消息处理适合简单场景和顺序处理的需求,而异步非阻塞消息处理适合高并发、高效率和容错性要求较高的场景。
在Java中,使用wait和notify/notifyAll来实现同步阻塞和异步非阻塞模型通信是常见的做法。
- 同步阻塞:在同步阻塞模型中,线程会一直等待某个条件满足,直到其他线程通知它条件已经满足。这种模型可以通过使用wait和notify/notifyAll方法来实现。在使用wait方法时,线程会释放它所持有的锁,然后进入等待状态。在条件被满足并且其他线程调用notify/notifyAll方法时,线程会重新获得锁并继续执行。这种模型可以保证线程安全,但可能会导致死锁、饥饿等问题。
- 异步非阻塞:在异步非阻塞模型中,线程不会一直等待某个条件,而是立即返回并执行其他操作。当条件被满足时,线程会通过回调函数或者事件驱动机制等方式得到通知。这种模型可以通过使用Java的NIO库或者CompletableFuture类来实现。
单线程间通信
wait & notify
wait & notify 两个函数均是java.lang.Object对象的借口,也就是说所有对象都有这两个函数。
- java.lang.Object#wait()
使当前线程等待,直到被唤醒,通常是通过被通知或中断来实现。在所有方面,该方法的行为就像调用了wait(0L, 0)一样。
- java.lang.Object#notify
唤醒一个正在等待此对象监视器(monitor)的单个线程。如果有任何线程在等待此对象,则选择其中一个线程进行唤醒。选择是任意的,并由实现自行决定。线程通过调用wait方法之一来等待对象的监视器。被唤醒的线程在当前线程释放此对象上的锁之前,无法继续执行。被唤醒的线程将以通常的方式与任何其他正在积极竞争同步此对象的线程进行竞争;例如,被唤醒的线程在成为下一个锁定此对象的线程方面没有可靠的优势或劣势。一次只能有一个线程拥有对象的监视器。此方法应该仅由拥有此对象监视器的线程调用。
线程以以下三种方式成为对象监视器的所有者:
- 通过执行该对象的同步实例方法。
- 通过执行同步语句体来同步该对象。
- 对于Class类型的对象,通过执行该类的同步静态方法。
使用队列测试单线程间通信
通过使用wait & notify函数设计一个EventQueue先进先出(FIFO)队列来演示单线程间通信。
package engineer.concurrent.battle.fcontact;
import java.util.LinkedList;
public class EventQueueSingle {
private final int max;
static class Event {}
private final LinkedList<Event> eventQueue = new LinkedList<>();
private final static int DEFAULT_MAX = 20;
public EventQueueSingle(int max) {
this.max = max;
}
public EventQueueSingle() {
this(DEFAULT_MAX);
}
public void enqueue(Event event) {
synchronized (eventQueue) {
if (eventQueue.size() >= max) {
try {
System.out.println("EventQueue is full, waiting for dequeue...");
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
eventQueue.addLast(event);
System.out.println(Thread.currentThread().getName() + ": enqueues success. and size is "+eventQueue.size());
eventQueue.notify();
}
}
public Event dequeue() {
synchronized (eventQueue) {
if (eventQueue.isEmpty()) {
try {
System.out.println("EventQueue is empty, waiting for enqueue...");
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": dequeues success. and size is "+eventQueue.size());
Event event = eventQueue.removeFirst();
eventQueue.notify();
return event;
}
}
}
EventQueueSingle有三个状态:
- 队列空,可以生产队列,不可以消费队列
- 队列满,可以消费队列,不可以生产队列
- 队列未满,可以生成和消费队列
EventQueueSingle提供了两个接口进行生产消费,EventQueueSingle使用了synchronized和 wait & notify 来实现生产消费的顺序和状态校验。
- enqueue进行生产,添加事件到队列末尾。使用
synchronized锁定队列,队列满的状态则调用wait函数进行等待,直到队列消费notify后进行对象的添加,并且通知可能的消费wait。 - dequeue进行消费,获取队列第一个事件。使用
synchronized锁定队列,队列空的状态则调用wait函数进行等待,直到队列生产notify后进行对象的添加,并且通知可能的生产wait。
测试代码如下:
package engineer.concurrent.battle.fcontact;
import java.util.concurrent.TimeUnit;
public class EventQueueSingleTest {
public static void main(String[] args) {
final EventQueueSingle queue = new EventQueueSingle();
new Thread(()->{
for (;;) {
queue.enqueue(new EventQueueSingle.Event());
}
},"producer").start();
new Thread(()->{
for (;;) {
queue.dequeue();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"consumer1").start();
new Thread(()->{
for (;;) {
queue.dequeue();
queue.dequeue();
try {
TimeUnit.MILLISECONDS.sleep(1500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"consumer2").start();
}
}
可能的输出结果如下:
producer: enqueues success. and size is 20
EventQueue is full, waiting for dequeue...
consumer2: dequeues success. and size is 20
consumer2: dequeues success. and size is 19
producer: enqueues success. and size is 19
producer: enqueues success. and size is 20
EventQueue is full, waiting for dequeue...
多线程间通信
多线程间通信需要用到Object的notifyAll函数,可以同时唤醒全部阻塞的线程,同样被唤醒的线程仍然需要争抢monitor的所有权。
以下是优化后修改使用notifyAll的EventQueue。
package engineer.concurrent.battle.fcontact;
import java.util.LinkedList;
public class EventQueue {
private final int max;
static class Event {}
private final LinkedList<Event> eventQueue = new LinkedList<>();
private final static int DEFAULT_MAX = 20;
public EventQueue(int max) {
this.max = max;
}
public EventQueue() {
this(DEFAULT_MAX);
}
public void enqueue(Event event) {
synchronized (eventQueue) {
while (eventQueue.size() >= max) {
try {
System.out.println("EventQueue is full, waiting for dequeue...");
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
eventQueue.addLast(event);
System.out.println(Thread.currentThread().getName() + ": enqueues success. and size is "+eventQueue.size());
eventQueue.notifyAll();
}
}
public Event dequeue() {
synchronized (eventQueue) {
while (eventQueue.isEmpty()) {
try {
System.out.println("EventQueue is empty, waiting for enqueue...");
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": dequeues success. and size is "+eventQueue.size());
Event event = eventQueue.removeFirst();
eventQueue.notifyAll();
return event;
}
}
}
测试代码如下:
package engineer.concurrent.battle.fcontact;
import java.util.concurrent.TimeUnit;
public class EventQueueTest {
public static void main(String[] args) {
final EventQueue queue = new EventQueue();
new Thread(()->{
for (;;) {
queue.enqueue(new EventQueue.Event());
}
},"producer").start();
new Thread(()->{
for (;;) {
queue.dequeue();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"consumer1").start();
new Thread(()->{
for (;;) {
queue.dequeue();
queue.dequeue();
try {
TimeUnit.MILLISECONDS.sleep(1500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"consumer2").start();
}
}
输出结果如下:
EventQueue is full, waiting for dequeue...
consumer1: dequeues success. and size is 20
producer: enqueues success. and size is 20
EventQueue is full, waiting for dequeue...
consumer2: dequeues success. and size is 20
consumer2: dequeues success. and size is 19
producer: enqueues success. and size is 19
producer: enqueues success. and size is 20
EventQueue is full, waiting for dequeue...
实现显示锁 MyLock
通过上面提到的wait和notify、notifyAll或方法可以简单的实现一个显示锁,这里命名为 MyLock 。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
public class MyLock implements Lock {
private boolean isLocked = false;
@Override
public synchronized void lock() {
while (isLocked) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
isLocked = true;
}
@Override
public synchronized void unlock() {
isLocked = false;
notifyAll();
}
@Override
public synchronized void lockInterruptibly() throws InterruptedException {
while (isLocked) {
wait();
}
isLocked = true;
}
@Override
public synchronized boolean tryLock() {
if (!isLocked) {
isLocked = true;
return true;
}
return false;
}
@Override
public synchronized boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
long startTime = System.currentTimeMillis();
long timeoutInMillis = unit.toMillis(time);
while (isLocked) {
long elapsedTime = System.currentTimeMillis() - startTime;
long remainingTime = timeoutInMillis - elapsedTime;
if (remainingTime <= 0) {
return false;
}
wait(remainingTime);
}
isLocked = true;
return true;
}
@Override
public synchronized Condition newCondition() {
throw new UnsupportedOperationException("newCondition method not supported");
}
}
测试代码如下:
package engineer.concurrent.battle.fcontact;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import static java.util.concurrent.ThreadLocalRandom.current;
public class MyLockTest {
private final MyLock myLock = new MyLock();
public void syncMethod(){
//加锁
myLock.lock();
try {
int randomInt = current().nextInt(10);
System.out.println(Thread.currentThread().getName()+" gets the lock");
TimeUnit.SECONDS.sleep(randomInt);
}catch (InterruptedException e){
e.printStackTrace();
}finally {
// 释放锁
myLock.unlock();
System.out.println(Thread.currentThread().getName()+" releases the lock");
}
}
public static void main(String[] args) {
MyLockTest mlt = new MyLockTest();
for (int i = 0; i < 10; i++) {
new Thread(mlt::syncMethod).start();
}
}
}
输出结果如下:
Thread-0 gets the lock
Thread-2 gets the lock
Thread-0 releases the lock
Thread-2 releases the lock
Thread-9 gets the lock
Thread-9 releases the lock
Thread-1 gets the lock
Thread-1 releases the lock
Thread-8 gets the lock
Thread-8 releases the lock
Thread-3 gets the lock
Thread-3 releases the lock
Thread-7 gets the lock
Thread-7 releases the lock
Thread-4 gets the lock
Thread-4 releases the lock
Thread-6 gets the lock
Thread-6 releases the lock
Thread-5 gets the lock
Thread-5 releases the lock
通过测试发现MyLock能够达到和synchronized一致的效果。
关于作者
来自一线全栈程序员nine的探索与实践,持续迭代中。
欢迎关注公众号“雨林寻北”或添加个人卫星codetrend(备注技术)。