ThreadLocal学习笔记

ThreadLocal的作用

让每个线程都有自己的local值,为每一个线程都提供了变量的副本。这就可以让每一个线程在某个时间访问到的对象都是该线程自有的,从而让线程之间不会相互干扰。

1、线程并发:在多线程并发的场景下使用ThreadLocal

2、传递参数:可以使用ThreadLocal来传递参数,不仅可以保证不同组件之间使用的是同一个线程中的参数,还可以减少直接传参带来的耦合度提高的问题

3、线程隔离:让不同线程之间的变量相互独立

与Synchronized区别

Synchronized实现线程隔离的原理是采用同步锁,通过对特定的资源进行上锁的操作来实现。

多个线程会因为要获取上锁了的资源而排队执行,那么程序会失去原有的并发性。

而ThreadLocal是通过为Thread中的资源创建多个副本来实现的,每个线程都有属于自己的一份资源副本,互不干扰。这样就避免了排队,保障了并发性,同时也还将线程隔离开来。这种就是典型的用空间换取时间的方法。

下面是经典的银行转账例子:

Jimmy哥向Yuki转账

我们可以用synchronized来实现线程同步:

package JDBC;

import Adui.Dui;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ResourceBundle;

//事务控制,防止数据出错
public class TaskControl {
    public static void main(String[] args) throws Exception {
        ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
        String url = bundle.getString("url");
        String username = bundle.getString("username");
        String password = bundle.getString("password");
        Class.forName("com.mysql.cj.jdbc.Driver");
        try {
            DUtil.connect_D(url, username, password);
            //开启事务,将JDBC的自动事务提交修改为false
            DUtil.connection.setAutoCommit(false);

            synchronized (TaskControl.class) {
                //事务一,支出
                String sql1 = "update money set balance = balance - ? where name = ?";
                PreparedStatement ps1 = DUtil.connection.prepareStatement(sql1);
                ps1.setInt(1, 10);
                ps1.setString(2, "Jimmy");
                ps1.executeUpdate();

                //模拟异常发生
//            String s = null;
//            s.toString();

                //事务二,收入
                String sql2 = "update money set balance = balance + ? where name = ?";
                PreparedStatement ps2 = DUtil.connection.prepareStatement(sql2);
                ps2.setInt(1, 10);
                ps2.setString(2, "Yuki");
                ps2.executeUpdate();

                //提交事务,事务结束
                DUtil.connection.commit();
            }

        } catch (Exception e) {
            //若有异常发生,回滚事务。然后事务结束
            try {
                DUtil.connection.rollback();
            } catch (SQLException ex) {
                throw new RuntimeException(ex);
            }
            e.printStackTrace();
        } finally {
            DUtil.close_D();
        }
    }
}

但是这样会牺牲掉一定的性能。

如果使用ThreadLocal的话可以这样写,将连接对象放入ThreadLocal中进行隔离,只要需要使用连接对象就用get方法进行取出,保证每一个线程获取到的连接都是该线程私有的。

package JDBC;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ResourceBundle;

//事务控制,防止数据出错
public class TaskControl {
    private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();

    public static void main(String[] args) throws Exception {
        ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
        String url = bundle.getString("url");
        String username = bundle.getString("username");
        String password = bundle.getString("password");
        Class.forName("com.mysql.cj.jdbc.Driver");
        try {
            Connection connection = DUtil.connect_D(url, username, password);
            connectionHolder.set(connection);

            //开启事务,将JDBC的自动事务提交修改为false
            connectionHolder.get().setAutoCommit(false);

            //事务一,支出
            String sql1 = "update money set balance = balance - ? where name = ?";
            PreparedStatement ps1 = connectionHolder.get().prepareStatement(sql1);
            ps1.setInt(1, 10);
            ps1.setString(2, "Jimmy");
            ps1.executeUpdate();

            //模拟异常发生
            // String s = null;
            // s.toString();

            //事务二,收入
            String sql2 = "update money set balance = balance + ? where name = ?";
            PreparedStatement ps2 = connectionHolder.get().prepareStatement(sql2);
            ps2.setInt(1, 10);
            ps2.setString(2, "Yuki");
            ps2.executeUpdate();

            //提交事务,事务结束
            connectionHolder.get().commit();
        } catch (Exception e) {
            //若有异常发生,回滚事务。然后事务结束
            try {
                if (connectionHolder.get() != null) {
                    connectionHolder.get().rollback();
                }
            } catch (SQLException ex) {
                throw new RuntimeException(ex);
            }
            e.printStackTrace();
        } finally {
            if (connectionHolder.get() != null) {
                DUtil.close_D();
                connectionHolder.remove();
            }
        }
    }
}

ThreadLocald的内部结构

内部结构如上图所示

Thread中有其中的一个属性是ThreadLocalMap,这个Map的结构与HashMap类似,里面存放的是<Key,Value>结构的数据Entry,ThreadLocal作为Key。

这些我们可以查阅源码来理解。

ThreadLocal最常用的方法

1、set方法

这是将指定对象存入到ThreadLocal中的方法。

具体涉及到的源码如下:

 public void set(T value) {
//先获取当前的线程对象
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
//做一个map的判空操作,如果map不存在则需要创建,否则直接将对象存入
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }


//创建Map后会将初始对象存入
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

//这里是具体的存放操作,会对Map中的key
        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.refersTo(key)) {
                    e.value = value;
                    return;
                }
                //这里做一个过时的判断,如果key的值是null则过时,后续会被清理
                if (e.refersTo(null)) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            //清理过时条目并重新哈希
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

2、get方法

获取指定数据的方法

具体涉及的源码如下:

 //获取数据的方法
   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;
            }
        }
        //如果 ThreadLocalMap 或 Entry 为空,设置初始值并返回该初始值。
        return setInitialValue();
    }


//可以看到在Map中获取值的时候是以ThreadLocal作为key的
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.refersTo(key))
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

//设置初始值,这个默认是null,可以通过重写该方法来修改初始值
    protected T initialValue() {
        return null;
    }

3、remove方法

在threadlocal资源使用完毕后将其释放,防止内存泄漏的发生。

  //移除ThreadLocal对象的方法
 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }

        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.refersTo(key)) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

还有一个问题就是为什么Entry要继承WeakReference?

首先,垃圾回收机制是会回收具有弱引用的对象的。而只要有强引用指向的对象是不会被回收的。即使ThreadLocal对象不再被程序中的其他部分引用,它仍然会被Entry强引用,从而无法被垃圾回收。这会导致内存泄漏。通过使用弱引用,当ThreadLocal对象不再被其他强引用持有时,垃圾回收器可以回收它,从而避免内存泄漏。

但是Entry中的value还在,它是被Map强引用的,所以还是会有风险。

还记得get和set是有一次对key判空的操作吧?

也就是说,如果我们忘记remove ThreadLocal,后续在下一次的set或者get过程中,也会对key为null的进行清理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值