不要玄之又玄:多线程相关知识点的通俗理解

线程和进程的区别

  • 进程是一个程序执行一次创建的,是系统运行程序的基本单位。
  • 线程是⼀个⽐进程更⼩的执⾏单位,一个进程在运行期间可以产生多个线程。多个线程可以共享进程的堆和⽅法区资源,每个线程有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈。
  • 线程是进程划分成的更⼩的运⾏单位。
  • 线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各线程则可以有共享的资源也有自己独有的资源。
  • 线程执⾏开销⼩,但不利于资源的管理和保护;⽽进程正相反

线程的三大特性

  • 原子性
  • 可见性
  • 有序性

在java中,lock、synchronized能够实现三个特性,volatile只能实现可见性和有序性

synchronized的实现和升级过程

从JDK1.6之前使用monitor对象实现线程互斥,在1.6时完成优化,通过偏向锁-轻量锁-重量锁的升级来平衡功能和性能。
参考

synchronized的缺点

  • synchronized 在已经获取了锁A的情况下去获取锁B,如果锁B获取不到,它不会释放锁A(请求和保持)
  • synchronized 获取锁B时获取不到时会进入阻塞状态,啥也干不了,也不能被中断/打断,也就无法去释放已获得的资源(不可剥夺)

这也是我们发生死锁时的两个条件:1,保持和等待;2,不可被剥夺/抢占

unsafe

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。
在这里插入图片描述

多线程的几个对象

  • 任务对象

    • Runable
      • 核心方法run方法
      • 需要通过线程池executor的execute方法执行
    • Callable:
      • 核心方法 call方法
      • 需要通过线程池executor的submit方法执行
  • 执行对象

    • executor:顶层接口,包含execute方法
    • ExecutorService:继承executor接口,包含submit和shutdown方法
  • 结果

    • Future:线程返回结果的顶层接口,包含get方法
    • FutureTask是Future的实现类
  • Java 线程的 6 种状态

    • NEW: 初始状态,线程被创建出来但没有被调用 start() 。
    • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
    • BLOCKED:阻塞状态,需要等待锁释放。
    • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)
    • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
    • TERMINATED:终止状态,表示该线程已经运行完毕。
    • 在这里插入图片描述
    • 比对进程的五态模型
      • 在这里插入图片描述
      • 线程的六态模型的区别是
        • 将就绪和运行中合并为运行态,因为通过时间片分配给线程,线程就绪和运行中之间的间隔时间非常短,所以不用单独拆成两个状态
        • 线程多了等待和超时等待

线程池

