[关闭]
@MRsunhuimin 2021-10-24T14:16:41.000000Z 字数 4713 阅读 170

多线程

总结


1. 进程和线程的区别?

    「进程是系统资源分配和调度的基本单位」,它能并发执行较高系统资源的利用率.

    「线程」是「比进程更小」的能独立运行的基本单位,创建、销毁、切换成本要小于进程,可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。

2. volatile 有什么作用?

    「1.保证内存可见性」
    可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。
    当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量「写入数据」的时候,虚拟机会「强制它被值刷新到主内存中」。当一个线程「读取」被volatile关键字修饰的值的时候,虚拟机会「强制要求它从主内存中读取」。

    「2.禁止指令重排序」
    cpu 是和缓存做交互的,但是由于 cpu 运行效率太高,所以会不等待当前命令返回结果从而继续执行下一个命令,就会有乱序执行的情况发生
    指令重排序是编译器和处理器为了高效对程序进行优化的手段,cpu  是与内存交互的,而 cpu 的效率想比内存高很多,所以 cpu 会在不影响最终结果的情况下,不等待返回结果直接进行后续的指令操作,而 volatile 就是给相应代码加了「内存屏障」,在屏障内的代码禁止指令重排序。

3. JMM 是什么?

    JMM 就是 「Java内存模型」(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)「屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果」。

    Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。「线程不能直接读写主内存中的变量」。

    每个线程的工作内存都是独立的,「线程操作数据只能在工作内存中进行,然后刷回到主存」。这是 Java 内存模型定义的线程基本工作方式。

4. 怎么保证线程安全?

    1.synchronized关键字

        可以用于代码块,方法(静态方法,同步锁是当前字节码对象;实例方法,同步锁是实例对象)


    2.lock锁机制

        Lock lock = new ReentrantLock();
        lock. lock();
        try {
            System. out. println("获得锁");
        } catch (Exception e) {

        } finally {
            System. out. println("释放锁");
            lock. unlock();
        }

5. synchronized 锁升级的过程

    在 Java1.6 之前的版本中,synchronized 属于重量级锁,效率低下,「锁是」 cpu 一个「总量级的资源」,每次获取锁都要和 cpu 申请,非常消耗性能。         

    在 「jdk1.6 之后」 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了,Jdk1.6 之后,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁,「增加了锁升级的过程」,由无锁->偏向锁->自旋锁->重量级锁

    增加锁升级的过程主要是「减少用户态到核心态的切换,提高锁的效率,从 jvm 层面优化锁」

                        Mark Word的状态变化
                25bit                   1bit        2bit
    锁状态                  4bit    
            23bit   2bit            是否是偏向锁    锁标志位
    轻量级锁            指向栈中锁记录的指针        00        
    重量级锁        指向互斥量(重量级锁)的指针    10       
    GC标记                  空                      11
    偏向锁  线程ID  EPoch   对象分代年龄    1       01
    无锁    对象HashCode    对象分代年龄    0       01

6. cas 是什么?

    cas 叫做 CompareAndSwap,「比较并交换」,很多地方使用到了它,比如锁升级中自旋锁就有用到,主要是「通过处理器的指令来保证操作的原子性」,它主要包含三个变量:
        「1.变量内存地址」
        「2.旧的预期值 A」
        「3.准备设置的新值 B」
    当一个线程需要修改一个共享变量的值,完成这个操作需要先取出共享变量的值,赋给 A,基于 A 进行计算,得到新值 B,在用预期原值 A 和内存中的共享变量值进行比较,「如果相同就认为其他线程没有进行修改」,而将新值写入内存    

    「CAS的缺点」
    「CPU开销比较大」:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,又因为自旋的时候会一直占用CPU,如果CAS一直更新不成功就会一直占用,造成CPU的浪费。

    「ABA 问题」:比如线程 A 去修改 1 这个值,修改成功了,但是中间 线程 B 也修改了这个值,但是修改后的结果还是 1,所以不影响 A 的操作,这就会有问题。可以用「版本号」来解决这个问题。

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

7. 聊聊 ReentrantLock 吧

    ReentrantLock 意为「可重入锁」,说起 ReentrantLock 就不得不说 AQS ,因为其底层就是「使用 AQS 去实现」的。

    ReentrantLock有两种模式,一种是公平锁,一种是非公平锁。

    公平模式下等待线程入队列后会严格按照队列顺序去执行
    非公平模式下等待线程入队列后有可能会出现插队情

「公平锁」
    第一步:「获取状态的 state 的值」
        如果 state=0 即代表锁没有被其它线程占用,执行第二步。
        如果 state!=0 则代表锁正在被其它线程占用,执行第三步。

    第二步:「判断队列中是否有线程在排队等待」
        如果不存在则直接将锁的所有者设置成当前线程,且更新状态 state 。
        如果存在就入队。

    第三步:「判断锁的所有者是不是当前线程」
        如果是则更新状态 state 的值。
        如果是则更新状态 state 的值。

