ThreadLocal学习笔记

ThreadLocal简介

ThreadLocal是在多线程编程的环境下,为每个线程创建一个单独的线程变量,互不影响。避免了多个线程同时操作共享变量时产生冲突而带来的一些问题。

ThreadLocal vs Synchronized

synchronized关键字也可以实现防止多线程共享变量冲突的问题。但是与TheadLocal还是存在不同。

首先,ThreadLocal是采用了空间换时间的设计思路,通过为每个线程绑定一个变量,避免了线程争取锁的等待。而synchronized则是明显的时间换空间

另外,在使用方面。Synchronized的目的是为了防止某个代码块在多线程场景下执行会产生的冲突问题,主要针对的是代码块逻辑,最终目的是确保数据的正确性。使用ThreadLocal的目的是让各个线程有一个单独的变量副本,可以方便的在本线程内进行参数传递,从而执行正确的操作逻辑。

以具体的例子来说,Synchronized经常用于多线程对某个变量进行修改的时候,比如同时进行i++操作。这个时候,不同的线程的操作是有互相依赖关系的,a线程对i+1之后,b线程希望的是能在a线程加完后再执行+1。而使用ThreadLocal时,各个线程对其他线程的共享变量副本其实是不关心的。比如用的最多的数据库Connection的例子,每个线程只希望自己的connection实例不被别的线程影响。

使用例子

以下例子都通过ThreadLocal实现了各个线程之间的变量互相独立,不会被干扰。

Session管理

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

数据库连接管理

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionManager {

    private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() {
        @Override
        protected Connection initialValue() {
            try {
                return DriverManager.getConnection("", "", "");
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return null;
        }
    };

    public Connection getConnection() {
        return dbConnectionLocal.get();
    }
}
  

ThreadLocal的设计与实现

在早期的版本中,ThreadLocal的实现是每个ThreadLocal实例创建一个Map,然后以thread为key,存放对应的value。

而在JDK8中,ThreadLocal的设计是,**每个Thread维护一个 **ThreadLocalMap,这个Map的Key是ThreadLocal实例本身,value是要存储的值。ThreadLocalMap是由ThreadLocal来维护的,采用了懒加载的设计模式,只有在第一次使用get或者set时,才会初始化。

这种设计的好处在于:

  1. 随着线程销毁,ThreadLocalMap也随着销毁,减少内存空间的占用。

ThreadLocal的设计以及get、set方法流程图

get/set流程图

get源码

  /**
     * 返回当前线程中保存ThreadLocal的值
     * 如果当前线程没有此ThreadLocal变量,
     * 则它会通过调用{@link #initialValue} 方法进行初始化值
     *
     * @return 返回当前线程对应此ThreadLocal的值
     */
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 对e进行判空 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 获取存储实体 e 对应的 value值
                // 即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        /*
        	初始化 : 有两种情况有执行当前代码
        	第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
        	第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
         */
        return setInitialValue();
    }

    /**
     * 初始化
     *
     * @return the initial value 初始化后的值
     */
    private T setInitialValue() {
        // 调用initialValue获取初始化的值
        // 此方法可以被子类重写, 如果不重写默认返回null
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        // 返回设置的值value
        return value;
    }

执行流程:

  1. 通过当前线程,获取当前线程维护的ThreadLocalMap。
  2. 以ThreadLocal自己为实例(传入this),从Map获取Entry并返回。
  3. 如果1中map不存在或者map中没有ThreadLocal对应的key,那么会执行初始化setInitialValue()流程。

