传统线程机制之THREADLOCAL实现线程范围的共享变量

ThreadLocal实现线程范围的共享变量

接下来讲,在javaEE当中用的比较多的一个知识,就是底层框架经常会用到的,就是线程范围内的共享数据。

什么叫线程范围内的共享数据?

174152_HxJS_3512041.png

就是,一个线程在运行的时候,是不是可能会调用A模块,B模块,C模块。可以调用A,B,C三个对象,ABC三个对象内部的代码是不是可能就要访问外面的变量啊,这三个对象用一个表达式或一个变量去访问外部的一个数据,假如我们把这个变量定义成一个static的,全局的。请问大家,ABC三个访问到的这个变量是不是同一个?是同一个。那我们现在要实现的就是说,ABC这三个对象访问的变量是同一个变量,大家想到了,是一个static的全局变量可以做这个,但是呢,我们要是第二个线程又来调用ABC这三个对象,这时候ABC这三个对象里面的变量或表达式访问的数据呢就不是刚才线程1运行时访问的那个数据,而是另外一个数据,上面这张图,看的应该很清楚了,就是说它们这三个对象在同一个线程身上,被调用的时候,它们访问的数据相同,换了线程呢,访问的数据就不同了,就是另外一个数据了,假设有我5个线程来访问这三个对象,请问大家,一共有几个数据呢?答案是5个。就是想实现这个效果。每个数据都是与线程有关的。然后呢,ABC三个模块可以去取得当前线程身上绑定的这个数据。不同的线程呢,这个数据就变了。

下面的代码演示,什么叫线程范围内的共享数据。

方案一,定义一个static的全局变量,将线程内共享的数据存到这个变量中。得到的结果是,所有的线程访问到的同一份数据,达不到每个线内部共享,但是线程与线程之间访问的是不同变量的效果。

public class ThreadScopeShareData {

 

    private static int data = 0;  

 

    public static void main(String[] args) {

        for (int i = 0; i < 2; i++) {

            new Thread(new Runnable() {

                @Override

                public void run() {

                    int data = new Random().nextInt();

                    System.out.println(Thread.currentThread().getName() + " has put data :" + data);

                  

                    new A().get();

                    new B().get();

                }

            }).start();

        }

    }

 

    static class A {

        public void get() {

            System.out.println("A from " + Thread.currentThread().getName() + " get data :" + data);

        }

    }

 

    static class B {

        public void get() {

            System.out.println("B from " + Thread.currentThread().getName() + " get data :" + data);

        }

    }

}

方案二,将线程内要共享的数据存入一个静态的map容器,map的键是当前线程,map的值是当前线程要共享的数据。

package cn.itcast.heima2;

 

import java.util.HashMap;

import java.util.Map;

import java.util.Random;

 

public class ThreadScopeShareData {

 

    private static Map<Thread, Integer> threadData = new HashMap<Thread, Integer>();

 

    public static void main(String[] args) {

        for (int i = 0; i < 2; i++) {

            new Thread(new Runnable() {

                @Override

                public void run() {

                    int data = new Random().nextInt();

                    System.out.println(Thread.currentThread().getName() + " has put data :" + data);

                    threadData.put(Thread.currentThread(), data);

                    new A().get();

                    new B().get();

                }

            }).start();

        }

    }

 

    static class A {

        public void get() {

            int data = threadData.get(Thread.currentThread());

            System.out.println("A from " + Thread.currentThread().getName() + " get data :" + data);

        }

    }

 

    static class B {

        public void get() {

            int data = threadData.get(Thread.currentThread());

            System.out.println("B from " + Thread.currentThread().getName() + " get data :" + data);

        }

    }

}

 

我们经常要用到这个线程范围内的数据共享,什么时候用到呢,大家学了数据库就好理解,开始事务的connection对象与提交事务的connection对象应该是线程内同一个。每个线程要有一个独立的连接,与线程绑定。如果每个线程用到的connection对象不是不同的,那么就会发生一个线程提交的时候把另外一个线程的事务也给提交了,而另外那个线程的原子性操作还没有做完,比如两个账户转账,张三刚把钱转出去,李四还没到账,事务就提价了,那么张三的钱就莫名其妙的蒸发了。

174212_FEIh_3512041.png

线程范围内共享的变量的作用:我这件事在这个线程范围内搞定,不要去影响别的线程的事,但是,我这个线程内,这几个模块之间,比如转入模块和转出模块,它们又是独立的,这几个模块之间要共享同一个对象,它们又要共享,又要独立,在线程内共享,在线程外独立,这个是有用的。

 

 

