多线程高频面试题

3 篇文章 0 订阅
1 篇文章 0 订阅

1. 线程的生命周期

  • 进入阻塞的原因
  1. 线程通过调用sleep进入睡眠状态
  2. 线程调用一个在I/O上被阻塞的操作
  3. 线程尝试得到一个锁,该锁被其他线程持有
  4. 线程正在等待某个触发条件
  • 导致死亡的原因
  1. run方法正常退出而导致死亡
  2. 一个未捕获的异常终止了run方法而使线程猝死
  • 实现Runnable接口和实现Callable接口的区别
  1. Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的
  2. 实现Callable接口的任务线程能返回执行结果,而实现Runnable接口的任务线程不能返回结果
  3. Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
  4. 加入线程池运行,Runnable使用ExecutorService的execute方法,Callable使用submit方法
  5. :Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取返回结果,当不调用此方法时,主线程不会阻塞

2.线程和进程的关系

  • 进程:是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
  • 线程:是进程的一个实体,是 cpu 调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
  • 在 Java 程序中怎么保证多线程的运行安全?
  1. 线程安全在三个方面体现:
    • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
    • 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized、volatile);
    • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before 原则)。
  2. Java 线程同步的几种方法?
    • 使用 Synchronized 关键字;
    • wait 和 notify;
    • 使用特殊域变量 volatile 实现线程同步;
    • 使用可重入锁实现线程同步;
    • 使用阻塞队列实现线程同步;
    • 使用信号量 Semaphore。

3.进程之间通信方式

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。IPC 的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams 等。其中 Socket 和 Streams 支持不同主机上的两个进程 IPC。
  • 管道
  1. 它是半双工的,具有固定的读端和写端;
  2. 它只能用于父子进程或者兄弟进程之间的进程的通信;
  3. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的 read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
  • 命名管道
  1. FIFO 可以在无关的进程之间交换数据,与无名管道不同;
  2. FIFO 有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
  • 消息队列
  1. 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符 ID 来标识;
  2. 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
  3. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
  4. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
  • 信号量
  1. 信号量(semaphore)是一个计数器。用于实现进程间的互斥与同步,而不是用于存储进程间通信数据;
  2. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存;
  3. 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作;
  4. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数;
  5. 支持信号量组。
  • 共享内存
  1. 共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区;
  2. 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。

4.直接调用run()方法和调用start()方法的区别

  • 每个线程都是通过某个特定 Thread 对象所对应的方法 run() 来完成其操作的,方法 run() 称为线程体。通过调用 Thread 类的 start() 方法来启动一个线程;
  • start() 方法来启动一个线程,真正实现了多线程运行。这时无需等待 run() 方法体代码执行完毕,可以直接继续执行下面的代码;这时此线程是处于就绪状态,并没有运行。然后通过此 Thread 类调用方法 run() 来完成其运行状态,这里方法 run() 称为线程体,它包含了要执行的这个线程的内容,run() 方法运行结束,此线程终止。然后 cpu 再调度其它线程;
  • run() 方法是在本线程里的,只是线程里的一个函数,而不是多线程的。如果直接调用 run(),其实就相当于是调用了一个普通函数而已,直接待用 run() 方法必须等待 run() 方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用 start() 方法而不是 run() 方法。

5.synchronize和volatile比较

  1. volatile是线程同步的轻量级实现,性能比synchronize较好,volatile只能修饰变量 synchronize可以修饰方法,代码块
  2. 多线程访问volatile不会发生阻塞
    synchronize会发生阻塞
  3. volatile能保证变量在私有内存和主存间的同步,不保证变量的原子性
    synchronize可保证变量原子性
  4. volatile保证变量在多线程之间的可见性
    synchronize保证多线程之间访问资源的同步性

6.synchronized和Lock锁的区别

  1. Lock是一个接口
    synchronized是一个关键字
  2. synchronized会自动释放锁
    Lock必须手动释放
  3. Lock可以让等待锁的线程响应中断
    synchronized会让等待锁的线程一直等待下去
  4. Lock是对块范围上锁的
    synchronized能够作用于类,方法和代码块

7.wait和sleep的不同

  1. sleep方法只是让出了CPU,并不会释放锁 wait会释放锁,调用notify方法,才会解除wait方法
  2. sleep可以在任何地方使用 wait方法只能在同步方法或同步块中使用
  3. sleep是Thread的方法,调用会暂停此线程指定的时间,到时间会自动恢复 wait是Object的方法,调用会放弃锁,进入等待队列,调用notify或notifyAll会唤醒线程

8.java可重入锁ReentrantLockReentrantLock实现了Lock

  1. ReentrantLock来自J.U.C包,是一个可重入的互斥锁
  2. ReentrantLock等待可中断,当持有锁的线程长时间不释放锁时,正在等待的线程可以选择 放弃等待,可中断特性对处理执行时间较长的同步块很有帮助
  3. ReentrantLock可以实现公平锁,多个线程等待一个锁时,必须按照申请锁的时间顺序来获得 锁;而非公平锁不能保证这一点,在锁被释放时,任何一个等待线程均有机会获得锁。 synchronized锁是不公平的;ReentrantLock默认也是不公平的,可以通过传参使用公平锁
  4. 锁绑定多个条件,一个ReentrantLock可以绑定多个Condition对象。

9.Synchronized与ReentrantLock区别

synchronized 是和 for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。
  1. synchronized竞争锁会一直等待
    reentrantLock的等待可中断
  2. synchronized无法实现公平锁
    reentrantLock可以实现公平锁
  3. synchronized等待和唤醒线程要结合加锁对象的wait,notify或notifyAll
    reentrantLock等待和唤醒线程要结合Condition的await,signal,signalAll
  4. synchronized从JMM层面实现的
    reentrantLock从代码层面实现
  5. synchronized出现异常会自动释放锁
    reentrantLock不会自动释放锁,需要在finally{ }代码块显示释放

