ThreadLocal 那点事儿(续集)

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

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

?


1

2

update
product
set price = ?
where id = ?

insert
into
log (created, description) values
(?, ?)

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

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

首先,我写一个 DBUtil 的工具类,封装了数据库的常用操作:

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

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

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

?


1

2

3

4

public
interface
ProductService {

    void
updateProductPrice(long
productId, int
price);

}

根据用户提出的需求,我想这个接口完全够用了。根据 productId 去更新对应 Product 的 price,然后再插入一条数据到 log 表中。

其实业务逻辑也不太复杂,于是我快速地完成了 ProductService 接口的实现类:

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

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() 方法:

?


1

2

3

4

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 吧?但我现在不敢给她任何回应,我确实有点怕她的。

我突然想起,她是用工具模拟的,也就是模拟多个线程了!那我自己也可以模拟啊,于是我写了一个线程类:

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

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() 方法:

?


1

2

3

4

5

6

7

8

9

10

11

12

// 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 给重构了:

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

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!

时间: 2024-08-02 12:04:30

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

ThreadLocal 那点事儿

ThreadLocal,直译为"线程本地"或"本地线程",如果你真的这么认为,那就错了!其实,它就是一个容器,用于存放线程的局部变量,我认为应该叫做 ThreadLocalVariable(线程局部变量)才对,真不理解为什么当初 Sun 公司的工程师这样命名. 早在 JDK 1.2 的时代,java.lang.ThreadLocal 就诞生了,它是为了解决多线程并发问题而设计的,只不过设计得有些难用,所以至今没有得到广泛使用.其实它还是挺有用的,不相信的话,我们一起

关于ThreadLocal 的用法

ThreadLocal,直译为"线程本地"或"本地线程",如果你真的这么认为,那就错了!其实,它就是一个容器,用于存放线程的局部变量,我认为应该叫做 ThreadLocalVariable(线程局部变量)才对,真不理解为什么当初 Sun 公司的工程师这样命名. 早在 JDK 1.2 的时代,java.lang.ThreadLocal 就诞生了,它是为了解决多线程并发问题而设计的,只不过设计得有些难用,所以至今没有得到广泛使用.其实它还是挺有用的,不相信的话,我们一起

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

1.问题: 先来看一段可能引起错误的代码: package test.date; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; public class ProveNotSafe { static SimpleDateFormat df = new SimpleDateFormat("dd-MMM-yyyy&qu

ThreadLocal的事儿

ThreadLocal作用 防止线程间的干扰 public interface Sequence { int getNumber(); } public class ClientThread extends Thread { private Sequence sequence; public ClientThread(Sequence sequence) { this.sequence = sequence; } @Override public void run() { for (int i

AOP 那点事儿(续集)

在上篇中,我们从写死代码,到使用代理:从编程式 Spring AOP 到声明式 Spring AOP.一切都朝着简单实用主义的方向在发展.沿着 Spring AOP 的方向,Rod Johnson(老罗)花了不少心思,都是为了让我们使用 Spring 框架时不会感受到麻烦,但事实却并非如此.那么,后来老罗究竟对 Spring AOP 做了哪些改进呢? 现在继续! 9. Spring AOP:切面 之前谈到的 AOP 框架其实可以将它理解为一个拦截器框架,但这个拦截器似乎非常武断.比如说,如果它拦

弄明白ThreadLocal类

1.ThreadLocal类的由来 因为有问题,人类就会想法设法的创造一些东西出来解决问题,嗯,这句话同意吧. 假如目前有这么一个问题:有个家庭,三个孩子都想看妈妈买的一本童话书,但是只有一本书,该如何是好? 方法一:家里没钱买第二本了,那就排队看,谁跑得快来到妈妈面前的就先看.后面来晚的,候着等着.等前面的看完再到你.于是 以时间换空间的synchronized 类出现了. 方法二:多大的事儿,你们爸爸有钱,随便任性.立马再买两本一模一样的,人手一本.于是以空间换时间的ThreadLocal类

ThreadLocal来管理事务

ThreadLocal (扩展) 1 ThreadLocal API ThreadLocal类只有三个方法: l  void set(T value):保存值: l  T get():获取值: l  void remove():移除值. 2 ThreadLocal的内部是Map ThreadLocal内部其实是个Map来保存数据.虽然在使用ThreadLocal时只给出了值,没有给出键,其实它内部使用了当前线程做为键. class MyThreadLocal<T> { private Map&

线程的私家小院儿:ThreadLocal

转载自simplemain老王的公众号 话说在<操作系统原理>这门课里面,我们学到了很多概念:进程.线程.锁.PV操作.读写者问题等等,其中关于进程.线程和锁的东西是我们平时工作中用到最多的:服务器接收到用户请求,需要用一个进程或者一个线程去处理,然后操作内存.文件或者数据库的时候,可能需要对他们进行加锁操作. 不过作为一个有追求的程序员,我们有些时候会不满足于此 .于是,我们开始对线程.锁开始了漫漫的优化之路.其中有一种情况是非常值得优化的:假定我们现在有一个web服务,我们的程序一般都会为

详细领悟ThreadLocal变量

关于对ThreadLocal变量的理解,我今天查看一下午的博客,自己也写了demo来测试来看自己的理解到底是不是那么回事.从看到博客引出不解,到仔细查看ThreadLocal源码(JDK1.8),我觉得我很有必要记录下来我这大半天的收获,今天我研究的最多的就是这两篇文章说理解.我在这里暂称为A文章和B文章.以下是两篇博文地址,我是在看完A文章后,很有疑问,特别是在A文章后的各位网页的评论中,更加坚定我要弄清楚ThreadLocal到底是怎么一回事.A文章:http://blog.csdn.net