接下来看,在JDK当中,他为我们提供了一个ThreadLocal类,用这个类可以很方便的为我们实现线程范围内的共享变量。这个类就相当于一个Map,下面我们通过代码来看一下这个类的使用:

首先,我们把我们自己用map实现的线程内共享变量的代码,用ThreadLocal改写一下:

 

import java.util.Random;

 

public class ThreadScopeShareData {

 

    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();

 

    public static void main(String[] args) {

        for (int i = 0; i < 2; i++) {

            new Thread(new Runnable() {

                @Override

                public void run() {

                    int data = new Random().nextInt();

                    System.out.println(Thread.currentThread().getName() + " has put data :" + data);

                    threadLocal.set(data);

                    new A().get();

                    new B().get();

                }

            }).start();

        }

    }

 

    static class A {

        public void get() {

            int data = threadLocal.get();

            System.out.println("A from " + Thread.currentThread().getName() + " get data :" + data);

        }

    }

 

    static class B {

        public void get() {

            int data = threadLocal.get();

            System.out.println("B from " + Thread.currentThread().getName() + " get data :" + data);

        }

    }

}

 

只需要你去定义一个ThreadLocal变量,往这个ThreadLocal里面放的数据,就是和当前线程相关的,从这里面取的数据,也就是当前线程上的那个数据。

如果,我程序里面又N个变量都是线程范围内共享的,我该怎么做呢?

一个ThreadLocal只代表一个变量,故其中只能放一个数据.如果你有多个变量都要线程内共享,那么你就要定义多个ThreadLocal对象。还有一种方式,就是打包。就是把这n个变量封装到一个类一面,实例化成一个对象。比如姓名,年龄,密码,我定义成一个叫User的实体,我把这个实体作为一个变量存在ThreadLocal里面。我一存不就存了一个实体,实体里面有N个基本数据。如果是这么做,我们可以让我们的设计更加优雅一点。这样就可以让外面看不到ThreadLocal的痕迹,请看下面示例代码:

初步代码:

 

import java.util.Random;

 

public class ThreadScopeShareData {

 

    private static ThreadLocal<MyThreadScopeData> threadLocal = new ThreadLocal<MyThreadScopeData>();

 

    public static void main(String[] args) {

        for (int i = 0; i < 2; i++) {

            new Thread(new Runnable() {

                @Override

                public void run() {

                    int j = new Random().nextInt();

                    MyThreadScopeData data = new MyThreadScopeData();

                    data.setName("张三:"+j);

                    data.setAge(j);

                    System.out.println(Thread.currentThread().getName() + " has put data :" + data.getName()+"@"+data.getAge());

                    threadLocal.set(data);

                    new A().get();

                    new B().get();

                }

            }).start();

        }

    }

 

    static class A {

        public void get() {

            MyThreadScopeData data = threadLocal.get();

            System.out.println("A from " + Thread.currentThread().getName() + " get data :" + data.getName()+"@"+data.getAge());

        }

    }

 

    static class B {

        public void get() {

            MyThreadScopeData data = threadLocal.get();

            System.out.println("B from " + Thread.currentThread().getName() + " get data :" + data.getName()+"@"+data.getAge());

        }

    }

}

class MyThreadScopeData {

 

        private String name;

 

        private int age;

 

        public String getName() {

            return name;

        }

 

        public void setName(String name) {

            this.name = name;

        }

 

        public int getAge() {

            return age;

        }

 

        public void setAge(int age) {

            this.age = age;

        }

    }

这个程序的代码写的非常烂,非常乱!现在我们用一种优雅的处理方式。

1,  把线程内共享的实体变成单例的。这类我们采用非迫切的懒汉模式(刚开始并不迫切的直接创建对象实例,只有当程序调用获取实例的方法的时候,再检查实例是否创建,如果没有创建,则创建,然后把实例返回给方法调用处。),懒汉模式在多线程环境下,可能会在内存里面出现多个实例。虽然我们获取到的永远是最后创建的那个实例,因为引用变量只有一个,后面创建的实例重新赋值给引用变量的时候,会覆盖掉引用变量之前的引用。虽然之前创建的对象不被引用,但是对象却还是存在堆中,要等到垃圾回收才会把那块内存清理出来。这样浪费内存,加大了程序的损耗。所以,我们在懒汉模式的创建和返回实例的方法处要加上synchronized关键字,使线程之间互斥,同一时间只有一个线程来操作这部分代码,保证它的原子性,这样就不会出现创建实例并赋值的操作过程被其它线程打断,从而出现创建了多余实例的问题。

 

import java.util.Random;

 

public class ThreadScopeShareData {

 

