我是靠谱客的博主 犹豫流沙,最近开发中收集的这篇文章主要介绍多线程 面试题,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

1、什么是进程?

进程是系统中正在运行的一个程序,程序一旦运行就是进程。

2、什么是线程?

是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

3、实现线程的方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口通过FutureTask包装器来创建Thread线程

4、Thread中的start()方法和run()方法的区别

  • start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。然后通过此Thread类调用方法run()来完成其运行操作的,这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程终止。然后CPU再调度其它线程。
  • run()方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码;程序中只有主线程——这一个线程,其程序执行路径还是只有一条,这样就没有达到写线程的目的。

5、线程的状态

  • new 状态(新建状态)

    new创建一个Thread对象时,并没处于执行状态,因为没有调用start方法启动该线程,那么此时的状态就是新建状态。

  • runnable状态(可执行状态 / 就绪状态)

    线程对象通过start方法进入runnable状态,启动的线程不一定会立即得到执行,线程的运行与否要看cpu的调度,我们把这个中间状态叫可执行状态(RUNNABLE)。

  • RUNNING状态(运行状态)

    一旦cpu通过轮询货其他方式从任务可以执行队列中选中了线程,此时它才能真正的执行自己的逻辑代码。

  • blocked状态(阻塞状态)

    线程正在等待获取锁。
    进入blocked状态,比如调用了sleep,或者wait方法。
    进行某个阻塞的io操作,比如因网络数据的读写进入blocked状态。
    获取某个锁资源,从而加入到该锁的阻塞队列中而进入blocked状态。

  • terminated状态(终止状态)

    TERMINATED是一个线程的最终状态,在该状态下线程不会再切换到其他任何状态了,代表整个生命周期都结束了。

    下面几种情况会进入TERMINATED状态:
    线程运行正常结束,结束生命周期
    线程运行出错意外结束
    JVM Crash 导致所有的线程都结束

在这里插入图片描述

6、sleep()方法

方法sleep()的作用是在指定的毫秒数内让当前的“正在执行的线程"休眠(暂停执行)。

7、停止线程

  • run方法执行完成,自然终止。
  • stop()方法,suspend()以及resume()都是过期作废方法,使用它们结果不可预期。
  • 大多数停止一个线程的操作使用Thread.interrupt()等于说给线程打一个停止的标记,此方法不会去终止一个正在运行的线程,需要加入一个判断才能可以完成线程的停止。

8、interrupted()和isInterrupted 的区别

  • interrupted:判断当前线程是否已经中断,会清除状态。
  • isInterrupted:判断线程是否已经中断,不会清除状态。

9、yield()

放弃当前cpu资源,将它让给其他的任务占用cpu执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得cpu时间片。

10、线程优先级(setPriority)

在操作系统中,线程可以划分优先级,优先级较高的线程得到cpu资源比较多,也就是cpu有限执行优先级较高的线程对象中的任务,但是不能保证一定优先级高,就先执行。

Java的优先级分为1~10个等级,数字越大优先级越高,默认优先级大小为5。超出范围则抛出:java.lang.lllegalArgumentException。

11、线程种类

Java线程有两种,一种是用户线程,一种是守护线程。

12、守护线程

守护线程是一个比较特殊的线程,主要被用做程序中后台调度以及支持性工作。当Java虚拟机中不存在非守护线程时,守护线程才会随着JVM一同结束工作。

12.1、Java中的守护线程

GC(垃圾回收器)

12.2、设置守护线程

Thread.setDaemon(true)

PS:Daemon属性需要在启动线程之前设置,不能再启动后设置。

13、Java虚拟机退出时Daemon线程中的finally块一定会执行?

Java虚拟机退出时Daemon线程中的finally块并不一定会执行。

14、设置线程上下文类加载器

获取线程上下文类加载器

public ClassLoader getContextClassLoader()

设置线程类加载器(可以打破Java类加载器的父类委托机制)

public void setContextClassLoader(ClassLoader cl)

15、join

join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。

15.1、为什么要用 join()方法?

很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法 。

