并发编程之 ThreadLocal

并发编程之 ThreadLocal

ThreadLocal并不是一个线程,而是为线程提供一个局部的变量副本,即提供线程粒度的变量。ThreadLocal可以针对线程将一个共享的变量为该线程创建一个副本,然后只供该线程使用。
使用例子: spring的事务使用了ThreadLocal,为每个线程存储链接。 Spring 会从数据库连接池中获得一个 connection,然会把 connection 放进 ThreadLocal 中,也就和线程绑定了,事务需 要提交或者回滚,只要从 ThreadLocal 中拿到 connection 进行操作。如果不在ThreadLocal中存储链接,如果需要调多次数据库,则需要每次调用数据库的时候从业务层传connection来保证执行一次方法使用的是一个数据库链接。

常用方法:ThreadLocal中包含get()、set()、remove()、initiialValue() 方法,分别为获取值、设置值、删除、初始化值 方法。
get():该方法返回当前线程所对应的线程局部变量,如果没有set过值,初始化时也没有init值,那么将返回null。如果该线程下设置过值,将返回设置过的值。
set():设置当前线程的线程局部变量的值。
remove():将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK 5.0 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动 被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它 可以加快内存回收的速度。
initialValue():返回该线程局部变量的初始值,该方法是一个 protected 的方法,显然是为 了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第 1 次调用 get() 或 set(Object)时才执行,并且仅执行 1 次。ThreadLocal 中的缺省实现直接返回一 个 null。

ThreadLocal原理:

在这里插入图片描述
ThreadLocal会为每个线程提供一个变量副本:创建ThreadLocal后,当线程调用set、get、initialValue等方法时,会检查当前线程没有创建过ThreadLocalMap,如果没有则会创建一个ThreadLocalMap,该ThreadLocalMap属于Thread里的一个属性,当获取该map时,先获取当前线程,然后获取线程的属性,所以一个线程只有一个ThreadLocalMap。ThreadLocalMap类中保存的是Entry[] (entry数组),entry数组中的key是当前线程创建的ThreadLocal对象,value是保存的值,每个线程可以创建多个ThreadLocal,所以是entry数组。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

ThreadLocal引发内存泄漏问题:

内存泄漏:应该被垃圾回收器回收的对象 没有被回收
内存溢出:内存只有4M,但程序需要用5M的内存,这时候会内存溢出。

package concurrent.threadLocal;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * threadLocal内存溢出
 */
public class ThreadLocalOOM {

    //创建线程池,大小为5个线程
    final static ThreadPoolExecutor poolExecutor
            = new ThreadPoolExecutor(5, 5, 1,
            TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());

    //创建一个大小为5M的对象
    private static class LocalClass{
        private byte[] a =new byte[1024*1024*5]; //5M
    }

    //创建ThreadLocal
    final static ThreadLocal threadLocal = new ThreadLocal();

    public static void main(String[] args) throws InterruptedException{
        //创建多个线程
        for (int i = 0; i < 2000; i++) {
            poolExecutor.execute(new Runnable(){
                @Override
                public void run() {
                    //直接创建对象
                    LocalClass localClass = new LocalClass();
                    //使用ThreadLocal
//                    threadLocal.set(new LocalClass());
                    //使用ThreadLocal后删除副本
                    threadLocal.set(new LocalClass());
                    threadLocal.remove();

                    System.out.println("线程占用5M");
                }
            });
            Thread.sleep(100);
        }

    }

}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

内存泄漏原因分析:

