记录一个学习资源池(连接池)时,从资源池获取资源的并发问题。

实验描述:

首先定义一个数据库连接池,然后从资源池中并发获取连接,接着进行对连接的操作,最后返还连接到资源池。

其中,连接池用 "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语言层面的同步控制方法。。。

 

未来会发生什么,我又该何去何从。。。 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值