16、synchronized

synchronized关键字可以时间一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象是对多个线程可见的,那么对该对象的所有读写都将通过同步的方式来进行。

16.1、synchronized包括哪两个jvm重要的指令?

  • monitor enter
  • monitor exit

16.2、synchronized关键字的用法

可以用于修饰代码块或方法

16.3、synchronized锁的是什么

普通同步方法—————>锁的是当前实例对象。
静态同步方法—————>锁的是当前类的Class对象。
同步代码块—————>锁的是synchonized括号里配置的对象。

17、Java对象头

synchronized用的锁是存在Java对象头里的。对象如果是数组类型,虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,用2个字宽存储对象头。

Tips:32位虚拟机中一个字宽等于4字节。

18、锁的升降级

Java SE1.6为了提高锁的性能。引入了“偏向锁"和轻量级锁”。

Java SE1.6中锁有4种状态。级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。

锁只能升级不能降级。

19、偏向锁

大多数情况,锁不仅不存在多线程竞争,而且总由同一线程多次获得。当一个线程访问同步块并获取锁时,会在对象头和栈帧中记录存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行cas操作来加锁和解锁,只需测试一下对象头Mark Word里是否存储着指向当前线程的偏向锁。

如果测试成功,表示线程已经获得了锁,如果失败,则需要测试下Mark Word中偏向锁的标示是否已经设置成1(表示当前时偏向锁)如果没有设置,则使用cas竞争锁,如果设置了,则尝试使用cas将对象头的偏向锁指向当前线程。

19.1、关闭偏向锁延迟

java6和7中默认启用,但是会在程序启动几秒后才激活,如果需要关闭延迟,则

-XX:BiasedLockingStartupDelay=O

19.2、关闭偏向锁

JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

Tips:如果你可以确定程序的所有锁通常情况处于竞态,则可以选择关闭。

20、轻量级锁

线程在执行同步块,jvm会现在当前线程的栈帧中创建用于储存锁记录的空间。并将对象头中的MarkWord复制到锁记录中。然后线程尝试使用cas将对象头中的Mark Word替换为之乡锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

20.1、轻量锁的解锁

轻量锁解锁时,会使原子操作cas将displaced Mark Word替换回对象头,如果成功则表示没有竞争发生,如果失败,表示存在竞争,此时锁就会膨胀为重量级锁。

21、重量级锁(Mutex Lock)

Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。

而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized 效率低的原因。

因此, 这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁” 。 JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。

JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

22、锁的对比

在这里插入图片描述

23、什么是原子操作

不可被中断的一个或一系列操作

24、Java如何实现原子操作

Java中通过锁和循环cas的方式来实现原子操作,JVM的CAS操作利用了处理器提供的CMPXCHG指令来实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。

25、CAS实现原子操作的3大问题

ABA问题,循环时间长消耗资源大,只能保证一个共享变量的原子操作

25.1、什么是ABA问题

问题:
因为cas需要在操作值的时候,检查值有没有变化,如果没有变化则更新,如果一个值原来是A,变成了B,又变成了A,那么使用cas进行检测时会发现发的值没有发生变化,其实是变过的。

解决:
添加版本号,每次更新的时候追加版本号,A-B-A—>1A-2B-3A。

从jdk1.5开始,Atomic包提供了一个类AtomicStampedReference来解决ABA的问题。

25.2、CAS循环时间长占用资源大问题

如果jvm能支持处理器提供的pause指令,那么效率会有一定的提升。

  • 它可以延迟流水线执行指令(de-pipeline),使cpu不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,有些处理器延迟时间是0。
  • 它可以避免在退出循环的时候因内存顺序冲突而引起的cpu流水线被清空,从而提高cpu执行效率。

25.3、CAS只能保证一个共享变量原子操作

  • 对多个共享变量操作时,可以用锁。
  • 可以把多个共享变量合并成一个共享变量来操作。比如,x=1,k=a,合并xk=1a,然后用cas操作xk。

Tips:java 1.5开始,jdk提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象来进行cas操作。

