Java并发知识点

2.3 Java 并发

  • sleep() 和 wait() 区别?

    • sleep()是线程类Thread的方法;作用是导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时候会自动恢复;调用sleep()不会释放对象锁。

      wait()Object类的方法;对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池。只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池,准备获得对象锁进行运行状态。

  • sleep() 方法和 yield() 区别?

    ​ 1.sleep()方法给其他线程运行机会是不考虑线程的优先级,因此会给低优先级的线程以运行的机会,而yield()方法只会给相同优先级或更高优先级的线程运行机会。
    ​ 2.线程执行sleep()会转入阻塞状态,所以执行sleep()方法的线程在指定的时间内肯定不会被执行,而yield()方法只是使当前线程重新回到可执行态,所以执行yield()方法的线程进入可执行态可能很快立即被执行。
    ​ 3.sleep()方法申明抛InterruptExcepiton,而yield()方法没有声明任何异常。
    4.sleep()比yield()更具有可移植性(和操作系统有关)。

  • 编写多线程程序有几种实现方式?

    Java 5 以前实现多线程有两种实现方法:一种是继承 Thread 类;另一种是实现

    Runnable 接口。两种方式都要通过重写 run()方法来定义线程的行为,推荐使用

    后者,因为 Java 中的继承是单继承,一个类有一个父类,如果继承了 Thread 类

    就无法再继承其他类了,显然使用 Runnable 接口更为灵活。

    补充:Java 5 以后创建线程还有第三种方式:实现 Callable 接口,该接口中的 call

    方法可以在线程执行结束时产生一个返回值

  • 线程的生命周期?

    • NEW - 新建
    • RUNNABLE - 等待被CPU调度
    • RUNNING - 正在运行
    • BLOCKED - 阻塞
    • TERMINATED - 结束
  • 为什么线程启动要调用 start(),而不是直接调用 run()?

    run()方法只是一个类中的普通方法,直接执行和普通的方法没有什么两样
    start()方法则不同,它首先做了创建线程等一系列工作,然后调用行的run()方法

  • interrupt()、interrupted()、isInterrupted() 区别?

    interrupt():将调用该方法的对象所表示的线程标记一个停止标记,并不是真的停止该线程。

    interrupted():获取当前线程的中断状态,并且会清除线程的状态标记。是一个是静态方法。

    isInterrupted():获取调用该方法的对象所表示的线程,不会清除线程的状态标记。是一个实例方法。

  • start()、run() 区别?

    • start()方法让一个线程进入就绪队列等待分配cpu,分到cpu后才调用实现的run()方法。
  • 说说乐观锁、悲观锁、自旋锁、共享锁、独占锁、重量级锁、轻量级锁?

    • https://blog.csdn.net/riemann_/article/details/96909445

    • 公平锁:加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程,先来先得。

    • 非公平锁:线程加锁时直接尝试获取锁,获取不到就自动到队尾等待。

    • 悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。

    • 乐观锁: 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。

    • 独享锁:该锁每一次只能被一个线程所持有。

    • 共享锁:该锁可被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占
      另外读锁的共享可保证并发读是非常高效的,但是读写和写写,写读都是互斥的。

      独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
      对于Synchronized而言,当然是独享锁。

    • 锁的状态:

      无锁状态
      偏向锁状态
      轻量级锁状态
      重量级锁状态
      锁的状态是通过对象监视器在对象头中的字段来表明的。
      四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。
      这四种状态都不是Java语言中的锁,而是Jvm为了提高锁的获取与释放效率而做的优化(使用synchronized时)。

    • 偏向锁: 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

    • 轻量级: 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

    • 重量级锁 : 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

    • 分段锁: 分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

  • 说说 synchronized?

    加锁 Monitor Enter state 计数器 +1

    退出 Monitor Exit state 计数器 -1

    1.5 之前

    这对指令的实现是依靠操作系统内部的互斥锁来实现的,期间会涉及到用户态到内存态的切换,所以这个操作是一个重量级的操作,性能较低。

    1.5 之后

    JVM对synchronized进行了优化,改了三个经历的过程

    偏向锁-》轻量级锁-》重量级锁

    偏向锁:

    在锁对象保存一个thread-id字段,刚开始初始化为空,当第一次线程访问时,则将thread-id设置为当前线程id,此时,我们称为持有偏向锁。

    当再次进入时,就会判断当前线程id与thread-id是否一致

    如果一致,则直接使用此对象

    如果不一致,则升级为轻量级锁,通过自旋锁循环一定次数来获取锁

    如果执行了一定次数之后,还是没能获取锁,则会升级为重量级锁。

    锁升级是为了降低性能的消耗。

    其他线程检查 计数器 > 0 等待

  • 说说 ReentrantLock ?

    • ReentrantLock一般需要try catch finally语句,在try中获取锁,在finally释放锁。需要手动释放锁。
    • ReentrantLock是轻量级锁。采用cas+volatile管理线程,不需要线程切换,获取锁线程觉得自己肯定能成功,这是一种乐观的思想(可能失败)。
    • ReentrantLock提供公平和非公平两种锁,默认是非公平的。公平锁通过构造函数传递true表示。
    • ReentrantLock 意为可重入锁,说起 ReentrantLock 就不得不说 AQS ,因为其底层就是使用 AQS 去实现的。类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架
  • 说说线程的死锁吧?

    • 死锁是指多个进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象(互相挂起等待),若无外力作用,它们都将无法推进下去——永远相互等待。

    • 1.死锁产生的主要原因:

      系统的资源不足。
      进程(线程)推进的顺序不对。
      资源的分配不当。

    • 2.死锁产生的四个必要条件:(划重点)

      互斥条件:进程(线程)申请的资源在一段时间中只能被一个进程(线程)使用。
      请求与等待条件:进程(线程)已经拥有了一个资源,但是又申请新的资源,拥有的资源保持不变 。
      不可剥夺条件:在一个进程(线程)没有用完,主动释放资源的时候,不能被抢占。
      循环等待条件:多个进程(线程)之间存在资源循环链。

  • 进程、线程和死锁

    • https://blog.csdn.net/qq_43040688/article/details/107130655
    • 进程是进程实体的运行过程,是系统进行资源分配和调度的一一个独立单位。
    • 进程实体由程序段、相关数据段和PCB组成
    • 进程控制块,是我们学习操作系统后遇到的第一个数据结构描述,它是对系统的进程进行管理的重要依据
    • 线程的提出源于在并发的时候,进程的切换需要消耗很大的时空开销,而线程的提出可以提高并发时系统的性能
  • 如何避免死锁?

    主要有以下四种方法:

    鸵鸟策略:因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。
    死锁检测与死锁恢复:检测主要是查看当前是否出现了环路等待;恢复可以通过杀死进程或者利用回滚
    死锁预防:在程序运行之前破坏发生死锁的条件,预防发生死锁 ,比如说破坏环路等待可以给资源统一编号,进程只能按编号顺序来请求资源。
    死锁避免 :使用银行家算法,假设给进程分配资源,看能不能找到一个安全序列,如果系统处于不安全状态,不一定会发生死锁;但是死锁时,系统一定处于不安全状态

  • 程序计数器为什么设计成私有?

