等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
等待/通知机制是线程池技术的基础,下面代码演示等待/通知机制:
/**
* 多线程里经典的 等待/通知机制演示
* 等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B
调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而
执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的
关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
*/
public class WaitNotify {
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 先启动等待方(消费者),再启动通知方(生产者)
new Thread(new Wait(),"wait thread").start();
Thread.sleep(10000);
new Thread(new Notify(),"notify thread").start();
}
/**
* 等待方(消费者)遵循以下原则:
* 1.获取对象的锁
* 2.如果条件不满足,那么调用对象的wait()方法,等待条件变化并被通知,被通知后仍要检查条件
* 3.条件满足则执行对应的逻辑
* 伪代码如下:
synchronized (对象) {
while (条件不满足) {
对象.wait();
}
对应的处理逻辑
}
*/
static class Wait implements Runnable {
@Override
public void run() {
synchronized (lock) {
while (flag) {
try {
System.out.println(Thread.currentThread().getName() + " waiting");
lock.wait(); // wait方法会让当前线程进入waiting状态,并释放锁,线程会在这里停下,等待其他线程唤醒
System.out.println(Thread.currentThread().getName() + " unWaited");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " start process");
}
}
}
/**
* 通知方(生产者)遵循以下原则:
* 1.获取对象的锁
* 2.改变条件
* 3.通知所有等待在对象上的线程
* 伪代码如下:
synchronized (lock) {
改变条件
对象.notifyAll();
}
*/
static class Notify implements Runnable {
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " notify all");
flag = false;
lock.notifyAll(); // 通知(唤醒)所有等待在该对象上的线程
}
}
}
}
开发人员经常会遇到这样的方法调用场景:调用一个方法时等待一段时间(一般来说是给定一个时间段),如果该方法能够在给定的时间段之内得到结果,那么将结果立刻返回,反之,超时返回默认结果。
等待超时模式就是在等待/通知范式基础上增加了超时控制,这使得该模式相比原有范式更具有灵活性,因为即使方法执行时间过长,也不会“永久”阻塞调用者,而是会按照调用者的要求“按时”返回。
// 对当前对象加锁
public synchronized Object get(long mills) throws InterruptedException {
long future = System.currentTimeMillis() + mills;
long remaining = mills;
// 当超时大于0并且result返回值不满足要求
while ((result == null) && remaining > 0) {
wait(remaining);
remaining = future - System.currentTimeMillis();
}
return result;
}
自定义一个连接池,模拟常用的数据库连接池功能:
package com.learn.pool;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.LinkedList;
/**
* 一个简单的自定义连接池
*/
public class ConnectionPool {
// 维护一个容器,用于存放连接
private LinkedList pool = new LinkedList<>();
// 构造对象时,传入线程池大小
public ConnectionPool(int poolSize) {
for (int i = 0; i < poolSize; i++) {
pool.addLast(ConnectionDriver.create());
}
}
/**
* 在超时时间内返回连接,如果在超时时间内无法获取可用连接,则返回null
*
* @param millis 超时时间
* @return
* @throws InterruptedException
*/
public Connection getConnection(long millis) throws InterruptedException {
synchronized (pool) {
if (millis <= 0) {
while (pool.isEmpty()) { //这里用while循环检查条件,即使被唤醒后也要再检查一遍
pool.wait(); // wait方法会让当前线程进入waiting状态,并释放锁,程序会在这里阻塞,等待(同一个锁对象下)其他线程唤醒
}
return pool.removeFirst();
} else {
Connection conn = null;
long future = System.currentTimeMillis() + millis; // 超时时间
long remaining = millis; // 等待持续时间
while (pool.isEmpty() && remaining > 0) {
pool.wait(remaining); // 设置等待超时时间,超时后自动返回,防止线程一直在这里等待
remaining = future - System.currentTimeMillis(); // 更新等待持续时间
}
if (!pool.isEmpty()) {
conn = pool.removeFirst();
}
return conn;
}
}
}
/**
* 释放连接,并通知其他线程
*
* @param connection
*/
public void releaseConnection(Connection connection) {
if (connection != null) {
synchronized (pool) {
// 将归还的连接放入线程池,并通知其他所有线程,让其他线程感知到归还了一个线程
pool.addLast(connection);
pool.notifyAll();
}
}
}
}
class ConnectionDriver {
/**
* 由于java.sql.Connection是个接口,最终的实现是由各数据库驱动提供方实现的,
* 考虑到这只是一个示例,这里通过动态代理构造了一个Connection,该代理的实现
* 仅仅是在commit方法调用前休眠100毫秒
*
* @return
*/
public static final Connection create() {
// 构造一个Connection的代理,在commit前休眠100毫秒
Connection conn = (Connection) Proxy.newProxyInstance(
ConnectionDriver.class.getClassLoader(),
new Class[]{Connection.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("commit")) {
Thread.sleep(100);
}
return "Connection init ok";
}
});
return conn;
}
}
再写一个测试类,用于测试连接池性能:
package com.learn.pool;
import java.sql.Connection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
*/
public class ConnectionPoolTest {
// 初始化一个自定义的线程池
static ConnectionPool pool = new ConnectionPool(10);
// 用于保证所有ConnectionRunner能够同时开始
static CountDownLatch start = new CountDownLatch(1);
// 用于保证所有ConnectionRunner结束后,main线程才继续进行
static CountDownLatch end;
public static void main(String[] args) throws InterruptedException {
// 并发获取连接的线程数
int threadCount = 20;
end = new CountDownLatch(threadCount);
// 每个线程获取连接的数量
int count = 20;
// 创建两个原子整数,用于记录所有线程获取到的连接数
AtomicInteger got = new AtomicInteger();
AtomicInteger notGot = new AtomicInteger();
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(new ConnectionRunner(count,got,notGot),"ConnectionRunnerThread");
thread.start();
}
start.countDown();//同时开始ConnectionRunner线程
end.await();//等待所有ConnectionRunner线程执行完
System.out.println("total invoke:" + (threadCount * count));
System.out.println("got connection:" + got);
System.out.println("not got connection:" + notGot);
}
static class ConnectionRunner implements Runnable {
int count;
AtomicInteger got;
AtomicInteger notGot;
public ConnectionRunner(int count, AtomicInteger got, AtomicInteger notGot) {
this.count = count;
this.got = got;
this.notGot = notGot;
}
@Override
public void run() {
try {
start.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
while (count > 0) {
try {
/**
* 从线程池中获取连接,如果超过1000ms无法获取到,将返回null
* 分别统计获取到连接的数量和未获取到连接的数量
*/
Connection connection = pool.getConnection(1000);
if (connection != null) {
try {
connection.commit();
} finally {
pool.releaseConnection(connection);
got.incrementAndGet();//自增
}
} else {
notGot.incrementAndGet();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
count--;
}
}
end.countDown();
}
}
}
并发线程的数量与连接获取数的关系(不同硬件配置可能结果会不同)
从表中的数据统计可以看出,在资源一定的情况下(连接池中的10个连接),随着客户端线程的逐步增加,客户端出现超时无法获取连接的比率不断升高。虽然客户端线程在这种超时获取的模式下会出现连接无法获取的情况,但是它能够保证客户端线程不会一直挂在连接获取的操作上,而是“按时”返回,并告知客户端连接获取出现问题,是系统的一种自我保护机制。数据库连接池的设计也可以复用到其他的资源获取的场景,针对昂贵资源(比如数据库连接)的获取都应该加以超时限制。