java集合 ThreadLocal理解

java集合

  1. 集合包含Collection接口和Map接口
  2. 顶层接口是collection,底下有2个常用子接口分别为list接口(有序可重复)和set接口(无序不可重复)
  3. list接口常用的实现类有Vector(数组,线程安全),ArrayList(数组,线程不安全),LinkList(链表)
  4. set接口常用的实现类有HashSet(底层HashMap),TreeSet(底层Treemap,二叉树里面的红黑树,可以实现有序,类实现comparable接口,重写compareTo方法)
  5. 顶层接口Map接口(键值对)常用的实现类有Hashtable(线程安全,不允许存放null键,null值),HashMap(哈希表和哈希函数(数组+链表)线程不安全,允许存放null键和null值),TreeMap(红黑树,对键进行排序),properties(Hashtable子类,读配置文件,都是String类型)

总结:

  1. 记一种思想:旧的都是线程安全的,也叫线程同步,但是效率低,最终被新的替代,但是新的效率是提高了但是线程不同步。(这个原理跟StringBuffer,StringBuilder一样)
    集合中例如:Vector和ArrayList,Hashtable和HashMap。
    有相关的工具类(Collections工具类)可以相互转换!
  2. String类重写了hashcode和equals方法,所以可以直接用来当做map的键。
  3. 关于Set保证键唯一:先比较hashcode(有存储地址等等信息),如果不相同,直接加入set即可;如果相同,再比较equals方法,equals方法相同就不加入,equals方法不相同就加入。(不同的对象hash值可能相同(所以需要再用equals判断),但是hash值不同的对象,一定不相同(所以直接加入集合即可)),联想到hash冲突。
  4. Hashset的底层实现是用Hashmap实现的,TreeSet的底层实现使用TreeMap实现的,所以说set是基于map实现的。

ThreadLocal(看第5条即可)

  1. ThreadLocal意思是线程局部变量,用来存变量的,虽然ThreadLocal结构是个map,但是一个ThreadLocal对象只存一个变量,该变量可以是一个对象,也可以是基本数据类型,有个特点是该变量在每一个用到它的线程中都是独立的一份,各线程间的那份互不影响。如果是封装成共享数据对象变量的话,我理解的是线程级别的单例(下面的例子就是根据这个想法写的一个实例),同理你可以在每个线程中重新new一个对象也能达到同样的目的。
  2. 关于每个Thread线程里面都维系着一个ThreadLocalMap对象,用来存ThreadLocal变量的,key是是ThreadLocal变量名,值是放进ThreadLocal里面的变量,所以一个线程中可以有多个ThreadLocal变量。
  3. 就是想在同一线程中获取的是同一个对象,其余线程里操作的是另一个实例对象,互不影响,为了保持线程自身对象安全不被其余线程污染,并不能解决共享变量的问题,你传进去的共享变量还是会存在线程安全问题,主要适用场景是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。保护该线程私有的变量的作用很明显。
  4. 在线程中存放一些就像session的这种特征变量,会针对不同的线程,有不同的值。
  5. 来自马士兵老师收获:每个线程都有个变量threadLocals(ThreadLocal.ThreadLocalMap threadLocals = null;)当你
public static void main(String[] args) {

        ThreadLocal<String> tl = new ThreadLocal<>();

        new Thread(new Runnable() {
            @Override
            public void run() {
                tl.set("测试一号");
                System.out.println(tl.get());//测试一号

            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                tl.set("测试二号");
                System.out.println(tl.get());//测试二号

            }
        }).start();

        System.out.println(tl.get());//null
    }
   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;
    }

进入set看源码发现会先获取当前线程,再获取当前线程的ThreadLocalMap。重点来了!!!map.set(this, value);set的key是this,而调用这个set方法的对象是tl,所以key是ThreadLocal对象tl,value是你要传的值,说明你也可以多放几个ThreadLocal对象(这里的set并不是把值set到ThreadLocal对象中,容易让人产生误解)。

再一个问题是

 //这个是上面set(value)方法里面的set(this,value)方法
  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);

          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) {
                  replaceStaleEntry(key, value, i);
                  return;
              }
          }

          tab[i] = new Entry(key, value);
          int sz = ++size;
          if (!cleanSomeSlots(i, sz) && sz >= threshold)
              rehash();
      }
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

