本篇是《ThreadLocal 那点事儿》的续集,如果您没看上一篇,就就有点亏了。如果您错过了这一篇,那亏得就更大了。

还是保持我一贯的 Style,用一个 Demo 来说话吧。用户提出一个需求:当修改产品价格的时候,需要记录操作日志,什么时候做了什么事情。

想必这个案例,只要是做过应用系统的小伙伴们,都应该遇到过吧?无外乎数据库里就两张表:product 与 log,用两条 SQL 语句应该可以解决问题:

update product set price = ? where id = ?
insert into log (created, description) values (?, ?)

But!要确保这两条 SQL 语句必须在同一个事务里进行提交,否则有可能 update 提交了,但 insert 却没有提交。如果这样的事情真的发生了,我们肯定会被用户指着鼻子狂骂:“为什么产品价格改了,却看不到什么时候改的呢?”。

聪明的我在接到这个需求以后,是这样做的:

首先,我写一个 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,这下子数据库连接就好操作了,牛逼吧!

然后,我定义了一个接口,用于给逻辑层来调用:

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 的基本用法。在实际工作中,推荐使用连接池来管理数据库连接。示例中的代码仅作参考,使用前请酌情考虑。

ThreadLocal 那点事儿(续集)的更多相关文章

  1. ThreadLocal 那点事儿

    原文出处: 黄勇 ThreadLocal,直译为“线程本地”或“本地线程”,如果你真的这么认为,那就错了!其实,它就是一个容器,用于存放线程的局部变量,我认为应该叫做 ThreadLocalVaria ...

  2. ThreadLocal使用场景案例

    本篇是<ThreadLocal 那点事儿>的续集,如果您没看上一篇,就就有点亏了.如果您错过了这一篇,那亏得就更大了. 还是保持我一贯的 Style,用一个 Demo 来说话吧.用户提出一 ...

  3. 架构探险——第二章(为web应用添加业务功能)

    第二章不使用框架完成了自己的Web应用. 重点: 服务层的完善优化过程,思路 在看这一段的时候引起了无数次的共鸣.相信大家在开始接触Java Web的时候,都做过类似的封装和优化. 第一版 在Serv ...

  4. 关于 SimpleDateFormat 的非线程安全问题及其解决方案

    一直以来都是直接用SimpleDateFormat开发的,没想着考虑线程安全的问题,特记录下来(摘抄的): 1.问题: 先来看一段可能引起错误的代码: package test.date; impor ...

  5. Java_基础_02_ThreadLocal

    二.参考资料 1.ThreadLocal 那点事儿 2.彻底理解ThreadLocal

  6. ThreadLocal,Lock的事儿

    ThreadLocal作用 防止线程间的干扰 public interface Sequence { int getNumber(); } public class ClientThread exte ...

  7. ThreadLocal 工作原理、部分源码分析

    1.大概去哪里看 ThreadLocal 其根本实现方法,是在Thread里面,有一个ThreadLocal.ThreadLocalMap属性 ThreadLocal.ThreadLocalMap t ...

  8. Java 线程 — ThreadLocal

    ThreadLocal 先来看看ThreadLocal的注释: This class provides** thread-local variables**. These variables diff ...

  9. 详细领悟ThreadLocal变量

    关于对ThreadLocal变量的理解,我今天查看一下午的博客,自己也写了demo来测试来看自己的理解到底是不是那么回事.从看到博客引出不解,到仔细查看ThreadLocal源码(JDK1.8),我觉 ...

随机推荐

  1. CentOS 7下设置Docker代理(Linux下Systemd服务的环境变量配置)

    Docker守护程序使用HTTP_PROXY,HTTPS_PROXY以及NO_PROXY环境变量在其启动环境来配置HTTP或HTTPS代理的行为.无法使用daemon.json文件配置这些环境变量. ...

  2. C#键盘事件处理父窗体子窗体

    :                             :                             MessageBox.Show(, , Keys.F1);            ...

  3. 自定义Directive使用ngModel

    我们知道ngModel是AngularJS中默认的一个Directive,用于数据的双向绑定.通常是这样使用的: <input type="text" ng-model=&q ...

  4. c++ try catch 问题

    以前都是用try{} catch(…){}来捕获C++中一些意想不到的异常, 今天看了Winhack的帖子才知道,这种方法在VC中其实是靠不住的.例如下面的代码: 以前都是用try{} catch(… ...

  5. vim less vi 不显示富文本 ESC

    如图: 使用 less -r xxx.log 即可显示如下

  6. PHP ~与各加速工具的性能对比~

    参与测试的加速器:Xcache,Opcache,hhvm   Xcache简介 前面已经介绍了PHP加速器的原理和功用(参见LAMP架构之PHP-FPM 服务器),xcache作为目前使用广泛的PHP ...

  7. oracle本地编译问题

    oracle10.2: --将过程重新编译为本地编译方式,提示有编译错误,经查提示未设置plsql_native_library_dir 参数 SQL> alter procedure p_xx ...

  8. ios开发版证书与企业证书相关文件申请安装及其使用方法

    本文主要讲述以下内容: ios开发版证书的申请, 企业证书的申请, appid的创建, provision profile的生成, 开发设备devices的绑定, 以及每个证书文件之间的关系, 最后使 ...

  9. 第三部分:Android 应用程序接口指南---第二节:UI---第二章 输入控件

    第2章 输入控件 输入控件是应用程序中用户接口的一种交互式组件.Android提供了大量的可供人们在UI中使用的控件,比如按钮.文本区域.(带滑块的)进度条.复选框.缩放按钮以及切换按钮等等. 在UI ...

  10. python3.6 使用pyinstaller 打包web程序的方法

    官方文档连接 (https://pyinstaller.readthedocs.io/en/stable/ ) 第一步,下载pywin32 首先下载pywin32,下面是下载链接,下载自己的系统版本对 ...