Java语言规范第3版对volatile定义如下:Java允许线程访问共享变量,为了保证共享变量能准确和一致的更新,线程应该确保排它锁单独获得这个变量。如果一个字段被声明为volatile,Java线程内存模型所有线程看到这个变量的值是一致的。

26、等待/通知机制

一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作。

27、wait()

方法wait()的作用是使当前执行代码的线程进行等待,wait()是Object类通用的方法,该方法用来将当前线程置入“预执行队列"中,并在wait()所在的代码处停止执行,直到接到通知或中断为止。

在调用wait之前线程需要获得该对象的对象级别的锁。代码体现上,即只能是同步方法或同步代码块内。调用wait()后当前线程释放锁。

28、notify/notify All

notify等于说将等待队列中的一个线程移动到同步队列中,而notifyAll是将等待队列中的所有线程全部移动到同步队列中。

29、ThreadLocal

主要解决每一个线程想绑定自己的值,存放线程的私有数据。

30、lock接口

锁可以防止多个线程同时共享资源。Java5前程序是靠synchronized实现锁功能。Java5之后,并发包新增Lock接口来实现锁功能。

31、Lock接口提供synchronized不具备的主要特性

在这里插入图片描述

32、重入锁ReentrantLock

支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

默认非公平锁。

33、重进入是什么意思?

重进入是指任意线程在获取到锁之后能够再次获锁而不被锁阻塞。

该特性主要解决以下两个问题:

  1. 锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是则再次成功获取。
  2. 所得最终释放。线程重复n次是获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。

34、公平锁和非公平锁的区别

公平性与否针对获取锁来说的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。非公平就是不按照先进先出的规则,而是刚刚获得锁的线程和等待了很久的线程都去竞争锁。

35、读写锁

读写锁允许同一时刻多个读线程访问,但是写线程和其他写线程均被阻塞。

读写锁维护一个读锁一个写锁,读写分离,并发性得到了提升。

Java中提供读写锁的实现类是ReentrantReadWriteLock。

36、Condition接口

提供了类似Object监视器方法,与Lock配合使用实现等待/通知模式。

37、ArrayBlockingQueue

一个由数据支持的有界阻塞队列,此队列FIFO原则对元素进行排序。队列头部在队列中存在的时间最长,队列尾部存在时间最短。

38、PriorityBlockingQueue(compareTo 排序实现优先)

一个支持优先级排序的无界阻塞队列,但它不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。

39、DelayQueue(缓存失效、定时任务 )

是一个支持延时获取元素的使用优先级队列的实现的无界阻塞队列。队列中的元素必须实现Delayed接口和Comparable接口,在创建元素时可以指定多久才能从队列中获取当前元素。

40、Java并发容器,你知道几个?

  • ConcurrentHashMap
  • ConcurrentLinkedQueue
  • DelayQueue

41、ConcurrentHashMap

并发安全版HashMap;java7中采用分段锁技术来提高并发效率,默认分16段。Java8放弃了分段锁,采用CAS,同时当哈希冲突时,当链表的长度到8时,会转化成红黑树。

42、ConcurrentLinkedQueue

基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用cas算法来实现。

43、什么是阻塞队列?

阻塞队列是一个支持两个附加操作的队列,这两个附加操作支持阻塞的插入和移除方法。

  1. 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满。
  2. 支持阻塞的移除方法:当队列空时,获取元素的线程会等待队列变为非空。

44、阻塞队列常用的应用场景?

常用于生产者和消费者场景,生产者是往队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列正好是生产者存放、消费者来获取的容器。

45、工作窃取算法

是指某个线程从其他队列里窃取任务来执行。当大任务被分割成小任务时,有的线程可能提前完成任务,此时闲着不如去帮其他没完成工作线程。此时可以去其他队列窃取任务,为了减少竞争,通常使用双端队列,被窃取的线程从头部拿,窃取的线程从尾部拿任务执行。

  • 优点:

    充分利用线程进行并行计算,减少了线程间的竞争。

  • 缺点:

    有些情况下还是存在竞争,比如双端队列中只有一个任务。这样就消耗了更多资源。

