java多线程学习

本文主要学习自b站黑马视频,还有摘抄自网上各个博客(一般有备注),如有侵权,请联系我,谢谢。

基础知识

程序和线程

  • 进程是资源分配的最小单位,线程是CPU调度的最小单位

  • 进程就是打开任务管理器后显示的那一堆应用进程,占有CPU,内存,硬盘,网络这些资源,一个进程在其执行的过程中可以产生多个线程。

  • 形象的讲,进程就是一个项目组,每个程序员就是里面的线程呀!当然一个程序员也可以叫做一个项目组,对应的就是一个进程只有一个线程。公司里面的任务是分配给项目组级别的,干活的就是其中的程序员。

  • 协程是轻量级的线程,只运行在用户态上,只需要切换CPU上下文(一堆寄存器),而线程则是切换到内核态,需要涉及到操作系统的线程调度,上下文还包括自己的堆栈。

线程调度的方式

  • 协同式线程调度:线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上去。
    优点:实现简单,切换操作对线程自己是可知的,所以一般没有什么线程同步问题。
    缺点:线程执行时间不可控制,甚至如果一个线程的代码编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。
  • 抢占式线程调度:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。
    优点:可以主动让出执行时间(例如Java的Thread::yield()方法),并且线程的执行时间是系统可控的,也不会有一个线程导致整个系统阻塞的问题。
    缺点:无法主动获取执行时间。
  • Java使用的就是抢占式线程调度,虽然这种方式的线程调度是系统自己的完成的,但是我们可以给操作系统一些建议,就是通过设置线程优先级来实现。Java语言一共设置了10个级别的线程优先级。在两个线程同时处于Ready状态时,优先级越高的线程越容易被系统选择执行。

用户态和内核态

https://segmentfault.com/a/1190000020808493

  • 内核态:CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序
  • 用户态:只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取
  • 一般应用程序一开始运行时都会处于用户态,当一些操作需要在内核权限下才能执行时,则会涉及一次从用户态到内核态的切换过程,当该操作执行完毕后,又会涉及一次从内核态到用户态的切换过程

线程的实现

  • 使用内核线程实现的方式被称为1:1实现。内核线程(Kernel Levvel Thread,KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。
  • 1:1 模型,即每一个用户线程都对应一个内核线程,每个线程的创建、调度、销毁都需要内核的支持,每次线程的创建、切换都会设计用户状态/内核状态的切换,性能开销比较大,并且单个进程能够创建的LWP的数量是有限的,但能够充分里用多核的优势,线程由内核来管理

线程的状态

  • 操作系统的状态
    • 细说线程状态.006.jpeg
    • 线程状态转换关系
  • Java 线程的状态
  • Java 线程状态变迁
  • 细说线程状态.007.jpeg
  • 上下文切换概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换
  • linux系统中的*RUNNABLERUNNING被Java合并成了RUNNABLE一种状态,而linux系统中的BLOCKED*被Java细化成了WAITINGTIMED_WAITINGBLOCKED三种状态

线程切换消耗时间主要是?

  1. 进程是由内核来管理和调度的,进程的切换只能发生在内核态,用户态切换内核态,再从内核态切换回用户态

  2. 需要保存上一个线程中现场。

    https://zhuanlan.zhihu.com/p/52845869

死锁

  • 线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源;

    • 一人拿了筷子,一人拿了碗,都在互相等待,本来可以轮流吃饭的,现在又不肯主动把自己手里的东西先给对方,这样会一直等下去

    死锁条件:

    • 互斥条件:即当资源被一个线程使用(占有)时,别的线程不能使用。
    • 请求和保持条件:持有者不肯将手里的东西拿出去
    • 不剥夺条件:只能自己用完了再交出去,不能被第三方强行夺走分配
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系,比如A等B,B等A,循环等待

线程Run和Start

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

创建线程的三种方式

  • 直接使用Thread

      // 匿名内部类方式创建 Thread
            Thread t = new Thread("t1") {
                @Override
                public void run() {
                    log.debug("running");
                }
            };
    
  • 使用Runable 配合 Thread

    Runable runable = new Runable(){
        public void run(){
            //要执行的任务
        }
    };
    Thread t = new Thread(runable);
    
  • Future Task 配合 Thread

     // 1. 使用 FutureTask 传入 Callable 接口方式创建
            FutureTask<Integer> future = new FutureTask<Integer>(() -> {
                log.debug("running...");
                Thread.sleep(2000); // 休眠
                return 100;
            });
            // 2. 传入 future, 因为 FutureTask 这个类是实现了 RunnableFuture 接口,RunnableFuture 继承了 Runnable 接口
            Thread t1 = new Thread(future, "t1");
            t1.start();
            // 3. 获取返回结果时
            // 当主线程获取 t1 线程的返回值时, 需要等 2 秒,此时主线程进入阻塞状态
            log.debug("{}",  future.get());
    

ThreadLocal

Java面试必问:ThreadLocal终极篇 淦!

Java面试必问,ThreadLocal终极篇

ThreadLocal介绍

  • 简单总结:提供了线程的局部变量,让每个线程可以通过set/get对这个局部变量进行操作,不会和其他线程的变量发生冲突,实现了线程的数据隔离

  • 类似于HashMap,可以保存key-value数据的结构

  • 但是只能保存一个key-value,需要存储多个key-value则需要多个ThreadLocal对象,并且各线程之间的数据互不干扰

  • 可以用来数据隔离,Spring的事务主要是ThreadLocal和AOP去做实现的

  • ThreadLocal对象也是对象,对象就在堆

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("xxxx");//设置
    String name = localName.get();//获取
} finally {
    localName.remove();//关闭
}