字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

  • 虚拟机栈和本地方法栈为什么设计成私有?

为了保证线程中的局部变量不被其他线程访问到,虚拟机栈和本地方法栈是线程私有的。

虚拟机栈:每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

  • 公平锁、非公平锁、可重入锁?

    公平锁:多个线程按照申请的顺序来获取锁。

    非公平锁:多个线程获取锁的先后顺序与申请锁的顺序无关。【ReentrantLock 默认非公平、synchronized】

    总结:非公平锁的吞吐量比公平锁大。

    可重入锁(又名递归锁):线程可以进入任何一个它已经获取锁的同步代码块中。

    可重入锁的最大作用:避免死锁

    自旋转:是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。

    好处:减少线程上下文切换的消耗,

    缺点:循环会消耗CPU

  • Semaphore 信号量

    问题一:什么是信号量?

    信号量就相当于一个计数器,通常用来限制线程的数量。每个线程操作前会先获取一个许可证,逻辑处理完成之后就归还这个许可证

    就好比我们去网吧上网,信号量初始化的大小就好比网吧中所有的电脑。当有人交费开机之后,可用电脑的数量就少了1台。依次类推,当有人下机之后可用的电脑数量又多了。当机器被用完之后新来的客人就只能等待前面的人下机,这就是归还许可证。

    问题二:信号量的应用场景?

    信号量的核心功能就是用来对资源做一定的限制防止出现崩塌现象。最适用的应用场景那就是限流,通过限流来保护对应的资源。

    在Spring Cloud中我们会用Hystrix来保护服务,进行熔断降级。在Hystrix中有两种模式,分别是线程池和信号量,说到这里大家明白了吧,信号量的作用。

    在限流层面,最简单的实现可以用信号量来实现本地限流操作,集群限流必须得依赖第三方中间件,比如Redis。

  • JDK 1.6 synchronized 作了那些优化?

    • 1.适应自旋锁

      自旋锁:为了减少线程状态改变带来的消耗 不停地执行当前线程

      2.锁消除:

      不可能存在共享数据竞争的锁进行消除

      3.锁粗化:

      将连续的加锁 精简到只加一次锁

      4.轻量级锁:

      无竞争条件下 通过CAS消除同步互斥

      5.偏向锁:

      无竞争条件下 消除整个同步互斥,连CAS都不操作。

  • 说说 ThreadPoolExecutor 构造方法的参数?

    • corePoolSize。核心线程池大小。这个参数是否生效取决于allowCoreThreadTimeOut变量的值,该变量默认是false,即对于核心线程没有超时限制,所以这种情况下,corePoolSize参数是起效的。如果allowCoreThreadTimeOut为true,那么核心线程允许超时,并且超时时间就是keepAliveTime参数和unit共同决定的值,这种情况下,如果线程池长时间空闲的话最终存活的线程会变为0,也即corePoolSize参数失效
    • maximumPoolSize。线程池中最大的存活线程数。这个参数比较好理解,对于超出corePoolSize部分的线程,无论allowCoreThreadTimeOut变量的值是true还是false,都会超时,超时时间由keepAliveTime和unit两个参数算出
    • keepAliveTime。超时时间。
    • unit。超时时间的单位,秒,毫秒,微秒,纳秒等,与keepAliveTime参数共同决定超时时间。
    • workQueue。当调用execute方法时,如果线程池中没有空闲的可用线程,那么就会把这个Runnable对象放到该队列中。这个参数必须是一个实现BlockingQueue接口的阻塞队列,因为要保证线程安全。有一个要注意的点是,只有在调用execute方法时,才会向这个队列中添加任务,那么对于submit方法呢,难道submit方法提交任务时如果没有可用的线程就直接扔掉吗?当然不是,看一下AbstractExecutorService类中submit方法实现,其实submit方法只是把传进来的Runnable对象或Callable对象包装成一个新的Runnable对象然后调用execute方法,并将包装后的FutureTask对象作为一个Future引用返回给调用者。Future的阻塞特性实际是在FutureTask中实现的,具体怎么实现感兴趣的话可以看一下FutureTask的源码。
    • threadFactory。线程工厂类。用于在需要的时候生成新的线程。默认实现是Executors.defaultThreadFactory(),即new 一个Thread对象,并设置线程名称,daemon等属性。
    • handler。这个参数的作用是当提交任务时既没有空闲线程,任务队列也满了,这时候就会调用handler的rejectedExecution方法。默认的实现是抛出一个RejectedExecutionException异常。
  • 说说 Java 阻塞队列?

    阻塞队列也就是 BlockingQueue ,这个类是一个接
    口,同时继承了 Queue 接口,这两个接口都是在JDK5 中加入的 。
    BlockingQueue 阻塞队列是线程安全的,在我们业务中是会经常频繁使用到的,如典型的生产者消费的场景,生产者只需要向队列中添加,而消费者负责从队列中获取。

    img

    阻塞队列常见方法
    首先我们从常用的方法出发,根据各自的特点我们可以大致分为三个大类,如下表所示:
    抛出异常

    add 添加一个元素 如果队列已满,添加则抛出 IllegalStateException 异常
    remove 删除队列头节点 当队列为空后,删除则抛出 NoSuchElementException 异常
    element 获取队列头元素 当队列为空时,则抛出 NoSuchElementException 异常
    返回无异常

    offer 添加一个元素 当队列已满,不会报异常,返回 false ,如果成功返回 true
    poll 获取队列头节点,并且删除它 当队列空时,返回 Null
    peek 单纯获取头节点 当队列为空时反馈 NULL
    阻塞

    put 添加一个元素 如果队列已满则阻塞
    take 返回并删除头元素 如果队列为空则阻塞

  • CyclicBarrier、CountDownLatch、Semaphore 的用法?

    • CountDownLatch (线程计数器)

      利用CountDownLatch 可以实现类似计数器的功能,比如主线程需要等待5个子线程执行完毕之后才能执行,就可以利用CountDownLatch实现

    • CyclicBarrier(回环栅栏)

      回环栅栏的作用就是整合一组线程,在这一组线程全部达到某个状态之后同时执行,其最重要的方法为await()

      1. public int await():用来挂起当前线程,直至所有线程都到达 barrier 状态再同时执行后续任
        务;
      2. public int await(long timeout, TimeUnit unit):让这些线程等待至一定的时间,如果还有
        线程没有到达 barrier 状态就直接让到达 barrier 的线程执行后续任务。
    • Semaphore(信号量)

      ​ Semaphore可以控制同时访问的线程个数,通过acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

  • 说说 volatile?

    volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制。

    当一个变量被volatile关键字修饰后,它将具备两种特性:

    1、保证此变量对所有线程的可见性。

    2、禁止指令重排序

  • 上下文切换?

    • https://ifeve.com/context-switch-definition/
    • 上下文切换(有时也称做进程切换或任务切换)是指 CPU 从一个进程或线程切换到另一个进程或线程。
    • 上下文是指某一时间点 CPU 寄存器和程序计数器的内容。寄存器是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
    • (1)挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处,(2)在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复,(3)跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程。
  • 说说 ThreadLocal ?

    threadlocal在线程间是隔离的,不共享,用于存储线程的变量

    即使多个线程使用同一个ThreadLocal,也只能访问自己的属性

    ThreadLocal是使用的Key/Value的结构实现,内部有一个ThreadLocalMap

    内存泄漏问题
    ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。

    在一些场景尤其是使用线程池)下,线程不会关闭,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal中key可以被回收,但是这些key为null的Entry的value就会一直存在一条强引用链,会造成内存泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。

  • 进程调度算法?

  • 线程有哪些基本状态?

