本篇是《ThreadLocal 那点事儿》的续集,若是您没看上一篇,就就有点亏了。若是您错过了这一篇,那亏得就更大了。 java
仍是保持我一向的 Style,用一个 Demo 来讲话吧。用户提出一个需求:当修改产品价格的时候,须要记录操做日志,何时作了什么事情。 mysql
想必这个案例,只要是作过应用系统的小伙伴们,都应该遇到过吧?无外乎数据库里就两张表:product 与 log,用两条 SQL 语句应该能够解决问题: 程序员
update product set price = ? where id = ? insert into log (created, description) values (?, ?)
But!要确保这两条 SQL 语句必须在同一个事务里进行提交,不然有可能 update 提交了,但 insert 却没有提交。若是这样的事情真的发生了,咱们确定会被用户指着鼻子狂骂:“为何产品价格改了,却看不到何时改的呢?”。 sql
聪明的我在接到这个需求之后,是这样作的: 数据库
首先,我写一个 DBUtil 的工具类,封装了数据库的经常使用操做: 多线程
public class DBUtil { // 数据库配置 private static final String driver = "com.mysql.jdbc.Driver"; private static final String url = "jdbc:mysql://localhost:3306/demo"; private static final String username = "root"; private static final String password = "root"; // 定义一个数据库链接 private static Connection conn = null; // 获取链接 public static Connection getConnection() { try { Class.forName(driver); conn = DriverManager.getConnection(url, username, password); } catch (Exception e) { e.printStackTrace(); } return conn; } // 关闭链接 public static void closeConnection() { try { if (conn != null) { conn.close(); } } catch (Exception e) { e.printStackTrace(); } } }
里面搞了一个 static 的 Connection,这下子数据库链接就好操做了,牛逼吧! ide
而后,我定义了一个接口,用于给逻辑层来调用: 工具
public interface ProductService { void updateProductPrice(long productId, int price); }
根据用户提出的需求,我想这个接口彻底够用了。根据 productId 去更新对应 Product 的 price,而后再插入一条数据到 log 表中。 性能
其实业务逻辑也不太复杂,因而我快速地完成了 ProductService 接口的实现类: 测试
public class ProductServiceImpl implements ProductService { private static final String UPDATE_PRODUCT_SQL = "update product set price = ? where id = ?"; private static final String INSERT_LOG_SQL = "insert into log (created, description) values (?, ?)"; public void updateProductPrice(long productId, int price) { try { // 获取链接 Connection conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交事务(开启事务) // 执行操做 updateProduct(conn, UPDATE_PRODUCT_SQL, productId, price); // 更新产品 insertLog(conn, INSERT_LOG_SQL, "Create product."); // 插入日志 // 提交事务 conn.commit(); } catch (Exception e) { e.printStackTrace(); } finally { // 关闭链接 DBUtil.closeConnection(); } } private void updateProduct(Connection conn, String updateProductSQL, long productId, int productPrice) throws Exception { PreparedStatement pstmt = conn.prepareStatement(updateProductSQL); pstmt.setInt(1, productPrice); pstmt.setLong(2, productId); int rows = pstmt.executeUpdate(); if (rows != 0) { System.out.println("Update product success!"); } } private void insertLog(Connection conn, String insertLogSQL, String logDescription) throws Exception { PreparedStatement pstmt = conn.prepareStatement(insertLogSQL); pstmt.setString(1, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date())); pstmt.setString(2, logDescription); int rows = pstmt.executeUpdate(); if (rows != 0) { System.out.println("Insert log success!"); } } }代码的可读性还算不错吧?这里我用到了 JDBC 的高级特性 Transaction 了。暗自庆幸了一番以后,我想是否是有必要写一个客户端,来测试一下执行结果是否是我想要的呢? 因而我偷懒,直接在 ProductServiceImpl 中增长了一个 main() 方法:
public static void main(String[] args) { ProductService productService = new ProductServiceImpl(); productService.updateProductPrice(1, 3000); }
我想让 productId 为 1 的产品的价格修改成 3000。因而我把程序跑了一遍,控制台输出:
Update product success!
Insert log success!
应该是对了。做为一名专业的程序员,为了万无一失,我必定要到数据库里在看看。没错!product 表对应的记录更新了,log 表也插入了一条记录。这样就能够将 ProductService 接口交付给别人来调用了。
几个小时过去了,QA 妹妹开始骂我:“我靠!我才模拟了 10 个请求,你这个接口怎么就挂了?说是数据库链接关闭了!”。
听到这样的叫声,让我浑身打颤,立马中断了个人小视频,赶忙打开 IDE,找到了这个 ProductServiceImpl 这个实现类。好像没有 Bug 吧?但我如今不敢给她任何回应,我确实有点怕她的。
我忽然想起,她是用工具模拟的,也就是模拟多个线程了!那我本身也能够模拟啊,因而我写了一个线程类:
public class ClientThread extends Thread { private ProductService productService; public ClientThread(ProductService productService) { this.productService = productService; } @Override public void run() { System.out.println(Thread.currentThread().getName()); productService.updateProductPrice(1, 3000); } }
我用这线程去调用 ProduceService 的方法,看看是否是有问题。此时,我还要再修改一下 main() 方法:
// public static void main(String[] args) { // ProductService productService = new ProductServiceImpl(); // productService.updateProductPrice(1, 3000); // } public static void main(String[] args) { for (int i = 0; i < 10; i++) { ProductService productService = new ProductServiceImpl(); ClientThread thread = new ClientThread(productService); thread.start(); } }
我也模拟 10 个线程吧,我就不信那个邪了!
运行结果然的让我很晕、很晕:
Thread-1
Thread-3
Thread-5
Thread-7
Thread-9
Thread-0
Thread-2
Thread-4
Thread-6
Thread-8
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:411)
at com.mysql.jdbc.Util.getInstance(Util.java:386)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1015)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:989)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:975)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:920)
at com.mysql.jdbc.ConnectionImpl.throwConnectionClosedException(ConnectionImpl.java:1304)
at com.mysql.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:1296)
at com.mysql.jdbc.ConnectionImpl.commit(ConnectionImpl.java:1699)
at com.smart.sample.test.transaction.solution1.ProductServiceImpl.updateProductPrice(ProductServiceImpl.java:25)
at com.smart.sample.test.transaction.ClientThread.run(ClientThread.java:18)
我靠!居然在多线程的环境下报错了,果真是数据库链接关闭了。怎么回事呢?我陷入了沉思中。因而我 Copy 了一把那句报错信息,在百度、Google,还有 OSC 里都找了,解答实在是千奇百怪。
我忽然想起,既然是跟 Connection 有关系,那我就将主要精力放在检查 Connection 相关的代码上吧。是否是 Connection 不该该是 static 的呢?我当初设计成 static 的主要是为了让 DBUtil 的 static 方法访问起来更加方便,用 static 变量来存放 Connection 也提升了性能啊。怎么搞呢?
因而我看到了 OSC 上很是火爆的一篇文章《ThreadLocal 那点事儿》,终于才让我明白了!原来要使每一个线程都拥有本身的链接,而不是共享同一个链接,不然线程1有可能会关闭线程2的链接,因此线程2就报错了。必定是这样!
我赶忙将 DBUtil 给重构了:
public class DBUtil { // 数据库配置 private static final String driver = "com.mysql.jdbc.Driver"; private static final String url = "jdbc:mysql://localhost:3306/demo"; private static final String username = "root"; private static final String password = "root"; // 定义一个用于放置数据库链接的局部线程变量(使每一个线程都拥有本身的链接) private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>(); // 获取链接 public static Connection getConnection() { Connection conn = connContainer.get(); try { if (conn == null) { Class.forName(driver); conn = DriverManager.getConnection(url, username, password); } } catch (Exception e) { e.printStackTrace(); } finally { connContainer.set(conn); } return conn; } // 关闭链接 public static void closeConnection() { Connection conn = connContainer.get(); try { if (conn != null) { conn.close(); } } catch (Exception e) { e.printStackTrace(); } finally { connContainer.remove(); } } }
我把 Connection 放到了 ThreadLocal 中,这样每一个线程之间就隔离了,不会相互干扰了。
此外,在 getConnection() 方法中,首先从 ThreadLocal 中(也就是 connContainer 中) 获取 Connection,若是没有,就经过 JDBC 来建立链接,最后再把建立好的链接放入这个 ThreadLocal 中。能够把 ThreadLocal 看作是一个容器,一点不假。
一样,我也对 closeConnection() 方法作了重构,先从容器中获取 Connection,拿到了就 close 掉,最后从容器中将其 remove 掉,以保持容器的清洁。
这下应该行了吧?我再次运行 main() 方法:
Thread-0
Thread-2
Thread-4
Thread-6
Thread-8
Thread-1
Thread-3
Thread-5
Thread-7
Thread-9
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
我去!总算是解决了,QA 妹妹,你应该会对我微笑一下吧?
感谢您的关注,分享是一种快乐,也但愿获得您的支持与批评!
注意:该示例仅用于说明 TheadLocal 的基本用法。在实际工做中,推荐使用链接池来管理数据库链接。示例中的代码仅做参考,使用前请酌情考虑。