ThreadLocalMap

  • 每个线程Thread都维护了自己的ThreadLocals变量
  • 而ThreadLocalMap是ThreadLocal的静态内部类
  • image-20210228202845537
  • 总结起来就是由当前线程Thread
    • 获取当前线程维护的ThreadLocalMap对象
      • 这个对象里面有一个Entry []数组
        • key(弱引用)就是ThreadLocal对象,value就是 值。

image-20210228203042697

ThreadLocalMap为什么如此设计?

  • 为什么不使用Thread为key

    • 这样一个线程Thread不可以存储多个变量
    • 太多线程这样操纵一个Map,寻找自己对应的私有变量存在哪里
    • Map存着太多个线程的变量,不知道什么时候能销毁
  • 数组是因为一个线程可以有多个ThreadLocal

  • 冲突如何解决:

    • 每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647

执行流程

调用get()

  • 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。
  • 判断当前的ThreadLocalMap是否存在,如果存在,则以当前的ThreadLocalkey,调用ThreadLocalMap中的getEntry方法获取对应的存储实体 e。找到对应的存储实体 e,获取存储实体 e 对应的 value值,即为我们想要的当前线程对应此ThreadLocal的值,返回结果值。
  • 如果不存在,则证明此线程没有维护的ThreadLocalMap对象,调用setInitialValue方法进行初始化。返回setInitialValue初始化的值。

调用set(T value)

  • 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。
  • 判断当前的ThreadLocalMap是否存在:
  • 如果存在,则调用map.set设置此实体entry
  • 如果不存在,则调用createMap进行ThreadLocalMap对象的初始化,并将此实体entry作为第一个值存放至ThreadLocalMap中。

作者:我去个地方
链接:https://www.zhihu.com/question/279007680/answer/454219334
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

内存泄漏

  • ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
  • 所以最好在finally代码块中关闭 : threadLocal对象.remove();
  • 但其实内存泄漏的概率很低
    • 首先只要ThreadLocal没有被回收(不置为null),key指向的就不会被GC回收掉
    • 第二次使用ThreadLocal,会自动清理为NULL的key

为什么key 是弱引用?

