多线程基础知识

in 笔记 with 0 comment

线程的六种状态

image.png

注:
1.Waiting状态可理解成主动立即让线程等待。
2.Timed Waiting状态可理解成代码超时等待。
3.Blocked状态是在线程拿不到锁(如:在抢synchronized的时候),但在等待抢lock锁的时候是Waiting状态。
4.线程执行完毕或者执行stop、interrupt犯法后会处于Terminated状态,且不能再star。

线程状态切换演示

线程中止

  1. Thread.stop是不正确的线程中止方式,可能导致线程安全问题,会强制中止线程,已废弃。
    image.png

  2. Thread.interrupt才是推荐的方式。将stop改为interrupt后,最终输出“i=1 j=2”,数据一致。interrupt方法会抛InterruptedException,不会存在线程安全问题。
    image.png

  3. 标志位方式。
    image.png

线程通信

要想实现多个线程之间的协同,如:线程执行先后顺序,获取某个线程执行的结果等等。
涉及到线程之间相互通信,分为下面四类:文件共享、网络共享、变量共享以及JDK提供的线程协调API

  1. 文件共享
    image.png

  2. 网络共享
    (和文件共享类似)

  3. 变量共享
    image.png

也就是【堆内存】中的变量是线程共享变量。

  1. jdk提供的线程协调API
    image.png

    1. wait/notify
      这对方法只能由同一对象锁的持有者线程调用,也就是写在同步块里面,否则会抛出ilegalMonitorStateException异常。
      wait方法会当前线程等待,加入该对象的等待集合中,并且释放对象锁。
      notify/notifyAll方法唤醒一个或所有正在等待这个对象锁的线程。
      虽然wait会释放锁,但是对顺序有要求,如果在notify被调用后才开始执行wait方法,线程会永远处于Waiting状态。
      image.png

    2. park/unpark
      线程调用park则等待“许可”,调用unpark会为执行线程提供“许可”。
      相比wait/notify,不要求park和unpark方法的调用顺序。可以多次调用unpark之后再调用park,线程也会直接运行,但不会叠加,也就是说,连续多次调用park方法,第一次会拿到“许可”直接运行,后续调用会进入等待。
      park不释放锁,所以park/unpark不要写在同步代码块(会导致死锁),和wait/notify正好相反
      image.png
      注:如果线程B连续调用了多次unpark函数,当线程A调用一次park函数就使用掉这个“许可”继续执行,如果线程A第二次再调用park,则进入等待状态。(unpark多次只需要一次park)

    3. suspend/resume(已废弃)
      suspend/resume是线程Thread的方法,但是被废弃,因为suspend也是不释放锁,很容易写出死锁代码(兼容了wait/notify和park/unpark的缺点,既不能写在同步代码块,又不能颠倒执行顺序)。
      image.png

  2. 伪唤醒
    image.png

线程封闭

  1. 线程封闭概念
    多线程访问共享可变数据时,涉及到线程间数据同步的问题。并不是所有时候都要用到共享数据,所以线程封闭的概念就提出来了,。
    数据都被封闭在各自的线程之中就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭
    线程封闭具体的体现有:ThreadLocal、局部变量

  2. ThreadLocal
    ThreadLocal是Java里的一种特殊变量。它是一个线程级别变量,每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了,在并发模式下是绝对安全的变量。

用法:ThreadLocal var = new ThreadLocal();
会自动在每个线程上创建一个T的副本,副本之间彼此独立,互不影响。
可以用ThreadLocal存储一些参数,以便在线程中多个方法中使用,用来代替方法传参的做法。

可以简单理解为:JVM维护了一个Map<Thread,T>,每个线程要用这个T的时候,就用当前的线程去Map里面取。

  1. 栈封闭
    局部变量的固有属性之一就是封闭在线程中。它们位于执行线程的【虚拟机栈】中,其他线程无法访问这个栈。
    局部变量可以理解成Java类中方法的变量,保存在【虚拟机栈】中的栈帧中的本地局部变量表
    image.png

CPU内存屏障

  1. CPU性能优化手段一一缓存
    image.png

    1. 多级缓存
      CPU高速缓存分为:L1、L2、L3等级,CPU读取顺序依次是:L1>L2>L3>内存>硬盘。
    2. 缓存同步协议
      多核CPU读取同样的数据进行缓存。
      MESI协议,它规定每条缓存有个状态位,同时定义了四个状态:修改态(Modified)、专有态(Exclusive)、共享态(Shared)、无效态(Invalid)。简单来说:CPU要处理控制自己的读写操作,也要监听其他CPU发出的通知,保证了多核CPU数据的一致
  2. CPU性能优化手段一一运行时指令重排
    image

  3. CPU高速缓存CPU指令重排两种优化手段都存在问题

    1. CPU缓存和内存中的数据不是实时同步的,可能存在同一时间点,各个CPU可能读取同一块内存地址的值不一致。
    2. 多核多线程的CPU中,指令还是可能出现超出as-is-serial允许以外的乱序现象。
  4. 内存屏障
    处理器提供了两种内存屏障(Memory Barrier)指令解决了以上两个问题

    1. 写内存屏障:在指令后插入Store Barrier,强制写入主内存,让其他线程可见,并且不能发生重排。
    2. 读内存屏障:在指令前插入Load Barrier,让高速缓存失效,强制重新从主内存加载数据。
  5. 结语
    无论MESI缓存协议的实现还是内存屏障的性能问题都体现在不同CPU厂家在优化中做出的努力,不同CPU厂商所付出的人力物力成本,最终体现在不同CPU性能差距上。