【面试题】多线程相关

来源:互联网 发布:js focus有什么 编辑:程序博客网 时间:2024/06/05 05:49

1.线程与进程的区别

  • 定义
    • 进程: 一个执行中的程序,每一个进程执行都有一个执行的顺序(执行单元),它是一个实体,由程序段、数据段和PCB(进程标识信息、处理器状态信息、进程控制信息)组成
    • 线程:是进程中的一个独立的控制单元,是操作系统能够进行运算调度的最小单位,一个进程中至少有一个线程。
  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源,进程有独立地址空间,而线程会共享地址空间
  • 线程是处理器调度和分派的基本单位;进程为重量级组件,线程为轻量级组件;
  • 线程独有的资源:线程ID、线程堆栈、线程优先级、寄存器组的值。共享的资源:进程代码段、全局变量。地址空间、信号的处理器、进程用户ID与进程组ID、进程打开的文件描述符。
  • 进程的切换代价远高于线程,同步和通信的实现也比线程复杂。
  • 多进程是指在操作系统中能同时运行多个任务(程序),多线程是指在同一应用程序中有多个功能流同时执行
  • 多线程的适用场景:完成重复性的工作、较费时的初始化工作、并发执行的运行效果以实现更复杂的功能
  • 不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。每个线程都拥有单独的栈内存用来存储本地数据。系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
  • 解决多线程的挑战:无锁并发编程、CAS、使用最少线程、协程(单线程多任务调度)

2.线程的基本状态及关系

  • 新建状态,New一个线程对象
  • 就绪状态,调用线程对象的start()方法
  • 运行状态,CPU调度处于就绪状态的线程,调度策略(抢占式、时间片轮转)
  • 阻塞状态,一个线程想要获取锁,而该锁被其他线程持有,分为等待阻塞(wait方法),同步阻塞(synchronized),其他阻塞(sleep或join方法)
  • 终止状态,线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

3.创建线程的方法

  • 继承Thread类、实现Runnable接口或Callable接口
  • 实现Runnable接口无返回值,实现Callable接口有返回值,使用FutureTask类接受返回值
  • suspend()方法容易发生死锁,stop方法不安全,这两个方法已经过时了。
  • Sleep释放资源,不释放锁,wait释放资源,也释放锁。wait方法必须在同步代码块中调用,在while循环中

4.synchronized与Lock的异同?

  • synchronized是基于JVM层面实现的,而Lock是基于JDK层面实现的,通过队列同步器AQS实现,
  • synchronized修饰非静态方法时锁的是当前实例对象(this);修饰静态方法时锁的是Class类对象;修饰类时,作用的是这个类的所有对象。若同步块内出现异常则会释放锁
  • ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义且它还具有可扩展性。是synchronized关键字的替代品
  • synchronized会自动释放锁,而Lock需要手工释放锁,并且必须在finally从句中释放(lock.unlock)。Lock具有更高的性能、强大的功能、灵活性。
  • Lock的优点:公平锁、能被中断地获取锁、尝试非阻塞的获取锁、等待唤醒机制的多个条件变量、可以在不同的范围,以不同的顺序获取和释放锁、超时获取锁
  • synchronized使用monitorenter和monitorexit指令实现的,JVM根据常量池中ACC_SYNCHRONIZED标示符来实现方法的同步:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
  • synchronized用的锁是存在Java对象头里,对象头用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、锁、偏向线程 ID、偏向时间戳等等。

5.常用方法

  • 中断线程
    • Interrupted是一个静态方法,它检测当前的线程是否被中断,会产生副作用,它将当前线程的中断状态重置为false,清除该线程的中断状态。
    • isInterrupt是一个实例方法,检验是否有线程被中断,但不会改变中断状态。
    • Interrupt()向线程发送中断请求,线程的中断状态将被设置为true,如果目前该线程被一个sleep调用阻塞,那么,InterruptedException异常被抛出
  • 线程的优先级
    • 可以调用 Thread 类的方法 getPriority() 和 setPriority()来存取线程的优先级,
    • 最高优先级为10,最低为1,默认是5
  • 其它
    • join():等待该线程终止
    • yield():暂停当前正在执行的线程,而去执行其他线程
    • setDamen(true) :守护线程,在最后运行,如GC线程,需要在调用start()方法前调用该方法
    • holdsLock():检测一个线程是否拥有锁