java线程池思想纠正
线程池不是从里面拿一个线程来进行工作。而是产出一个任务,交给线程池,线程池将任务交付给空闲的线程去执行。

  • ThreadPoolExecutor

    • 核心参数
      • 核心线程数
      • 最大线程数
      • 等待队列最大长度
      • 超时存活时间
      • 线程工厂
    • Executors工具类实现的线程池
      • FixedThreadPool:固定线程数,使用无界等待队列,大量任务堆积等待队列中会出现OOM
      • SingleThreadExecutor:线程池只有一个线程,使用无界等待队列,大量任务堆积等待队列中会出现OOM
      • CachedThreadPool:使用的同步队列,不存储等待任务,只要任务进来,就创建线程执行任务,且允许创建的线程数量为 Integer.MAX_VALUE,会出现OOM
      • ScheduledThreadPool:使用的无界的延迟阻塞队列,内容使用的最小堆排列,同样大量任务堆积等待队列中会出现OOM
    • 线程池的拒绝策略
      • 抛出异常之后拒绝任务
      • 将任务回退给调用者,使用调用者的线程来执行任务
      • 直接丢弃掉
      • 丢弃等待队列中的最早进来的未处理的任务请求
    • 线程池中任务进来的判断流程
      • 在这里插入图片描述
      • 可以看出是先判断等待队列是否可以放入,才判断是否是小于最大线程数,然后创建线程
  • 线程池创建核心参数设置

    • 计算密集型
      • 可以将核心线程数设置为 N(CPU 核心数)+1
      • 最大线程数不适合过大(最大线程数可以稍大于核心线程数),根据实际情况调整。
    • IO密集型
      • 设置核心最大进程数为2N
      • 设置最大线程数为4N
    • 混合型
      • 计算 + 等待
      • 需要结合上面两种进行调优
  • 线程池的参数调整过程

    • 首先根据上面类型设置理论数 (在设置线程数的时候可以稍微往2N方向去考虑)
    • 根据实际情况进行压测
    • 根据压测结果进行调整
    • 循环步骤2和3,直到得到最佳效果
  • 线程池尽量不要放耗时任务

    • 线程池本身的目的是为了提高任务执行效率,避免因频繁创建和销毁线程而带来的性能开销。如果将耗时任务提交到线程池中执行,可能会导致线程池中的线程被长时间占用,无法及时响应其他任务,甚至会导致线程池崩溃或者程序假死。
  • 线程池是如何保证线程不被销毁的呢?

    • 如果队列中没有任务时,核心线程会一直阻塞在获取任务的方法,直到返回任务。而任务执行完后,又会进入下一轮 work.runWork()中循环
  • 线程池中的线程会处于什么状态?

    • RUNNABLE,WAITING,因为要么就在执行任务,要么就在阻塞等待获取
  • 核心线程与非核心线程有区别吗?

    • 没有。被销毁的线程和创建的先后无关。即便是第一个被创建的核心线程,仍然有可能被销毁。大于核心线程数时,线程空闲且超过超时时间会被销毁。

死锁

  • 产生死锁的四个必要条件
    • 互斥条件:该资源任意一个时刻只由一个线程占用。
    • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
    • 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
    • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
  • 如何预防死锁? 破坏死锁的产生的必要条件即可
    • 破坏请求与保持条件:一次性申请所有的资源。
    • 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
    • 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
  • 常见的预防死锁的方案
    • 银行家算法:是对资源的分配的实现,每次线程请求资源时,判断资源是否足够,如果当前资源分配给线程足够支撑线程完成并释放资源,则分配资源,反之拒绝分配。要求一次性申请所有的资源,并且安装顺序分配资源。破坏请求与保持,破坏循环等待

线程间的同步的方式

  • 互斥:
    • 只有拿到互斥对象的线程才能访问资源
    • synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源
  • 信号量:
    • 允许多个线程访问资源,当时线程数必须在信号量允许的范围内
    • Semaphore(信号量)可以用来控制同时访问特定资源的线程数量
  • 事件:通过事件通知的方式实现线程同步。

AQS的关系

  • AQS 是抽象队列同步器(AbstractQueuedSynchronizer),是在java.util.concurrent.locks包下
  • AbstractQueuedSynchronizer这个类是一个抽象类,主要是被子类继承可以很方便实现构造出同步器(ReentrantLock 互斥同步和Semaphore 共享同步)
  • 原理:当一个线程请求资源时,通过CAS操作,判断是否允许线程持有资源,如果CAS操作失败,则将当前线程封装为一个node节点,加入一个双向等待队列的尾部(遵循FIFO)。
  • 通过继承AQS类,需要实现几个方法
    • 在这里插入图片描述
    • boolean tryAcquire(int arg) 独占模式尝试获取锁,AQS中未实现,由子类去实现
    • tryRelease(int arg)尝试释放独占锁,AQS中未实现,由子类去实现,成功释放返回true
    • tryAcquireShared(int arg)尝试获取共享锁,返回负数表示失败,0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源,AQS中未实现,由子类实现
    • tryReleaseShared(int arg)尝试释放锁,释放后允许唤醒后续等待结点返回true,否则返回false,AQS中未实现,需要由子类实现
    • isHeldExclusively() 共享资源是否被独占

通过AQS实现的lock有

  • 可重入锁 ReentrantLock 支持公平、非公平锁
  • 可重入读写锁 ReentrantReadWriteLock 支持公平、非公平锁
  • 信号量 Semaphore 支持公平、非公平锁
  • 线程计数器 CountDownLatch 不支持公平、非公平锁
  • 线程池中worker ThreadPoolExecutor中的worker 不支持公平、非公平锁

