实验描述:
首先定义一个数据库连接池,然后从资源池中并发获取连接,接着进行对连接的操作,最后返还连接到资源池。
其中,连接池用 "LinkedList pool" 保存连接,获取连接和归还连接时用synchronized同步块锁住pool。
从连接池中获取连接前,首先判断连接池里是否有可用的连接,若没有,则通过wait()方法使线程等待;当其他线程释放连接时,通过notifyAll()方法唤醒等待队列中的线程。
问题描述:
Exception in thread "pool-1-thread-9" Exception in thread "pool-1-thread-8" java.util.NoSuchElementException
at java.util.LinkedList.removeFirst(LinkedList.java:270)
at line.entertains.concur.pool.resources.ConnectionPool.fetchConnection(ConnectionPool.java:55)
at line.entertains.concur.pool.resources.Test2$MyTask.run(Test2.java:38)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
即从LinkedList pool获取连接时,pool大小为0。
具体代码:
连接池:
import java.sql.Connection;
import java.util.LinkedList;
/**
* 简单的定长连接池
*
* @author line
* @date 2019年3月31日 下午10:26:41
*/
public class ConnectionPool {
private LinkedList<Connection> pool = new LinkedList<>();
public ConnectionPool(int poolSize) {
if (poolSize < 0) {
throw new IllegalArgumentException("pool size must > 0!");
}
while (poolSize-- > 0) {
pool.addLast(ConnectionDriver.createConnection());
}
}
/**
* 释放连接,并把连接返还给连接池
* 若发生竞争,则阻塞当前线程
* 若连接池之前没有空闲连接,则尝试知等待队列里的线程
* @param connection
*/
public void releaseConnection(Connection connection) {
if (connection != null) {
synchronized (pool) {
pool.addLast(connection);
if (pool.size() == 1)
pool.notifyAll();
}
}
}
/**
* 请求连接池获取连接,
* 若发生竞争,则阻塞当前线程
* 若连接池没有空闲的连接,则加入等待队列
* @return
* @throws InterruptedException
*/
public Connection fetchConnection() throws InterruptedException {
synchronized (pool) {
if (pool.size() == 0)
pool.wait();
// while (pool.size() == 0)
// pool.wait();
return pool.removeFirst();
}
}
}
模拟连接:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.concurrent.TimeUnit;
/**
* JDK动态代理来模拟sql提交
*
* @author line
* @date 2019年3月31日 下午10:31:55
*/
public class ConnectionDriver implements InvocationHandler {
/**
*
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("commit")) {
TimeUnit.MILLISECONDS.sleep(3);
return "commit success";
}
return null;
}
public static Connection createConnection() {
return (Connection) Proxy.newProxyInstance(ConnectionDriver.class.getClassLoader(),
new Class<?>[] { Connection.class }, new ConnectionDriver());
}
}
实验方法以及Runnable实现:
import java.sql.Connection;
import java.sql.SQLException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
*
* @author line
* @date 2019年3月31日 下午10:50:36
*/
public class Test2 {
private static ConnectionPool pool = new ConnectionPool(2);
public static void main(String[] args) {
ExecutorService threads = Executors.newFixedThreadPool(30);
MyTask task = new MyTask();
int taskCount = 100;
while (taskCount-- > 0)
threads.execute(task);
threads.shutdown();
}
static class MyTask implements Runnable {
/**
* 对数据进行操作
*/
@Override
public void run() {
Connection connection = null;
try {
connection = pool.fetchConnection();
connection.commit();
} catch (InterruptedException | SQLException e) {
} finally {
pool.releaseConnection(connection);
}
}
}
}
本以为通过synchronized同步锁住LinkedList pool之后,应该不会有并发问题。。经过分析和debug之后,发现连接池实现中的wait()方法返回后,pool的确有可能为空的。
假设现有5个线程,t1, ... ,t5,竞争2个连接,c1, ... ,c2:
t1率先进入同步块,t2-t5阻塞在同步块入口
t1判断pool不为空,并顺利获取到连接c1,然后退出同步块
t2进入同步块,同样获取到连接c2,然后退出同步块
t3-t5也依次进入同步块,分别判断当前pool为空,调用wait()方法,被动退出同步块
t1完成操作后,释放连接,首先进入同步块,然后将c1添加至pool,并调用notifyAll()方法,唤醒所有在pool中等待的线程t3,t4,t5,最后退出同步块
唤醒后的t3率先进入到同步块,此时pool中存在刚刚被t1归还的连接c1,t3成功获取到c1,退出同步块
问题来了,t4进入到同步块,但是pool中之前被t1归还的连接c1,已经被t3给取走了,因此LinkeList的removeLast()方法抛出了一个NoSuchElementException异常。
解决办法:
对比书上的代码清单之后发现,书上用while来判断pool是否为空,但是我用的是if,所以导致了这个问题。改成了while就行了。
其他:
本来还想这用释放连接时,用notify()而不是notifyAll()来唤醒一个等待的线程,但是还会有问题,通过jvisualvm查看线程状态,发现有的线程没有被唤醒,仍然处于等待状态。由于暂时还不知道怎么在eclipse的debug模式中,指定被下一个进入同步块的线程,只能简单的稍作分析。
还是假设现有5个线程,t1, ... ,t5,竞争2个连接,c1, ... ,c2:
t1,t2依次进入到同步块,并分别获取了连接c1,c2
t3,t4,t5也依次进入到同步块,但此时pool为空,只能等待,被动退出同步块
t1,t2操作完毕之后释放资源,t1率先进入同步块,调用notify()方法唤醒了t3(假设唤醒了t3)之后退出同步块,但此时紧跟着进入同步块的线程,不是刚刚被唤醒的t3,而是也要释放连接的t2,
结果t2进入同步块后,调用的notify()方法同样也唤醒了t1之前唤醒的线程t3(这一点可能不严谨),导致少唤醒了一个线程,最终导致有的线程会一直处于等待状态。
由于目前水平有限,只能作简单的推测,后边需要补充synchonized的阻塞队列与等待队列相关的知识,来解决这个notify()的问题。
或许可以先深入研究下JUC中的lock的同步队列与等待队列,毕竟JUC是java语言层面的同步控制方法。。。
未来会发生什么,我又该何去何从。。。