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

要点

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

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

  • 什么是线程安全?
    • 不安全:资源不同步,脏读脏写;
      如多个线程的工作内存读写主存时的不同步
      “进程安全”问题不存在,
      因为进程之间内存相互独立,各自独享内存的,
      一个进程被杀掉的话,其所有内存都还给物理内存了;
      可能共享CPU时间片;
      线程是存在于进程当中的,
      同一个进程中的线程之间是可以共享内存的;

    • 线程安全产生的原因:可变资源(内存)线程间共享(关键词“可变”和“共享”)
      线程间不共享的资源不用考虑线程安全了;

  • PS:每一个线程都有自己的一个内存副本<Java内存模型>

  • 如何实现线程安全?
    • 不共享资源
      共享才会产生线程安全问题,
      所以尽量不共享;

    • 共享不可变资源(volatile、final)

      • 禁止重排序
    • 有条件地共享可变资源

      • (更改刷新的)可见性
        一个线程对共享资源的修改,其他线程能够马上看到!
        实现:某个线程对共享资源进行了更新时,要马上刷新到主存!
      • 操作原子性
      • 禁止重排序

不共享资源

  • 可重入函数
    传入一个参数进函数,经过一系列的运算,
    再把运算结果返回出去,
    中间不会涉及到任何对外部内存的访问、修改,
    没有副作用,
    像这样没有副作用的函数,
    先天就具备线程安全的优势:

ThreadLocal实现不共享资源

  • 虽然说每个线程都会去访问一个ThreadLocal对象
    但实际上最终访问的 都是自己线程内部的一个副本

    比如下图中的token,
    对应的场景如,
    一个服务器提供了很多个服务,
    每个服务的话,
    每个用户进来请求,服务器都会为这个用户 开一个线程 来提供服务,
    这个时候,
    因为每个用户 就都是属于不同的线程的,
    而ThreadLocal便是类似于服务器的设计,
    这里每个线程都去访问这个token的时候,
    都会有一个自己的 String的 一个副本,
    这样线程间便不会互相干扰;

    如此便是实现了不共享资源
    也就没有线程安全的问题了;
    自己线程之内,不管怎么设置,都不会影响到其他线程;
    【UUID,唯一识别码(Universally Unique Identifier),可以由Java工具类生成,用来唯一标注一个元素,如标注线程】
    下面是一个用例:
  • ThreadLocal原理
    看一下ThreadLocal源码set方法!!!!!!!!
    可以看到,
    ThreadLocal的底层,其实是绑定到线程上的一个ThreadLocalMap
    当前线程没有ThreadLocal时,就先为线程创建一个;【createMap(t, value)
    当前线程有ThreadLocalt.threadLocals != null】,
    则添加值的时候置入键值对map.set(this,value)
    使用的key,即this
    当前调用线程它对应的ThreadLocal类对象引用t.threadLocals】,
    value企图传入的值
    也就是说,
    线程不同,ThreadLocal 为线程创建的threadLocals 就不同;
    访问ThreadLocal.get 时,
    又是根据 线程各自 的 threadLocals 【即key】来取value
    那 不同的子线程 访问同一个 主线程的ThreadLocal,
    key不同,
    它们访问ThreadLocal的set、get时 处理的值,肯定也是不一样的!
    • ThreadLocal中这个ThreadLocalMap是,储存在、绑定在线程上的:

总结!!!

  • 两个点总结ThreadLocal特性:

    • 唯一 一个ThreadLocal对象,作为全局变量定义在主线程
      为访问它(set())的N子线程
      开启(createMap()N相互独立ThreadLocalMap
      因此,每一个子线程访问主线程中的这个独一无二的ThreadLocal对象的时候,
      总会访问到子线程自身对应的底层数据存储结构 ThreadLocalMap

    • 不同线程,访问一个ThreadLocal对象的时候,
      访问的是(绑定不同线程的)不同底层数据结构ThreadLocalMap
      读写的是不同的数据



      实现了,
      同属主线程的一系列子线程间的,
      资源不共享,解决的了线程安全问题;

      【服务器是一个服务端里边,
      操作很多个线程,每个线程服务每个不同的用户;

      ThreadLocal是一个ThreadLocal实例里边,
      操作很多个ThreadLocalMap
      每个ThreadLocalMap服务不同的子线程


      另外我们可以发现Android的消息机制中,
      正是把Looper交给ThreadLocal保管了,
      所以同个线程的所有Handler中关联的Looper其实是同一个Looper的副本,
      Handler通过Looper找到对应的MessageQueue,
      把自己负责的Message加进去:

    实战案例如下:

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++非原子性操作,
由此可能造成脏读脏写:





最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 线程池ThreadPoolExecutor corepoolsize:核心池的大小,默认情况下,在创建了线程池之后...
    irckwk1阅读 668评论 0 0
  • 一、线程状态转换新建(New)可运行(Runnable)阻塞(Blocking)无限期等待(Waiting)限期等...
    达微阅读 546评论 1 2
  • Java SE 基础: 封装、继承、多态 封装: 概念:就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽...
    Jayden_Cao阅读 2,048评论 0 8
  • JAVA并发编程与高并发解决方案 - 并发编程 三 相关文章 JAVA并发编程与高并发解决方案 - 并发编程 一 ...
    chuIllusions丶阅读 2,727评论 1 7
  • 九种基本数据类型的大小,以及他们的封装类。(1)九种基本数据类型和封装类 (2)自动装箱和自动拆箱 什么是自动装箱...
    关玮琳linSir阅读 1,836评论 0 47