6.ThreadLocal的原理

  • ThreadLocal提供了线程局部 (thread-local) 变量。而访问变量的每个线程都有自己的局部变量,它独立于变量的初始化副本(线程本地变量)
  • ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,单大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
  • 用于解决多线程中数据因并发产生不一致问题。
  • ThreadLocal与线程同步无关,ThreadLocal对象建议使用static修饰
  • 只要该线程对象被gc回收,就不会出现内存泄露,但在ThreadLocal设为null和线程结束这段时间不会被回收的,就发生了内存泄露。
  • 在ThreadLocal类中有一个静态内部类ThreadLocalMap(线程的局部变量空间),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本,每个线程可能存在多个ThreadLocal。

7.常用的锁

  • 内置锁Synchronized和显示锁ReentrantLock,Condition依赖于Lock,通过lock.newCondition()获取条件。

  • CountDownLatch 闭锁,基于AQS,一个线程(或者多个)等待另外N个线程完成某个事情之后才能执行,当一个线程完成任务后,调用countDown方法,计数器值减1。当计数器为0时,表示所有的线程已经完成任务,等待的主线程被唤醒继续执行。可用于死锁检测

  • CyclicBarrier回环栅栏,基于ReentrantLock,N个线程相互等待(await方法),任何一个线程完成之前,所有的线程都必须等待

  • Semaphore是基于计数的信号量。它可以设定一个阈值,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞,适用流量控制

  • Exchanger可以在两个线程之间交换数据,只能是2个线程,不支持更多的线程之间互换数据,如线程A调用Exchange对象的exchange()方法后,他会陷入阻塞状态,直到线程B也调用了exchange()方法,然后以线程安全的方式交换数据,之后线程A和B继续运行

  • ReadWriteLock 维护了一个读锁(共享锁)和一个写锁(排他锁)。提升并发程序性能的锁分离技术,只要没有writer,读取锁可以由多个reader 线程同时保持。写入锁是独占的。适用于一写多读的情况。它最多支持65535个写读锁。读读之间不阻塞,其它情况阻塞。

  • 使用Iterator迭代容器或使用使用for-each遍历容器,在迭代过程中修改容器会抛出ConcurrentModificationException异常。用CopyOnWriteArrayList在遍历操作为主的情况下来代替同步的List

  • 独占锁:会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁,如Synchronized

  • 可重入锁:自己可以再次获取自己内部的锁,如Synchronized、lock
  • 乐观锁:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS
  • 公平锁:线程获取锁的顺序按照线程加锁的顺序来分配,即FIFO
  • 非公平锁:一种获取锁的抢占机制,是随机获得锁的,读写锁(默认)
  • 偏向锁:如果一个线程获得了锁,那么锁就进入偏向模式,对象头的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,就获取锁
  • 轻量级锁:认为大部分锁,在整个同步周期内都不存在竞争,加锁解锁操作是需要依赖多次CAS原子指令的
  • 自旋锁:让该线程等待一段时间,不会被立即挂起

  • 锁优化:减少锁的持有时间(方法锁改为对象锁)、减小锁粒度(分段锁)、锁分离(读写锁)、锁粗化(使用完公共资源后,立即释放锁)、锁清除、无锁(CAS)

8.死锁的产生

  • 指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象。例线程1先锁住a对象后睡3秒,然后再锁住b对象,而线程2先锁住b对象,然后睡1秒再锁住a对象,开启两个线程后它们相互等待就发生了死锁。
  • 发生死锁的四个必要条件
    • 互斥条件:一个资源每次只能被一个进程使用
    • 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放
    • 不可剥夺:进程已获得的资源,在末使用完之前,不能强行剥夺
    • 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系
  • 解决死锁的方法
    • 预防死锁:破坏产生死锁的四个必要条件中的一个或者几个
    • 避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态
    • 检测死锁:允许系统在运行过程中发生死锁。但可及时地检测出死锁的发生
    • 解除死锁:当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来
    • 银行家算法