10.AQS—AbstractQueuedSynchronizer类

AQS 核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AQS是AbstractQueuedSynchronizer的简称。AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。

内部有一个int类型的state变量,被volatile关键字修饰,保证线程之间可见性还有一个Node内部类(生成同步队列和等待队列),ASQ拥有一个同步队列和多个等待队列AQS定义了俩种资源共享方式:独占模式和共享方式。

11.ThreadLocal

  1. ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。
    它采用采用空间来换取时间的方式,解决多线程中相同变量的访问冲突问题。
  2. 每个Thread的对象都有一个ThreadLocalMap,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型。
    在该类中,我觉得最重要的方法就是两个:set()和get()方法。当调用ThreadLocal的get()方法的时候,会先找到当前线程的ThreadLocalMap,然后再找到对应的值。set()方法也是一样。
  3. ThreadLocal特性:
    ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是
    • Synchronized是通过线程等待,牺牲时间来解决访问冲突
    • ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。

12.volatile的定义和实现原理

volatile 关键字是用来保证有序性可见性的。这跟 Java 内存模型有关。我们所写的代码,不一定是按照我们自己书写的顺序来执行的,编译器会做重排序,CPU 也会做重排序的,这样做是为了减少流水线阻塞,提高 CPU 的执行效率。这就需要有一定的顺序和规则来保证,不然程序员自己写的代码都不知道对不对了,所以有 happens-before 规则,其中有条就是 volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作、有序性实现的是通过插入内存屏障来保证的。
被 volatile 修饰的共享变量,就具有了以下两点特性:

  • 保证了不同线程对该变量操作的内存可见性;
  • 禁止指令重排序。一个字段被声明为volatile,java线程内存模型确保所有的线程看到这个变量的值是一致的
    1. 对有volatile变量修饰的共享变量写操作的时候,生成的汇编指令会多出一行lock前缀的代码 lock前缀指令会引发两件事情:
      • 将当前处理器缓存行的数据写回到系统内存中
      • 这个写回的操作会使得在其它CPU里缓存了该地址的数据无效
    2. volatile两条实现原则
      • Lock前缀指令会引起处理器缓存回写到内存
      • 一个处理器的缓存回写到内存会导致其它的处理器缓存无效

13.synchronized的实现原理和应用

  • 表现形式
    1.对于普通方法,锁是当前实例对象
    2.对于静态方法,锁是当前类的Class对象
    3.对于同步方法,锁是Synchronized括号内的配置对象
    当一个线程试图访问同步代码块时,首先必须得到锁,退出或异常必须释放锁
  • 实现原理
    JVM基于加入和退出Monitor对象来实现方法同步和代码块同步
    代码块同步使用monitorenter和monitorexit指令实现
    • monitorenter指令是在编译后插入到同步代码块的开始位置
    • monitorexit指令是插入到方法结束处或者异常处
    • 每个monitorenter必须有对应的monitorexit与之配对
    • 一个monitor被持有之后,处于锁定状态, monitor 对象存在于每个 Java 对象的对象头中,线程执行到monitorenter,会尝试获取对象的锁
    • 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权。monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么 Java 中任意对象可以作为锁的原因。当计数器为 0 则可以成功获取,获取后将锁计数器设为 1 也就是加 1。相应的在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
    • synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

14.Synchronized的俩个用法

  • 对象锁:方法锁 同步代码块锁
  • 代码块:手动指定锁对象
  • 方法锁:synchronized修饰普通方法,锁对象默认this
  • 类锁:修饰静态方法 指定锁类Class对象
  • 多线程访问同步方法的7种情况
    1.俩个线程同时访问一个对象的同步方法 串行执行
    2.俩个线程访问的是俩个对象的同步方法 并行执行 锁对象不同
    3.俩个线程访问的是synchronized的静态方法 串行访问
    4.同时访问同步方法与非同步方法 并行访问 非同步方法不受同步方法影响
    5.访问一个对象的不同的普通同步方法 串行访问 默认锁this
    6.同时访问静态synchronized和非静态synchronized方法 并行访问 锁不同 .class对象 this对象
    7.方法抛异常后,会释放锁
  • 总结:
    1.一个锁只能被一个线程获取,没有拿到锁的线程必须等待(第1,5种情况)
    2.每个实例都对应有自己的一把锁,不同实例之间互不影响;
    例外:锁对象是*.class已经synchronized修饰的是static方法的时候,所有对象公用一把类锁 (第2,3,4,6种情况)
    3.无论是方法正常执行完毕或者方法抛出异常,都会释放锁(第7种)
  • 性质
    • 可重入
      • 什么是可重入:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获得该锁
      • 好处:避免死锁,提升封装性
      • 粒度:线程而非调用
      • 证明:
        情况1:同一个方法可重入
        情况2:可重入不要求是同一个方法
        情况3:可重入不要求是同一个类
    • 不可中断
      • 一旦该锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁,如果比尔永远不释放锁,我只能一直等下去。
      • 原理
        • 加锁和释放锁的原理
          获取和释放锁的时机:内置锁
        • 可重入原理
          1.JVM负责跟踪对象被加锁的次数
          2.线程第一次给对象加锁的时候,计数变为1.每当这个相同的线程在此对象上再次获得锁时, 计数递增
          3.每当任务离开时,计数递减,当计数为0的时候,锁完全被释放
        • 可见性原理缺陷
          1.效率低:锁的释放情况少,试图获取锁时不能设定超时,不能中断一个正在试图获取锁的线程
          2.不够灵活:加锁和释放锁的实际单一,每个锁仅有单一的条件
          3.无法知道是否成功获取到锁
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值