ThreadLocal 概述
ThreadLocal类用来提供线程内部的局部变量,不同的线程之间不会相互干扰
这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量
在线程的生命周期内起作用,可以减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度
可把ThreadLocal当成是JavaWeb中的域
使用
常用方法
方法名 | 描述 |
ThreadLocal() | 创建ThreadLocal对象 |
public void set( T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public T remove() | 移除当前线程绑定的局部变量,该方法可以帮助JVM进行GC |
protected T initialValue() | 返回当前线程局部变量的初始值 |
作用举例:
分析以下Dao层代码存在什么问题:
其中,DBUtil.getConnection();始终都是获取返回获取的新的Connection对象。
package com.powernode.bank.mvc;
import com.powernode.bank.utils.DBUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class AccountDao {
/**
* 插入账户信息
* @param act 账户信息
* @return 1表示插入成功
*/
public int insert(Account act) {
Connection conn = null;
PreparedStatement ps = null;
int count = 0;
try {
conn = DBUtil.getConnection();
String sql = "insert into t_act(actno, balance) values(?,?)";
ps = conn.prepareStatement(sql);
ps.setString(1, act.getActno());
ps.setDouble(2, act.getBalance());
count = ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(conn, ps, null);
}
return count;
}
/**
* 根据主键删除账户
* @param id 主键
* @return
*/
public int deleteById(Long id){
Connection conn = null;
PreparedStatement ps = null;
int count = 0;
try {
conn = DBUtil.getConnection();
String sql = "delete from t_act where id = ?";
ps = conn.prepareStatement(sql);
ps.setLong(1, id);
count = ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(conn, ps, null);
}
return count;
}
/**
* 更新账户
* @param act
* @return
*/
public int update(Account act) {
Connection conn = null;
PreparedStatement ps = null;
int count = 0;
try {
conn = DBUtil.getConnection();
String sql = "update t_act set balance = ? , actno = ? where id = ?";
ps = conn.prepareStatement(sql);
ps.setDouble(1, act.getBalance());
ps.setString(2, act.getActno());
ps.setLong(3, act.getId());
count = ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(conn, ps, null);
}
return count;
}
/**
* 根据账号查询账户
* @param actno
* @return
*/
public Account selectByActno(String actno){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
Account act = null;
try {
conn = DBUtil.getConnection();
String sql = "select id,balance from t_act where actno = ?";
ps = conn.prepareStatement(sql);
ps.setString(1, actno);
rs = ps.executeQuery();
if (rs.next()) {
Long id = rs.getLong("id");
Double balance = rs.getDouble("balance");
// 将结果集封装成java对象
act = new Account();
act.setId(id);
act.setActno(actno);
act.setBalance(balance);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(conn, ps, rs);
}
return act;
}
/**
* 获取所有的账户
* @return
*/
public List<Account> selectAll() {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
List<Account> list = new ArrayList<>();
try {
conn = DBUtil.getConnection();
String sql = "select id,actno,balance from t_act";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()) {
// 取出数据
Long id = rs.getLong("id");
String actno = rs.getString("actno");
Double balance = rs.getDouble("balance");
// 封装对象
/*Account account = new Account();
account.setId(id);
account.setActno(actno);
account.setBalance(balance);*/
Account account = new Account(id, actno, balance);
// 加到List集合
list.add(account);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(conn, ps, rs);
}
return list;
}
}
如上述Dao层(数据访问)类中,该类具有一系列对数据库进行操作的方法,我们会在Service层(业务逻辑层)中对其进行方法的调用,由于该类需要Connection对象来进行prepareStatement方法的调用,问题就在于:DBUtil.getConnection();始终都是获取返回获取的新的Connection对象,而我们是在Service层调用Connection响应的方法进行事务的控制,这就导致了二者不是相同的Connection对象,无法进行事务的控制。
改进:(通过传参来使得Service层与Dao层的Connection对象相同)
public class AccountDao {
/**
* 插入账户信息
* @param act 账户信息
* @return 1表示插入成功
*/
public int insert(Account act, Connection conn) {
PreparedStatement ps = null;
int count = 0;
try {
String sql = "insert into t_act(actno, balance) values(?,?)";
ps = conn.prepareStatement(sql);
ps.setString(1, act.getActno());
ps.setDouble(2, act.getBalance());
count = ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(null, ps, null);
}
return count;
}
/**
* 根据主键删除账户
* @param id 主键
* @return
*/
public int deleteById(Long id, Connection conn){
PreparedStatement ps = null;
int count = 0;
try {
String sql = "delete from t_act where id = ?";
ps = conn.prepareStatement(sql);
ps.setLong(1, id);
count = ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(null, ps, null);
}
return count;
}
/**
* 更新账户
* @param act
* @return
*/
public int update(Account act, Connection conn) {
PreparedStatement ps = null;
int count = 0;
try {
System.out.println(conn);
String sql = "update t_act set balance = ? , actno = ? where id = ?";
ps = conn.prepareStatement(sql);
ps.setDouble(1, act.getBalance());
ps.setString(2, act.getActno());
ps.setLong(3, act.getId());
count = ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(null, ps, null);
}
return count;
}
/**
* 根据账号查询账户
* @param actno
* @return
*/
public Account selectByActno(String actno, Connection conn){
PreparedStatement ps = null;
ResultSet rs = null;
Account act = null;
try {
System.out.println(conn);
String sql = "select id,balance from t_act where actno = ?";
ps = conn.prepareStatement(sql);
ps.setString(1, actno);
rs = ps.executeQuery();
if (rs.next()) {
Long id = rs.getLong("id");
Double balance = rs.getDouble("balance");
// 将结果集封装成java对象
act = new Account();
act.setId(id);
act.setActno(actno);
act.setBalance(balance);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(null, ps, rs);
}
return act;
}
/**
* 获取所有的账户
* @return
*/
public List<Account> selectAll(Connection conn) {
PreparedStatement ps = null;
ResultSet rs = null;
List<Account> list = new ArrayList<>();
try {
String sql = "select id,actno,balance from t_act";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()) {
// 取出数据
Long id = rs.getLong("id");
String actno = rs.getString("actno");
Double balance = rs.getDouble("balance");
// 封装对象
/*Account account = new Account();
account.setId(id);
account.setActno(actno);
account.setBalance(balance);*/
Account account = new Account(id, actno, balance);
// 加到List集合
list.add(account);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DBUtil.close(null, ps, rs);
}
return list;
}
}
但这样代码不美观且麻烦。
由于在Service层中调用Dao层,两者属于同一个线程,使用我们可以使用ThreadLocal,来实现不传参的情况下获取到的是同一个Connection。
hreadLocalMap 模拟实现
JDK8 之前的设计
每个ThreadLocal都创建一个ThreadLocalMap,用线程作为ThreadLocalMap的key,要存储的局部变量作为ThreadLocalMap的value,这样就能达到各个线程的局部变量隔离的效果
public class DBUtil {
// 静态变量特点:类加载时执行,并且只执行一次。
// 全局的大Map集合
private static MyThreadLocal<Connection> local = new MyThreadLocal<>();
/**
* 每一次都调用这个方法来获取Connection对象
* @return
*/
public static Connection getConnection(){
Connection connection = local.get();
if (connection == null) {
// 第一次调用:getConnection()方法的时候,connection一定是空的。
// 空的就new一次。
connection = new Connection();
// 将new的Connection对象绑定到大Map集合中。
local.set(connection);
}
return connection;
}
}
import java.util.HashMap;
import java.util.Map;
/**
* 自定义一个ThreadLocal类
*/
public class MyThreadLocal<T> {
/**
* 所有需要和当前线程绑定的数据要放到这个容器当中
*/
private Map<Thread, T> map = new HashMap<>();
/**
* 向ThreadLocal中绑定数据
*/
public void set(T obj){
map.put(Thread.currentThread(), obj);
}
/**
* 从ThreadLocal中获取数据
* @return
*/
public T get(){
return map.get(Thread.currentThread());
}
/**
* 移除ThreadLocal当中的数据
*/
public void remove(){
map.remove(Thread.currentThread());
}
}
Thread是lang包下的。lang包下的都不需要导直接可以用。
由于每一次都是调用DBUtil的getConnection方法来获取Connection对象,而DBUtil的getConnection方法调用MyThreadLocal.get();方法,所以我们所需要Connection的类、DBUtil类、MyThreadLocal类都是同一线程,而MyThreadLocal类通过Map使得线程与Connection对象是一一对应的,只要线程相同,那么Connection对象相同。
基于ThreadLocal改进的DBUtil
package com.powernode.bank.utils;
import java.sql.*;
import java.util.ResourceBundle;
public class DBUtil {
private static ResourceBundle bundle = ResourceBundle.getBundle("resources/jdbc");
private static String driver = bundle.getString("driver");
private static String url = bundle.getString("url");
private static String user = bundle.getString("user");
private static String password = bundle.getString("password");
// 不让创建对象,因为工具类中的方法都是静态的。不需要创建对象。
// 为了防止创建对象,故将构造方法私有化。
private DBUtil(){}
// DBUtil类加载时注册驱动
static {
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
// 这个对象实际上在服务器中只有一个。
private static ThreadLocal<Connection> local = new ThreadLocal<>();
/**
* 这里没有使用数据库连接池,直接创建连接对象。
* @return 连接对象
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
Connection conn = local.get();
if (conn == null) {
conn = DriverManager.getConnection(url, user, password);
local.set(conn);
}
return conn;
}
/**
* 关闭资源
* @param conn 连接对象
* @param stmt 数据库操作对象
* @param rs 结果集对象
*/
public static void close(Connection conn, Statement stmt, ResultSet rs){
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (conn != null) {
try {
conn.close();
// 思考一下:为什么conn关闭之后,这里要从大Map中移除呢?
// 根本原因是:Tomcat服务器是支持线程池的。也就是说一个人用过了t1线程,t1线程还有可能被其他用户使用。从而拿到一个已经关闭了的Connection,引发系统错误
// 也就是说当close之后,这个connection对象就已经失效了,存在里边儿也没有意义了;如果还有下一个新的且相同线程进行进来,那么如果不remove,那么会访问到已经失效的connection
local.remove();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
关闭Connection对象的时候记得把它从ThreadLocal的Map集合中移除。