首先补充一点jvm的知识:
Objecto=newObject();
这个 o,我们可以称之为对象引用,而 newObject()我们可以称之为在内存 中产生了一个对象实例。当写下 o=null 时,只是表示 o 不再指向堆中 object 的对象实例,不代表这 个对象实例不存在了。 强引用就是指在程序代码之中普遍存在的,类似“Objectobj=newObject()” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。 软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象, 在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行 第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 之后,提供了 SoftReference 类来实现软引用。 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱 引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时, 无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在 JDK1.2 之 后,提供了 WeakReference 类来实现弱引用。 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象 实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用 来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象 实例被收集器回收时收到一个系统通知。在 JDK1.2 之后,提供了 PhantomReference 类来实现虚引用。
在这里插入图片描述
根据我们前面对 ThreadLocal 的分析,我们可以知道每个 Thread 维护一个 ThreadLocalMap,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需 要存储的 Object,也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。仔细观察 ThreadLocalMap,这个 map 是使用ThreadLocal的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。 因此使用了 ThreadLocal 后,引用链如图所示
在这里插入图片描述
图中的虚线表示弱引用。 这样,当把 threadlocal 变量置为 null 以后,没有任何强引用指向 threadlocal 实例,所以 threadlocal 将会被 gc 回收。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前 线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强 引用链:ThreadRef->Thread->ThreaLocalMap->Entry->value,而这块 value 永 远不会被访问到了,所以存在着内存泄露。 只有当前 thread 结束以后,currentthread 就不会存在栈中,强引用断开, CurrentThread、Mapvalue 将全部被 GC 回收。最好的做法是不在需要使用 ThreadLocal 变量后,都调用它的 remove()方法,清除数据。 其实考察 ThreadLocal 的实现,我们可以看见,无论是 get()、set()在某些时 候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是 这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。 只有 remove()方法中显式调用了 expungeStaleEntry 方法。

结论:使用ThreadLocal后都调用它的remove()方法。

ThreadLoca错误使用导致不安全:

public class ThreadLocalUnsafe implements Runnable {

    public static Number number = new Number(0);

    public void run() {
        //每个线程计数加一
        number.setNum(number.getNum()+1);
      //将其存储到ThreadLocal中
        value.set(number);
        SleepTools.ms(2);
        //输出num值
        System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
    }

    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new ThreadLocalUnsafe()).start();
        }
    }

    private static class Number {
        public Number(int num) {
            this.num = num;
        }

        private int num;

        public int getNum() {
            return num;
        }

        public void setNum(int num) {
            this.num = num;
        }

        @Override
        public String toString() {
            return "Number [num=" + num + "]";
        }
    }

}

在这里插入图片描述

等待 / 通 知 机 制

是指一个线程 A 调用了对象 O 的 wait()方法进入等待状态,而另一个线程 B 调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait() 方法返回,进而执行后续操作。上述两个线程通过对象 O 来完成交互,而对象 上的 wait()和 notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通 知方之间的交互工作。 notify(): 通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程 获取到了对象的锁,没有获得锁的线程重新进入 WAITING 状态。 notifyAll(): 通知所有等待在该对象上的线程 wait() 调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断 才会返回.需要注意,调用 wait()方法后,会释放对象的锁 wait(long) 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有 通知就超时返回 wait(long,int) 对于超时时间更细粒度的控制,可以达到纳秒。
等待和通知的标准范式 等待方遵循如下原则。
1)获取对象的锁。 2)如果条件不满足,那么调用对象的 wait()方法,被通知后仍要检查条件。 3)条件满足则执行对应的逻辑。
在这里插入图片描述
通知方遵循如下原则。 1)获得对象的锁。 2)改变条件。 3)通知所有等待在对象上的线程。
在这里插入图片描述
在调用 wait()、notify()系列方法之前,线程必须要获得该对象的对象级 别锁,即只能在同步方法或同步块中调用 wait()方法、notify()系列方法,进 入 wait()方法后,当前线程释放锁,在从 wait()返回前,线程与其他线程竞 争重新获得锁,执行notify()系列方法的线程退出调用了notifyAll的synchronized 代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会 继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的 线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。 notify和notifyAll应该用谁 尽可能用 notifyall(),谨慎使用 notify(),因为 notify()只会唤醒一个线程,我 们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。

