架构探险笔记6-ThreadLocal简介
什么是ThreadLocal?
ThreadLocal直译为“线程本地”或“本地线程”,如果真的这么认为,那就错了!其实它就是一个容器,用于存放线程的局部变量,应该叫ThreadLocalVariable(线程局部变量)才对。
早在JDK1.2的时代,java.lang.ThreadLocal就诞生了,它是为了解决多线程并发问题而设计的,只不过设计得有些难用而已,所以至今没有得到广泛的应用。
一个序列号生成器的程序可能同时会有多个线程并发访问它,要保证每个线程得到的序列号都是自增的,而补鞥呢互相干扰。
先定义一个接口:
public interface Sequence {
int getNumber();
}
每次调用getNumber方法可获取一个序列号,下次再调用时,序列号会自增。
在做一个线程类:
public class ClientThread extends Thread{
private Sequence sequence;
public ClientThread(Sequence sequence) {
this.sequence = sequence;
}
@Override
public void run() {
for (int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName() + " =>"+sequence.getNumber());
}
}
}
在线程中连续输出三次线程名与其对应的序列号。
我们不用ThreadLocal,先做一个实现类:
public class SequenceA implements Sequence {
private static int number = 0;
@Override
public int getNumber() {
number = number +1;
return number;
}
public static void main(String[] args) {
Sequence sequence = new SequenceA();
ClientThread thread1 = new ClientThread(sequence);
ClientThread thread2 = new ClientThread(sequence);
ClientThread thread3 = new ClientThread(sequence);
thread1.start();
thread2.start();
thread3.start();
}
}
序列号初始值是0,在main方法中模拟了三个线程,运行后结果如下:

分析发现,线程之间共享的static变量无法保证对于不同线程而言是安全的,也就是说,此时无法保证"线程安全"。
那么如何才能做到“线程安全”呢?对于这个案例,就是说不同的线程可拥有自己的static变量,如何实现呢?下面看另一个实现:
public class SequenceB implements Sequence {
//private static int number = 0;
private static ThreadLocal<Integer> numberContainer = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
@Override
public int getNumber() {
numberContainer.set(numberContainer.get()+1);
return numberContainer.get();
}
public static void main(String[] args) {
Sequence sequence = new SequenceB();
ClientThread thread1 = new ClientThread(sequence);
ClientThread thread2 = new ClientThread(sequence);
ClientThread thread3 = new ClientThread(sequence);
thread1.start();
thread2.start();
thread3.start();
}
}
通过ThreadLocal封装了一个Integer类型的numberContainer静态成员变量,并且初始值是0。再看getNumber方法,首先从numberContainer中get出当前的值,加1,随后set到numberContainer中,最后在numberContainer中get出当前的值并返回。
是不是很绕?但是很强大!我们不妨把ThreadLocal看作是一个容器,这样理解起来就简单了。所以,这里故意用了Container这个词作为后缀来命名ThreadLocal变量。

每个线程独立了,同样是static变量,对于不同的线程而言,它没有被共享,而是每个线程各一份,这样也就保证了线程安全。也就是说,ThreadLocal为每一个线程提供了一个独立的副本。
搞清楚ThreadLocal的原理后,总结一下API:
public void set(T value):将值放入线程局部变量中;
public T get():从线程局部变量中获取值;
public void remove():从线程局部变量中移除值(有助于JVM垃圾回收);
protected T initialValue():返回线程局部变量中的初始值(默认为null)。
为什么initialValue方法是protected的呢?就是为了提醒程序员,这个方法是要程序员来实现的,要给这个线程局部变量设置一个初始值。
自己实现ThreadLocal
熟悉了原理之后与这些API之后,可以想想ThreadLocal里面不就是封装了一个Map吗?我们自己可以写一个ThreadLocal了:
package com.autumn.threadlocal; import java.util.Collections;
import java.util.HashMap;
import java.util.Map; /**
* @program: MyThreadLocal
* @description: 模式ThreadLocal
* @author: Created by Autumn
* @create: 2018-11-21 17:09
*/
public class MyThreadLocal<T> {
private Map<Thread,T> container = Collections.synchronizedMap(new HashMap<Thread, T>()); public void set(T value){
container.put(Thread.currentThread(),value);
} public T get(){
Thread thread = Thread.currentThread();
T value = container.get(thread);
if (value == null && !container.containsKey(thread)){
value = initialValue();
container.put(thread,value);
}
return value;
} public void remove(){
container.remove(Thread.currentThread());
} protected T initialValue(){
return null;
}
}
上面定义了一个山寨版的ThreadLocal,其中定义了一个同步Map(这个操作会在map上加锁)
写个类运行一下
/**
* @program: SequenceB
* @description: 用ThreadLocal实现线程共享
* @author: Created by Autumn
* @create: 2018-11-21 15:45
*/
public class SequenceC implements Sequence {
//private static int number = 0;
private static MyThreadLocal<Integer> numberContainer = new MyThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
@Override
public int getNumber() {
numberContainer.set(numberContainer.get()+1);
return numberContainer.get();
} public static void main(String[] args) {
Sequence sequence = new SequenceC();
ClientThread thread1 = new ClientThread(sequence);
ClientThread thread2 = new ClientThread(sequence);
ClientThread thread3 = new ClientThread(sequence); thread1.start();
thread2.start();
thread3.start();
}
}
返回结果