「非公平锁」
    获取状态的 state 的值
    如果 state=0 即代表锁没有被其它线程占用,则设置当前锁的持有者为当前线程,该操作用 CAS 完成。
    如果不为0或者设置失败,代表锁被占用进行下一步。

    此时「获取 state 的值」
    如果是,则给state+1,获取锁
    如果不是,则进入队列等待
    如果是0,代表刚好线程释放了锁,此时将锁的持有者设为自己
    如果不是0,则查看线程持有者是不是自己

8. 线程池有哪些参数?

    「1.corePoolSize」:「核心线程数」,线程池中始终存活的线程数。

    「2.maximumPoolSize」: 「最大线程数」,线程池中允许的最大线程数。

    「3.keepAliveTime」: 「存活时间」,线程没有任务执行时最多保持多久时间会终止。

    「4.unit」: 「单位」,参数keepAliveTime的时间单位,7种可选。

    「5.workQueue」: 一个「阻塞队列」,用来存储等待执行的任务,均为线程安全,7种可选。

    「6.threadFactory」: 「线程工厂」,主要用来创建线程,默认正常优先级、非守护线程。

    「7.handler」:「拒绝策略」,拒绝处理任务时的策略,4种可选,默认为AbortPolicy。

9. 线程池的执行流程?

    1.开始
    2.提交任务
    3.线程池是否还在进行 是4 否 任务拒绝 end
    4.线程数小于核心数   是 添加工作线程并执行end  否5
    5.阻塞队列已满       是6    否 添加任务到阻塞队列等待工作获取进行end
    6.线程数小于最大线程数  是 添加工作线程并执行end  否 任务拒绝 end


    判断线程池中的线程数「是否大于设置的核心线程数」

        如果「没有满」,则「放入队列」,等待线程空闲时执行任务
        如果队列已经「满了」,则判断「是否达到了线程池设置的最大线程数」
        如果「没有达到」,就「创建新线程」来执行任务
        如果已经「达到了」最大线程数,则「执行指定的拒绝策略」
        如果「小于」,就「创建」一个核心线程来执行任务
        如果「大于」,就会「判断缓冲队列是否满了」

10. 线程池的拒绝策略有哪些?

    「AbortPolicy」:直接丢弃任务,抛出异常,这是默认策略

    「CallerRunsPolicy」:只用调用者所在的线程来处理任务

    「DiscardOldestPolicy」:丢弃等待队列中最旧的任务,并执行当前任务

    「DiscardPolicy」:直接丢弃任务,也不抛出异常

11. 介绍一下四种引用类型?

    「强引用 StrongReference」
  1. Object obj = new Object();
  2. //只要obj还指向Object对象,Object对象就不会被回收
    垃圾回收器不会回收被引用的对象,哪怕内存不足时,JVM 也会直接抛出 OutOfMemoryError,除非赋值为 null。

    「软引用 SoftReference」
        软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。

    「弱引用 WeakReference」
        弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。

    「虚引用 PhantomReference」
        虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用,NIO 的堆外内存就是靠其管理。

12. 聊聊 ThreadLocal 吧

    ThreadLocal其实就是「线程本地变量」,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离。

    ThreadLocal 有一个「静态内部类 ThreadLocalMap」,ThreadLocalMap 又包含了一个 Entry 数组,「Entry 本身是一个弱引用」,他的 key 是指向 ThreadLocal 的弱引用,「弱引用的目的是为了防止内存泄露」,如果是强引用那么除非线程结束,否则无法终止,可能会有内存泄漏的风险。

    但是这样还是会存在内存泄露的问题,假如 key 和 ThreadLocal 对象被回收之后,entry 中就存在 key 为 null ,但是 value 有值的 entry 对象,但是永远没办法被访问到,同样除非线程结束运行。「解决方法就是调用 remove 方法删除 entry 对象」。

13. 一个对象的内存布局是怎么样的?

    「1.对象头」: 对象头又分为 「MarkWord」 和 「Class Pointer」 两部分。
        「MarkWord」:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位,gc记录信息等等。
        「ClassPointer」:用来指向对象对应的 Class 对象(其对应的元数据对象)的内存地址。在 32 位系统占 4 字节,在 64 位系统中占 8 字节

    「2.Length」:只在数组对象中存在,用来记录数组的长度,占用 4 字节

    「3.Instance data」: 对象实际数据,对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。(这里不包括静态成员变量,因为其是在方法区维护的)

    「4.Padding」:Java 对象占用空间是 8 字节对齐的,即所有 Java 对象占用 bytes 数必须是 8 的倍数,是因为当我们从磁盘中取一个数据时,不会说我想取一个字节就是一个字节,都是按照一块儿一块儿来取的,这一块大小是 8 个字节,所以为了完整,padding 的作用就是补充字节,「保证对象是 8 字节的整数倍」。
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注