当前位置: 首页 > 后端技术 > Java

线程安全设计

时间:2023-04-01 16:56:07 Java

目录StatelessobjectsImmutableobjectsThread-specificobjectsThread-specificobjectsthread-specificobjectsmaycauseproblems对象是对操作和数据的封装(对象=操作+数据)。对象中包含的数据称为对象的状态。它包含对象的实例变量和静态变量中的数据,也可能包含对对象中其他变量的引用。实例变量或静态变量。如果一个类的实例在没有共享状态的情况下被多个线程共享,那么它被称为无状态对象。这个类的对象是一个有状态的对象classA{Integerage;}这个类的对象会引用其他有状态的对象,所以是一个有状态的对象@ComponentclassB{@AutowireprivateAa;}只有操作没有状态-statelessclassC{publicvoidtest(){......}}本身和引用是无状态的-stateless@ComponentclassD{@AutowireprivateCc;}一个类,即使没有实例变量或静态变量可能仍具有共享状态,如下enumSingleton{INSTANCE;私有EnumSingleton单例实例;Singleton(){singletonInstance=newEnumSingleton();}publicEnumSingletongetInstance(){//对singletonInstance做一些配置操作//比如singletonInstance.num++;返回单例实例;}}classSingletonTest{publicvoidtest(){Singletonst=Singleton.INSTANCE;st.getInstance();}}enum的INSTANCE只会被实例化一次,所以如果没有对应操作的注解,这就是一个完美的单例,其他类对它的引用是无状态的。但是如果在getInstance方法中操作singletonInstance变量,那么singletonInstance在多线程环境下就会有线程安全问题。在下面的例子中问题更明显,num变量是一个共享状态变量。枚举单例{INSTANCE;私人整数;publicintdoSomething(){num++;返回数;}}classSingletonTest{publicvoidtest(){Singletonst=Singleton.INSTANCE;st.doSomething();}}还有一种情况就是静态变量的使用。静态变量直接与类关联,不会随着实例的创建而改变。因此,当Test没有实例变量和静态变量时,直接在方法中通过类名来操作静态变量。还是会造成线程安全问题,就是Test中有共享状态变量的调用。classA{staticintnum;}classTest{publicvoiddoSomething(){A.num++;}}总结:无状态对象不得包含任何实例变量或可更新的静态变量(包括来自相应类变量或静态变量的上层类的实例)。但是,不包含任何实例变量或静态变量的类不一定是无状态对象。使用无状态类和只有静态方法的Servlet类是无状态对象的典型应用。Servlet一般由Web服务器(tomcat)托管,控制其创建、运行、销毁等生命周期。通常,一个Servlet类位于Web服务器中。只有一个实例会被管理,但是它会被多个线程的请求访问,它处理请求的方法没有加锁修改。如果这个类中包含实例变量或者静态变量,就会出现线程安全问题,所以一般来说,Servlet实例都是无状态对象。不可变对象不可变对象(ImmutableObjects)是指一旦创建状态就不能改变的对象。不可变对象满足以下条件。类由final字段修饰,以防止通过继承改变类的定义。所有的成员变量都需要被final修饰。一方面保证变量不能被修改,另一方面当final属性对其他线程可见时,必须对其进行初始化(有的博文必须是private+final,其实是final保证数据不能被修改)。如果这个字段是一个可变的引用对象,你需要修改这个字段为private,并且不提供修改这个引用对象状态的方法。对象在初始化时没有逃逸(防止对象在初始化时被修改(匿名类)):一个没有被初始化的对象被其他线程感知到,称为对象逃逸,可能导致程序错误。下面是对象可能如何逃脱。在构造函数中,将其分配给共享变量。在构造函数中,将this作为参数传递给其他方法。在构造函数中,启动一个基于匿名类的线程。以下是一个不可变对象。通常情况下,我们创建一个实例后,就不能再改变它的状态,所以如果需要修改它,只能重新创建一个实例来代替它。finalclassScore{最终分数;私人最终学生学生;publicSc??ore(intscore,Student学生){this.score=score;this.student=学生;}publicStringgetName(){returnstudent.getName();}classtest(){......publicvoidupdate(){Scores=newScore(...);}}使用不可变对象会对垃圾回收效率产生影响,既有正面影响,也有负面影响。负面:由于每当要更新不可变对象时都必须重新创建一个新的不可变对象,因此过于频繁地创建对象会增加垃圾回收的频率。正:一般来说,如果一个对象中有一个成员变量是对象引用,那么变量对象中的引用对象一般在新生代,变量对象本身在老年代,但是不可变对象一般在老年代是引用对象,不可变对象本身在新生代。当修改一个可变对象的实例变量值时,如果该对象已经在老年代,那么当垃圾回收器进行下一轮次要收集(MinorCollection)时,老年代包含卡(Card)中的所有对象,oldgeneration中存放对象的存储单元,一个Card的大小为512bytes)必须扫描以确定oldgeneration中是否有对象持有要回收对象的引用。因此老年代对象持有新生代对象的引用会增加minorcollections的开销。迭代器模式可以用来减少不可变对象占用的内存空间。1线程专有对象对于一个非线程安全的对象,每一个访问它的对象都会创建一个该对象的实例,每个线程只能访问自己创建的。一个对象实例,这个对象实例称为线程特定对象(TSO,ThreadSpecificObject)或线程局部变量。ThreadLoacl\相当于线程访问自己特有对象的代理,线程可以通过这个对象创建和访问自己的线程专有对象。ThreadLocal实例为访问它的每个线程提供了该线程的线程特定对象。方法函数publicTget()获取线程局部变量关联的当前线程的线程专有对象publicvoidset(Tvalue)重新关联线程局部变量protectedT对应的当前线程的线程专有对象initialValue()这个方法线程局部变量的返回值(对象)是初始状态下线程局部变量对应的当前线程的线程专有对象。publicvoidremove()删除线程局部变量与当前线程对应的线程专有对象之间的关联。简单的ThreadLocal使用方法及源码分析a.testA();}};t.setName("t1");t.开始();}}classA{finalstaticThreadLocalTL=newThreadLocal(){@OverrideprotectedStringinitialValue(){return"A";}};voidtestA(){Stringstr=TL.get();System.out.println("str="+str);TL.set("B");str=TL.get();System.out.println("str="+str);}}----------------------------------------str=Astr=B如上我们在ThreadLocalDemo类中新建一个ThreadLocal实例,使用匿名类anonymousclass重写了initialValue方法。让我们调试一下,看看整个样本是如何流动的。在上图中的位置给get方法打断点,因为这个线程对应的threadLocals是null值,所以必须先进入setInitialValue方法进行初始化。在这个方法中,主要是获取我们要代理的对象。如果在声明ThreadLocal时没有重写initialValue()方法,这里会得到一个null值。然后为当前线程创建一个ThreadlocalMap实例并写入元素,最后返回对象实例,ThreadLocal.get()方法获取值。然后我们进入set方法观察这个过程。也会先获取当前线程的threadlocals,判断是否为空。如果为空,就会为他初始化一个,并将set的参数存放在threadlocals中。否则,它会直接设置Threadlocal→值存储在这个容器中。set方法的过程基本上就是将我们的ThreadLocal(TL)实例要代理的对象实例(“B”)插入或修改到ThreadLocalMap的Entry数组中,然后通过hashcode确定数组中的对象实例ThreadLocal(TL)的位置。通过对threadLocalHashCode的跟踪,我们发现它是ThreadLocal类中的一个自增静态变量。以上是在线程存储入口数组中查找相关的threadLocal实例,插入替换方法:当找到threadLocal有对应的入口时,直接替换该值。当在遍历过程中发现无效条目时,将替换该条目的条目。键值替换不存在以上两种情况。在空槽中插入一个新条目。newEntry(key,value)插入新的entry后,需要遍历Entry数组找到值为null的Entry,对应的将entry设置为null,然后判断Entry数组是否需要展开。set结束后,再次调用get获取ThreadLocal代理的实例对象,因为之前线程的threadlocals已经初始化完毕,并且有一个Entry对应当前的ThreadLocal(TL)对象(initValue初始化为A的时候先调用get,再调用set是B),所以可以直接获取到目标代理的实例对象(“B”)。通常,线程局部变量会被声明为静态变量,因为它们只会在类加载时被创建一次。如果声明为实例变量,则每次创建类的实例时都会创建线程局部变量,会造成资源浪费。可能由特定于线程的对象引起的问题。数据退化和混乱。如上图所示,由于TL是一个静态变量,所以每次newTask()都会导致Thread-A执行完Task-1,不会重新初始化TL。再次执行Task-2时,由于它们是同一个线程执行的,它们通过TL获取的Map对象实例都是线程专有对象,这可能会导致Task-2获取到Task-1数据的操作,这可能造成数据混乱。所以在这种情况下,在获取到ThreadLocal代理的对象实例之后,我们需要对其做一些前置操作,比如清除上面的HashMap对象实例。TL.get().clear();很多时候我们也会使用ThreadLocal来传递一些数据,例如:在ThreadLocal中存储token等信息,但是为了防止下一个任务获取到这个请求token信息,需要在拦截器的后处理器中将其移除设备,这个操作是为了防止数据混乱!内存泄漏问题内存泄漏→指的是一个对象永远无法被虚拟机垃圾回收,一直占用某块内存无法释放。内存泄漏增加会导致可用内存越来越少,甚至可能导致内存溢出。根据前面的ThreadLocal源码分析,我们将ThreadLocal对象实例及其代理对象以key-value的形式存储在Thread对应的ThreadLocalMap中,而key-value实际存储在ThreadLocalMap的Entry数组中,即也就是说,我们会将key-value封装成一个Entry对象。从上图可以看出Entry对ThreadLocal实例的引用是弱引用。当没有其他对象的强引用时,ThreadLocal实例会被虚拟机回收。此时Entry中的key会被编程为null,即此时Entry会发生变化。进入无效条目。另外Entry对线程专有对象的引用是强引用,所以如果Entry变成了无效的entry,由于强引用的关系,这个线程专有对象不会被回收,即如果无效的entry不会被回收长时间删除如果清理或从不清理,会长期占用内存,造成内存泄漏。那么什么时候清理无效条目呢?在前面的ThreadLocal源码分析中,ThreadLocal.set()操作(插入ThreadLocalMap)会导致无效Entry的替换或清理,但是如果没有对本线程的ThreadLocalMap进行插入操作,无效的Entry会一直占据记忆。因此,为了防止这种现象的发生,我们需要养成良好的习惯。每次使用ThreadLocal对象后,手动调用ThreadLocal.remove方法清理无效条目(通常在线程结束后调用)。回填?