Java多线程 ThreadLocal原理解析_java

Java多线程 ThreadLocal原理解析_java

1、什么是变量

变量,线程局部变量,同一个 所包含的对象java threadlocal,在不同的 中有不同的副本。

这里有几点需要注意:

提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。 变量通常被 修饰。当一个线程结束时,它所使用的所有 相对的实例副本都可被回收。

总的来说, 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

2、实现原理

首先 是一个泛型类Java多线程 ThreadLocal原理解析_java,保证可以接受任何类型的对象。

因为一个线程内可以存在多个 对象,所以其实是 内部维护了一个 Map ,这个 Map 不是直接使用的 ,而是 实现的一个叫做 的静态内部类。而我们使用的 get() 、set() 方法其实都是调用了这个类对应的 get() 、set() 方法。

例如下面的 set 方法:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

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;
            }
        }
        return setInitialValue();
    }

方法:

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

是个静态的内部类:

static class ThreadLocalMap {
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;
        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
        /**
         * The number of entries in the table.
         */
        private int size = 0;
        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0
        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
        /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 = 0) ? i - 1 : len - 1);
        }
        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
...
}

最终的变量是放在了当前线程的 中,并不是存在 上,可以理解为只是的封装java threadlocal,传递了变量值。

3、内存泄漏问题

实际上 中使用的 key 为 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 中使用这个 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

实现中已经考虑了这种情况,在调用 set() 、get() 、() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 () 方法,并且之后也不再调用 get() 、set() 、() 方法的情况下。

4、使用场景

如上文所述, 适用于如下两种场景

每个线程需要有自己单独的实例

实例需要在多个方法中共享,但不希望被多线程共享

对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。 可以以非常方便的形式满足该需求。

对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。 使得代码耦合度更低,且实现更优雅。

1)存储用户

一个简单的用来存储的例子:

private static final ThreadLocal threadSession = new ThreadLocal();
    public static Session getSession() throws InfrastructureException {
        Session s = (Session) threadSession.get();
        try {
            if (s == null) {
                s = getSessionFactory().openSession();
                threadSession.set(s);
            }
        } catch (HibernateException ex) {
            throw new InfrastructureException(ex);
        }
        return s;
    }

2)解决线程安全的问题

比如Java7中的不是线程安全的,可以用来解决这个问题:

public class DateUtil {
    private static ThreadLocal format1 = new ThreadLocal() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
    public static String formatDate(Date date) {
        return format1.get().format(date);
    }
}

这里的.()就是线程安全的了。(Java8里的

java.time..

() 是线程安全的,Joda time里的也是线程安全的)。