[分析ThreadLocal的弱引用与内存泄漏问题-Java8](https://www.cnblogs.com/-beyond/p/13125195.html)

  • 因为5是强引用的话,即是4断了,ThreadLocal就会一直存在,也会内存泄漏
  • 5设计成弱引用的话,即是key为null了,由于6是强引用,所以Entry(value)仍旧不能被回收,还是会内存泄漏,但是value在下一次ThreadLocalMap调用set,get,remove的时候会被清除,因为这些操作都会清理过期的Entry。

img

为什么建议ThreadLocal修饰为Static

  • 在Java语言中,static表示“静态”的意思,使用场景可以用来修饰成员变量和成员方法,当然也可以是静态代码块。static的主要作用在于创建独立于具体对象的域变量或者方法。这里我们不对static展开累述,只要讲明白static变量即可。static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本【存放在方法区】,它当且仅当在类初次加载时会被初始化【加final和不加final的static变量初始化的位置不一样】。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。也就是说,在一个线程内,没有被static修饰的ThreadLocal变量实例,会随着所在的类多次创建而被多次实例化,虽然ThreadLocal限制了变量的作用域,但这样频繁的创建变量实例是没有必要的

    作者:码农变老了
    链接:https://www.jianshu.com/p/ee9e1d0247a6

synchronized

部分摘抄自https://www.cnblogs.com/aspirant/p/11470858.html

使用

  1. 当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this);
  2. 当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁;
  3. 当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例;

同步原理

image-20210205003951517

  • 就是尝试获取锁(Java对象的对象头的mark word中指向的monitor)

    image-20210205001317047

  • 然后执行代码

  • 解锁

锁升级过程

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态

  • 偏向锁

    Java 6中引入了偏向锁来做进一步优化:只有第一次使用 CAS将线程ID设置到对象的Mark Word头,之后
    发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所

    • 偏向锁被撤销的情况

      1. 调用对象的hashcode

        • 调用了对象的hashCode,但偏向锁的对象MarkWord中存储的是线程id,如果调用hashCode会导致偏向锁被撤销
        • 轻量级锁的hashcode存放在锁记录
        • 重量级锁的hashcode会放在Monitor
      2. 有其他线程使用(有竞争)

        • 偏向锁会被撤销变为不可偏向,变为轻量级锁
      3. 调用wait/notify

        • 这是重量级锁才有的,所以这肯定将偏向锁升级为重量级锁

        个人理解:偏向锁相当于在没其他人使用房间的时候,门口上刻上自己的名字,方便查看,但是在不方便刻字(需要刻hashcode)情况,或者有人需要使用房间的情况下,就不方便 刻字来识别了,改用其他方法(轻量级锁或者重量级锁)来竞争使用。

人话:大部分时间只有一个人在使用房间,直接在房间刻上这个人名字识别就行,减少检查次数(cas操作)

  • 轻量级锁

    关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁

    image-20210205163622248

    • 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝

    • 使用CAS操作尝试将对象Mark Word中的Lock Word更新为指向当前线程Lock Record的指针

      • 更新失败,则自旋等待(会占用cpu时间)

        • 自旋后仍未获得锁,就膨胀为重量级锁
        • 为Object对象申请Monitor锁,让Object指向重量级锁地址
        • 自己进入Monitor的EntryList阻塞去等待
        • image-20210205214447139

    人话:现在有多个人使用房间,暂时未冲突,则放书包,每次进来检查看书包(cas操作)就行,没必要锁上锁

  • 重量级锁

    Synchronized是通过对象内部的一个叫做监视器锁(Monitor)来实现的,操作系统实现线程之间的切换这就需要从用户态转换到核心态,

    • 公平性:注意synchronized是非公平锁,上来就直接抢
    • 性能:同时Monitor是利用操作系统的信号量mutex来操作的,java代码运行在用户态,当调用synchronized时候,此时会发出一个中断信号, 保存当前线程的CPU环境, CPU由用户态转向内核态,这样会导致性能变差。而CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了。操作系统与synchronized的关系
    • 可重入性:是用monitor中的一个计数器属性实现的,已经持有相同锁的线程再次进入代码块就会+1
    • 锁消除:锁消除即删除不必要的加锁操作,根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。
    • 锁粗化:如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

