java集合
- 集合包含Collection接口和Map接口
- 顶层接口是collection,底下有2个常用子接口分别为list接口(有序可重复)和set接口(无序不可重复)
- list接口常用的实现类有Vector(数组,线程安全),ArrayList(数组,线程不安全),LinkList(链表)
- set接口常用的实现类有HashSet(底层HashMap),TreeSet(底层Treemap,二叉树里面的红黑树,可以实现有序,类实现comparable接口,重写compareTo方法)
- 顶层接口Map接口(键值对)常用的实现类有Hashtable(线程安全,不允许存放null键,null值),HashMap(哈希表和哈希函数(数组+链表)线程不安全,允许存放null键和null值),TreeMap(红黑树,对键进行排序),properties(Hashtable子类,读配置文件,都是String类型)
总结:
- 记一种思想:旧的都是线程安全的,也叫线程同步,但是效率低,最终被新的替代,但是新的效率是提高了但是线程不同步。(这个原理跟StringBuffer,StringBuilder一样)
集合中例如:Vector和ArrayList,Hashtable和HashMap。
有相关的工具类(Collections工具类)可以相互转换! - String类重写了hashcode和equals方法,所以可以直接用来当做map的键。
- 关于Set保证键唯一:先比较hashcode(有存储地址等等信息),如果不相同,直接加入set即可;如果相同,再比较equals方法,equals方法相同就不加入,equals方法不相同就加入。(不同的对象hash值可能相同(所以需要再用equals判断),但是hash值不同的对象,一定不相同(所以直接加入集合即可)),联想到hash冲突。
- Hashset的底层实现是用Hashmap实现的,TreeSet的底层实现使用TreeMap实现的,所以说set是基于map实现的。
ThreadLocal(看第5条即可)
- ThreadLocal意思是线程局部变量,用来存变量的,虽然ThreadLocal结构是个map,但是一个ThreadLocal对象只存一个变量,该变量可以是一个对象,也可以是基本数据类型,有个特点是该变量在每一个用到它的线程中都是独立的一份,各线程间的那份互不影响。如果是封装成共享数据对象变量的话,我理解的是线程级别的单例(下面的例子就是根据这个想法写的一个实例),同理你可以在每个线程中重新new一个对象也能达到同样的目的。
- 关于每个Thread线程里面都维系着一个ThreadLocalMap对象,用来存ThreadLocal变量的,key是是ThreadLocal变量名,值是放进ThreadLocal里面的变量,所以一个线程中可以有多个ThreadLocal变量。
- 就是想在同一线程中获取的是同一个对象,其余线程里操作的是另一个实例对象,互不影响,为了保持线程自身对象安全不被其余线程污染,并不能解决共享变量的问题,你传进去的共享变量还是会存在线程安全问题,主要适用场景是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。保护该线程私有的变量的作用很明显。
- 在线程中存放一些就像session的这种特征变量,会针对不同的线程,有不同的值。
- 来自马士兵老师收获:每个线程都有个变量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中获取即可。
另外一个例子是动态数据源的使用,我们可以使用ThreadLocal来保证当次线程调用中只使用一种数据源