我们发现了这是个弱引用,弱引用所指向的对象如果只有当前弱引用,无别的引用,那gc的时候直接会清除掉那个堆中的对象,避免了内存泄漏(假设是个强引用,那tl为null了,但map中的key还指向那个堆中ThreadLocal,导致不释放ThreadLocal对象,造成内存泄漏,所以是弱引用),一旦堆中的ThreadLocal对象没有了强引用只有这个弱引用了,那就可以清除掉堆中ThreadLocal了,清除掉后key将会变为null,但是整个Entry还有value没有释放,所以我们最好remove掉不用的键值对,虽然我们在get,set的时候会自动清理掉key为null的,但不用最好第一时间清理掉,防止内存泄漏。

再一个问题就是线程池中的线程被多次使用,所以线程池中每次回收回来的线程都会有个操作将线程中的ThreadLocalMap属性置为null,这也是为了防止上一个线程留下的脏数据。

再一个例子:
spring中@transaction事务中的数据库连接connection,该连接是每个线程用的同一个,该连接就是用的ThreadLocal.

扩展:
引用类型分为强软弱虚
强引用:不会被gc掉,就算内存溢出OOM也不会被干掉
软引用:空间充足时gc不会被干掉,堆空间不足时发生gc就会干掉
弱引用:只要gc就会干掉
虚引用:get()方法获取不到值,主要用来作为一个通知管理直接内存的,当虚引用的对象被清除不会直接清掉,而是会加入一个队列,后续操作一般是将这个虚引用所指向的直接内存释放掉,用的很少


测试的有点乱,ShardData2就是我们需要放到一个线程一个实例的类,这个类的定义是重点
package com.heima.test;

import java.util.Random;

public class TestThreadlocal {//专门用来测试的

    public static void main(String[] args) {
       // SharedData sharedData = new SharedData();//想要实现拿过来一个数据类后然后进行多线程操作,可以对自定义的优雅的数据集进行添加属性,编写复杂的方法,主要是对拿过来的数据进行封装到这个类中,然后就可以进行操作了!
        MyThread myThread = new MyThread();//这里面的属性会被共享,因为下面的线程都是用这一个实例创建的
        Thread thread1 = new Thread(myThread);
        Thread thread2 = new Thread(myThread);
        thread1.start();
        thread2.start();

    }
}

class MyThread implements Runnable{//自定义线程类

    private ThreadLocal<SharedData> threadLocal=new ThreadLocal<SharedData>();

    //SharedData sharedData;//这个是有状态的bean,因为里面存数据;因为是通过同一个子类开启的2个线程所以放在这里会被共享,存在多线程问题,所以你可以放到run方法中变成每一个线程创建一个
    //虽然你可以放到ThreadLocalMap中,但是多线程获取到的时候,是单独存储了一份引用,但是根据这个引用再去找到堆中的对象的时候,这个堆中的对象的属性可以被其余线程更改,所以说我们需要每一个线程都拿到单独的一个实例,然后在该线程中只操作这一份实例
    //如果这个变量可以直接拿来用拿来读,不做改变,那这没问题,一旦改变就会有问题!这也是多线程安全的本质,你只读肯定不会有线程安全问题,主要是根据业务肯定有改的地方
    //例如像dao层获取数据库连接的时候就用的ThreadLocal,每一个访问的线程都拥有自己单独的一个数据库链接实例,并且是不做更改的,只是使用(里面有链接数据库地址,连接用户名,连接密码等等)
    //如果不改变里面的值,也就是说不存在线程安全问题,那么设计成单例更好

    //如果防止了结束上一个线程关闭了连接,导致下一个线程也不能用了,但是想起我们以前是通过每次开启一个线程操作数据库的时候,是new一个数据库链接对象的方式进行操作的,需要频繁的new和关闭链接,然而
    //放到线程内部每次都new一个对象也是一样,目前来看没看到ThreadLocal的作用
    //其实想了一下:如果类是没有状态的,就算有状态存数据,但是只是去读取数据,不做修改,也不会出现线程安全问题,所以用一首单例岂不更好
    //单例可以在多线程之间通信,减少实例的数量,节约资源
    //说到多线程之间可以通信,完全可以用有状态的bean来存数据并设计成单例的,然后你需要严格的控制对这个有状态的bean进行操作,例如用锁等进行同步处理,避免执行到一半,让另一个线程又操作了这个单例对象,使得最终的结果出现错误
    //除了用锁还可以用这种ThreadLocal方式
    //对于网站计数器对象的例子:首先存数据了有状态的,也设计成单例,但是一想有状态的bean,如果多线程来访问,还是会存在问题,所以需要用到同步
    //一个单例模式的方法可以同时被多个线程处理,多个线程如果不是同时处理这一个对象的共有属性,则不会出现线程问题,如果两个线程同时访问同一个方法的时候,如果这个方法中没有都有的属性,则不需要加锁,反之则需要加锁。

//    public MyThread(SharedData sharedData){
//        this.sharedData = sharedData;
//    }