缺陷

  • 效率低,性能低
  • 不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象)
  • 不知道是否获得锁:Lock可以拿到状态,synchronized只能一直抢

多线程api

wait/notify原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E51cnDRl-1616851856062)(https://cdn.jsdelivr.net/gh/lingling-fa/typora-imgs@pic/img/20210228233027.png)]

  • WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList重新竞争,竞争成功的会变成RUNABLE的状态,竞争失败的会变成BLOCK状态。
  • notify() 方法随机唤醒对象的等待池中的一个线程;notifyAll() 唤醒对象的等待池中的所有线程
    • 强调一下,notify()在1.8的hotspot中,notify()唤醒的是最早进入等待队列的,而不是随机
    • notify是否真的是随机

wait和sleep区别

  • wait必须配合snchronized使用,而sleep可以在任何地方使用
  • wait针对的是锁对象,是Object的方法,sleep是Thread的静态方法,针对的是线程
  • sleep是主动让出cpu,但不释放锁,而wait会释放锁给其他线程去争抢,睡眠后可以被notify唤醒
  • 有一个易错的地方,当调用t.sleep()的时候,会暂停线程t。这是不对的,因为Thread.sleep是一个静态方法,它会使当前线程而不是线程t进入休眠状态。
  • 简单总结:wait()会释放对象锁资源而sleep()不会释放对象锁资源。但是 wait 和sleep 都会释放cpu资源

join原理

join也是Thread方法,底层是用wait实现的

https://blog.csdn.net/u013425438/article/details/80205693?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&dist_request_id=97f44d74-eb32-4a40-87c3-6097982a9501&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.control

t.join()方法只会使主线程进入等待池并等待t线程执行完毕后才会被自动唤醒。并不影响同一时刻处在运行状态的其他线程。

park/unpark原理

https://www.jianshu.com/p/e3afe8ab8364

  • 是LockSupport里的方法

  • 以thread为操作对象

  • 操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性。

  • unpark操作可以在park操作之前

JUC

内存模型(JMM)

JMM是一组抽象概念,描述的是一组规范,各个线程中在自己的工作内存中存储着主内存中的变量副本拷贝java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。

JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。JMM体现在以下几个方面

  • 原子性:原子性指的是一个操作是不可分割,保证指令不会受到线程上下文切换的影响

    JMM只能保证基本的原子性,只能保证 i = 2 这种,但不能保证 i ++ 这种

    同理volatile也不能保证原子性

    synchronized 能同时保证原子性和可见性

  • 可见性:所有线程都能看到共享内存的最新状态,保证指令不会受cpu缓存的影响

    在Java中提供了一个volatile关键字来保证可见性。当一个主内存中的共享变量被volatile关键字修饰时,一个线程对该变量的修改会被立即刷新(store)到主内存,保证其他线程看到的值一定是最新的。

    当然我们也可以通过synchronized和Lock通过加锁将多线程进行同步也就是串行执行来保证共享变量的可见性。

    image-20210223013909799

  • 有序性-保证指令程序运行结果不会受cpu指令并行优化的影响

    • 首先,为了提高程序的执行效率,编译器在生成指令序列时和CPU执行指令序列时,都有可能对指令进行重排序。
  • happen-before规则

    • happen-before 在这里不能理解成在什么之前发生,它和时间没有任何关系。解释成“生效可见于” 更准确。
    • 先行发生原则是Java内存模型中定义的两项操作之间的偏序关系,例如操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等
      • 定义一:如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。比如 int a = 3,int b = a +1,这个规则就保证第二行代码中的 a 是 3,而不是其他值。
    • 定义二:**两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。**如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
      • happen-before有八个具体的规则,其中一个是volitile变量规则:被volitile修饰的变量写操作先行发行于读操作。
    • JMM定义的有序性是指保证程序运行结果不会受cpu指令并行优化的影响

volatile

  • 保证可见性(及时通知其他线程此被volatile修饰的变量已经发生了变化)
  • 不保证原子性(只能保证单次读/写的原子性,某个线程正在做某个业务时,中间不被加塞,多个线程执行++,结果会少于预期值,因为多线程会覆盖掉前面线程的值)
    • 用synchronized可以保证原子性
    • AtomicInteger
    • cas操作
  • 禁止指令重排(编译器会优化多线程下的指令排序)

volatile原理

volatile原理讲解

  • volatile的底层实现原理是内存屏障,底层是用Lock前缀指令(该指令是JVM层面的,JVM层面有四种内存屏障,会可以导致硬件层面的读/写内存屏障发生)实现的

    • 对volatile变量的写指令后会加入写屏障

      在该屏障之前,共享变量的任何改动都会更新到主存中,让其他线程可见

    • 对volatile变量的读指令前会加入读屏障,

      在该屏障之后,共享变量的读取,加载的是主存中的最新数据

  • 举例子:若对声明了volatile的变量进行写操作JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存航的数据写会主存。但是,就算回到主存,还要保证其他处理器的缓存是一致的。每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是否过期了,当处理器发现自己缓存行对应的内存地址被修改时,就会设置当前缓存行为无效,需要对数据进行修改的时候会重新从主存中加载。

  • 嗅探机制工作原理:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。

CAS

image-20210209163818869

compare and swap,功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,为原子操作

cas算法中共享变量是用volatile修饰

  • volatile保证该其他线程能看到最新值,但不能解决指令交错(原子性)的问题
  • cas操作解决了原子性问题,cas是通过硬件命令保证原子性

cas效率比synchronized高

  • 有锁情况下,没获得锁则会会发生上下文切换,代价比较高
  • 无锁情况下,因为线程要保持运行,需要额外cpu支持,当没有分到时间片时候,也会导致上下文切换

volitile缺点:

  • 开销大,有循环,需要一直尝试

  • 只能保证一个共享变量的原子性

  • ABA问题(站在硬件层面没有这个问题,而是在代码或思想层面上使用才会出现这种问题)

    • https://www.zhihu.com/question/65372648

    • 解决办法:加时间戳(版本号)(JDK原子包atomic中已经有类这样实现了,解决了ABA问题)

  • 会导致cache一致性流量过大,Core1和Core2可能会同时把主存中某个位置的值Load到自己的L1 Cache中,当Core1在自己的L1 Cache中修改这个位置的值时,会通过总线,使Core2中L1 Cache对应的值“失效”,而Core2一旦发现自己L1 Cache中的值失效(称为Cache命中缺失)则会通过总线从内存中加载该地址最新的值,,如果有很多线程都共享同一个对象,当某个Core CAS成功时必然会引起总线风暴,这就是所谓的本地延迟本质上偏向锁就是为了消除CAS,降低Cache一致性流量

image-20210209165920247

AQS

https://www.cnblogs.com/waterystone/p/4920797.html

https://www.pdai.tech/md/java/thread/java-thread-x-lock-AbstractQueuedSynchronizer.html

  • 什么是AQS? 为什么它是核心?

    • AQS定义:它维护了一个volatile int state(代表共享资源)和一个虚拟双向等待队列(多线程争用资源被阻塞时会进入此队列)
    • img
  • AQS的核心思想是什么? 它是怎么实现的? 底层数据结构等

    • CLH队列(双向虚拟队列)
  • AQS有哪些核心的方法?

    • 获取资源 acquire()

      • tryAcquire(),尝试去获取独占资源
      • addWaiter(Node),添加到队尾 (有自旋)
      • acquireQueued:在等待队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回(有自旋)
      • img
    • 释放资源 release

      • 用unpark()唤醒等待队列中最前边的那个未放弃线程
    • 只有前驱节点是头节点时才能尝试获取同步状态,目的是维护FIFO队列,保持顺序

  • AQS定义什么样的资源获取方式?

    • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
      • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
      • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
    • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
  • AQS底层使用了什么样的设计模式? 模板

ReentrantLock

  • 内部结构

    • 
      public class ReentrantLock implements Lock, java.io.Serializable { //实现了Lock接口
          // 序列号
          private static final long serialVersionUID = 7373984872572414699L;    
          // 同步队列
          private final Sync sync;
      }
      
    • 默认sync = new NonfairSync(); //默认是非公平队列

image

  • 加锁成功的情况

    image-20210221204631585

    • 利用CAS修改state为1
  • 加锁失败的情况

    image-20210221204753066

    • image-20210221205044180
  • image-20210221205757665

  • image-20210221205659236

  • 释放锁的过程

    • 释放成功

      image-20210221210117279

    • 释放失败

    • image-20210221210438708

    • 如果是非公平锁,会cas获得状态,如果状态为0,直接抢锁

    • 如果是公平锁,不会直接被抢,会检查一下AQS队列里有没有老二(前驱节点),让他先获得锁

可重入原理

  • image-20210221210907017

可打断原理

  • 不可打断模式下,即使被打断了,也仍停留在AQS队列中,继续运行,只是打断标记被设置为true,没什么影响

    image-20210221211629213

  • 打断模式下

  • image-20210221211733355

条件变量实现原理

await流程

  • 每个条件变量对应着一个等待队列(ConditionObject),单向链表

  • image-20210221213529706

  • 然后full release 释放锁

  • image-20210221213643190

signal流程

  • image-20210221213739027
  • image-20210221214126077
  • image-20210221214147654

ReentrantReadWriteLock使用

  • 多线程一起读的时候不会互斥,但是一读一写会互斥,写需要等读完才能获取锁写写两个锁也会互斥

image-20210221215123588

  • 缓存更新策略,必须要先更新数据库才对

    image-20210221220034660

  • image-20210221220115084

读写锁原理

读写锁用的是同一个sycn同步器,因此等待队列、state等也是同一个

主要是state变成0_1(32位被拆成两部分),分成两部分供读写锁分开使用

image-20210221235852732

image-20210222001411918

当目前运行的t1(写)解锁时候,会唤醒接下来所有的读锁,直到写锁前停下来

image-20210222001532576

当state重回0_0时候,又会唤醒t4(写)线程。

Semaphore原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ChUeTFYr-1616851856098)(https://cdn.jsdelivr.net/gh/lingling-fa/typora-imgs@pic/img/20210222001932.png)]

image-20210222001954664

image-20210222002047771

线程池

https://mp.weixin.qq.com/s?__biz=MzI4Njg5MDA5NA==&mid=2247484214&idx=1&sn=9b5c977e0f8329b2bf4c29d230c678fb&chksm=ebd74237dca0cb212f4505935f9905858b9166beddd4603c3d3b5386b5dd8cf240c460a8e7c4&scene=21#wechat_redirect

美团技术文章

b站视频笔记

好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

使用

  • Runable和Callable区别
    • Runable无法抛出异常
    • Callable会抛出异常
  • 执行 execute()方法和 submit()方法的区别是什么呢?
    • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
    • submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功
  • 创建线程池

线程数设置

  • cpu密集型:

    • 线程数 = cpu核数+1
    • +1 是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费
  • IO密集型:

    • I/O 密集型运算 CPU 不总是处于繁忙状态,可以利用多线程提高它的利用率。

    • 线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间

    • 例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式 4 * 100% * 100% / 50% = 8

ThreadPoolExecutor

image-20210307155028440

image-20210307171934441

图2 ThreadPoolExecutor运行流程

构造方法

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

关键参数

  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数
    • maximumPoolSize - corePoolSize = 救急线程数
  • keepAliveTime:救急线程空闲时的最大生存时间
  • unit:时间单位
  • workQueue:阻塞队列(存放任务)
    • 有界阻塞队列 ArrayBlockingQueue
    • 无界阻塞队列 LinkedBlockingQueue
    • 最多只有一个同步元素的队列 SynchronousQueue
    • 优先队列 PriorityBlockingQueue
  • threadFactory:线程工厂(给线程取名字)
  • handler:拒绝策略
    • 抛异常
    • 丢弃任务
    • 挤掉最前面的任务
    • 将任务分给调用线程来执行

image-20210307160047042

ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量

image-20210307173712934

执行细节

  • execute
    1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
    2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
    3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中
    4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务
    5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常
    6. 图4 任务调度流程
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        //如果线程池中运行的线程数量<corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

        //如果线程池中运行的线程数量>=corePoolSize,且线程池处于RUNNING状态,且把提交的任务成功放入阻塞队列中,就再次检查线程池的状态,
            // 1.如果线程池不是RUNNING状态,且成功从阻塞队列中删除任务,则该任务由当前 RejectedExecutionHandler 处理。
            // 2.否则如果线程池中运行的线程数量为0,则通过addWorker(null, false)尝试新建一个线程,新建线程对应的任务为null。
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 如果以上两种case不成立,即没能将任务成功放入阻塞队列中,且addWoker新建线程失败,则该任务由当前 RejectedExecutionHandler 处理。
        else if (!addWorker(command, false))
            reject(command);
    }
  • 线程从阻塞队列中获取任务:getTask

      	<img src="https://p0.meituan.net/travelcube/49d8041f8480aba5ef59079fcc7143b996706.png" alt="图6 获取任务流程图" style="zoom: 50%;" />
    
  • work线程

    • private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
          final Thread thread;//Worker持有的线程
          Runnable firstTask;//初始化的任务,可以为null
      }
      
    • Worker这个工作线程,实现了Runnable接口,并持有一个线程thread

    • Worker是通过继承AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS

      • 目的就是实现不可重入的特性去反应线程现在的执行状态
      • 1.lock方法一旦获取了独占锁,表示当前线程正在执行任务中。
      • 2.如果正在执行任务,则不应该中断线程。
      • 3.如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。
      • 4.线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,
    • try {
        while (task != null || (task = getTask()) != null) {
          //执行任务
        }
      } finally {
        processWorkerExit(w, completedAbruptly);//获取不到任务时,主动回收自己
      }
      
  • 关闭线程池

    • shutdown只是将线程池的状态设置成SHUTDOWN状态,停止接收外部submit的任务,内部正在跑的任务和队列里等待的任务,会执行完后,才真正停止
    • shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表 ,终止线程的方法是Thread.interrupt(),当然如果线程中没有对应的方法,是无法成功阻止当前执行任务的线程的。

