【JUC】ThreadLocal

ThreadLocal解决线程隔离问题

ThreadLocal类可以高效解决线程间数据隔离的问题
先看效果:

  • 未使用ThreadLocal
    所有线程在对同一个对象的同一个属性进行读写操作,产生数据冲突
public class ThreadLocalDemo {
    private String var;

    public String getVar() {
        return var;
    }

    public void setVar(String var) {
        this.var = var;
    }

    public static void main(String[] args) {
        ThreadLocalDemo demo = new ThreadLocalDemo();
        for(int i = 0; i < 20; i++) {
            Thread thread = new Thread(() -> {
            	// 所有线程都在操作同一个对象demo的var属性,产生并发过程中的数据冲突
                demo.setVar(Thread.currentThread().getName() + "的数据");
                System.out.println("-----------------------"); // 本行起到sleep(time)的作用
                System.out.println(Thread.currentThread().getName() + " ---> " + demo.getVar());
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }

}

输出:

线程1 ---> 线程2的数据
-----------------------
线程2 ---> 线程3的数据    // 出现和期待结果不一样的情况
-----------------------
线程4 ---> 线程4的数据
-----------------------
线程0 ---> 线程4的数据
-----------------------
线程3 ---> 线程4的数据

可以看到并发执行时出现了问题,var的值和预期结果不一致

  • 使用ThreadLocal
    仅改动get/set函数
public class ThreadLocalDemo {
    private String var;

    ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public String getVar() {
        return threadLocal.get(); // 通过ThreadLocal获取属性值
    }

    public void setVar(String var) {
        threadLocal.set(var); // 通过ThreadLocal设置属性值
    }

    public static void main(String[] args) {
        ThreadLocalDemo demo = new ThreadLocalDemo();
        for(int i = 0; i < 20; i++) {
            Thread thread = new Thread(() -> {
                demo.setVar(Thread.currentThread().getName() + "的数据");
                System.out.println("-----------------------"); // 本行起到sleep(time)的作用
                System.out.println(Thread.currentThread().getName() + " ---> " + demo.getVar());
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }

}

不会再出现并发过程中数据冲突的问题,每次都会取得期望的数据

-----------------------
-----------------------
-----------------------
线程3 ---> 线程3的数据
-----------------------
线程1 ---> 线程1的数据
-----------------------
-----------------------
线程6 ---> 线程6的数据
线程2 ---> 线程2的数据
线程0 ---> 线程0的数据
线程4 ---> 线程4的数据
-----------------------
线程8 ---> 线程8的数据
-----------------------
线程5 ---> 线程5的数据
-----------------------
线程7 ---> 线程7的数据
-----------------------
线程9 ---> 线程9的数据
-----------------------
线程10 ---> 线程10的数据
-----------------------
线程12 ---> 线程12的数据
-----------------------
-----------------------
线程14 ---> 线程14的数据
-----------------------
线程15 ---> 线程15的数据
-----------------------
线程18 ---> 线程18的数据
-----------------------
线程16 ---> 线程16的数据
-----------------------
线程11 ---> 线程11的数据
线程13 ---> 线程13的数据
-----------------------
线程17 ---> 线程17的数据
-----------------------
线程19 ---> 线程19的数据

下面通过源码看下为什么ThreadLocal可以解决线程间数据安全问题

  • 每个Thread对象都持有一个ThreadLocal.ThreadLocalMap对象
  • ThreadLocalMap是ThreadLocal的一个内部类
  • ThreadLocalMap保存了很多个键值对Entry<ThreadLocal, value>
class Thread implements Runnable {
    ...
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ...
}



public class ThreadLocal<T> {
	...
	...
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    static class ThreadLocalMap {
		...
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        ...
        private Entry[] table;
        ...
}

下面结合源码逐步分析main()执行过程:

main: 
demo.setVar(Thread.currentThread().getName() + "的数据");

// 会执行如下代码:
ThreadLocalpublic void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前线程
     ThreadLocalMap map = getMap(t); // 获取当前线程的threadLocals属性(类型是ThreadLocal.ThreadLocalMap)
     if (map != null) 
         map.set(this, value); // 在当前线程中放入Entry<当前ThreadLocal, var>
     else
         createMap(t, value);
 }

可以看到使用ThreadLocal本质是在需要为线程和变量设置关联关系时,在每个线程的threadLocals属性中加入一个Entry<ThreadLocal, value>来创建一个value的副本,这样每个线程内部都有一份变量,互不干扰,通过这样一种方式完成了线程和变量的绑定,一个线程可以绑定多个变量。

下面看看如何获取绑定的变量值

main:
System.out.println(Thread.currentThread().getName() + " ---> " + demo.getVar());

ThreadLocalDemo:
public String getVar() {
  	return threadLocal.get();
}

ThreadLocalpublic T get() {
   Thread t = Thread.currentThread(); // 获取当前线程
    ThreadLocalMap map = getMap(t); // 获取当前线程的threadLocals属性
    if (map != null) { // 遍历threadLocals中所有Entry以 this(一个ThreadLocal对象)为Key值寻找
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

综上可以看到ThreadLocal本质就是为每个线程创建一个变量副本,线程之间操作的同名变量地址不同,实现了线程间的数据隔离

ThreadLocal的高效性

上述示例当然可以用synchronized来保证数据的安全

public static void main(String[] args) throws Exception {
        ThreadLocalDemo demo = new ThreadLocalDemo();
        for(int i = 0; i < 20; i++) {
            Thread thread = new Thread(() -> {
                synchronized (ThreadLocalDemo.class) {
                    demo.setVar(Thread.currentThread().getName() + "的数据");
                    System.out.println("-----------------------"); // 本行起到sleep(time)的作用
                    System.out.println(Thread.currentThread().getName() + " ---> " + demo.getVar());
                }
            });
            thread.setName("线程" + i);
            thread.start();

        }
    }

但是显然,这样丧失了并发性,也会频繁的加锁,解锁增加上下文切换的开销

和synchronized保证数据隔离的方法不同,ThreadLocal以空间换时间,每个线程存有一个变量副本,所以效率上会更高。
synchronized侧重的是保证线程间对临界资源的同步访问问题,ThreadLocal侧重的是保证线程间数据的隔离。

ThreadLocal应用

一个经典的使用场景是,在数据库的交互过程中使用ThreadLocal来降低代码的耦合度
示例如下,一个比较常见的转账的service和dao伪代码

Service:
public class AccountService {

    private AccountDAO accountDAO = new AccountDAO();
    public boolean tranfer(String outUser, String inUser, int money) {

        Connection connection = null;
        connection = JdbcUtils.getConnection(); // 获取连接
        try {
            connection.setAutoCommit(false); // 开启事务
            accountDAO.sub(outUser, money, connection); // outUser账号金额减少
            accountDAO.add(inUser, money, connection); // inUser账号金额增加
            connection.commit(); // 提交事务
            connection.close(); 
        } catch (SQLException e) {
            try {
                connection.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }
        return false;
    }
}

Dao:
public class AccountDAO {

    public void add(String userName, int money, Connection connection) {
		use connection do someting...
    }

    public void sub(String userName, int money, Connection connection) {
		use connection do someting...
    }
}

JdbcUtilspublic class JdbcUtils {
    public static  Connection getConnection() {
        从连接池获取连接并返回
    }
}

在一个事务中,Service层会在DAO层调用之前开启事务,保证下面的转账动作(包括转入和转出动作)是原子操作,DAO层在执行操作时,也必须要保证和Service开启事务使用的connection是同一个对象,否则Service层开启事务的动作毫无意义。这就导致Service层必须将开启事务所使用的connection作为参数传入DAO层,结果就是在设计DAO层时,形参列表多了一个参数,Service层和DAO层高度耦合。

那么为什么不直接在DAO层进行connection的获取和事务的开启呢?
可以看下这样做的结果:

Dao:
public class AccountDAO {

    public void add(String userName, int money) {
    	Connection connection = null;
        connection = JdbcUtils.getConnection(); // 获取连接
        connection.setAutoCommit(false); // 开启事务
		do someting...
		connection.commit(); // 提交事务
        connection.close(); 
    }

    public void sub(String userName, int money) {
    	Connection connection = null;
        connection = JdbcUtils.getConnection(); // 获取连接
        connection.setAutoCommit(false); // 开启事务
		do someting...
		connection.commit(); // 提交事务
        connection.close(); 
    }
}

显然这样会导致每进行一次DAO层操作都要开启关闭一次连接,频繁的开启关闭本身就是对资源的一种浪费。同时,明显DAO层代码变得臃肿。
这也是为什么在数据库操作中,事务的开启关闭等对connection对象的操作放在Service层的原因。
那么现在要到达以下目的:

  1. 减少Service层和DAO层耦合关系,让DAO层不需要在入参中加入connection
  2. 保证Service层和DAO层使用同一个connection

这就要使用到ThreadLocal
对JdbcUtils进行如下修改:

public class JdbcUtils {
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
    private static DataSource ds;
    static {
        try {
            ds = DruidDataSourceFactory.createDataSource(new Properties());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static  Connection getConnection() throws SQLException {
        Connection connection = threadLocal.get(); // 使用ThreadLocal的get()方法获取
        if(connection == null) {
            connection = ds.getConnection();
            threadLocal.set(connection); // 对当前Thread和connection进行绑定
        }
        return connection;
    }

}

这样DAO层和Service层就可以如下修改:

Service:
public class AccountService {

    private AccountDAO accountDAO = new AccountDAO();
    public boolean tranfer(String outUser, String inUser, int money) {

        Connection connection = null;
        connection = JdbcUtils.getConnection(); // 获取连接
        try {
            connection.setAutoCommit(false); // 开启事务
            accountDAO.sub(outUser, money); // outUser账号金额减少
            accountDAO.add(inUser, money); // inUser账号金额增加
            connection.commit(); // 提交事务
            connection.close(); 
        } catch (SQLException e) {
            try {
                connection.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }
        return false;
    }
}

Dao:
public class AccountDAO {

    public void add(String userName, int money) throws SQLException{
    	Connection connection = JdbcUtils.getConnection();
		use connection do someting...
    }

    public void sub(String userName, int money) throws SQLException{
    	Connection connection = JdbcUtils.getConnection();
		use connection do someting...
    }
}

显然DAO层使用的connection一定是Service层使用的connection,原理参见前文

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值