CAS

CAS(Compare-and-Swap/Exchange)是一种无锁算法,通过值比较来保证操作的原子性。
在这里插入图片描述

使用AtomicInteger代码讲解CAS的逻辑,核心代码如下

    private volatile int value;
     public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

value使用volatile修改保证可见性
this是当前对象的引用
valueOffset是要修改字段在对象地址上的偏移量
newValue是需要新设置的值

    public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));

        return var5;
    }

首先通过getIntVolatile本地方法获取旧值,循环使用compareAndSwapInt这个方法比对和设置值

CAS的好处

无锁方案相对互斥锁方案,最大的好处就是性能。互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,既解决了问题,又没有带来新的问题,可谓绝佳方案。

无锁方案相对于互斥锁方案,优点非常多,首先性能好,其次是基本不会出现死锁问题(但可能出现饥饿和活锁问题,因为自旋会反复重试),最后我们所有原子类的方法都是针对一个共享变量的,如果你需要解决多个变量的原子性问题,建议还是使用互斥锁方案。原子类虽好,但使用要慎之又慎。

对于ABA问题可以采用版本号的方式解决,例如AtomicStampedReference 和 AtomicMarkableReference

CAS存在问题

  • 自旋(循环)时间长开销很大,如果CAS失败,会一直进行尝试。
  • 只能保证一个共享变量的原子操作,对多个共享变量操作时
  • ABA问题,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发

CAS和AQS的关系

  • AQS是一个用来构建线程同步的框架,例如,ReentrantLock和Semaphore等同步器都是基于AQS实现的
  • AQS利用CAS来保证其内部状态(state)的原子性更新

Lock API

所以针对 synchronized 的弊端,JUC 下提供了的新的锁Lock

  • 在使用 synchronized 时,我们要配合使用 wait , notify/notifyAll 来进行线程的等待和通知
  • 在使用 Lock 时我们需要使用 Condition 中提供的await() 、 signal() 、 signalAll()

Semaphore

信号量可以分为以下三种类型:

  • 互斥信号量:任务之间通过互斥信号量访问临界资源,这其实就是锁机制(资源数为1)
  • 计数信号量:任务之间竞争性的访问共享资源(根据实际资源数设置参数)
  • 二值信号量:任务之间的同步机制(开始时资源数为0,一个线程完成之后释放资源,资源数变为1)

ThreadLocal

四大引用

强引用(StrongReference):最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj = new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

软引用(SoftReference):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存流出异常。通过 SoftReference 构建软引用。