46、JDK并发包中提供了哪几个比较常见的处理并发的工具类?

提供并发控制手段: CountDownLatch、CyclicBarrier、Semaphore

线程间数据交换: Exchanger

47、CountDownLatch与CyclicBarrier区别

  • CountDownLatch:

    计数器:计数器只能使用一次。

    等待:一个线程或多个等待另外n个线程完成之后才能执行。

  • CyclicBarrier:

    计数器:计数器可以重置(通过reset()方法)。

    等待: n个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

48、Semaphore

用来控制同时访问资源的线程数量,通过协调各个线程,来保证合理的公共资源的访问。

应用场景:流量控制,特别是公共资源有限的应用场景,比如数据链接,限流等。

49、Exchanger

Exchanger是一个用于线程间协作的工具类,它提供一个同步点,在这个同步点上,两个线程可以交换彼此的数据。比如第一个线程执行exchange()方法,它会一直等待第二个线程也执行exchange,当两个线程都到同步点,就可以交换数据了。

一般来说为了避免一直等待的情况,可以使用exchange(V x,long timeout,IlimeUnit unit),设置最大等待时间。

Exchanger可以用于遗传算法。

50、为什么使用线程池

几乎所有需要异步或者并发执行任务的程序都可以使用线程池。合理使用会给我们带来以下好处。

  • 降低系统消耗:重复利用已经创建的线程降低线程创建和销毁造成的资源消耗。
  • 提高响应速度:当任务到达时,任务不需要等到线程创建就可以立即执行。
  • 提供线程可以管理性:可以通过设置合理分配、调优、监控。

51、线程池工作流程

  1. 判断核心线程池里的线程是否都有在执行任务,否->创建一个新工作线程来执行任务。是->走下个流程。
  2. 判断工作队列是否已满,否->新任务存储在这个工作队列里,是->走下个流程。
  3. 判断线程池里的线程是否都在工作状态,否->创建一个新的工作线程来执行任务, 是->走下个流程。
  4. 按照设置的策略来处理无法执行的任务。

52、创建线程池参数有哪些,作用?

public ThreadPoolExecutor( int corePoolSize,
							int maximumPoolSize,
							long keepAliveTime,
							TimeUnit unit,
							BlockingQueue<Runnable> workQueue,
							ThreadFactory threadFactory,
							RejectedExecutionHandler handler)
  1. corePoolSize:核心线程池大小

    当提交一个任务时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建,等待需要执行的任务数大于线程核心大小就不会继续创建。

  2. maximumPoolSize:线程池最大数

    允许创建的最大线程数,如果队列满了,并且已经创建的线程数小于最大线程数,则会创建新的线程执行任务。如果是无界队列,这个参数基本没用。

  3. keepAlivelime:线程保持活动时间

    线程池工作线程空闲后,保持存活的时间,所以如果任务很多,并且每个任务执行时间较短,可以调大时间,提高线程利用率。

  4. unit:线程保持活动时间单位

    天(DAYS)、小时(HOURS)、分钟(MINUTES、毫秒MILLISECONDS)、微秒(MICROSECONDS)、纳秒(NANOSECONDS)

  5. workQueue:任务队列,保存等待执行的任务的阻塞队列。

    一般来说可以选择如下阻塞队列:
    ArrayBlockingQueue:基于数组的有界阻塞队列。
    LinkedBlockingQueue:基于链表的阻塞队列。
    SynchronizedQueue:一个不存储元素的阻塞队列。
    PriorityBlockingQueue:一个具有优先级的阻塞队列。

  6. threadFactory:设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。

  7. handler:饱和策略也叫拒绝策略。当队列和线程池都满了,即达到饱和状态。所以需要采取策略来处理新的任务。默认策略是AbortPolicy。

53、向线程池提交任务

可以使用execute()和submit()两种方式提交任务。

  • execute():

    无返回值,所以无法判断任务是否被执行成功。

  • submit():

    用于提交需要有返回值的任务。线程池返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()来获取返回值,get()方法会阻塞当前线程知道任务完成。get(long timeout,TimeUnit unit)可以设置超市时间。

