关于多线程:保障线程安全的设计

5次阅读

共计 5676 个字符,预计需要花费 15 分钟才能阅读完成。

目录

  • 无状态对象
  • 不可变对象
  • 线程特有对象
  • 线程特有对象可能造成的问题

无状态对象

有状态和无状态的区别:有状态 - 会存储数据、无状态 - 不会存储数据

对象就是操作和数据的封装(对象 = 操作 + 数据),对象所蕴含的数据就被称为该对象的状态,它蕴含对象的实例变量和动态变量中的数据,也有可能蕴含对象中援用其它变量的实例变量或动态变量。如果一个类的实例被多个线程共享不会存在共享状态,那么则称其为无状态对象。

这个类的对象是一个有状态对象
class A{Integer age;}

这个类的对象中会援用其它有状态的对象所以它是一个由状态对象
@Component
class B{
  @Autowire
  private A a;  
}

只有操作没有状态 - 无状态
class C{public void test(){......}
}

自身和援用都是无状态 - 无状态
@Component
class 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 = A
str = 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 办法来清理有效条目(个别在线程完结后调用)。


  1. 前面补 ↩
正文完
 0