img

new(新建):

当程序使用 new 创建一个线程后, 该线程处于新建状态, 此时它和其它 java 对象一样, 仅仅由 JVM 为其分配内存并初始化成员变量值.

runnable(可运行状态):

实际上可以细分成两种状态: ready(就绪) 和 running(运行) 状态.

ready(就绪):

当线程对象调用 start() 方法后, 该线程处于就绪状态, 进入线程队列排队. 此时该状态线程并未开始执行, 仅表示可以运行了. 至于该线程何时运行, 取决于 CPU 调度器的调度.

running(运行):

表示某线程对象被 CPU 调度器调度, 执行线程体. 就绪状态和运行状态时可以互相切换的, 切换的原因依旧参照 CPU 调度器调度了哪一个线程.

blocked(阻塞):

正在运行的线程遇到某个特殊情况, 比如同步, 等待I/O操作完成等. 进入阻塞状态的线程会让出 CPU 资源, 并暂时停止自己的执行.

waiting(等待):

有时一个可运行状态线程转变成等待状态, 它会等待另一个线程来执行一个任务, 一个等待状态的线程只有通过另一个线程通知它转到可运行状态, 才能继续执行.

timed waiting(计时等待):

计时等待状态是等待状态的升级版, 它会有一个定时器, 在特定时间后自动唤醒该线程对象, 让其进入可运行状态.

