专栏首页李蔚蓬的专栏Android | 如何使程序实现线程安全(拓展关键词:ThreadLocal、重排序、volatile/final)

Android | 如何使程序实现线程安全(拓展关键词:ThreadLocal、重排序、volatile/final)

要点

  • 是否对线程安全有初步的了解(初级)
  • 是否对线程安全的产生原因有思考(中级) 优化线程安全要注意什么?
  • 是否知道final、volatile关键字的作用(中级)
  • 是否清楚1.5之前Java DCL 为什么有缺陷(中级)
  • 是否清楚地知道如何编写线程安全的程序(高级)
  • 是否对ThreadLocal的使用注意事项有认识(高级)

是否清楚地知道如何编写线程安全的程序

  • 什么是线程安全?
    • 不安全:资源不同步,脏读脏写; 如多个线程的工作内存读写主存时的不同步; “进程安全”问题不存在, 因为进程之间内存相互独立,各自独享内存的, 一个进程被杀掉的话,其所有内存都还给物理内存了; 可能共享CPU时间片; 线程是存在于进程当中的, 同一个进程中的线程之间是可以共享内存的;
    • 线程安全产生的原因:可变资源(内存)线程间共享(关键词“可变”和“共享”) 线程间不共享的资源不用考虑线程安全了;
  • PS:每一个线程都有自己的一个内存副本<Java内存模型>
  • 如何实现线程安全?
    • 不共享资源 共享才会产生线程安全问题, 所以尽量不共享;
    • 共享不可变资源(volatile、final)
      • 禁止重排序
    • 有条件地共享可变资源
      • (更改刷新的)可见性 一个线程对共享资源的修改,其他线程能够马上看到! 实现:某个线程对共享资源进行了更新时,要马上刷新到主存!
      • 操作原子性
      • 禁止重排序

不共享资源

  • 可重入函数: 传入一个参数进函数,经过一系列的运算, 再把运算结果返回出去, 中间不会涉及到任何对外部内存的访问、修改, 没有副作用, 像这样没有副作用的函数, 先天就具备线程安全的优势:
  • ThreadLocal实现不共享资源 虽然说每个线程都会去访问一个ThreadLocal对象, 但实际上最终访问的 都是自己线程内部的一个副本; 比如下图中的token, 对应的场景如, 一个服务器提供了很多个服务, 每个服务的话, 每个用户进来请求,服务器都会为这个用户 开一个线程 来提供服务, 这个时候, 因为每个用户 就都是属于不同的线程的, 而这里每个线程都去访问这个token 的时候, 都会有一个自己的 String的 一个副本, 这样线程间便不会互相干扰; 如此便是实现了不共享资源, 也就没有线程安全的问题了; 自己线程之内,不管怎么设置,都不会影响到其他线程:
  • ThreadLocal原理 看一下ThreadLocal的set方法; 可以看到, ThreadLocal的底层,其实是绑定到线程上的一个ThreadLocalMap, 添加值的时候置入键值对map.set(this,value), 使用的key,实际上就是this,即ThreadLocal类对象引用, value则企图传入的值; 既然是数据结构是绑定到线程上的, 也就是说, 假设,两个访问ThreadLocal的引用 它们所处的线程 是不一样的话, 那么,它们访问ThreadLocal的set、get时 处理的值,肯定也是不一样的!
    • ThreadLocal中这个ThreadLocalMap是,储存在、绑定在线程上的:

  • 两句话总结ThreadLocal特性:
    • 唯一 一个ThreadLocal对象,作为全局变量定义在主线程 为访问它(set())的N子线程 开启(createMap()N相互独立ThreadLocalMap 因此,每一个子线程访问主线程中的这个独一无二的ThreadLocal对象的时候, 总会访问到子线程自身对应的底层数据存储结构 ThreadLocalMap
    • 不同线程,访问一个ThreadLocal对象的时候, 访问的是(绑定不同线程的)不同底层数据结构ThreadLocalMap 读写的是不同的数据 实现了, 同属主线程的一系列子线程间的, 资源不共享,解决的了线程安全问题;

    实战案例如下:

package test;

public class ThreadLocalTest {
    
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();

    public static class MyRunnable implements Runnable {
          
        @Override
        public void run() {
            threadLocal.set((int) (Math.random() * 100D));
            System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
        }
    }
  
  
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable(), "A");
        Thread t2 = new Thread(new MyRunnable(), "B");
        Thread t3 = new Thread(new MyRunnable(), "C");

        t1.start();
        t2.start();
        t3.start();
    }


}

