2.4.2-线程安全性

优质
小牛编辑
130浏览
2023-12-01

1.1 定义

  • 线程安全:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
  • 线程不安全:如果一个类对象同时可以被多个线程访问,如果不做同步处理,可能表现出线程不安全现象(抛出异常、逻辑错误)。

    1.2 原子性

    1.2.1 定义

    提供了互斥访问,同一时刻(时间段)只能有一个线程对它进行操作。避免脏读:数据读取过程中发生了写操作,数据发生了改变。

    1.2.2 实现方法

    1.2.2.1 synchronized

    同一对象同时只能被 synchronized 一次。
  • 修饰动态方法
  • 修饰静态方法
  • 修饰代码块,synchronized(this){}
  • 修饰代码块,synchronized(引用类型){}
  • 修饰代码块,synchronized(类.class){}

    1.2.2.2 ReentrantLock

    1.2.2.3 ReentrantReadWriteLock

    1.2.2.4 StampedLock

    1.2.2.4.1 功能
    读写锁虽然实现了读和读的并发,但读写之间是冲突的。
    1.2.2.4.2 使用步骤
  • 主线程使用 new StampedLock() 方法创建 StampedLock 对象。
  • 写前使用 stampedLock.writeLock() 方法加写锁。写锁不会阻塞 tryOptimisticRead() 方法,但会阻塞 readLock()方法,改变 stamp。
  • 读前使用 long stamp = s1.tryOptimisticRead() 方法获取版本号,读取数据。
  • 确定版本号是否合法(一致),如果是则直接返回结果。
  • 如果不是,则将乐观锁升级为悲观锁,如果对象正在被修改,则本线程挂起。读取,释放读锁,返回结果。

    1.2.2.5 CAS

    public final int incrementAndGet() {
          return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
      }
    
    // var1 this
    // var2 valueOffset
    // var4 1
    public final int getAndAddInt(Object var1, long var2, int var4) {
          // var5 变量当前值
          int var5;
          //如果变量当前值
          do {
              var5 = this.getIntVolatile(var1, var2);
          } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    
          return var5;
      }
    
    // 
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    
    if (var4 == var1.get(var1,var2)) {
      var4 = var5
      return true;
    } else {
      return false;
    }
    

    1.3 可见性

    1.3.1 定义

    一个线程对主内存的修改可以及时被其他线程观察到,例如取款和余额查询业务。

    1.3.2 实现方法

    如果没有 violate ,可能当前线程看似完成了赋值操作(即运行到下一步),但数据没同步进主存。

    1.4 有序性

    1.4.1 定义

    一个线程观察其他线程的执行,由于指令重排序的存在,该观察结果一般杂乱无序。

    1.4.2 实现方法

    1.4.2.1 happens-before 原则

  • 同一线程内保证顺序性。
  • 解锁(Lock、synchronized)操作 happened-before 加锁操作。
  • violate 关键字修饰的变量在逻辑上但写操作,happened-before 随后对该变量的读操作。
  • 实现方式:内存屏障。

    2. 线程安全策略

    2.1 不可变对象

    2.1.1 final 关键字

  • final 修饰的类不允许继承。
  • final 修饰的方法不允许重写。
  • final 修饰的变量在定义时必须初始化,后期不能修改,并且该变量在子类中也不能被修改,final 修饰的入参在函数中不能被修改,但 final 修饰对象的成员变量能被修改,修饰的容器内的元素能被修改。
  • 针对引用对象,使用 final 定义对象和其成员变量保证不变性。
  • 针对容器对象,有两种方式保证不变性:

    2.2 线程封闭

    2.2.1 ThreadLocal

    2.2.1.1 定义

    将对象封装到一个线程中,只有该线程能访问该对象。

    2.2.1.2 实现步骤

    public class Thread implements Runnable {
      ......
      ThreadLocal.ThreadLocalMap threadLocals = null;
      ......
    
    public T get() {
          Thread t = Thread.currentThread();
          ThreadLocalMap map = getMap(t);
          if (map != null) {
              ThreadLocalMap.Entry e = map.getEntry(this);
              if (e != null) {
                  @SuppressWarnings("unchecked")
                  T result = (T)e.value;
                  return result;
              }
          }
          return setInitialValue();
      }
    
    ThreadLocalMap getMap(Thread t) {
          return t.threadLocals;
      }
    
    public void set(T value) {
          Thread t = Thread.currentThread();
          ThreadLocalMap map = getMap(t);
          if (map != null)
              map.set(this, value);
          else
              createMap(t, value);
      }
    

    2.2.1.3 使用步骤

  • 当Controller被调用时,调用RequestHolder的getId()静态方法,getId方法调用ThreadLocal对象的get方法。
  • 定义Filter,调用RequestHolder的add方法将当前线程的id添加进ThreadLocal。
  • 定义Interceptor,在完成请求响应后清除数据。
  • 注入 Filter 和Interceptor。
  • 好处:避免每次都从Request中取用户信息。

    2.3 线程安全对象

    2.3.1 定义

    为了省略对于线程不安全类的并发访问需要进行的同步处理。

    2.3.2 实现方法

    2.3.2.1

  • StringBuilder:线程不安全。
  • StringBuffer:线程安全,方法使用synchronized关键字。

2.3.2.2

  • SimpleDateFormat:线程不安全。
  • DateTimeFormatter:线程安全,joda-time包。

2.3.2.3 AutomicXXX

线程安全,使用 CAS 实现。

2.4 被守护对象

2.4.1 定义

(lock、synchorized),对于共享变量的修改和读取使用锁或同步机制。

2.4.2 实现方法

参考本文章节 1.2.2。

参考资料