• Java:多线程


    1. 为什么要使用多线程?

    1)从计算机底层来说

    • 线程可比作轻量级进程,是程序执行的最小单位,线程间切换和调度的成本远远小于进程;
    • 另外,多核CPU可支持多个线程同时执行,降低了线程上下文切换的开销。
      单核计算机时代: 主要是为了提高CPU和IO设备等的综合利用率(如果只有一个线程,CPU执行时,IO空闲,IO执行时,CPU空闲,利用率只有50%,多线程可以一个CPU计算式,另一个进行IO操作)
      多核计算机时代: 主要是为了提高计算机CPU利用率(如果只用一个CPU核心,其他核心空闲,多线程可以充分利用多个CPU核心)

    2)从互联网发展趋势来说

    • 当前系统动不动就会需要百万级甚至千万级的并发量;
    • 多线程并发编程是支持高并发系统的基础,利用好多线程可提高系统整体并发能力及性能。

    2. 使用多线程可能带来什么问题?(怎么解决这些问题?)

    • 内存泄漏
    • 上下文切换
    • 死锁

    3. 什么是上下文切换?

    • 一个任务在CPU时间片用完时,会先保存自己的任务状态再切换到其他任务,这样下次加载时就从保存的位置开始执行。任务从保存到再加载的过程就叫做上下文切换。

    详解:多线程编程时,一般线程数会大于CPU核心数。而一个CPU核心一次只能执行一个线程。为使所有线程都得到有效的执行,CPU采取的策略是每个线程分配一个时间片并轮转的方式。一个线程执行完自己的时间片就会重新进入就绪状态,CPU让给其他线程使用,等待下次时间片。这个过程就是一次上下文切换。

    4. Java中有哪些锁(锁的分类)?

    1)由ReentrantLock和Synchronized创建的一系列锁

    • 公平锁与非公平锁(Synchronized只能实现非公平锁)
    • 互斥锁与非互斥锁
    • 可重入锁与非可重入锁
    • 编译优化角度 -- 锁消除和锁粗化
    • 不同的位置使用Synchronized -- 类锁和对象锁

    2)从锁的设计理念来分

    • 悲观锁(Java中Synchronized)
    • 乐观锁(版本号机制、CAS算法)

    CAS是一种更新的原子操作,比较当前值与传入值是否一样,一样则更新,否则失败。

    5. 什么是线程死锁?

    • 多个线程同时被阻塞 ,它们中的一个或多个在等待某个资源被释放。由于线程进入无限期阻塞等待,程序无法正常终止,即发生死锁。
    • 例如:线程A持有资源1,线程B持有资源2,线程A和线程B都企图得到对方的资源,因此陷入互相等待状态,形成死锁。
    • 这个例子满足形成死锁的四个必要条件:
      a. 互斥条件
      b. 请求保持条件
      c. 不可剥夺条件
      d. 循环等待条件

    6. 如何避免线程死锁?

    • 为了避免死锁,只需破坏四个必要条件中的一个即可。
      a. 破坏互斥条件:这个条件无法破坏,因为锁的存在就是为了使线程互斥的(临界资源只能被互斥的访问)。
      b. 破坏请求保持条件:一次性申请所有的资源。
      c. 破坏不可剥夺条件:申请资源没有申请到时,释放自己持有的资源。
      d. 破坏循环等待条件:靠按需申请资源来预防 。按某一顺序来申请资源,释放资源则反序释放。

    7. 谈谈对Synchronized关键字的了解?

    • Synchronized解决的是多线程访问资源的同步性。Synchronized可以保证同一时刻只有一个线程可以访问其修饰的方法或代码块。
    • Java 6 之前(synchronized属于重量级锁):
      a. 监视器锁(Monitor)依赖底层操作系统 Mutex Lock来实现;
      b. Java的多线程会映射到底层操作系统的多线程之上;
      c. 挂起或唤醒一个线程需要底层操作系统帮忙;
      d. 操作系统实现线程切换需要从用户态转换到内核态,这个状态转换需要很长的时间。
    • Java 6(JDK1.6) 之后:
      a. Java官方从JVM层面对synchronized进行了较大优化,锁效率已经优化的很好了;
      b. JDK1.6对锁引入了大量优化,如自旋锁、自适应自旋锁、锁消除、锁粗化、轻量级锁、偏向锁等技术来减少锁操作的开销。

    8. 说说怎么使用Synchronized关键字?

    • 修饰实例方法。作用于当前 对象实例加锁,进入同步代码块前,需要先获得当前 对象实例的锁。
    • 修饰静态方法。给当前 类加锁,会作用于类的所有对象实例,进入同步代码块前,要获得当前class的锁。
      a. 静态方法不属于任何一个实例对象 ,是类成员(static表示这是当前类的一个静态资源,不管new了多少个对象实例,只有一份)。
      b. 如果线程A调用一个实例对象 的非静态synchronized方法,线程B可以调用这个实例对象所属类 的静态synchronized方法。
      c. 因为访问静态synchronized方法占用的是当前类的锁访问非静态synchronized方法占用的是当前实例对象锁
    • 修饰同步代码块。指定对象加锁,对给定对象/类加锁。
      a. synchronized(this|object) 表示进入指定同步代码块前要获得给定对象 的锁。
      b. synchronized(类.class)表示进入指定同步代码块前要获得当前class类 的锁。

    总结

    • synchronized加到静态方法或synchronized(class)都是给当前类加锁。
    • synchronized加到实例方法上是给对象实例上锁。
    • 尽量不要使用synchronized(String a),因为在JVM中,字符串常量池具有缓存功能。

    9. Synchronized关键字底层原理?

    • synchronized修饰同步代码块的情况
      a. 底层使用monitorenter和monitorexit指令 ,monitorenter指向同步代码块开始的地方,monitorexit指向同步代码块结束的地方。monitorenter指令执行时,线程试图获取锁,也就是获得对象监视器monitor的持有权。
      b. monitorenter尝试获取同步代码块的锁 => 如果对象未锁定/当前线程已经持有这个锁 => 锁计数器+1。否则阻塞等待,直到持有锁的线程释放锁。
      c. monitorexit是线程退出同步代码块时:锁计数器-1 => 锁计数器减为0,锁释放。
    • synchronized修饰方法的情况
      synchronized修饰方法时并没有使用monitorenter和monitorexit指令,而是使用的是ACC_SYNCHRONIZED 标识。JVM在调用时通过这个标识来区分这个方法是不是同步方法,从而执行相应的同步调用。

    两者本质上都是对象监视器monitor的获取。

    10. volatile关键字

    首先了解一下Java内存模型JMM

    • JDK1.2之前
      JMM总是从主存(共享内存)中读取变量,是不需要特别注意的。
      2

    • 当前
      a. 线程可以把变量保存在本地内存中,而不是在主存中进行读写。
      b. 这样就可能出现一个线程在主存中修改了一个值,而另一个线程还在使用它本地内存中这个变量值的拷贝,造成数据不一致 (存在的问题)。
      2

    要解决这个问题就需要用volatile来修饰变量,这就指示JVM,这个变量是共享且不稳定的,每次使用它,都要去主存(共享内存)中读取。
    volatile关键字除了防止JVM指令重排,一个重要的作用就是保证变量的可见性。

    11. 说说synchronized关键字和volatile关键字的区别

    synchronized关键字和volatile关键字两者是互补的关系,不是对立的关系!

    • volatile是线程同步的轻量级实现,性能比synchronized好。但是volatile只能用于变量,而synchronized可以修饰方法以及代码块。(性能和作用范围不同)
    • volatile只能保证数据的可见性,不能保证数据的原子性,而synchronized都可以保证。(能保证的数据性质不同)
    • volatile解决的是变量在多线程之间的可见性,synchronized解决的是多线程访问资源的同步性。(解决的问题不同)

    12. 使用线程池的好处?

    • 降低资源消耗:线程池可以重复使用已有的线程,减少了线程创建和销毁造成的资源消耗;
    • 提高响应速度:任务到达时,不用等线程创建,可以直接执行;
    • 提高线程的可管理性:线程是稀缺资源,如果无限的创建线程,不仅会增加资源消耗,还会降低系统稳定性,线程池可以对线程进行统一的分配、调优和监控。

    13. 如何创建线程池?

    Executor方法 (《阿里巴巴Java开发手册不允许使用》)

    • newFixedThreadPool(int threads)
    • newCachedThreadPool()
    • newSingleThreadExecutor()
    • newScheduledThreadPool(int curPoolSize)

    不允许使用的原因是易造成内存泄漏(OOM)
    造成OOM的原因(以newFixedThreadPool方法为例):

       public static ExecutorService newFixedThreadPool(int nThreads) {
         return new ThreadPoolExecutor(nThreads, nThreads,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>());
    
    2

    newFixedThreadPool与newSingleThreadExecutor导致OOM的原因都为以上原因;
    newCachedThreadPool与newScheduledThreadPool创建的线程数都可能是Integer.MAX_VALUE,而创建这么多线程,必然有可能导致OOM。
    参考链接:https://blog.csdn.net/yan88888888888888888/article/details/83927609

    ThreadPoolExecutor

    • 可以直接使用ThreadPoolExecutor,自己为它的队列设置容量。这样任务个数超过指定容量就会抛出异常。

    14. ThreadPoolExecutor类

    • ThreadPoolExecutor类提供了四个构造方法;
    • 以下展示包含参数最多的一个构造方法:
     /**
      * ⽤给定的初始参数创建⼀个新的ThreadPoolExecutor。 
      */
      public ThreadPoolExecutor(int corePoolSize, 
                                int maximumPoolSize,
                                long keepAliveTime,
                                TimeUnit unit, 
                                BlockingQueue workQueue, 
                                ThreadFactory threadFactory, 
                                RejectedExecutionHandler handler) { 
           if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) 
               throw new IllegalArgumentException(); 
           if (workQueue == null || threadFactory == null || handler == null) 
               throw new NullPointerException(); 
           this.corePoolSize = corePoolSize; 
           this.maximumPoolSize = maximumPoolSize; 
           this.workQueue = workQueue; 
           this.keepAliveTime = unit.toNanos(keepAliveTime); 
           this.threadFactory = threadFactory; 
           this.handler = handler; 
       } 
    

    15. ThreadPoolExecutor类构造函数参数分析

    最重要的三个参数

    • corePoolSize
    • maximumPoolSize
    • workQueue

    其他重要参数

    • keepAliveTime:当线程池中的线程数大于corePoolSize时,核心线程池外的线程不会立即销毁,而是等待keepAliveTime时间后再被回收销毁。
    • unit
    • theadFactory:executor创建新线程时会用到。
    • handler:饱和策略。

    16. ThreadPoolExecutor饱和策略

    • AbortPolicy
    • CallerRunsPolicy
    • DiscardOldestPolicy
    • DiscardPolicy

    参考:JavaGuide

    步履不停
  • 相关阅读:
    AcWing 2476. 树套树(线段树套splay)
    LeetCode 1191 K 次串联后最大子数组之和
    LeetCode 668 乘法表中第k小的数
    Java003-String字符串
    云原生CD工具spinnaker部署(容器化灰度发布)
    刷题日记
    8.22 校内模拟赛 题解报告
    关于 CDQ分治(复习)
    解题营_数论
    解题营_动规
  • 原文地址:https://www.cnblogs.com/yuanyunjing/p/15165439.html
Copyright © 2020-2023  润新知