运行结果:

  • ThreadLocalMap 跟 WeakHashMap 很像
  • 本身对于对象的持有都是弱引用的; 区别是 ThreadLocalMap不用去监听ReferenceQueue, (监听ReferenceQueue还是有一定的开销的) 因,ThreadLocalMap适用于对象较少的场景, 另外, 线程退出时会自动移除;
  • 关于Hash冲突的解决方法也是不一样的, 单链表法传统HashMap解决办法开放地址法则适合对象比较少的情况, 即线性探测、平方探测、双散列法等等;
  • ThreadLocal的使用建议:
    • 声明为全局静态final成员 ThreadLocal在一个主线程中有一个实例就够了, 没必要每次创建子线程都整一个出来, 并且我们set value的时候, 我们是以ThreadLocal的this为key的, ThreadLocal这个对象的引用最好是独一的、不可更改的! 不设置final的话,还有另外的问题, 还要考虑什么时候去初始化它,还要考虑可见性, 这就还要考虑加锁了;
    • 避免存储大量对象 因, 底层数据结构、Hash冲突的解决方案和Hash计算算法, 已经做了限制;
    • 用完后及时移除对象 ThreadLocal自身没有监听机制, 如果你设置的ThreadLocal的存在周期非常的长, 那对应的线程就会一直存在, 其引用不会被回收,有内存泄漏风险

共享不可变资源(加final/volatile,禁止重排序)

首先普及一下重排序,等下涉及到

  • 什么是重排序?重排序是指令的重排序。 为了提高性能,编译器和处理器常常会对指令做重排序, 重排序就会导致多线程执行的时候有数据不一致问题, 导致程序结果不是理想结果。
  • 重排序分为三类:
    • 编译器重排序:不改变单线程程序语义前提下,重新安排执行顺序
    • 指令级并行重排序: 指令并行技术可以将多条指令重叠执行, 如果不存在数据依赖性, 处理器会改变语句对应的机器指令执行顺序
    • 内存系统重排序
案例:
  • 定义一个类: 两个成员,x为final,y不为final;
class FinalFieldExample{
    final int x;
    int y;

    public FinalFieldExample(){
        x = 3;
        y = 4;
    }
}

假设Thread1 为 writer线程,初始化了一个FinalFieldExample实例f, Thread2 为 reader线程,读取实例f 的x、y值,赋值给 i、j; 那么表面上我们是期待结果是 i = 3, j = 4的:

  • 实际上的情况可能会不如我们期待的那样子, 由于虚拟机的实现或者CPU架构的特征, 指令是可能发生重排序的, 重排序会把非final的变量赋值指令 排序到构造方法之外, 这样的结果自然是, x因为是final的所以自然会在构造方法之内进行赋值, 但y是非final的, 有可能构造方法执行完了, y的赋值指令还没有走完, 这个时候因为构造方法走完, reader读的时候发现f 是不等于null的, 就会把未完成赋值的y 的值给读出来, 那结果j的值就是0了:

所以,各单位请注意! final啊,它还有一个禁止重排序的作用, 即,禁止被final修饰的代码对应的指令被重排序

补充:volatile volatile除了能保证线程间的可见性 也能禁止重排序!!

  • 从1.5开始,其语义被增强了,明确了禁止重排序的作用; 1.4以前,即便使用双重校验锁的单例模式,也是有问题的; 单例模式案例(两种加volatile的情况,正常):