public class Express {
    public final static String CITY = "ShangHai";
    private int km;/*快递运输里程数*/
    private String site;/*快递到达地点*/

    public Express() {
    }

    public Express(int km, String site) {
        this.km = km;
        this.site = site;
    }

    /* 变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理*/
    public synchronized void changeKm(){
        this.km = 101;
        notify();
    }

    /* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
    public  synchronized  void changeSite(){
        this.site = "BeiJing";
        notifyAll();
    }

    /*线程等待公里的变化*/
    public synchronized void waitKm(){
        while(this.km<100){
            try {
                wait();
                System.out.println("Check Site thread["
                                +Thread.currentThread().getId()
                        +"] is be notified");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("the Km is "+this.km+",I will change db");
    }

    /*线程等待目的地的变化*/
    public synchronized void waitSite(){
        while(this.site.equals(CITY)){//快递到达目的地
            try {
                wait();
                System.out.println("Check Site thread["+Thread.currentThread().getId()
                		+"] is be notified");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("the site is "+this.site+",I will call user");
    }
}

=================================

线程不安全之其余demo:

public class demo{

    //验证ThreadLocal的栈封闭性
    public static void main(String[] args) {
        testThreadLocal();
    }
    private static void testThreadLocal() {
        ThreadLocal<Student> objectThreadLocal = new ThreadLocal<Student>();
        //创建线程
        for (int i = 0; i < 2; i++) {
            int j=i;
            new Thread(){
                public void run(){
                    Student student= new Student();
                    if(0==j){ //第一个线程值是小明
                        student.setName("小明");
                         System.out.println("小明放进去了=======");
                        try{
                            Thread.sleep(1000);
                        }catch (Exception e){
                            System.out.println("=====");
                        }
                    }else {   //第一个线程值是小红
                        student.setName("小红");
                         System.out.println("小红放进去了=======");
                    }
                    //获取初始值
                    Student getStudent1 = objectThreadLocal.get();
                    if(null==getStudent1){
                        System.out.println("==="+getStudent1);
                    }else{
                        System.out.println("==="+getStudent1.getName());
                    }
                    //写入值
                    objectThreadLocal.set(student);
                    Student getStudent2 = objectThreadLocal.get();
                    System.out.println(getStudent2.getName());
                    //删除值
                    objectThreadLocal.remove();
                }
            }.start();
        }
    }
}

class Student{
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

demo运行结果:

小明放进去了=======
小红放进去了=======
===null
小红
===null
小明

demo代码说明:创建了一个共享变量objectThreadLocal,然后创建了两个线程,线程1中set的值是“小明”,线程2中的值是小红。现将小明放入线程1,然后将小红放入线程2,之后线程1休眠(目的是将两个线程的执行顺序颠倒),之后先get线程2,然后get线程1,可以发现虽然线程2后set值,但线程1获取到的值还是线程1set进去的,所以说明虽然线程1和线程2都是共用的objectThreadLocal,但各自保存了自己的变量,即objectThreadLocal创建了线程粒度的副本供各自线程调用。

原理:ThreadLocal的set方法的源码是调用的map,将当前线程作为Key,set的值作为value,存入map中,所以实现了线程粒度的变量。

源码分析:

//get方法
public T get() {
        Thread t = Thread.currentThread();//获取当前线程的引用
        ThreadLocalMap map = getMap(t);//获取ThreadLocal对象里面的map
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();  //map为null时返回的数据
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;    //Thread里面的变量 :ThreadLocal.ThreadLocalMap threadLocals = null;
    }
    
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);  //map中放入当前线程需要set的值
        else
            createMap(t, value);  //创建map
        return value;
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
//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);
    }

//remove方法
	public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

注意:使用ThreadLocal是别忘了remove(),如果不使用remove(),很可能会出现一些问题。例如:如果在线程池中用到了ThreadLocal,那么线程将一直存在,会导致map越来越大,而且get时即便没有set值,也可能会得到值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值