54、关闭线程池

可以通过shutdown()或shutdownNow()来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt来中断线程,所以无法响应终端的任务可以能永远无法停止。

shutdownNow首先将线程池状态设置成STOP;然后尝试停止所有的正在执行或者暂停的线程,并返回等待执行任务的列表。

shutdown只是将线程池的状态设置成shutdown状态,然后中断所有没有正在执行任务的线程。

只要调用两者之一,isShutdown就会返回true,当所有任务都已关闭,isTerminaed就会返回true。

一般来说调用shutdown方法来关闭线程池,如果任务不一定要执行完,可以直接调用shutdownNow方法。

55、线程池的组成

一般的线程池主要分为以下 4 个组成部分:

  1. 线程池管理器:用于创建并管理线程池
  2. 工作线程:线程池中的线程
  3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
  4. 任务队列:用于存放待处理的任务,提供一种缓冲机制

56、Java中synchronized 和 ReentrantLock 有什么不同?

相似点:

这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的。

区别:

这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。

Synchronized经过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止 。

由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以
下3项:

  1. 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于 Synchronized来说可以避免出现死锁的情况。
  2. 公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
  3. 锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象 。

57、什么是乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

java 中的乐观锁基本都是通过 CAS 操作实现的, CAS 是一种更新的原子操作, 比较当前值跟传入值是否一样,一样则更新,否则失败。

58、什么是悲观锁

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。

59、什么是自旋锁

自旋锁原理非常简单, 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。

如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

60、Condition 类和 Object 类锁方法区别区别

  1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效
  2. Condition 类的 signal 方法和 Object 类的 notify 方法等效
  3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
  4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

61、Semaphore 信号量

Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。 Semaphore 可以用来构建一些对象池,资源池之类的, 比如数据库连接池

实现互斥锁(计数器为 1)
我们也可以创建计数为 1 的 Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。

62、volatile关键字

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。

63、分段锁

分段锁也并非一种实际的锁,而是一种思想 ConcurrentHashMap 是学习分段锁的最好实践。

64、锁优化

  • 减少锁持有时间

    只用在有线程安全要求的程序上加锁

  • 减小锁粒度

    将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。
    降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。

  • 锁分离

    最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能,具体也请查看[高并发 Java 五]JDK 并发包 1。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如LinkedBlockingQueue 从头部取出,从尾部放数据。

  • 锁粗化

    通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度, 如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。

  • 锁消除

    锁消除是在编译器级别的事情。 在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。

65、上下文

是指某一时间点 CPU 寄存器和程序计数器的内容。

66、死锁

死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

产生死锁的必要条件:

  1. 互斥条件:所谓互斥就是进程在某一时间内独占资源。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

67、活锁

任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,
失败,尝试,失败。

活锁和死锁的区别在于:

  • 处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待。
  • 活锁有可能自行解开,死锁则不能。

68、饥饿

一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执
行的状态。

Java 中导致饥饿的原因:

  1. 高优先级线程吞噬所有的低优先级线程的 CPU 时间。
  2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前 持续地对该同步块进行访问。
  3. 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。

69、sleep()和wait() 有什么区别?

  1. 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。
  2. sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态 。
  3. 在调用 sleep()方法的过程中, 线程不会释放对象锁。
  4. 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

70、拒绝策略

线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:

  • AbortPolicy:直接抛出异常。
  • CallerRunsPolicy:调用者所在的线程来运行任务。
  • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
  • DiscardPolicy:不处理,直接丢掉。

当然可以根据自己的应用场景,实现RejectedExecutionHandler接口自定义策略。

71、什么是线程组,为什么在 Java 中不推荐使用?

ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。
为什么不推荐使用?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使用线程池。

线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。

结束!


											结束也是开始

最后

以上就是犹豫流沙为你收集整理的多线程 面试题的全部内容,希望文章能够帮你解决多线程 面试题所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(64)

评论列表共有 0 条评论

立即
投稿
返回
顶部