如果不加volatile,就可能会出现类似重排序的问题了: 有可能重排序之后, 构造方法的调用的指令被排到了后面, 这时候程序 还没等构造方法 执行完毕 就把分配好内存的实例赋值给了引用 这时候这个引用因为没有经过构造方法, 所以还没有被初始化, 此时Thread1解锁, Thread2直接把这个没有初始化完的引用拿去使用了, 就可能出现问题了!

所以千万注意,使用单例模式的时候 一定要为单例加上volatile关键字!

有条件地共享可变资源

保证可见性的方法
  • 使用final关键字
  • 使用volatile关键字
  • 加锁,锁释放时会强制将缓存刷新到主内存 不过加锁要注意, 加锁只是 对另外跟你这个线程 同样使用一个锁 的那些线程, 才能保证可见性, 如果某个线程没有加锁,它就不一定能够看到了; 加了锁的, 锁释放时会强制将缓存刷新到主内存, 为什么刚说,其他线程加锁 才能看到 本线程 访问的主内存的对应值, 因为资源只有加锁, 才会去主内存刷新, 才会跟其他 同样对本资源 加了锁的线程 保持同步! 不对共享资源加锁的线程 可能拿着 自己运行内存的数据副本 就去读、写、运算、更新操作了; 如此便可能造成文首所说的,脏读脏写等线程不安全的情况!

保证原子性
  • 加锁,保证操作的互斥性, 实现执行控制, 加锁的代码会实现原子性;
  • 使用CAS指令(Unsafe.compareAndSwapInt 不过Unsafe不是公开的, 需要用到反射才能用得到它;
  • 使用原子数值类型(如AtomicInteger
  • 使用原子属性更新器(AtomicReferenceFieldUpdater

经典案例,a++, ++操作符不是原子性的, 任何编程语言在进行a++操作的时候, 都会先把值从a中读出来,给到一个临时变量如tmp中, tmp加一, 之后再把tmp写回到a中, 全程经过了三步操作,不是一个不可拆分的运算单元, 即,非原子性!

如下图,两个线程同时进行a++, 因为a++非原子性操作, 由此可能造成脏读脏写:


本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java | 如何停止一个线程

    这个时候 如果在外边使用这个线程的引用去调用它的interrupt(), 那其实不会对for循环的运行产生影响, 因为这种情况不支持;

    凌川江雪
  • EventBus原理解析笔记以及案例实战(结合demo)

    按下Activity1中的Button, 会跳转到Activity2; 按下Activity2中的button, 会通过EventB...

    凌川江雪
  • Tip | Android的消息机制

    凌川江雪
  • 多线程并发神器--ThreadLocal

    什么是ThreadLocal 可以理解成线程本地变量,传统的线程对一个变量操作时操作的是同一个对象,也存在线程安全的问题。 ThreadLocal是一个变量的本...

    Java技术栈
  • ThreadLocal原理探究

    多线程访问同一个共享变量特别容易出现并发问题,特别是多个线程需要对一个共享变量进行写入时候,为了保证线程安全,一般需要使用者在访问共享变量的时候进行适当的同步,...

    加多
  • ThreadLocal原理探究

    多线程访问同一个共享变量特别容易出现并发问题,特别是多个线程需要对一个共享变量进行写入时候,为了保证线程安全,一般需要使用者在访问共享变量的时候进行适当的同步,...

    加多
  • ThreadLocal详细的使用

    我们知道在Java中用static关键字可以实现变量的共享,那么在多线程环境中可以用ThreadLocal让每一个线程都有自己的私用数据。首先我们先看一下共享变...

    吉林乌拉
  • Java多线程编程-(4)-线程本地ThreadLocal的介绍与使用

    我们通过上两篇的学习,我们已经知道了变量值的共享可以使用public static变量的形式,所有的线程都使用同一个被public static修饰的变量。

    Java后端技术
  • Java笔记备忘录——解析Spring单例模式与线程安全

    Spring框架里的bean,或者说组件,获取实例的时候都是默认的单例模式,这是在多线程开发的时候要尤其注意的地方。

    慕容千语
  • 初学者第65节生产者消费者(七)

    生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费...

    用户5224393

扫码关注云+社区

领取腾讯云代金券