    @Override
    public void run() {

       // SharedData sharedData =new SharedData();
        Random random = new Random();
        int a=random.nextInt(100);
       // sharedData.setNum(a);
        System.out.println(Thread.currentThread().getName()+"初始的值为:"+a);
       // threadLocal.set(sharedData);//其实key就是threadLocal对象,值这里是个对象的引用,所以说这里的对象的具体的一个属性值是会变的


        ModuleA moduleA = new ModuleA();
        //moduleA.doSomethingA(threadLocal.get());


        ModuleB moduleB = new ModuleB();
       // moduleB.doSomethingB(threadLocal.get());
        ///////////////////////////////////////////////////////////////
     
        SharedData2.getInstance().setNum(a);//优雅方式对每个线程中独有的那个线程级变量进行赋值,因为每一个线程一个独立的对象

        moduleA.doSomethingA2();
        moduleB.doSomethingB2();


    }
}

class SharedData{//共享数据类

    private int num;

    public int getNum() {
        return num;
    }

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

class ModuleA{//对共享数据进行操作的A业务类

    public void doSomethingA(SharedData sharedData){
        sharedData.setNum(sharedData.getNum()+1);
        System.out.println(Thread.currentThread().getName()+"A业务看到的共享数据值为"+sharedData.getNum());
    }
    public void doSomethingA2(){//优雅方式的共享数据类获取值

        System.out.println(Thread.currentThread().getName()+"A2业务看到的共享数据值为"+ SharedData2.getInstance().getNum());
    }
}

class ModuleB{//对共享数据进行操作的B业务类
    public void doSomethingB(SharedData sharedData){
        System.out.println(Thread.currentThread().getName()+"B业务看到的共享数据值为"+sharedData.getNum());
    }

    public void doSomethingB2(){//优雅方式的共享数据类获取值
        System.out.println(Thread.currentThread().getName()+"B2业务看到的共享数据值为"+ SharedData2.getInstance().getNum());
    }

}

class SharedData2{//优雅的共享数据类:外界只需调用方法使用即可,外界根本看不到ThreadLocal,本类就是线程单例:在每一个线程中只有一个实例

    private SharedData2(){};//构造方法私有,外界无法生成该类对象实例,只能通过后面的公有静态方法生成实例

    //找到问题了,这个首先我们不是只创建这一个实例,所以没必要设置这一个实例,其次这是个静态的成员变量,是所有对象共享的,所以我出现了多线程问题
   // private static SharedData2 sharedData2 = null;//这里弄成成员变量跟单例一样,这也是懒汉模式,为了在后面只生成这一个实例


    private static ThreadLocal<SharedData2> threadLocal= new ThreadLocal<SharedData2>();//放线程独有变量

    public static SharedData2 getInstance(){//想获得该类对象只能通过该方法获得
        SharedData2 sharedData2=threadLocal.get();//先从当前线程的threadLocal中获取,看看是否有,有就直接返回,没有就需要创建一个对象然后存到threadLocal
        if(sharedData2==null){
            sharedData2=new SharedData2();
            threadLocal.set(sharedData2);
        }
        return sharedData2;
    }

    private int num;//私有成员变量,在堆里,堆里都是一个一个的实例,一个实例一个变量,所以没有线程安全问题

    public int getNum() {
        return num;
    }

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

应用场景例子

第一个例子:数据库连接



private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
    public Connection initialValue() {  
        return DriverManager.getConnection(DB_URL);  
    }  
};  
  
public static Connection getConnection() {  
    return connectionHolder.get();  
}

第二个例子:一个请求过来,会带有一堆的c参数(可以理解为客户端的一些标志),我们的应用处理过程中,大部分地方又不需要关心该参数,可能在某个请求他人接口的时候需要了,如果我们把所有代码都带上这个c参数,那么未免代码看着太过丑陋,这种情况下,我们可以构建一个filter,在请求过来的时候,在filter中将c参数放置到ThreadLocal中,在整个调用链中如果需要使用,直接从ThreadLocal中获取即可。
java集合 ThreadLocal理解_第1张图片
另外一个例子是动态数据源的使用,我们可以使用ThreadLocal来保证当次线程调用中只使用一种数据源

你可能感兴趣的:(java基础)