9.生产者消费者问题的五种实现

生产者消费者模型:两个线程间共享数据

  • wait() / notify()方法
  • await() / signal()方法
  • BlockingQueue阻塞队列:put、get方法
  • Semaphore方法实现同步:acquire、release方法
  • PipedInputStream / PipedOutputStream:只能用于多线程模式,用于单线程下可能会引发死锁。

10.CAS算法

  • CAS,即比较并交换,是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。通过unsafe类的compareAndSwap方法实现的,参数是要修改对象,对象的偏移量,修改前的值,预期值。在 Intel 处理器中,比较并交换通过指令cmpxchg实现。比较是否和给定的数值一致,如果一致则修改,不一致则不修改。

  • CAS 是一种无锁的非阻塞算法的实现,当且仅当内存值V 的值等于预期值A 时,CAS 通过原子方式用更新值B 来更新V 的值,否则不会执行任何操作

  • CAS的缺点

    • ABA问题,即在a++之间,a可能被多个线程修改过了,只不过回到了最初的值,这时CAS会认为a的值没有变。解决方法:增加修改计数或引入版本号,AtomicStampedReference:预期引用和预期标志都与当前相等,则更新。
    • 循环时间长开销大,只能保证一个共享变量的原子操作
  • 原子类都是通过CAS实现的,如AtomicInteger,常用方法getAndAdd、getAndIncrement、addAndGet()等

11.对volatile的理解?

  • volatile 禁止指令重排序并保证可见性,并不保证原子性,synchronized可以保证原子性,也可以保证可见性,volatile是synchronized的轻量级实现,性能较好。

  • 指令重排序:初始化一个对象分为3步,分配内存空间、初始化对象、将内存空间的地址赋值给对应的引用。由于JVM可以对指令进行重排序,顺序不能程序执行顺序

  • volatile修饰后,CPU会加上Lock前缀指令,处理器缓存会写到主存,线程的本地内存失效,别的线程只能从主存中读取数据。而本地内存的值会立马刷新到主存中去。

  • volatile并不能保证原子性,如count++ 。而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。JDK8中的LongAdder对象比AtomicLong性能更好(减少乐观锁的重试次数)。

  • volatile 还能提供内存屏障,在写一个 volatile 域时,能保证任何线程都能看到你写的值,同时,在写之前,也能保证任何数值的更新对所有线程是可见的,因为内存屏障会将其他所有写的值更新到缓存。

  • volatile 还能提供原子性,如读 64 位long 和 double 是非原子操作,但 volatile 类型的 double 和 long 就是原子的。因为对这两种类型的写是分为两部分。

  • volatile 提供 happen-before原则,确保一个线程的修改能对其他线程是可见的。

    • 程序顺序规则:一个线程中的每个操作,先行发生于该线程中的任意后序操作。
    • 监视器锁规则:对一个锁的解锁,先行发生于随后对这个锁的加锁
    • volatile变量规则:对一个volatile域的写,先行发生于任意后续对这个volatile域的读。
    • 传递性:如果A先行发生于B,而B又先行发生于C,那么A先行发生于C
    • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
    • 线程终止规则 :如果线程A执行B.join()方法并成功返回,那线程B中的任意操作先行发生于线程A
    • 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。

12.阻塞队列

  • BlockingQueue提供了线程安全的队列访问方式,阻塞队列实现的原理:使用通知模式实现。即当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空

  • AQS是构建锁的基础框架,通过内置的FIFO队列来完成资源获取线程的排队工作,其中内部状态state,头节点和尾节点,都是通过volatile修饰,保证了多线程之间的可见。子类重写tryAcquire和tryRelease方法,通过CAS指令修改状态变量state,修改成功的线程表示获取到该锁,没有修改成功,或者发现状态state已经是加锁状态,则通过一个Waiter对象封装线程,添加到等待队列中,并挂起等待被唤醒。

  • 一共有7种阻塞队列

    • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列,底层使用了Condition来实现
    • LinkedBlockingQueue :一个由链表结构组成的无界阻塞队列,默认容量Integer.MAX_VALUE
    • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列,当调用take方法时才去排序
    • SynchronousQueue:一个不存储元素的阻塞队列,每个put操作必须等待一个take操作,否则不能继续添加元素
    • DelayQueue:一个使用优先级队列实现的无界阻塞队列,队列中的元素必须实现Delay接口,应用场景:缓存系统、定时任务调度
    • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列
    • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列

