什么是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简介的更多相关文章

  1. 架构探险笔记11-与Servlet API解耦

    Servlet API解耦 为什么需要与Servlet API解耦 目前在Controller中是无法调用Servlet API的,因为无法获取Request与Response这类对象,我们必须在Di ...

  2. 架构探险笔记4-使框架具备AOP特性(上)

    对方法进行性能监控,在方法调用时统计出方法执行时间. 原始做法:在内个方法的开头获取系统时间,然后在方法的结尾获取时间,最后把前后台两次分别获取的系统时间做一个减法,即可获取方法执行所消耗的总时间. ...

  3. 架构探险笔记12-安全控制框架Shiro

    什么是Shiro Shiro是Apache组织下的一款轻量级Java安全框架.Spring Security相对来说比较臃肿. 官网 Shiro提供的服务 1.Authentication(认证) 2 ...

  4. 架构探险笔记5-使框架具备AOP特性(下)

    开发AOP框架 借鉴SpringAOP的风格,写一个基于切面注解的AOP框架.在进行下面的步骤之前,确保已经掌了动态代理技术. 定义切面注解 /** * 切面注解 */ @Target(Element ...

  5. 架构探险笔记3-搭建轻量级Java web框架

    MVC(Model-View-Controller,模型-视图-控制器)是一种常见的设计模式,可以使用这个模式将应用程序进行解耦. 上一章我们使用Servlet来充当MVC模式中的Controller ...

  6. 《深入Linux内核架构》笔记 --- 第一章 简介和概述

    Linux将虚拟地址空间划分为两个部分,分别称为内核空间和用户空间 各个系统进程的用户空间是完全彼此分离的,而虚拟地址空间顶部的内核空间总是同样的,无论当前执行的是哪个进程. 尽管Intel处理器区分 ...

  7. 读《架构探险——从零开始写Java Web框架》

    内容提要 <架构探险--从零开始写Java Web框架>首先从一个简单的 Web 应用开始,让读者学会如何使用 IDEA.Maven.Git 等开发工具搭建 Java Web 应用:接着通 ...

  8. ThreadLocal 简介 案例 源码分析 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  9. Unity 游戏框架搭建 2018 (一) 架构、框架与 QFramework 简介

    约定 还记得上版本的第二十四篇的约定嘛?现在出来履行啦~ 为什么要重制? 之前写的专栏都是按照心情写的,在最初的时候笔者什么都不懂,而且文章的发布是按照很随性的一个顺序.结果就是说,大家都看完了,都还 ...

随机推荐

  1. Python3基础 list append 向尾部添加一个元素

             Python : 3.7.0          OS : Ubuntu 18.04.1 LTS         IDE : PyCharm 2018.2.4       Conda ...

  2. 【Dalston】【第三章】声明式服务调用(Feign)

    当我们通过RestTemplate调用其它服务的API时,所需要的参数须在请求的URL中进行拼接,如果参数少的话或许我们还可以忍受,一旦有多个参数的话,这时拼接请求字符串就会效率低下,并且显得好傻.那 ...

  3. LuoguP3792 由乃与大母神原型和偶像崇拜

    题目地址 题目链接 题解 由乃题还是毒瘤啊orz 显然的一个结论是,如果保证不重复,维护区间min,max然后判断max-min+1==r-l+1是否成立即可 但是有重复 于是就要orz题解区的各位大 ...

  4. asp.net core mvc 中在C# 代码中写 js 或html 文本

    https://blog.csdn.net/orichisonic/article/details/62046621 使用<text>这个伪元素来强制Razor从编译模式返回到内容模式: ...

  5. Sql Ado.net 学习笔记之连接字符串

    https://www.cnblogs.com/heng95/p/5902019.html 连接字符串 SQL Client .net数据提供程序在连接到数据库时极其灵活,它提供了多种用以生成连接字符 ...

  6. P1494 [国家集训队]小Z的袜子(莫队算法)

    莫队板子 代码 #include <cstdio> #include <algorithm> #include <cstring> #include <cma ...

  7. 浅谈IIS 和 asp.net的应用之间的关系

    IIS可以理解为一个web服务器. 用于提供web相关的各种服务. IIS6.0中添加了一个新的功能, application pool. application pool的作用是将运行在同一个ser ...

  8. facebook api之Access Tokens

    Access Tokens When someone connects with an app using Facebook Login and approves the reqest for per ...

  9. WijmoJS 全面支持 Angular 7

    概述 首先恭喜Angular团队发布Angular 7.0.0版本! 对于大多数开发人员,只需要执行一个命令就可以更新到Angular 7: ng update \@angular/cli \@ang ...

  10. 【NOI 2016】优秀的拆分

    Problem Description 如果一个字符串可以被拆分为 \(AABB\) 的形式,其中 \(A\) 和 \(B\) 是任意非空字符串,则我们称该字符串的这种拆分是优秀的. 例如,对于字符串 ...