    private static ThreadLocal<MyThreadScopeData> threadLocal = new ThreadLocal<MyThreadScopeData>();

 

    public static void main(String[] args) {

        for (int i = 0; i < 2; i++) {

            new Thread(new Runnable() {

                @Override

                public void run() {

                    int j = new Random().nextInt();

                    MyThreadScopeData data = new MyThreadScopeData();

                    data.setName("张三:"+j);

                    data.setAge(j);

                    System.out.println(Thread.currentThread().getName() + " has put data :" + data.getName()+"@"+data.getAge());

                    threadLocal.set(data);

                    new A().get();

                    new B().get();

                }

            }).start();

        }

    }

 

    static class A {

        public void get() {

            MyThreadScopeData data = threadLocal.get();

            System.out.println("A from " + Thread.currentThread().getName() + " get data :" + data.getName()+"@"+data.getAge());

        }

    }

 

    static class B {

        public void get() {

            MyThreadScopeData data = threadLocal.get();

            System.out.println("B from " + Thread.currentThread().getName() + " get data :" + data.getName()+"@"+data.getAge());

        }

    }

}

class MyThreadScopeData {

        private static MyThreadScopeData instance = null;

       

        private MyThreadScopeData(){}

       

        public static synchronized MyThreadScopeData getInstance(){

            if(instance==null){

               return new  MyThreadScopeData();

            }

            return instance;

        }

 

        private String name;

 

        private int age;

 

        public String getName() {

            return name;

        }

 

        public void setName(String name) {

            this.name = name;

        }

 

        public int getAge() {

            return age;

        }

 

        public void setAge(int age) {

            this.age = age;

        }

    }

 

2,  使用单例模式后,这个对象只能创建一个了,而线程内共享变量是每个线程内都会有一个它的实例,一个线程范围内有一个,如果有N个线程,就应该有N个实例,所以说,代码肯定不能这么写。那我先把它写成单例呢是想告诉大家,我的代码和这个单例很相似。接下来,我们来改造上面的代码:

首先,我们在线程共享的实体的类文件中声明一个私有的ThreadLocal类型的变量,并实例化给引用赋值,这个ThreadLocal用来存储一个线程内共享的变量。这个ThreadLocal就是专门用来装这个本类对象的。

接下来,我们要改造获取单例实例的方法,先从ThreadLocal里面去获取实例,然后判断,如果没用从ThreadLocal里面拿到实例,那就创建这个实例,并将它设置给ThreadLocal对象。如果拿到了,就返回这个从ThreadLocal获取到的对象。

接下来,我们看,这个时候这个获取实例的方法还需要使用到synchronized关键字来实现线程互斥吗?A线程来调用这个方法,那么A线程是不是就拿那个跟A线程自己有关的实例,如果没拿到就创建,就存到ThreadLocal里面去,如果这时候正好刚创建完对象,还没有给引用变量赋值,B线程来了,B线程来从ThreadLocal中获取跟自己有关的那个实例,这时候跟之前的A线程没有任何关系,它们本身每个线程关联的ThreadLocal里面的实例是不一样的,所以这时候根本不需要线程互斥,方法上不需要加上synchronized关键字。线程间各拿各的,互不影响。那么现在,当我们要去取数据的时候,就很简单了。这时候我们调用getInstance方法,拿到的就是跟本线程有关的实例对象。为了让这个获取实例的方法更能构见名知意,我们把getInstance方法的名字改为getThreadInstance,这样,这个类调用者用起来就很舒服了,一看这个方法名就知道如果我想得到一个仅跟当前线程有关的实例,只要调用这个方法就可以了。这样,这个ThreadLocal变量就被隐藏起来了,当调用者调用这个类的时候,调用者想把这个类的对象放到与当前线程相关里面去,或者取与当前线程相关的这个类的实例对象,人家呢只需要调用我们的这个方法,搞到的就是与当前线程相关的那个实例对象。至于怎么搞到的,它内部怎么回事,调用者不需要知道。你调用我这个方法,我这个方法得到的肯定就是它的一个实例对象,并且就是跟当前线程绑定的。在这个线程范围内的任意一个地方调,得到的都是同一个实例对象,这样,代码就比较优雅了。这种代码一口气写不出来,那怎么办呢?单例总会写吧,单例是在任意一个地方调用,返回的都是那一个对象。现在我们要改成,在这个线程范围内的任意一个地方调,都是那一个,换一个线程的任意一个地方调都是另外一个,那也就说,把ThreadLocal封装到这个类的内部,人家作为使用者,用起来就非常方便。这个类的设计呢,这样就比较好了。

代码改造如下:

public class ThreadLocalTest {

 

    public static void main(String[] args) {

        for (int i = 0; i < 2; i++) {

            new Thread(new Runnable() {

                @Override

                public void run() {

                    int j = new Random().nextInt();

                    System.out.println(Thread.currentThread().getName() + " has put data :" + j);

                    MyThreadScopeData.getThreadInstance().setName("name" + j);

                    MyThreadScopeData.getThreadInstance().setAge(j);

                    new A().get();

                    new B().get();

                }

            }).start();

        }

    }

 

    static class A {

        public void get() {

            MyThreadScopeData data = MyThreadScopeData.getThreadInstance();

            System.out.println("A from " + Thread.currentThread().getName() + " get data :" + data.getName()+"@"+data.getAge());

        }

    }

 

    static class B {

        public void get() {

            MyThreadScopeData data = MyThreadScopeData.getThreadInstance();

            System.out.println("B from " + Thread.currentThread().getName() + " get data :" + data.getName()+"@"+data.getAge());

        }

    }

}

 

class MyThreadScopeData {

   

    private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal<MyThreadScopeData>();

   

    private MyThreadScopeData() {

    }

 

    public static/* synchronized */MyThreadScopeData getThreadInstance() {

        MyThreadScopeData instance = map.get();

        if (instance == null) {

            instance = new MyThreadScopeData();

            map.set(instance);

        }

        return instance;

    }   

 

    private String name;

 

    private int age;

 

    public String getName() {

        return name;

    }

 

    public void setName(String name) {

        this.name = name;

    }

 

    public int getAge() {

        return age;

    }

 

    public void setAge(int age) {

        this.age = age;

    }

}

 

主要的重点是,要项这样去设计自己的线程范围内共享的对象。我这个对象的实例是与每个线程都相关的,那么这个设计就交给我这个对象自己的类文件吧。其它的用户在任一线程上调用我的方法,自然的就是拿到与这个线程有关的实例。我们的类一设计出来就是说,它是专门与线程绑定的,就是在线程的任意地方调用我的一个方法,就可以得到一个与这个线程有关的这个实例对象,这个实例对象不需要用户去创建,这种设计思路,需要掌握。Struts2用的就是这样一种思想。每一个线程来了,一个请求就是一个线程,每一个请求来了将激活一个线程,这个请求,或者说这个线程,就会有一个容器对象,这个容器对象就是把这个线程,或者说这个请求相关的所有的东西全都装在容器里面,这个容器里面装的东西是只对当前这个请求有效,要换了另外一个请求,就是另外一个容器对象,那个容器装的就是与那个请求有关的所有的东东。确实有这样一种需要。就这么设计那个容器。类似与我们现在的MyThreadScopeData类。

ThreadLocal有一个需要特别注意的,他有一个clear的动作,用于清空ThreadLocal。

那我们现在来思考,有没有可能会出现这么一种情况:现在有一个ThreadLocal变量,实际上就是一个map,这个map里面有多少条记录呢?第一个线程来访问它的时候,这里面就有了一条记录,第二个线程来访问它的时候,就有了第二条,第三条线程来访问它的时候就有了3条记录,那么,最后有1万个线程来访问它。就是说有一个客户端来请求你,请求tomcat服务器,是不是启动一个线程,这就放了一个数据,这个客户端走了,这个数据是不是还在里面,这个线程结束了,这样,一个tomcat服务器经过一个月的运行,可能被100万个,1000万个,1亿个人访问过了,是不是就有了1亿个线程啦,在这种情况下,这个map里面就放了1亿条数据(题外话:因为这时候这个容器map还被引用者,所以这个map是不会被GC回收的,而map里面又引用了这些数据对象,所以这些数据对象也不会被GC回收,这里我们可以使用java提供的弱引用技术来实现这些数据的内存空间的回收),并且第一天数据的那个线程都死了一个月了,它还呆在这里面,就浪费了内存资源,也就是说,你在每个线程都结束的时候,你应该把,先不说清空整个ThreadLocal,你要把这条线程相关的这条记录给清空掉。这是我们的一个想象,应该把它给清空掉。如果不清空,时间久了,这个ThreadLocal里面装的东西何其多啊!

            下面是jdk api文档中关于ThreadLocal的一段描述:

每个线程都保持对其线程局部变量副本[z1] 的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

所以,我们并不需要担心ThreadLocal的内存空间无法释放。ThreadLocal里面就采用了弱引用技术,下面我们来看部分源码:

ThreadLocalMap是一个自定义哈希映射,仅适用于维护线程本地值。 没有任何操作被导出到ThreadLocal类之外。 该类是包私有的,允许在Thread类中声明字段。 为了处理非常大和长期使用的用法,哈希表条目使用WeakReferences作为键。 但是,由于不使用引用队列,所以只有当表开始用尽空间时,才会删除陈旧的条目。

这个哈希映射中的条目扩展了WeakReference,使用它的主要引用字段作为键(它始终是一个ThreadLocal对象)。 请注意,null键(即entry.get()== null)表示该键不再被引用,因此该条目可以从表中删除。 这些条目在下面的代码中被称为“陈旧条目”。

关于ThreadLocal的remove()方法,把这个ThreadLocal与当前线程有关的变量给拿掉。大家搞了一堆ThreadLocal变量,最后是不是想,当某一个线程死掉的时候,大家是不是想把这个线程相关的变量给remove掉,你怎么知道线程结束了呢?就是说,这个线程即将死掉了,这个线程之前往ThreadLocal里面还为这个线程绑定了一个变量,你顺带的还想把这个线程相关的变量从那个ThreadLocal里面把它拿掉,就是线程结束之前把ThreadLocal中跟这个线程有关的变量拿掉,怎么去拿掉呢,调用ThreadLocal对象的remove()方法就拿掉了,但是,我们现在的问题是,在什么时候去调这个方法呢,你怎么知道这个线程要完蛋了呢?这时我们要去Thread类里面有没有一个注册方法(回调方法),在线程死亡之前调用这个注册方法,做一些收尾工作。类似于监听器,提前注册,说,当我走的时候通知你,但是你要提前在我这里登记。就是说人家什么时候走,你不知道,必须是人家在走的时候调用你,你不能去询问别人,说哥们你的状态是不是快死了?因为你不知道什么时候去问,不可能一天到晚去问的,要是,你不管,等到他最后走的时候,他通知你,他主动来跟你告别,你是不是就知道他要走了。不能不停的去轮询他,而要他来通知我们。这叫注册监听器,回调。比如说虚拟机有各类叫Runtime,他有个getRuntime()方法会返回一个Runtime对象,就是它自己的实例,这个runtime对象就代表运行的那个虚拟机,就代表整个虚拟机,等到了这个虚拟机的实例以后,调用一个方法叫void addShutdownHook(Thread hook),就增加一个线程,在虚拟机最后死的时候,整个虚拟机死的时候,不是说某个线程死的时候,整个虚拟机死的时候呢,他会调用这个Thread身上的run方法运行,如果你想在整个虚拟机死的时候,给你发封邮件,你怎么做?你new一个Thread,那个Thread写上发邮件的代码,把这个Thread对象传进去,那么每次虚拟机死的时候就给你发邮件。这就是每次虚拟机死的时候通知你。那,现在要做的是线程死的时候通知我。我想应该也会有类似的机制来告诉我。这个在网上并没有查到,需要自己来实现。线程调用你的A模块,线程调用你的B模块,你在A模块或B模块里面是不会知道线程什么时候结束的。线程是整个操作系统启动,他就一路往下运行,然后,它在运行的过程当中调用了我的代码,我的代码能反过来知道这个线程什么时候结束吗?不知道的。

l  ThreadLocal的作用和目的:用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。

l  每个线程调用全局ThreadLocal对象的set方法,就相当于往其内部的map中增加一条记录,key分别是各自的线程,value是各自的set方法传进去的值。在线程结束时可以调用ThreadLocal.clear()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的ThreadLocal变量。

l  ThreadLocal的应用场景:

Ø  订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。

Ø   银行转账包含一系列操作: 把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的帐户对象的方法。

Ø  例如Strut2的ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在哪个模块中getContext方法,拿到的都是同一个。

l  实现对ThreadLocal变量的封装,让外界不要直接操作ThreadLocal变量。

Ø  对基本类型的数据的封装,这种应用相对很少见。

Ø  对对象类型的数据的封装,比较常见,即让某个类针对不同线程分别创建一个独立的实例对象。

总结:一个ThreadLocal代表一个变量,故其中里只能放一个数据,你有两个变量都要线程范围内共享,则要定义两个ThreadLocal对象。如果有一个百个变量要线程共享呢?那请先定义一个对象来装这一百个变量,然后在ThreadLocal中存储这一个对象。

 [z1]副本可以理解为实例的实际内存空间,副本的概念是这样的,

类变量属于静态变量,所有对它的引用实际上都指向同一个实际的内存地址。

而,实例变量表示每个对象自己保存一份,不同的对象各自维护着自己的那一份,这里的副本可以理解为某个变量实际的内存空间。

转载于:https://my.oschina.net/kangxi/blog/1822023

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值