13.线程池的工作原理?

  • 为什么使用线程池:线程是稀缺资源,合理的使用线程池对线程进行统一分配、调优和监控;多线程可以解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力

  • 多线程应用场景:需要大量的线程来完成任务,且完成任务的时间比较短,对性能要求苛刻的应用,接受突发性的大量请求

  • 使用线程池的优点:降低资源消耗,提高响应速度,提高线程的可管理性。

  • 线程池的处理流程
    提交任务先给核心线程池,若核心线程池未满,创建任务,否则放入等待队列,若等待队列未满就将任务存储在等待队列中,否则放入线程池中,若线程池未满创建任务,否则执行拒绝策略。

  • ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,BlockingQueue, ThreadFactory,RejectedExecutionHandler),其中阻塞队列有前四种。拒绝策略:抛异常(默认)、直接运行该任务、丢弃队列最前端任务执行当前任务,丢弃该任务。

  • 提交任务的方法,execute()方法向线程池提交任务,返回类型是void,它定义在Executor接口中, 而submit()方法也向线程池提交任务,但返回Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,

  • 关闭线程池:shutdownNow、shutdown,原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程。

  • ScheduledThreadPoolExecutor支持周期性任务的调度,执行任务的步骤:线程1从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())。然后执行该任务,再修改该任务的time变量为下次将要被执行的时间。最后修改time后的任务放回DelayQueue中(add方法)。

  • FutureTask实现Future、Runnable接口,可以交给Executor执行,也可以由调用线程直接执行,FutureTask可用于异步获取执行结果或取消执行任务的场景,适合用于耗时的计算、执行多任务计算,FutureTask在高并发环境下能确保任务只执行一次,

  • Executors工厂类提供四个方法,

    • newFixedThreadPool方法 :创建固定大小的线程池,核心线程数等于最大线程数,使用LinkedBlockingQuene,预热后,线程池中的线程数达到corePoolSize,新任务将在无界队列中等待,不会拒绝任务所以最大线程数是一个无效参数。
    • newCachedThreadPool() 方法 :大小无界的线程池,适用于执行很多的短期异步任务的小程序,使用不存储元素的SynchronousQueue作为阻塞队列,默认缓存60s,最大线程数是最大整型值,
    • newSingleThreadExecutor方法:初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,使用LinkedBlockingQueue作为阻塞队列。
    • newScheduledThreadPool方法:创建固定大小的线程,可以延迟或定时的执行任务,当调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或scheduleWith- FixedDelay()方法时,向DelayQueue添加一个ScheduledFutureTask,线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。
  • 但在使用线程池一般不允许使用Executors创建,而是通过ThreadPoolExecutor的方式,因为这样可以更加明确线程池的运行规则,规避资源耗尽的风险,因为

    • FixedThreadPool和SingleThreadPool允许请求队列的长度为Integer.MAX.VALUE,可能会堆积大量请求,从而导致OOM
    • CachedThreadPool和ScheduledThreadPool允许创建线程的数量为Integer.MAX.VALUE,可能会创建大量的线程,从而导致OOM

14.Fork/Join框架是什么?

  • Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架

  • 采用工作窃取模式:某个线程从其他队列里窃取任务来执行,通常会使用双端队列,需要继承RecursiveAction类,重写compute方法,首先需要判断任务如果足够小就执行任务。否则就必须分割 成两个子任务,每个子任务在调用fork方法时,又会进入到compute方法。使用join方法会等待子任务执行完并得到其结果。



本人才疏学浅,若有错,请指出,谢谢!
如果你有更好的建议,可以留言我们一起讨论,共同进步!
衷心的感谢您能耐心的读完本篇博文!

原创粉丝点击