terminated(终止):

即死亡状态, 表示线程终止. 当线程成功执行完成或线程抛出未捕获的 Exception 和 Error 或调用线程的 stop 方法时进入该状态.

  • 并发和并行区别?

    • 并发和并行都可以是很多线程,
    • 并发 多个cpu同时执行
    • 并行 多个线程被cpu交替执行
  • 线程和进程区别?

    1、功能不同

    进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础

    线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

    2、工作原理不同

    在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器程序是指令、数据及其组织形式的描述,进程是程序的实体。

    线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。

    3、作用不同

    进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。

    通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。

  • 了解 CAS 吗?

    CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。这听起来可能有一点复杂但是实际上你理解之后发现很简单,接下来,让我们跟深入的了解一下这项技术。现在CPU内部已经执行原子的CAS操作

    • ABA 问题
    • 性能问题
  • AQS 了解吗?

    • 如ReentrantLock独占锁、ReentrantReadWriteLock读写锁、Semaphore信号量(共享锁)等,而这些锁有一个共同的基础类:AbstractQueuedSynchronizer。
    • https://blog.csdn.net/GV7lZB0y87u7C/article/details/92260574
    • AQS是一个抽象类,不可以被实例化,它的设计之初就是为了让子类通过继承来实现多样的功能的。它内部提供了一个FIFO的等待队列,用于多个线程等待一个事件(锁)。它有一个重要的状态标志——state,该属性是一个int值,表示对象的当前状态(如0表示lock,1表示unlock)。AQS提供了三个protected final的方法来改变state的值,分别是:getState、setState(int)、compareAndSetState(int, int)。根据修饰符,它们是不可以被子类重写的,但可以在子类中进行调用,这也就意味着子类可以根据自己的逻辑来决定如何使用state值。
posted @ 2021-04-20 12:07  AronJudge  阅读(98)  评论(0编辑  收藏  举报