public class Context {
    private String name;
    private String cardId;
    public String getCardId() {
        return cardId;
    }
    public void setCardId(String cardId) {
        this.cardId = cardId;
    }
    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

public class ExecutionTask implements Runnable {
    private QueryFromDBAction queryAction = new QueryFromDBAction();
    private QueryFromHttpAction httpAction = new QueryFromHttpAction();
    @Override
    public void run() {
        final Context context = new Context();
        queryAction.execute(context);
        System.out.println("The name query successful");
        httpAction.execute(context);
        System.out.println("The cardId query successful");
        System.out.println("The Name is " + context.getName() + " and CardId " + context.getCardId());
    }
}

public class QueryFromDBAction {
    public void execute(Context context) {
        try {
            Thread.sleep(1000L);
            String name = "Jack " + Thread.currentThread().getName();
            context.setName(name);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
    public void execute(Context context) {
        String name = context.getName();
        String cardId = getCardId(name);
        context.setCardId(cardId);
    }
    private String getCardId(String name) {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "444555" + Thread.currentThread().getId();
    }
}

public class ContextTest {
    public static void main(String[] args) {
        IntStream.range(1, 5)
                .forEach(i ->
                        new Thread(new ExecutionTask()).start()
                );
    }
}

The name query

The name query

The name query

The name query

The query

The Name is Jack -0 and

The query

The Name is Jack -1 and

The query

The Name is Jack -2 and

The query

The Name is Jack -3 and

问题:需要在每个调用的方法中传入进去

public void execute(Context context) {
}

3)使用重新设计一个上下文设计模式

public final class ActionContext {
    private static final ThreadLocal threadLocal = new ThreadLocal() {
        @Override
        protected Object initialValue() {
            return new Context();
        }
    };
    public static ActionContext getActionContext() {
        return ContextHolder.actionContext;
    }
    public Context getContext() {
        return threadLocal.get();
    }
    private static class ContextHolder {
        private final static ActionContext actionContext = new ActionContext();
    }
}

public class Context {
    private String name;
    private String cardId;
    public String getCardId() {
        return cardId;
    }
    public void setCardId(String cardId) {
        this.cardId = cardId;
    }
    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

public class ExecutionTask implements Runnable {
    private QueryFromDBAction queryAction = new QueryFromDBAction();
    private QueryFromHttpAction httpAction = new QueryFromHttpAction();
    @Override
    public void run() {
        queryAction.execute();
        System.out.println("The name query successful");
        httpAction.execute();
        System.out.println("The cardId query successful");
        final Context context = ActionContext.getActionContext().getContext();
        System.out.println("The Name is " + context.getName() + " and CardId " + context.getCardId());
    }
}

public class QueryFromDBAction {
    public void execute() {
        try {
            Thread.sleep(1000L);
            String name = "Jack " + Thread.currentThread().getName();
            ActionContext.getActionContext().getContext().setName(name);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class {

    public void execute() {
        Context context = ActionContext.getActionContext().getContext();
        String name = context.getName();
        String cardId = getCardId(name);
        context.setCardId(cardId);
    }
    private String getCardId(String name) {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "444555" + Thread.currentThread().getId();
    }
}

public class ContextTest {
    public static void main(String[] args) {
        IntStream.range(1, 5)
                .forEach(i ->
                        new Thread(new ExecutionTask()).start()
                );
    }
}

The name query

The name query

The name query

The name query

The query

The Name is Jack -3 and

The query

The query

The Name is Jack -0 and

The query

The Name is Jack -2 and

The Name is Jack -1 and

这样写 执行过程中不会看到的定义和声明

注意:在使用之前记得将上个线程中旧值清除调,否则会重复调用(比如线程池操作)

4)注意事项脏数据

线程复用会产生脏数据。由于结程池会重用对象java threadlocal,那么与绑定的类的静态属性变量也会被重用。如果在实现的线程run()方法体中不显式地调用() 清理与线程相关的信息,那么倘若下一个结程不调用set() 设置初始值,就可能get() 到重用的线程信息,包括 所关联的线程对象的value值。

内存泄漏

通常我们会使用使用关键字来修饰(这也是在源码注释中所推荐的)。在此场景下,其生命周期就不会随着线程结束而结束,寄希望于对象失去引用后,触发弱引用机制来回收Entry的Value就不现实了。如果不进行() 操作,那么这个线程执行完成后,通过对象持有的对象是不会被释放的。

以上两个问题的解决办法很简单,就是在每次用完时, 必须要及时调用 ()方法清理。

父子线程共享线程变量

很多场景下通过来透传全局上下文,会发现子线程的value和主线程不一致。比如用来存储监控系统的某个标记位,暂且命名为。某次请求下所有的都是一致的,以获得可以统一解析的日志文件。但在实际开发过程中,发现子线程里的为nullJava多线程 ThreadLocal原理解析_java,跟主线程的并不一致。这就需要使用al来解决父子线程之间共享线程变量的问题,使整个连接过程中的一致。

公告:
1. 本站所有资源来源于用户上传和网络,如有侵权请联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系站长处理!
6. 本站不售卖代码,资源标价只是站长收集整理的辛苦费!如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。
7. 站长QQ号码 2205675299

资源库 - 资源分享下载网 » Java多线程 ThreadLocal原理解析_java

常见问题FAQ

关于资源售价和售后服务的说明?
本站所有资源的标价均为本站收集资源的辛苦费,不代表资源本身的价值。软件是高智慧高价值的商品,不可能是白菜价。本站资源标价只是赞助费用,收取的赞助费仅用来维持本站的日常运营!毕竟收集整理资料需要投入云计算资源和站长大量的精力。
代码有没有售后服务和技术支持?
由于代码的运行具有不可预见性,本站不保证代码完整可运行,不提供技术支持和售后服务。 本站原创代码都是站长自己开发的,可以有偿提供技术支持服务。 网站里标明【亲测】的代码都是站长亲测过的,其他的代码由于精力有限,没有一一测试,不能保证代码就一定能够使用,更没有技术支持服务,下载前请自行斟酌。
有没有搭建服务?
由于搭建服务比较费时费力,所以本站除了原创代码外均不提供搭建服务。本站分享代码纯属兴趣爱好,不以盈利为目的,请勿咨询有没有搭建服务,谢谢理解。
链接地址失效了怎么办?
请带上资源链接地址联系客服,工作时间内我们看到后将第一时间回复。
关于解压密码
本站资源一般都没有加密,如果发现需要解压密码的,那么就输入 hao.35dc.com 试试。

发表评论

资源库,由老程序员细心甄别、精心筛选,只为提供优质的源码资源

关于我们 联系我们