目录
- 无状态对象
- 不可变对象
- 线程特有对象
- 线程特有对象可能造成的问题
无状态对象
有状态和无状态的区别:有状态-会存储数据、无状态-不会存储数据
对象就是操作和数据的封装(对象 = 操作 + 数据),对象所蕴含的数据就被称为该对象的状态,它蕴含对象的实例变量和动态变量中的数据,也有可能蕴含对象中援用其它变量的实例变量或动态变量。如果一个类的实例被多个线程共享不会存在共享状态,那么则称其为无状态对象。
这个类的对象是一个有状态对象class A{ Integer age;}这个类的对象中会援用其它有状态的对象所以它是一个由状态对象@Componentclass B{ @Autowire private A a; }只有操作没有状态 - 无状态class C{ public void test(){ ...... }}自身和援用都是无状态 - 无状态@Componentclass D{ @Autowire private C c;}
一个类即便不存在实例变量或动态变量依然可能存在共享状态,如下
enum Singleton{ INSTANCE; private EnumSingleton singletonInstance; Singleton(){ singletonInstance = new EnumSingleton(); } public EnumSingleton getInstance(){ //对singletonInstance做一些配置操作 //例如 singletonInstance.num++; return singletonInstance; }}class SingletonTest{ public void test(){ Singleton st = Singleton.INSTANCE; st.getInstance(); }}
enum的INSTANCE只会被实例化一次,所以如果没有正文所对应的操作,这就是一个完满的单例,且其它类对它的援用都是无状态的。然而如果在getInstance办法中对singletonInstance变量有所操作,那么在多线程环境中singletonInstance会呈现线程平安问题。上面的例子这个问题更显著,num变量就是一个共享状态的变量。
enum Singleton{ INSTANCE; private int num; public int doSomething(){ num++; return num; }}class SingletonTest{ public void test(){ Singleton st = Singleton.INSTANCE; st.doSomething(); }}
还有一种状况是动态变量的应用,动态变量与类(class)间接关联,不会随着实例的创立而扭转,所以当Test没有实例变量和动态变量的状况下,在办法中通过类名间接操作动态变量,依然会造成线程平安问题,即Test中存在共享状态变量的调用。
class A{ static int num;}class Test{ public void doSomething(){ A.num++; }}
总结:无状态对象必定是不蕴含任何实例变量或者可更新动态变量(包含来自相应类的下层类的实例变量或者动态变量)。然而,一个类不蕴含任何实例变量或者动态变量却不肯定是无状态对象。
应用无状态类和只有静态方法的类
Servlet类就是无状态对象的典型利用,Servlet个别被web服务器(tomcat)托管,管制它的创立、运行、销毁等生命周期,个别一个Servlet类在web服务器中只会有一个实例被托管,然而它会被多个线程申请拜访,并且,其解决申请的办法service并没有锁润饰,如果这个类外面蕴含实例变量或者是动态变量就会产生线程平安问题,所以个别状况下Servlet实例是无状态对象。
不可变对象
不可变对象(Immutable Object)指的是一经创立就不可扭转状态的对象。
不可变对象满足以下条件
- 类是被final字段润饰,避免通过继承来扭转类的定义
- 所有成员变量须要应用final润饰,一方面保障变量不能被批改,另一方面保障final属性对其它线程可见时,必然是初始化实现的(有的博文写必须是private+final,其实final就保障了数据不可批改)。
- 如果这个字段是可变的援用对象,须要用private润饰这个字段且不提供批改这个援用对象状态的办法。
对象在初始化过程中没有逸出(避免对象在初始化过程中被批改状态(匿名类)):一个还没初始化实现的对象被其它线程感知到,这就被称作是对象逸出,从而可能导致程序运行谬误。上面是可能导致对象逸出的形式。
- 在结构器中将this赋值给一个共享变量
- 在结构器中将this作为参数传递给其它办法
- 在结构器中启动基于匿名类的线程
上面是一个不可变对象,失常状况下咱们创立实例后就不能再扭转它的状态,所以须要对他进行批改的话只能从新创立一个实例来代替它。
final class Score{ final int score; private final Student student; public Score(int score,Student student){ this.score = score; this.student = student; } public String getName(){ return student.getName(); }}class test(){ ...... public void update(){ Score s = new Score(...); }}
不可变对象的应用会对垃圾回收的效率产生影响,既有踊跃的一方面影响,又有消极的影响。
负面:因为只有想对不可变对象做出更新,就得从新创立一个新的不可变对象,过于频繁的创建对象会减少垃圾回收的频率。
侧面:一般来说对象中存在成员变量是对象援用的话,那么个别在可变对象中这个援用对象是在年老代,而可变对象自身在老年代,然而不可变对象个别是援用对象处于老年代,而不可变对象自身处于年老代。批改一个状态可变对象的实例变量值的时候,如果这个对象曾经位于年轻代中,那么在垃圾回收器进行下一轮主要回收(Minor Collection)的时候,年轻代中蕴含这个对象的卡片(Card,年轻代中存储对象的存储单位,一个Card的大小为512字节)中的所有对象都必须被扫描一遍,以确定年轻代中是否有对象看待回收的对象持有援用。因而,年轻代对象持有对年老代对象的援用会导致主要回收的开销减少。
能够采纳迭代器模式来缩小不可变对象的内存空间占用1
线程特有对象
对于一个一个非线程平安的对象,每个拜访它的对象都创立一个该对象的实例,每个线程只能拜访本人创立的对象实例,这个对象实例就被称作为线程特有对象(TSO,Thread Specific Object)或线程局部变量。
ThreadLoacl\<T>相当于线程拜访其特有对象的代理,线程能够通过这个对象创立并拜访各自线程的特有对象。
ThreadLocal实例为每个拜访它的线程提供了一个该线程的线程特有对象。
办法 | 性能 |
---|---|
public T get() | 获取与该线程局部变量关联的以后线程的线程特有对象 |
public void set(T value) | 从新关联该线程局部变量所对应的以后线程的线程特有对象 |
protected T initialValue() | 该办法的返回值(对象)就是初始状态下该线程局部变量所对应的以后线程的线程特有对象 |
public void remove() | 删除该线程局部变量与相应的以后线程的线程特有对象之间的关联关系 |
ThreadLocal的简略应用办法及源码剖析
public class ThreadLocalDemo { public static void main(String[] args) { Thread t = new Thread(){ @Override public void run() { A a = new A(); a.testA(); A.TL.remove(); } }; t.setName("t1"); t.start(); }}class A{ final static ThreadLocal<String> TL = new ThreadLocal<String>(){ @Override protected String initialValue() { return "A"; } }; void testA(){ String str = TL.get(); System.out.println("str = " + str); TL.set("B"); str = TL.get(); System.out.println("str = " + str); }}--------------------------------------str = Astr = B
如上咱们在ThreadLocalDemo类中新建一个ThreadLocal实例且应用匿名类匿名类将initialValue办法重写。上面调试查看一下整个样例的流转形式。
在上图地位给get办法打一个断点
因为此线程对应的threadLocals是一个空值所以要先进入到setInitialValue办法对其进行初始化。
在这个办法中次要就是获取咱们要代理的对象,如果在申明ThreadLocal的时候不重写initialValue()办法则在这个中央获取的是一个空值。而后就给以后线程创立一个ThreadlocalMap实例并写入元素,最初将对象实例返回,ThreadLocal.get()办法就获取到了值。
而后咱们再进入set办法察看其中的流程
同样会先获取当先线程的threadlocals判断其是否为空,如果为空将会帮他初始化一个,并将set的参数存入到threadlocals,否则将间接将Threadlocal→value存入这个容器中。
set办法的流程根本就是向ThreadLocalMap的Entry数组中插入或批改咱们这个ThreadLocal(TL)实例要代理的对象实例("B"),而后就是通过ThreadLocal(TL)的hashcode来确定这个对象实例在数组中的地位。
基于对treadLocalHashCode的追溯,咱们发现其就是ThreadLocal类中一个自增的动态变量。
下面是对于相干的threadLocal实例在线程的存储条目数组中的查找,及插入替换形式:
- 当查找到threadLocal有对应的条目时会间接将value替换
- 当遍历的的时候发现了有效条目,将这个条目标key和value替换
- 没有上述两种状况,在为null的槽位插入新的条目new Entry(key,value)
在插入新的条目后要对Entry数组进行遍历查找value为null的Entry,并将其对应条目置为null,而后判断Entry数组是否须要进行扩容操作。
当set完结后,再次调用get获取ThreadLocal代理的实例对象,因为之前当先线程的threadlocals曾经被初始化并且存在以后ThreadLocal(TL)对象对应的Entry(第一次get的时候调用initValue初始化为A前面又set为B),所以能够间接取得指标代理的实例对象("B")。
个别线程局部变量都会被申明为动态变量,因为这样只会在类加载的时候被创立一次,如果申明为实例变量,那么每次创立一个类的实例这个线程局部变量都会被创立一次,这样会造成资源的节约。
线程特有对象可能造成的问题
数据进化与错乱问题
如上图因为TL是一个动态变量,所以每次new Task()是不会对TL从新初始化,就会导致当Thread-A在执行完Task-1后再执行Task-2时,因为执行它们的时同一个线程,所以,它们通过TL获取到的Map对象实例都是同一个线程特有对象,这就导致Task-2可能会获取到Task-1操作的数据,这样就可能造成数据错乱问题。
所以在这种状况下,咱们须要在获取到ThreadLocal代理的对象实例后,须要先对其做一些前置操作,如对下面的HashMap对象实例进行清空。
TL.get().clear();
很多时候咱们也会用ThreadLocal传递一些数据,例如:在ThreadLocal中存储token等信息,然而为了不让下一个工作获取到这次的申请token信息,须要在拦截器的后置处理器中将其remove掉,此操作就是为了避免数据错乱!
内存透露问题
内存透露→指的是一个对象永远不能被虚拟机垃圾回收,始终占用某一块内存无奈开释。内存透露的减少会导致可用的内存越来越少,甚至可能导致内存溢出。
由之前的ThreadLocal源码剖析可知,咱们将ThreadLocal对象实例和它代理的对象以key-value的形式存入到Thread对应的ThreadLocalMap中,而真正存储这个key-value的是ThreadLocalMap一个的Entry数组,也就是咱们会将key-value封装成Entry对象。
由上图可知Entry对于ThreadLocal实例的援用是以个弱援用,在没有其它对象强援用时ThreadLocal实例会被虚拟机回收掉 ,这时候Entry中的key就会编程null,也就是此时Entry会变成一个有效条目。
另外,Entry对于线程特有对象的援用是强援用,所以如果Entry变成了有效条目后,这个线程特有对象因为强援用的关系并不会被回收,也就是说,如果有效条长时间不会被清理或者永远不被清理那么就会对内存长时间的占用,营造出内存透露的景象。
那么有效条目什么时候会被清理呢?之前ThreadLocal的源码剖析中,ThreadLocal.set()操作(对ThreadLocalMap的插入)时能够引发生效Entry的替换或者清理,然而如果始终没有对这个线程的ThreadLocalMap的插入的操作的话有效条目就会始终占用内存。所以为了避免这种景象的产生,咱们须要养成一个良好的习惯,在每次对ThreadLocal对象应用结束后,手动的调用ThreadLocal.remove办法来清理有效条目(个别在线程完结后调用)。
- 前面补 ↩