Executors

  • newFixedThreadPool(线程固定大小)

    • public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
              return new ThreadPoolExecutor(nThreads, nThreads,  //核心线程和最大线程是一致的
                                            0L, TimeUnit.MILLISECONDS,
                                            new LinkedBlockingQueue<Runnable>(),
                                            threadFactory);
          }
      
    • 无救急线程

    • 阻塞队列是无界的,可以放无数个(LinkedBlockingQueue)

  • newCachedThreadPool

    • 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着
      • 全部都是救急线程(60s 后没有任务就回收)
      • 救急线程可以无限创建
    • 队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交 货)SynchronousQueue,同步移交:不会放到队列中,而是等待线程执行它。如果当前线程没有执行,很可能会新开一个线程执行。
    • SynchronousQueue 也是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)。
    • 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。
    • 适合任务数比较密集,但每个任务执行时间较短的情况
  • newSingleThreadExecutor

    • 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
    • newSingleThreadExecutor 和 newFixedThreadPool 区别:
      和自己创建单线程执行任务的区别:自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
  • newScheduledThreadPool

    • 用来执行定时任务

Tomcat

image-20210307165345227

多线程算法

单例模式(双层检查锁)

image-20210209125508766

优点:运行块

缺点:synchronized不能保证外边的代码原子性 同步性等,还没调用构造方法就被其他线程拿去用了

