zoukankan      html  css  js  c++  java
  • 线程封闭

    一、什么是线程封闭

           在多线程编程中,在对共享的数据进行访问时,通常需要进行同步。一种避免使用同步的方式就是不共享数据。如果仅仅只是在单线程内访问数据,那么就不需要进行同步。这种技术就叫做线程封闭(Thread Confinement),这是实现线程安全性的最简单方式之一。某个对象封闭在一个线程中时,这种用法将自动实现线程安全。

    二、实现线程封闭的方式

    1、Ad-hoc线程封闭

    Ad-hoc线程封闭是指,维护线程封闭的职责完全由程序实现来承担。如可见性修饰符或局部变量能将对象封闭在目标线程中。就这么简单,不要被它看不懂的名称所迷惑。Ad-hoc线程封闭非常脆弱,因此尽量少使用。

    2、栈封闭

            这里的栈指的就是JAVA虚拟机栈。JAVA方法是在JAVA虚拟机栈中执行的,每个方法执行都会在虚拟机栈中创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成就对应着一个栈帧从入栈到出栈的过程。因此局部变量都会被封装进栈帧中。对于基本类型的局部变量来说,无论如何都不会破坏线程封闭。对有引用类型来说,因为真正的实例会存放在JAVA堆中,因此要防止对象溢出。简单来说栈封闭就是使用局部变量,但对于引用类型,要防止溢出。

    3、ThreadLocal类

            维持线程封闭性的一种更加规范方法是使用ThreadLocal类,这个类能使线程中某个值与保存值的对象关联起来。ThreadLocal类提供了get和set等访问接口或者方法,这些方法为每个使用该变量的线程都存在一份独立的副本,因此get总是放回当前执行线程在调用set设置的最新值。看一下下面代码例子:

    public class ConnectionManager {
        private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
            public Connection initialValue() {
                Connection conn = null;
                try {
                    conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password");
                } catch (SQLException e) {
                    e.printStackTrace();
                }
                return conn;
            }
        };
    
        public static Connection getConnection() {
            return connectionHolder.get();
        }
    
        public static void setConnection(Connection conn) {
            connectionHolder.set(conn);
        }
    }

            通过调用ConnectionManager.getConnection()方法,每个线程获取到的,都是自己独立拥有的一个的Connection对象副本,第一次获取时,是通过initialValue()方法的返回值来设置值的。通过ConnectionManager.setConnection(Connection conn)方法设置的Connection对象,也只会和当前线程绑定。这样就实现了Connection对象在多个线程中的完全隔离。在Spring容器中管理多线程环境下的Connection对象时,采用的思路和以上代码非常相似。

            那么线程变量和线程是怎么绑定的呢?Connection对象的副本到底存储在哪里。接下来我们来分析一些ThreadLocal的源码。

    三、ThreadLocal源码分析

            ThreadLocal类的实现其实不复杂,我们常用的有get()、set()、remove()方法,下面我们先看它的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);
    }
    
    ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    set()方法首先获取到当前执行的线程,然后getMap方法获取到一个ThreadLocalMap的实例,接着使用ThreadLocal实例作为key,将value保存到map中,如果获取到的map是空,则会创建一个ThreadLocalMap类的实例。通过getMap()和creatMap()方法我们可以知道Thread类中保存着一个ThreadLocaMap实例的引用,因此每一个线程value实际上是保存在Thread的实例中。如下Thread类

    public
    class Thread implements Runnable {
               ......
        ThreadLocal.ThreadLocalMap threadLocals = null;
     .         ......
    }

        接下来我们看一下ThreadLocalMap这个类,ThreadLocalMap类是ThreadLocal类中定义的一个静态的内部类。主要代码如下:

     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;
                }
            }
    }

    这里需要看的是Entry这个类,Entry继承了WeakReference类,表示一个弱引用,关于弱引用接下会专门写一篇博客讲解。ThreadLocal实例和value的关系正是存储在这里。再看一下ThreadLocalMap的构造函数

     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);
    }

    构造方法也比较简单,首先初始化一个长度为16的Entry类型的数组,接着通过ThreadLocal实例的threadLocalHashCode和初始容量做与运算,获取到要存储的位置,接着放入一个Entry实例。接下来设置最糟糕情况下的加载因子,这里的值为INITIAL_CAPACITY值的2/3。接下来看一些ThreadLocalMap的set()方法:

    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); //遍历数组查找ThreadLocal实例相同的key,如果找到,则就会保存这个value for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) {
    //会清理其中的没有key的enter replaceStaleEntry(key, value, i);
    return; } } tab[i] = new Entry(key, value); int sz = ++size;
    //如果清理了其中没有key的enter,并且数据的长度大于负载因子,会进行rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

    通过这个方法我们了解到,当执行set()放法的时候,不仅会把新的值放进去,还会清理到那些因为垃圾回收没有key的无效Entry实例。get()方法和remove()方法同样会有这些操作,那么为什么要这么做呢,请接着看。

    四、ThreadLocal的内存泄露预防

     我们先看一些ThreadLocal在内存中是如何存储的。

              ThreadLocal本身并不存值,而是作为一个Key让线程从ThreadLocalMap中能获取到value。Entry中的Key是弱引用,所以jvm在垃圾回收时如果外部没有强引用来引用它,ThreadLocal必然会被回收。但是,作为ThreadLocalMap的key,ThreadLocal被回收后,ThreadLocalMap就会存在key为null,但value不为null的Entry。若当前线程一直不结束,可能是作为线程池中的一员,线程结束后不被销毁,或者分配(当前线程又创建了ThreadLocal对象)使用了又不再调用get/set方法,就可能引发内存泄漏。其次,就算线程结束了,操作系统在回收线程或进程的时候不是一定杀死线程或进程的,在繁忙的时候,只会清除线程或进程数据的操作,重复使用线程或进程(线程id可能不变导致内存泄漏)。因此,key弱引用并不是导致内存泄漏的原因,而是因为ThreadLocalMap的生命周期与当前线程一样长,并且没有手动删除对应key。

            那么,为什么要将Entry中的key设为弱引用?相反,设置为弱引用的key能预防大多数内存泄漏的情况。如果key 使用强引用,引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。如果key为弱引用,引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

    总之。在使用完ThreadLocal时,及时调用它的的remove方法清除数据。

  • 相关阅读:
    Java 过滤器
    理解Java中的弱引用(Weak Reference)
    AOP编程
    利用ThreadLocal管理事务
    Redis设计与实现-附加功能
    Redis设计与实现-主从、哨兵与集群
    Redis设计与实现-客户端服务端与事件
    Redis设计与实现-持久化篇
    Redis设计与实现-内部数据结构篇
    重温软件架构设计-程序员向架构师转型必备
  • 原文地址:https://www.cnblogs.com/ChenBingJie123/p/13217275.html
Copyright © 2011-2022 走看看