说在前面
阅读本文之前,请确保你已经了解了Java中Future模式。若没有,ring出门左转,参考博文:【小家java】一个例子让就能你彻底理解Java的Future模式,Future类的设计思想
手动写一个数据库连接池
要求:该连接池能够复用数据库连接,并且能在高并发情况下正常工作
package test; import java.util.concurrent.ConcurrentHashMap; public class ConnectionPool { private ConcurrentHashMap<String, Connection> pool = new ConcurrentHashMap<String, Connection>(); public Connection getConnection(String key) { Connection conn = null; if (pool.containsKey(key)) { conn = pool.get(key); } else { conn = createConnection(); pool.putIfAbsent(key, conn); } return conn; } public Connection createConnection() { return new Connection(); } class Connection {} }
我们用了ConcurrentHashMap,这样就不必把getConnection方法置为synchronized,当多个线程同时调用getConnection方法时,性能大幅提升。
难道就这么简单?那你就too young too simple了。
比如我们现在有如下一个场景:
某一时刻,同时有3个线程进入getConnection方法,调用pool.containsKey(key)都返回false,然后3个线程各自都创建了连接。虽然ConcurrentHashMap的put方法只会加入其中一个,但还是生成了2个多余的连接。如果是真正的数据库连接,那会造成极大的资源浪费。
所以我们的问题来了,为了减少资源的浪费,需要解决如何在多线程访问getConnection方法时,只执行一次createConnection。
结合之前Future模式的实现分析:当3个线程都要创建连接的时候,如果只有一个线程执行createConnection方法创建一个连接,其它2个线程只需要用这个连接就行了。再延伸,把createConnection方法放到一个Callable的call方法里面,然后生成FutureTask。我们只需要让一个线程执行FutureTask的run方法,其它的线程只执行get方法就好了。
啥都不说了,上代码:
package test; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class ConnectionPool { private ConcurrentHashMap<String, FutureTask<Connection>> pool = new ConcurrentHashMap<String, FutureTask<Connection>>(); public Connection getConnection(String key) throws InterruptedException, ExecutionException { FutureTask<Connection> connectionTask = pool.get(key); if (connectionTask != null) { return connectionTask.get(); } else { //有可能多个线程同时进入从而创建多个callable,但木有关系 Callable<Connection> callable = new Callable<Connection>() { @Override public Connection call() throws Exception { return createConnection(); } }; FutureTask<Connection> newTask = new FutureTask<Connection>(callable); //因为使用的ConcurrentHashMap,所以这里最终只会put成功有一个线程的数据。当put成功之后会返回v。如果返回的v是null, connectionTask = pool.putIfAbsent(key, newTask); if (connectionTask == null) { connectionTask = newTask; connectionTask.run(); } return connectionTask.get(); } } public Connection createConnection() { return new Connection(); } class Connection { } }
有必要解释下putIfAbsent():如果指定的键未与某个值关联(或映射到null),则将其与给定值关联并返回null,否则返回当前值。 简单的若,如果key不存在就把值放进去并且返回null,如果key存在,就不放进去并且返回
public static void main(String[] args) { Map<String,String> map = new HashMap<>(); System.out.println(map.put("a","a")); // null System.out.println(map.putIfAbsent("b","b")); //null System.out.println(map.putIfAbsent("b","c")); //b 注意此处返回值为b而不是c //put的用法 和putIfAbsent有些许区别 System.out.println(map.put("c","c")); //null System.out.println(map.put("d","d")); //null System.out.println(map.put("c","d")); //c 注意此处返回值为c而不是d }
由此课件,map的put/putIfAbsent成功后的返回值,返回的是oldValue,而不是新的值。这点各位自己看看源码就一目了然了,有时候是需要注意返回值的
咱们模拟这上面的情况,推演一遍:
当3个线程同时进入else语句块时,各自都创建了一个FutureTask,但是ConcurrentHashMap只会加入其中一个。第一个线程执行pool.putIfAbsent方法后返回null,然后connectionTask被赋值,接着就执行run方法去创建连接,最后get。后面的线程执行pool.putIfAbsent方法不会返回null,就只会执行get方法。
在并发的环境下,通过FutureTask作为中间转换,成功实现了让某个方法只被一个线程执行。