image-20210209130818929

所以需要给INSTANCE 添加一个 volatile

image-20210209131458426

image-20210209131519550

交替输出

  • wait/notify

    public class Test27 {
        public static void main(String[] args) {
            WaitNotify wn = new WaitNotify(1, 5);
            new Thread(() -> {
                wn.print("a", 1, 2);
            }).start();
            new Thread(() -> {
                wn.print("b", 2, 3);
            }).start();
            new Thread(() -> {
                wn.print("c", 3, 1);
            }).start();
        }
    }
    
    /*
    输出内容       等待标记     下一个标记
       a           1             2
       b           2             3
       c           3             1
     */
    class WaitNotify {
        // 打印               a           1             2
        public void print(String str, int waitFlag, int nextFlag) {
            for (int i = 0; i < loopNumber; i++) {
                synchronized (this) {
                    while(flag != waitFlag) {
                        try {
                            this.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(str);
                    flag = nextFlag;
                    this.notifyAll();
                }
            }
        }
    
        // 等待标记
        private int flag; // 2
        // 循环次数
        private int loopNumber;
    
        public WaitNotify(int flag, int loopNumber) {
            this.flag = flag;
            this.loopNumber = loopNumber;
        }
    }
    
  • ReentrantLock

    public static void main(String[] args) throws InterruptedException {
            AwaitSignal awaitSignal = new AwaitSignal(5);
            Condition a = awaitSignal.newCondition();
            Condition b = awaitSignal.newCondition();
            Condition c = awaitSignal.newCondition();
            new Thread(() -> {
                awaitSignal.print("a", a, b);
            }).start();
            new Thread(() -> {
                awaitSignal.print("b", b, c);
            }).start();
            new Thread(() -> {
                awaitSignal.print("c", c, a);
            }).start();
    
            Thread.sleep(1000);
            awaitSignal.lock();
            try {
                System.out.println("开始...");
                a.signal(); //一开始唤醒a休息室
            } finally {
                awaitSignal.unlock();
            }
    
        }
    class AwaitSignal extends ReentrantLock{
        private int loopNumber;
    
        public AwaitSignal(int loopNumber) {
            this.loopNumber = loopNumber;
        }
        //            参数1 打印内容, 参数2 进入哪一间休息室, 参数3 下一间休息室
        public void print(String str, Condition current, Condition next) {
            for (int i = 0; i < loopNumber; i++) {
                lock();
                try {
                    current.await();//等待
                    System.out.print(str);
                    next.signal();//唤醒下个休息室
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    unlock();
                }
            }
        }
    }
    
  • park/unpark

    public class Test31 {
    
        static Thread t1;
        static Thread t2;
        static Thread t3;
        public static void main(String[] args) {
            ParkUnpark pu = new ParkUnpark(5);
            t1 = new Thread(() -> {
                pu.print("a", t2);
            });
            t2 = new Thread(() -> {
                pu.print("b", t3);
            });
            t3 = new Thread(() -> {
                pu.print("c", t1);
            });
            t1.start();
            t2.start();
            t3.start();
    
            LockSupport.unpark(t1);
        }
    }
    
    class ParkUnpark {
        public void print(String str, Thread next) {
            for (int i = 0; i < loopNumber; i++) {
                LockSupport.park();//调用方法的线程先阻塞住
                System.out.print(str);//打印
                LockSupport.unpark(next);//唤醒下一个线程
            }
        }
    
        private int loopNumber;
    
        public ParkUnpark(int loopNumber) {
            this.loopNumber = loopNumber;
        }
    }
    

  static Thread t1;
  static Thread t2;
  static Thread t3;
  public static void main(String[] args) {
      ParkUnpark pu = new ParkUnpark(5);
      t1 = new Thread(() -> {
          pu.print("a", t2);
      });
      t2 = new Thread(() -> {
          pu.print("b", t3);
      });
      t3 = new Thread(() -> {
          pu.print("c", t1);
      });
      t1.start();
      t2.start();
      t3.start();

      LockSupport.unpark(t1);
  }

}

class ParkUnpark {
public void print(String str, Thread next) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();//调用方法的线程先阻塞住
System.out.print(str);//打印
LockSupport.unpark(next);//唤醒下一个线程
}
}

  private int loopNumber;

  public ParkUnpark(int loopNumber) {
      this.loopNumber = loopNumber;
  }

}


## 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值