只是把ThreadLocal换成了MyThreadLocal而已,运行效果和之前的一样,也是正确的。
提示:当在一个类中使用了static成员变量的时候,一定要多问问自己,这个static成员变量考虑“线程安全”了吗?也就是说,多个线程需要独享自己的static成员变量吗?如果需要考虑,不妨用ThreadLocal。
ThreadLocal使用例子
ThreadLocal具体有哪些使用案例呢?
首先要说的就是通过ThreadLocal存放JDBC Connection,以达到事务控制的能力。
记得在很久以前,用户提出过一个需求,需求就很繁琐,就一句话:
当修改产品价格的时候,需要记录操作日志,什么时候做了什么事情。
想必这个案例,只要是做个应用系统的小伙伴都应该遇到过。不外乎数据库里就两张表:product与log,用两条sql语句应该就可以解决问题:
update product set price = ? and id = ?
insert into log(created,description) values(?,?)
但要确保这两条sql语句必须在同一个事务里进行提交,否则有可能update提交了,但是insert却没有提交。
为了解决这个问题,首先我们写一个DBUtil的工具类
/**
* @program: DBUtil
* @description: 数据库配置工具类
* @author: qiuyu
* @create: 2018-11-28 05:52
**/
public class DBUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(DBUtil.class);
//数据库配置
private static final String DRIVER = "com.mysql.jdbc.Driver";
private static final String URL = "jdbc:mysql://222.222.221.198:3306/customer";
private static final String USERNAME = "root";
private static final String PASSWORD = "root"; //定义一个数据库连接
private static Connection conn = null; /**
* 获取数据库连接
* @return
*/
public static Connection getConnection(){
try {
/*JDBC获取连接*/
Class.forName(DRIVER);
conn = DriverManager.getConnection(URL,USERNAME,PASSWORD);
} catch (Exception e) {
e.printStackTrace(); //在catlina.out中打印
LOGGER.error("get connection failure",e);
}
return conn;
} /**
* 关闭数据库连接
* @param conn
*/
public static void closeConnection(Connection conn){
if (conn!=null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
LOGGER.error("close connection failure",e);
}
}
}
}
里面设置了一个static的Connection,这下数据库连接就好操作了。
然后定义一个借口用于逻辑层调用:
/**
* 接口 - 更新数据添加日志表记录
*/
public interface ProductService {
void updateProductPrice(long id,int price);
}
根据productId去更新对应的Product的price,然后再插入一条数据到log表中。
实现类
/**
* @program: ProductServiceImpl
* @description: ProductService实现类
* @author: qiuyu
* @create: 2018-11-28 06:04
**/
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(createid,description) value (?,?)"; @Override
public void updateProductPrice(long id, int price) {
try {
//获取连接
Connection conn = DBUtil.getConnection();
conn.setAutoCommit(false); //关闭自动提交事物(开启事物) //执行操作
updateProduct(conn,UPDATE_PRODUCT_SQL,id,price); //更新产品
insertLog(conn,INSERT_LOG_SQL,"create product."); //插入日志 //提交事物
conn.commit();
}catch (Exception e){
e.printStackTrace();
}finally {
DBUtil.closeConnection(); //关闭连接
}
} private void updateProduct(Connection conn,String updateProdutSQL,long productId,int productPrice) throws SQLException {
PreparedStatement pstmt = conn.prepareStatement(updateProdutSQL);
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 SQLException {
PreparedStatement pstmt = conn.prepareStatement(insertLogSQL);
pstmt.setString(1,new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").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 service = new ProductServiceImpl();
service.updateProductPrice(1,3000);
}
运行程序

作为一名专业的程序员,为了万无一失,我一定要到数据库里再看看。没错!product表对应的记录更新了,log表也插入了一条记录。这样就可以将ProductService接口交付给别人来调用了。
几个小时过去了,QA妹妹开始对着我嚷:“那谁!我刚才模拟10个请求,你这个接口怎么就挂了?报错说是数据库连接关闭了!”。
她是用工具模拟的,也就是模拟多个线程了!那我也可以模拟,于是写了一个线程类:
/**
* @program: ClientThread
* @description: 线程类
* @author: qiuyu
* @create: 2018-11-28 07:16
**/
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);
}
}
用这个线程去调用ProductService的方法,看看是不是有问题。此时,还要再修改一下main方法:
public static void main(String[] args) {
/*调用*/
/*ProductService service = new ProductServiceImpl();
service.updateProductPrice(1,3000);*/
/*多线程调用*/
for (int i=1;i<10;i++){
ProductService service = new ProductServiceImpl();
ClientThread thread = new ClientThread(service);
thread.start();
}
}
模拟十个线程,运行结果如下:

没想到!竟然在多线程的环境下报错了,果然是数据库连接关闭了。怎么回事呢?我陷入了沉思中。在百度、Google,还有OSC上都查找了那句报错信息,解答实在是千奇百怪。
既然是跟Connection有关系,那就将主要精力放在检查Connection相关的代码上。是不是Connection不应该是static呢?当初设计成static的主要目的是为了让DBUtil的static方法访问更加方便,用static变量来存放Connection也提高了性能。怎么办呢?
后来看到OSC上非常火爆的一片文章“ThreadLocal”那点事,才终于明白了,原来要使每个线程都拥有自己的连接,而不是共享同一个连接,否则“线程一”有可能会关闭“线程二”的连接,所以“线程二”就报错了。
于是将DBUtil重构:
public class DBUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(DBUtil.class);
//数据库配置
private static final String DRIVER = "com.mysql.jdbc.Driver";
private static final String URL = "jdbc:mysql://222.222.221.198:3306/demo2";
private static final String USERNAME = "root";
private static final String PASSWORD = "root";
//定义一个数据库连接
//private static Connection conn = null;
//定义一个用于放置数据库连接的局部线程变量(是每个线程拥有自己的连接)
private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>();
/**
* 获取数据库连接
* @return
*/
public static Connection getConnection(){
Connection conn = connContainer.get(); //从ThreadLocal中获取conn
try {
if (conn == null) { //从ThreadLocal中拿到的conn如果为null
/*JDBC获取连接*/
Class.forName(DRIVER);
conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
}
} catch (Exception e) {
e.printStackTrace(); //在catlina.out中打印
LOGGER.error("get connection failure",e);
}finally {
connContainer.set(conn);
}
return conn;
}
/**
* 关闭数据库连接
*/
public static void closeConnection(){
Connection conn = connContainer.get(); //从ThreadLocal中获取conn
if (conn!=null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
LOGGER.error("close connection failure",e);
}finally {
connContainer.remove(); //从ThreadLocal中删除当前线程的conn
}
}
}
}
把Connection放入到了ThreadLocal中,这样每个线程之间就隔离了,不会互相干扰了。
此外,在getConnection方法中,首先从ThreadLocal(也就是ConnContainer)中获取Connection,如果没有,就通过JDBC来创建连接,最后再把创建好的连接放入这个ThreadLocal中。可以把ThreadLocal看作一个容器。
同样也对closeConnection方法做了重构,先从容器中获取Connection,拿到了就close掉,最后从容器中将其remove掉,以保持容器的清洁。
注意:该示例仅用于ThreadLocal的基本用法。在实际工作中,推荐使用连接池来管理数据库连接。
该demo中虽然每次都从当前线程中获取Connection,Connection是线程隔离的,但是Mysql连接不是隔离的,Connection对象有可能用到其他Mysql的连接,如果mysql的连接关闭了,但是Connection对象没关闭会造成 java.sql.SQLException: No operations allowed after connection closed.
架构探险笔记6-ThreadLocal简介的更多相关文章
- 架构探险笔记11-与Servlet API解耦
Servlet API解耦 为什么需要与Servlet API解耦 目前在Controller中是无法调用Servlet API的,因为无法获取Request与Response这类对象,我们必须在Di ...
- 架构探险笔记4-使框架具备AOP特性(上)
对方法进行性能监控,在方法调用时统计出方法执行时间. 原始做法:在内个方法的开头获取系统时间,然后在方法的结尾获取时间,最后把前后台两次分别获取的系统时间做一个减法,即可获取方法执行所消耗的总时间. ...
- 架构探险笔记12-安全控制框架Shiro
什么是Shiro Shiro是Apache组织下的一款轻量级Java安全框架.Spring Security相对来说比较臃肿. 官网 Shiro提供的服务 1.Authentication(认证) 2 ...
- 架构探险笔记5-使框架具备AOP特性(下)
开发AOP框架 借鉴SpringAOP的风格,写一个基于切面注解的AOP框架.在进行下面的步骤之前,确保已经掌了动态代理技术. 定义切面注解 /** * 切面注解 */ @Target(Element ...
- 架构探险笔记3-搭建轻量级Java web框架
MVC(Model-View-Controller,模型-视图-控制器)是一种常见的设计模式,可以使用这个模式将应用程序进行解耦. 上一章我们使用Servlet来充当MVC模式中的Controller ...
- 《深入Linux内核架构》笔记 --- 第一章 简介和概述
Linux将虚拟地址空间划分为两个部分,分别称为内核空间和用户空间 各个系统进程的用户空间是完全彼此分离的,而虚拟地址空间顶部的内核空间总是同样的,无论当前执行的是哪个进程. 尽管Intel处理器区分 ...
- 读《架构探险——从零开始写Java Web框架》
内容提要 <架构探险--从零开始写Java Web框架>首先从一个简单的 Web 应用开始,让读者学会如何使用 IDEA.Maven.Git 等开发工具搭建 Java Web 应用:接着通 ...
- ThreadLocal 简介 案例 源码分析 MD
Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...
- Unity 游戏框架搭建 2018 (一) 架构、框架与 QFramework 简介
约定 还记得上版本的第二十四篇的约定嘛?现在出来履行啦~ 为什么要重制? 之前写的专栏都是按照心情写的,在最初的时候笔者什么都不懂,而且文章的发布是按照很随性的一个顺序.结果就是说,大家都看完了,都还 ...
随机推荐
- CodeForces 509C Sums of Digits(贪心乱搞)题解
题意:a是严格递增数列,bi是ai每一位的和,告诉你b1~bn,问你怎样搞才能让an最小 思路:让ai刚好大于ai-1弄出来的an最小.所以直接模拟贪心,如果当前位和前一个数的当前位一样并且后面还能生 ...
- 【做题】CFedu41G. Partitions——推式子
实际上这题的题面还是颇有意思,对两个划分不同的定义暗示了第二类斯特林数,模数是\(1000000007\)又表明这题不是NTT. 那么一开始的想法是考虑每个集合的贡献.设这个集合为\(S\),那么它的 ...
- python 之 运算符
Python 运算符 Python 运算符 什么是运算符? 本章节主要说明Python的运算符.举个简单的例子 4 +5 = 9 . 例子中,4和5被称为操作数,"+"号为运算 ...
- 【转载】SeleniumIDE入门
http://www.open-open.com/lib/view/open1452488109558.html
- 两个DIV并排显示
今天做的一个项目,需要做3个div,一个是总框(Div1),另外两个是子框,按比例填满div1,我设置好两个div的width和height,发现效果是两个子div上下显示,如图所示: 要想将两个DI ...
- Hash——字符串匹配(求s1在s2中出现的次数)
题目描述: 这是一道模板题. 给定一个字符串 A 和一个字符串 B ,求 B 在 A 中的出现次数.A 和 B中的字符均为英语大写字母. 求A 在 B 中出现了几次.(可重叠) 样例输入: 3 BA ...
- rostopic demo
发布者 #!/usr/bin/env python2. # -*- coding: utf- -*- import rospy from hdw_driver.msg import update_fi ...
- java 二维数组的行列长度
在 java 中,其实只有一维数组,而二维数组是在一维数组中嵌套实现的.比如 int[][] a = {{},{},{},{}} 要取行数和某一行的列数可以 : int rowLen = a.leng ...
- 数据库 Mysql 使用,优化,索引
数据库事务的隔离级别,由低到高 : READ UNCOMMITTED(读未提交数据):允许事务读取未被其他事务提交的变更数据,会出现脏读.不可重复读和幻读问题. READ COMMITTED(读已提交 ...
- 《剑指offer》第六十八题(树中两个结点的最低公共祖先)
// 面试题68:树中两个结点的最低公共祖先 // 题目:输入两个树结点,求它们的最低公共祖先. #include <iostream> #include "Tree.h&quo ...