弱引用(WeakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。通过 WeakReference 构建弱引用

虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。通过PhantomReference 构建虚引用

上面说的对象会被GC收回,前提都是对象不再存在强引用的情况下。

ThreadLocal基本方法

  • 使用ThreadLocal目的:保证线程所修改的属性不被其他线程修改
  • ThreadLocal的方法get的实质是获取当前线程内部的ThreadLocalMap的get方法,key是ThreadLocal,通过hashcode方法计算出ThreadLocal对应的索引下标,然后获取ThreadLocalMap中对应的内容
  • ThreadLocal的方法set的实质是对当前线程内部的ThreadLocalMap进行set,首先判断线程的ThreadLocalMap是否存在,不存在则创建一个并复制到线程内部。set过程中会创建一个ThreadLocal的弱引用作为key进行设置。

ThreadLocal中的软引用引发的问题

  • ThreadLocalMap是Map结构,自然也会发生Hash冲突,HashMap通过拉链发解决的。ThreadLocalMap则是通过开放地址法解决冲突,在计算出的Hash下标的数组中发现已经存在entry,则会向后移动下标,继续找可以存放数据的地方。
  • ThreadLocalMap中每一个key、value都是通过entry存放的,但是由于entry中的key是ThreadLocal的弱引用,所以当ThreadLocal的强引用被赋值null,在下次GC的过程中就会导致ThreadLocal对象被回收,导致entry中key为null,entry也就会被认为是过期数据。
  • 为什么key要使用ThreadLocal的弱引用,而不是直接使用强引用?如果在entry内部ThreadLocal是强引用,在ThreadLocal threadLocal = null;时,相当于ThreadLocal对应的对象已经不能再使用了,但是ThreadLocalMap中取值需要通过ThreadLocal作为key才能get到内容,相当于Map中的数据永远无法获取到,会导致内存泄漏。
  • 而使用ThreadLocal的弱引用,在ThreadLocal强引用被删除,而entry中的key被gc之后就会变为null,通过这个特征就可以在代码中进行回收了。
  • 如何回收,在ThreadLocalMap的set,get方法中都有主动清除存在entry但是key为null得过期数据的逻辑。
  • 如果一直不调用get,set方法,存过期的entry在就会造成内存泄漏。所以在不适用ThreadLocal时,通过remove主动删除过期数据。

几个锁的对应和理解

  • 公平锁和非公平锁:等待锁的过程中是否有插队的
  • 悲观锁和乐观锁:操作之前是否要上锁
  • 共享锁和互斥锁:一个资源是否可以被多个线程持有

ReentrantLock 的概念

  • ReentrantLock是java.util.concurrent.locks包下的,通过AQS实现的悲观锁
  • 原理:ReentrantLock内部有一个抽象类Sync,这个类继承AbstractQueuedSynchronizer(AQS),同时在ReentrantLock内部对Sync有两种实现,公平锁和非公平锁。生成ReentrantLock对象的过程其实就是构建Sync实例对象的过程。同时上锁也是通过Sync实例对象的lock方法实现的。(这块建议看下源码,很好理解)
  • ReentrantLock 的特性是可重入,这块跟是否是公平锁没关系,在获取锁的时候,会判断持有当前资源的线程是不是当前线程,如果是的话,可以直接获取锁
  • 非公平锁,一个线程进来直接会尝试一下CAS,不成功则进入等待队列。成功则会在等待队列前占有资源(直接越过了排队过程,对等待队列中的其他线程是不公平的)
  • 公平锁,在实例化ReentrantLock 对象的时候,通过传入布尔值,来创建公平锁。线程进来,如果有等待的线程,则先进等待队列排队,依次获取资源。

ReadWriteLock

  • 主要是通过分场景优化性能,提升易用性(用于读多写少的场景)
  • 读写锁与互斥锁的一个重要区别就是读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。
  • 基本原则
    • 读锁时,允许多个线程同时读共享变量;但读的时候禁止写
    • 写锁时,只允许一个线程写共享变量;
    • 写锁时,此时禁止读线程读共享变量

StampedLock

  • 他的性能就比读写锁还要好

  • ReadWriteLock 支持两种模式:一种是读锁,一种是写锁。

  • StampedLock 支持三种模式,分别是:写锁、悲观读锁和乐观读。

  • 写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁

  • StampedLock 支持乐观读的方式

    • 允许多个线程同时读的情况下还能允许一个线程获取写锁,也就是说不是所有的写操作都被阻塞
    • 读之前,调用乐观读tryOptimisticRead()获取一个stamp,然后读取值,然后再验证一下stamp是否已经被修改(通过validate(stamp)验证)。如果发现被修改,升级为悲观读锁。
  • 不支持重入锁

CountDownLatch 线程计数器

  • 可以等待多个线程就绪之后,启动下一步
  • 可以多个线程等待一个线程再启动下一步

不足

  • CountDownLatch是一次性的,计数器的值只能在构造方法中初始化

CyclicBarrier

  • 是可循环使用(Cyclic)的屏障(Barrier)
  • 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,最后一个线程到达屏障时,屏障才会打开
  • 因为可以reset()。所以CyclicBarrier能处理更为复杂的业务场景时,如果计算发生错误,可以重置计数器,并让线程们重新执行一次

并发体系总结

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贝多芬也爱敲代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值