set源码

  /**
     * 设置当前线程对应的ThreadLocal的值
     *
     * @param value 将要保存在当前线程对应的ThreadLocal的值
     */
    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
    }

 /**
     * 获取当前线程Thread对应维护的ThreadLocalMap 
     * 
     * @param  t the current thread 当前线程
     * @return the map 对应维护的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
	/**
     *创建当前线程Thread对应维护的ThreadLocalMap 
     *
     * @param t 当前线程
     * @param firstValue 存放到map中第一个entry的值
     */
	void createMap(Thread t, T firstValue) {
        //这里的this是调用此方法的threadLocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

执行流程:

  1. 通过当前线程,获取当前线程维护的ThreadLocalMap。
  2. 以当前ThreadLocal的实例为key,设置一个value。
  3. 如果获取Map时,发现map还为空。那么会调用createMap()方法先创建map。

remove方法

/**
     * 删除当前线程中保存的ThreadLocal对应的实体entry
     */
     public void remove() {
        // 获取当前线程对象中维护的ThreadLocalMap对象
         ThreadLocalMap m = getMap(Thread.currentThread());
        // 如果此map存在
         if (m != null)
            // 存在则调用map.remove
            // 以当前ThreadLocal为key删除对应的实体entry
             m.remove(this);
     }

remove方法的逻辑很简单,调用map的remove方法删除以本ThreadLocal实例为key的Entry

ThreadLocalMap的设计与实现

ThreadLocalMap实际上并没有继承Map接口,而是自己实现了Map功能。其内部的Entry也是独立实现的。

![image.png](https://img-blog.csdnimg.cn/img_convert/6e33cdfab2a12fd503b4c537fe9583e3.png#clientId=ubfd504fe-78ec-4&from=paste&height=384&id=u0c91481d&margin=[object Object]&name=image.png&originHeight=384&originWidth=650&originalType=binary&ratio=1&size=53895&status=done&style=none&taskId=u1d8e8bc6-0e04-463f-ad9d-274a0f5262a&width=650)

Map的成员变量如下,可以发现,设计理念和HashMap其实非常相似。

   /**
     * 初始容量 —— 必须是2的整次幂
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * 存放数据的table,Entry类的定义在下面分析
     * 同样,数组长度必须是2的整次幂。
     */
    private Entry[] table;

    /**
     * 数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值。
     */
    private int size = 0;

    /**
     * 进行扩容的阈值,表使用量大于它的时候进行扩容。
     */
    private int threshold; // Default to 0
    

Entry的核心源码:

/*
 * Entry继承WeakReference,并且用ThreadLocal作为key.
 * 如果key为null(entry.get() == null),意味着key不再被引用,
 * 因此这时候entry也可以从table中清除。
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

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;
    
    //获取当前threadLocal对象在数组中索引的位置
    int i = key.threadLocalHashCode & (len-1);
	
    //获取当前i位置上的key
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
		//对比key是否相等,如果相等,直接替换为新值
        if (k == key) {
            e.value = value;
            return;
        }
		//如果为空,说明此ThreadLocal已经被回收了,那么调用replaceStaleEntry方法对该key以及其前后
        //做一次探测式清扫,清理回收已经过期的Entry,源码见下方
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    //如果清理完无用Entry,并且数组中的数据大小 > 阈值的时候对当前的Table进行重新哈希 
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

replaceStaleEntry源码

 private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            //这里采用的是从当前的staleSlot 位置向前面遍历,i--
            //这样的话是为了把前面所有的的已经被垃圾回收的也一起释放空间出来
            //(注意这里只是key 被回收,value还没被回收,entry更加没回收,所以需要让他们回收),
            //同时也避免这样存在很多过期的对象的占用,导致这个时候刚好来了一个新的元素达到阀值而触发一次新的rehash
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                 //slotToExpunge 记录staleSlot左手边第一个空的entry 到staleSlot 之间key过期最小的index
                if (e.get() == null)
                    slotToExpunge = i;

            // 这个时候是从数组下标小的往下标大的方向遍历,i++,刚好跟上面相反。
            //这两个遍历就是为了在左边遇到的第一个空的entry到右边遇到的第一空的 entry之间查询所有过期的对象。
            //注意:在右边如果找到需要设置值的key(这个例子是key=15)相同的时候就开始清理,然后返回,不再继续遍历下去了
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //说明之前已经存在相同的key,所以需要替换旧的值并且和前面那个过期的对象的进行交换位置,
                //交换的目的下面会解释
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    //这里的意思就是前面的第一个for 循环(i--)往前查找的时候没有找到过期的,只有staleSlot
                    // 这个过期,由于前面过期的对象已经通过交换位置的方式放到index=i上了,
                    // 所以需要清理的位置是i,而不是传过来的staleSlot
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                        //进行清理过期数据
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // 如果我们在第一个for 循环(i--) 向前遍历的时候没有找到任何过期的对象
                // 那么我们需要把slotToExpunge 设置为向后遍历(i++) 的第一个过期对象的位置
                // 因为如果整个数组都没有找到要设置的key 的时候,该key 会设置在该staleSlot的位置上
                //如果数组中存在要设置的key,那么上面也会通过交换位置的时候把有效值移到staleSlot位置上
                //综上所述,staleSlot位置上不管怎么样,存放的都是有效的值,所以不需要清理的
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // 如果key 在数组中没有存在,那么直接新建一个新的放进去就可以
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // 如果有其他已经过期的对象,那么需要清理他
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

getEntry源码

private Entry getEntry(ThreadLocal<?> key) {
    //先获取索引地址
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    //如果索引地址上的Entry不为空且就是要找的key,直接返回
    if (e != null && e.get() == key)
        return e;
    else
        //否则向后查找,并且在此方法中发现了为null的key会对其进行清理
        return getEntryAfterMiss(key, i, e);
}

ThreadLocal的内存泄露问题

内存溢出与内存泄露

内存溢出指的是已经没有足够的内存分配给申请者。

内存泄露是指之前的创建的对象无法被正确回收,导致系统内存的浪费。长时间的内存泄露必然会导致内存溢出。

Java对象的引用类型

强引用:我们平时使用的最多的引用类型,通过直接new一个对象创建的就是强引用。在GC时,只要通过GC Roots可达,那么就不会被回收

软引用:使用SoftReference<T> ref包装的类型,就是软引用。在一次GC后内存仍然不够,那么就会回收软引用类型。

弱引用:使用WeakReference<T> 包装的类型。每次GC都会对弱引用回收。

虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。这和弱引用类似,但是虚引用一定要配合引用队列使用,这里不细说。

内存泄露问题分析

从上面ThreadLocalMap的源码可以看到,Entry是继承了被弱引用包装的ThreadLocal的,Entry extends WeakReference<ThreadLocal<?>>,并且以ThreadLocal作为key,所以Entry中的key和ThreadLocal对象之间的引用关系是弱引用。

我们先假设如果这层引用关系采用的是强引用,会发生什么。

如果ThreadLocal使用的是强引用

首先看看,如果ThreadLocal使用的是强引用会发生什么

![image.png](https://img-blog.csdnimg.cn/img_convert/61406353186012f78a1b367d14ba0784.png#clientId=ubfd504fe-78ec-4&from=paste&height=585&id=u320d4bff&margin=[object Object]&name=image.png&originHeight=585&originWidth=1057&originalType=binary&ratio=1&size=340124&status=done&style=none&taskId=u2b575368-d481-480c-b4bc-591c5640de8&width=1057)

假设我们在线程中不使用当前ThreadLocal了,那么ThreadLocal Ref -> 堆中 ThreadLocal对象这一条引用链会断开。但是由于Entry的key到ThreadLocal对象是强引用,所以Entry的Key不会被回收。

并且当前线程(Current Thread)一直在运行,thread会维护着自己的ThreadLocalMap,因此底下这条强引用链threadRef->currentThread->threadLocalMap->entry也一直存在。

所以,这就会导致即使我们即使用完了当前的ThreadLocal对象,Entry(包括了Key和Value)仍然不会被回收,导致内存泄露。


弱引用情况分析

我们知道实际上,Entry中ThreadLocal作为key使用的是弱引用,那么是否采用弱引用的方式就能避免内存泄露呢?答案是不能,见如下分析:

![image.png](https://img-blog.csdnimg.cn/img_convert/f425554db3340c419a6521cbc9d99369.png#clientId=ubfd504fe-78ec-4&from=paste&height=418&id=u3f60e28f&margin=[object Object]&name=image.png&originHeight=418&originWidth=819&originalType=binary&ratio=1&size=202409&status=done&style=none&taskId=uf6ca6918-caca-4d5a-affc-435aca9df0b&width=819)

当ThreadLocal不再使用后,ThreadLocal Ref -> 堆中 ThreadLocal对象这一条引用链消失,并且由于key到ThreadLocal对象是弱引用。因此,ThreadLocal对象可以被正确回收,此时Entry中的key对象会变为null

但是,底下的强引用链threadRef->currentThread->threadLocalMap->entry是依然存在的,这和上面强引用的情况是一致的。所以此时Entry仍然无法被回收,依然会导致内存泄露。但是和强引用相比,此时Entry中只包含了value。


总结:ThreadLocal出现内存泄露的原因—— 由于ThreadLocalMap是由Thread维护的,其生命周期和Thread一样长。而Entry对象又没有被 整个 remove掉,最终导致了内存泄漏。这也是为什么线程池使用ThreadLocal容易出现内存泄漏的原因,线程被重复使用而不会被销毁,如果不及时remove掉Entry就发生泄露了。

为什么要用弱引用

既然弱引用也无法防止内存泄露的情况,那为什么还要专门用弱引用呢?通过上面ThreadLocalMap的源码可以发现,在使用get或者set方法时,都会对key做检查,如果发现为null,那么对其进行清理,把value也设为null。

这样的话,即使程序员忘记用remove手动移除掉Entry,在下一次get或者set操作时,ThreadLocalMap内部也会帮我们做清理工作。比起使用强引用,就多了一层检查机制。


但是这并不意味着这种检查机制能保证不会发生内存泄漏。

  1. 当Map中存放了大量Key为Null的Entry,且不再调用remove,set,getEntry方法时,依旧会发生内存泄漏。
  2. 如果使用了static ThreadLocal,那么因为静态变量的在类被创建时就被加载,并且在类被销毁时才被释放,他的生命周期比普通实例对象更长,因此也更容易产生内存泄露的问题。(上图中上面那条引用链存活时间变久)

Hash冲突解决

ThreadLocalMap采用了和HashMap不一样的方式解决Hash冲突问题。HashMap采用的是拉链法+红黑树,而ThreadLocalMap采用的则是**开放地址法中的线性探测法。**实现方式是,如果发现存在Hash冲突,那么就继续往后找一个为空的地址,只要数组够大,总能找到放下的地方。

具体实现看看ThreadLocalMap中的涉及到解决Hash冲突的源码

ThreadLocalMap的构造函数

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //根据INITIAL_CAPACITY,初始化table
    table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
    //计算索引(关键)
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //设置值
    table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
    //因为是初始化,所以当前Map的大小直接设为1
    size = 1;
    //设置扩容阈值,为INITIAL_CAPACITY的2/3
    setThreshold(INITIAL_CAPACITY);
}

这一段代码的关键在于索引的计算:int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 。首先看threadLocalHashCode是如何计算出来的

 	private final int threadLocalHashCode = nextHashCode();
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
	//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用
    private static AtomicInteger nextHashCode =  new AtomicInteger();
     //特殊的hash值,使用它是为了让哈希码能均匀的分布在2的N次方的数组里
    private static final int HASH_INCREMENT = 0x61c88647;

源码中定义了一个AtomicInteger类型,每次获取hash码的当前值然后用线程安全的方式加上0x61c88647,0x61c88647是一个魔数,可以让元素更均匀的分布在2的N次方的数组里,这里不展开讨论这个数字,感兴趣可以自己搜索了解一下。

& (INITIAL_CAPACITY - 1) 就等于对INITIAL_CAPACITY取模,但是要求INITIAL_CAPACITY必须是2的整数次幂,原因参考这篇文章:位运算取余。和直接取模运算相比,位运算的计算速度更快。

看到这里我们也明白了为什么要控制Map的长度为2的n次方了。一是配合魔数0x61c88647让元素分布更均,二是配合位运算让计算更快。

ThreadLocalMap中的set方法

源码:

private void set(ThreadLocal<?> key, Object value) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        //计算索引(重点代码,刚才分析过了)
        int i = key.threadLocalHashCode & (len-1);
        /**
         * 使用线性探测法查找元素(重点代码)
         */
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            //ThreadLocal 对应的 key 存在,直接覆盖之前的值
            if (k == key) {
                e.value = value;
                return;
            }
            // key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,
           // 当前数组中的 Entry 是一个陈旧(stale)的元素
            if (k == null) {
                //用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
    	//ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。
            tab[i] = new Entry(key, value);
            int sz = ++size;
            /**
             * cleanSomeSlots采用的是对数级扫描,而非全部扫描,如果扫描中发现过期Entry会对其进行回收,
             * 如果全部扫描完成后没有发现过期Entry,方法会返回false。
             * 
             * 当cleanSomeSlots返回的是false,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行				 * rehash(执行一次全表的扫描清理工作)
             * rehash,对全表进行过期Entry回收,如果全表回收完后使用量仍然大于长度的2/3,那么对表进行resize
             * 扩容流程。 具体流程可以查看源码,这里不仔细讨论。
             **/
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
}

 /**
     * 获取环形数组的下一个索引
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }


首先会用key.threadLocalHashCode & (len-1)得到索引地址,刚刚已经分析过。 接着通过这个索引地址在for循环中向后查找,分以下三种情况:

  1. 索引上存放的Entry的key和传入的key一致,那么直接将原本的value值修改为新的value值。
  2. 索引上的key为null,说明这是一个已过期的Entry,调用replaceStaleEntry()对数组进行清理并把key和value存入数组中
  3. ThreadLocal对应的key不存在,并且没有过期的Entry。那么会在空位置新建一个Entry:tab[i] = new Entry(key, value);。在创建完后,还会执行对应的过期Entry回收或者表的resize操作,见上面源码注释。

另外值得一提的是,在Map中向后探测数组时,使用的是nextIndex()方法。这个方法的逻辑是,每次都往后探测一位,当要超过数组长度时,将探测位设置到0。可以理解成在探测时,整个Entry[] 数组被当成了一个环处理。

参考文章:

https://www.cnblogs.com/aspirant/p/8991010.html

https://blog.csdn.net/weixin_44050144/article/details/113061884

https://www.pdai.tech/md/java/thread/java-thread-x-threadlocal.html#threadlocal%E9%80%A0%E6%88%90%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E7%9A%84%E9%97%AE%E9%A2%98

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java并发编程 背景介绍 并发历史 必要性 进程 资源分配的最小单位 线程 CPU调度的最小单位 线程的优势 (1)如果设计正确,多线程程序可以通过提高处理器资源的利用率来提升系统吞吐率 (2)建模简单:通过使用线程可以讲复杂并且异步的工作流进一步分解成一组简单并且同步的工作流,每个工作流在一个单独的线程中运行,并在特定的同步位置交互 (3)简化异步事件的处理:服务器应用程序在接受来自多个远程客户端的请求时,如果为每个连接都分配一个线程并且使用同步IO,就会降低开发难度 (4)用户界面具备更短的响应时间:现代GUI框架中大都使用一个事件分发线程(类似于中断响应函数)来替代主事件循环,当用户界面用有事件发生时,在事件线程中将调用对应的事件处理函数(类似于中断处理函数) 线程的风险 线程安全性:永远不发生糟糕的事情 活跃性问题:某件正确的事情迟早会发生 问题:希望正确的事情尽快发生 服务时间过长 响应不灵敏 吞吐率过低 资源消耗过高 可伸缩性较低 线程的应用场景 Timer 确保TimerTask访问的对象本身是线程安全的 Servlet和JSP Servlet本身要是线程安全的 正确协同一个Servlet访问多个Servlet共享的信息 远程方法调用(RMI) 正确协同多个对象中的共享状态 正确协同远程对象本身状态的访问 Swing和AWT 事件处理器与访问共享状态的其他代码都要采取线程安全的方式实现 框架通过在框架线程中调用应用程序代码将并发性引入应用程序,因此对线程安全的需求在整个应用程序中都需要考虑 基础知识 线程安全性 定义 当多个线程访问某个类时,这个类始终能表现出正确的行为,那么就称这个类是线程安全的 无状态对象一定是线程安全的,大多数Servlet都是无状态的 原子性 一组不可分割的操作 竞态条件 基于一种可能失效的观察结果来做出判断或执行某个计算 复合操作:执行复合操作期间,要持有锁 锁的作用 加锁机制、用锁保护状态、实现共享访问 锁的不恰当使用可能会引起程序性能下降 对象的共享使用策略 线程封闭:线程封闭的对象只能由一个线程拥有并修改 Ad-hoc线程封闭 栈封闭 ThreadLocal类 只读共享:不变对象一定是线程安全的 尽量将域声明为final类型,除非它们必须是可变的 分类 不可变对象 事实不可变对象 线程安全共享 封装有助于管理复杂度 线程安全的对象在其内部实现同步,因此多个接口可以通过公有接口来进行访问 保护对象:被保护的对象只能通过特定的锁来访问 将对象封装到线程安全对象中 由特定锁保护 保护对象的方法 对象的组合 设计线程安全的类 实例封闭 线程安全的委托 委托是创建线程安全类的最有效策略,只需要让现有的线程安全类管理所有的状态 在现有线程安全类中添加功能 将同步策略文档化 基础构建模块 同步容器类 分类 Vector Hashtable 实现线程安全的方式 将状态封装起来,对每个公有方法都进行同步 存在的问题 复合操作 修正方式 客户端加锁 迭代器 并发容器 ConcurrentHashMap 用于替代同步且基于散列的Map CopyOnWriteArrayList 用于在遍历操作为主要操作的情况下替代同步的List Queue ConcurrentLinkedQueue *BlockingQueue 提供了可阻塞的put和take方法 生产者-消费者模式 中断的处理策略 传递InterruptedException 恢复中断,让更高层的代码处理 PriorityQueue(非并发) ConcurrentSkipListMap 替代同步的SortedMap ConcurrentSkipListSet 替代同步的SortedSet Java 5 Java 6 同步工具类 闭锁 *应用场景 (1)确保某个计算在其需要的所有资源都被初始化后才能继续执行 (2)确保某个服务在其所依赖的所有其他服务都已经启动之后才启动 (3)等待知道某个操作的所有参与者都就绪再继续执行 CountDownLatch:可以使一个或多个线程等待一组事件发生 FutureTask *应用场景 (1)用作异步任务使用,且可以使用get方法获取任务的结果 (2)用于表示一些时间较长的计算 状态 等待运行 正在运行 运行完成 使用Callable对象实例化FutureTask类 信号量(Semaphore) 用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量 管理者一组虚拟的许可。acquire获得许可(相当于P操作),release释放许可(相当于V操作) 应用场景 (1)二值信号量可用作互斥体(mutex) (2)实现资源池,例如数据库连接池 (3)使用信号量将任何一种容器变成有界阻塞容器 栅栏 能够阻塞一组线程直到某个事件发生 栅栏和闭锁的区别 所有线程必须同时到达栅栏位置,才能继续执行 闭锁用于等待事件,而栅栏用于等待线程 栅栏可以重用 形式 CyclicBarrier 可以让一定数量的参与线程反复地在栅栏位置汇集 应用场景在并行迭代算法中非常有用 Exchanger 这是一种两方栅栏,各方在栅栏位置上交换数据。 应用场景:当两方执行不对称的操作(读和取) 线程池 任务与执行策略之间的隐形耦合 线程饥饿死锁 运行时间较长的任务 设置线程池的大小 配置ThreadPoolExecutor 构造参数 corePoolSize 核心线程数大小,当线程数= corePoolSize的时候,会把runnable放入workQueue中 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了” keepAliveTime 保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。 workQueue 保存任务的阻塞队列 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务 threadFactory 创建线程的工厂 handler 拒绝策略 unit 是一个枚举,表示 keepAliveTime 的单位(有NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS,7个可选值 线程的创建与销毁 管理队列任务 饱和策略 AbortPolicy DiscardPolicy DiscardOldestPolicy CallerRunsPolicy 线程工厂 在调用构造函数后再定制ThreadPoolExecutor 扩展 ThreadPoolExecutor afterExecute(Runnable r, Throwable t) beforeExecute(Thread t, Runnable r) terminated 递归算法的并行化 构建并发应用程序 任务执行 在线程中执行任务 清晰的任务边界以及明确的任务执行策略 任务边界 大多数服务器以独立的客户请求为界 在每个请求中还可以发现可并行的部分 任务执行策略 在什么(What)线程中执行任务? 任务按照什么(What)顺序执行(FIFO、LIFO、优先级)? 有多少个(How Many)任务能并发执行? 在队列中有多少个(How Many)任务在等待执行? 如果系统由于过载而需要拒绝一个任务,那么应该选择哪一个(Which)任务?另外,如何(How)通知应用程序有任务被拒绝? 在执行一个任务之前或之后,应该进行什么(What)动作? 使用Exector框架 线程池 newFixedThreadPool(固定长度的线程池) newCachedThreadPool(不限规模的线程池) newSingleThreadPool(单线程线程池) newScheduledThreadPool(带延迟/定时的固定长度线程池) 具体如何使用可以查看JDK文档 找出可利用的并行性 某些应用程序中存在比较明显的任务边界,而在其他一些程序中则需要进一步分析才能揭示出粒度更细的并行性 任务的取消和关闭 任务取消 停止基于线程的服务 处理非正常的线程终止 JVM关闭 线程池的定制化使用 任务和执行策略之间的隐性耦合 线程池的大小 配置ThreadPoolExecutor(自定义的线程池) 此处需要注意系统默认提供的线程池是如何配置的 扩展ThreadPoolExector GUI应用程序探讨 活跃度(Liveness)、性能、测试 避免活跃性危险 死锁 锁顺序死锁 资源死锁 动态的锁顺序死锁 开放调用 在协作对象之间发生的死锁 死锁的避免与诊断 支持定时的显示锁 通过线程转储信息来分析死锁 其他活跃性危险 饥饿 要避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题。在大多数并发应用程序中,都可以使用默认的线程优先级。 糟糕的响应性 如果由其他线程完成的工作都是后台任务,那么应该降低它们的优先级,从而提高前台程序的响应性。 活锁 要解决这种活锁问题,需要在重试机制中引入随机性(randomness)。为了避免这种情况发生,需要让它们分别等待一段随机的时间 性能与可伸缩性 概念 运行速度(服务时间、延时) 处理能力(吞吐量、计算容量) 可伸缩性:当增加计算资源时,程序的处理能力变强 如何提升可伸缩性 Java并发程序中的串行,主要来自独占的资源锁 优化策略 缩

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值