Android多线程通信总结(四)

2,144 阅读7分钟

1. 如何停止一个线程

  • stop方法可以停止线程,但是这种方法并不是线程安全的,会涉及到锁的安全问题,所以已经被官方废弃,所以不能简单的停止线程。

    为什么不能简单的停止一个线程?

    因为当一个线程执行任务的时候,它会占用cpu以及内存的资源,并且锁住这些资源,如果我们要将线程异常终止,那么当前线程就来不及对资源进行清理,那么下一个线程在访问当前资源的时候就会产生异常信息。

  • 从逻辑上停止线程中执行的任务,线程虽然不能停止,但是任务是可以了,任务和线程是相互绑定的关系,也就是一种相互协作的任务执行模式。所以我们应该在任务上去添加停止的逻辑,而不是在线程中添加停止方法。

    中断处理的方式有:

    interrupt、volatile boolean标志位

    • interrupt:系统级的中断方式

      当我们调用thread.interrupt之后,如果当前执行的逻辑抛出了InterruptException,那我们可以直接在catch当前异常的位置进行任务的结束处理,如果我们没有捕获异常,那就需要根据interrupted或者isInterrupted来结束当前正在操作的任务。

      interrupted:是静态方法,获取当前线程的中断状态,并清空。

      • 当前运行的线程
      • 重复调用之后返回的就是false;

      isInterrupted:是非静态方法,获取该线程的中断状态,不清空。

      • 调用的线程对象对应的线程
      • 可重复调用,中断清空前一直返回true

      interrupted和isInterrupted两者之间的区别主要是因为JNI底层的逻辑决定的,interrupted中增加了一个清空的方法,同时对该方法进行了锁操作,

    • volatile boolean标志位

      boolean标志位,线程调用的过程中我们可以通过添加一个标记来对当前执行的任务进行处理,如果当标志改变了,那么任务就不需要继续执行了,及时终止任务,并作出相应处理,但是这种情况会有线程间可见性的问题,也就是说我们更改了值之后,线程中不会立即查看到,因为java虚拟机的内存模型,每个线程都需要从主存拷贝一份数据到工作区间,修改值之后再同步回主存中,所以这个时候需要对boolean标志位增加一个volatile关键字,告诉虚拟机当前属性是易变的。

2.如何写出一个线程安全的程序

  • 什么是线程安全

    可变资源(内存)线程间共享

  • 如何实现线程安全的程序

    1. 不共享资源

      可重入函数,中间不会执行任何内存,直接返回操作后的数据

      ThreadLocal:

      绑定到线程上的ThreadLocalMap,内部由ThreadLocalMap进行存储,key就是ThreadLocal, value就是我们要保存的值,

    ThreadLocalMapWeakHashMap
    对象持有弱引用弱引用
    对象GC不影响不影响
    引用清除线程退出时清除GC后移除(ReferenceQueue)
    Hash冲突开放定址法单链表法
    Hash计算神奇数字的倍数对象Hashcode再散列
    适用场景对象较少通用

    使用建议:

    • 声明为静态的final成员,全局只需要一个就可以了

    • 避免存储大量对象,因为其内部是一个map

    • 用完之后及时移除对象

    1. 共享不可变资源

      禁止重排序,当我们创建一个对象的时候,对象内部的非final的成员可能会在构造方法之外被赋值,也就是代码执行的顺序发生了改变,那么此时就会造成输出结果的异常,

      final的特性:

      • 定义为final的成员变量,不能被更改
      • 定义为final的类,不能被集成,其内部所有方法都被隐藏添加final关键字
      • 定义为final的方法,不能被重写
      • final关键字可以起到禁止重排序的左右
      • jdk1.8之前如果我们要在匿名内部类中需要访问外部的局部变量,那么这个局部变量必须用final修饰,因为匿名内部类中访问的外部的局部变量是将局部变量的值拷贝到内部类里面使用,如果不定义成final,那么当外部的局部变量值发生改变,但是内部类里面的值没有改变,可能就会导致程序的运行结果出现异常。
      • jdk1.8之后,匿名内部类访问外部类的局部变量,那么这个局部变量不再需要定义成final,因为其内部中维护着外部类的引用,在内部类中调用该变量,其实是外部类的实例来调用的,也就是两个实例引用着同一个内存地址,保证了数据的一致性
    2. 共享可变资源

      • 可见性

        final,volatile,加锁,锁释放的时候会强制将缓存刷新到主内存中,所以如果想要通过锁来保证可见性,那么需要对同的线程进行加锁的操作,

      • 操作原子性

        需要注意:a++不是原子性操作

        • 加锁,保证操作的互斥性
        • 使用CAS指令,底层原理是 unsafe类和 自旋锁
        • 使用原子数值类型(AtomicInteger)
        • 使用原子属性更新器(AtomicReferenceFieldUpdater)
      • 禁止重排序

        final,volatile,

3.ConcurrentHashMap如何支持并发访问

1.版本迭代过程
  • JDK5:分段锁

    通过hash算法得到的值,高位用来寻找segment(段),低位用来寻找当前段对应的table中的值。

    缺陷:

    当我们传进来的整数小于3万的时候,hash之后获得的值高位都是15,这样去找segment的时候,只会去找到最后一个段,失去了分段锁的优点。

  • JDK6:优化二次Hash算法

    计算hash值的时候,是高低位均匀分布。

  • JDK7:段懒加载,volatile & cas

jdk7之前实例化CHM的时候会把所有的段全部初始化,jdk7的时候需要哪个段初始化哪个

  • JDK8:摒弃段,基于HashMap原理实现并发实现

    ​ 直接去掉段的概念,直接对table进行加锁

2.CHM如何计数
  • JDK5-7基于段元素个数求和,二次不同就加锁
  • JDK8引入CounterCell,本质上也是分段计数
3.CHM是弱一致性的
  • 添加元素之后不一定马上能读到
  • 清空之后可能仍然会有元素
  • 遍历之前的段元素的变化会读到
  • 遍历之后的段元素变化读不到
  • 遍历时元素发生变化不抛异常
4.HashTable的缺点
  1. 大锁:对hashtable对象加锁
  2. 长锁:直接对方法加锁
  3. 读写锁共用:只有一把锁,从头锁到尾
5.CHM的解法
  1. 小锁:分段锁(5-7)桶节点锁(8)
  2. 短缩:先尝试获取锁,失败再加锁
  3. 分离读写锁:读失败再加锁(5-7),volatile读CAS写(7-8)
6.如何进行锁优化
  • 长锁不如短锁:尽可能只锁必要的部分
  • 大锁不如小锁:尽可能对加锁的对象拆分
  • 公锁不如私锁:尽可能将锁的逻辑放到私有代码中
  • 嵌套锁不如扁平锁:尽可能在代码设计时避免嵌套锁
  • 分离读写锁:读的次数比写的次数多
  • 粗化高频锁:尽可能合并处理频繁过短的锁
  • 消除无用锁:尽可能不加锁,可以用volatile和cas替代锁

多线程通信总结列表

Android多线程通信总结一

Android多线程通信总结二-IntentService

Android多线程通信总结三-面试必问Handler

Android多线程通信总结四

我是Android大师哥。成功并不是你拥有了多少,而是你帮助了多少人,又有多少人因你而感动。 谢谢大家,我们下期再见。