抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

并发面试题总结

谈面试

  1. 面试主要分为两块:

    • 一块是考查工程师对基础知识(包括了技术广度、深度、对技术的热情度等)的掌握程度,因为基础知识决定了一个技术人员发展的上限
    • 另一块是考察工程师的工程能力,比如:做过哪些项目?遇到最难的问题怎样解决的?说说最有成就感的一项任务?工程能力是考察工程师当下能为公司带来的利益。当然还有其它考核方面:抗压性、合作能力。
  2. Java只是一门语言,即使是Java工程师也不能局限于Java,要从面向对象语言本身,甚至从整个计算机体系,从工程实际出发看Java。

  3. 很多知识在一般公司的开发中是用不到的,常有人戏称:“面试造火箭,工作拧螺丝”,但这只是通常情况下公司对程序员的标准——迅速产出,完成任务。所以,工程师为了自己职业的发展不能局限于公司对自己的要求,不能停留在应用层面,要能够很好地掌握基础知识,要多看源码,自己多实践,学成记得产出,比如多为开源社区贡献代码,帮助初学者指路等。

其实“面试造火箭,工作拧螺丝”的背后其实是大家都普遍认可基础知识的重要性。

常见面试题

1.在java中守护线程和用户线程的区别?

java中的线程分为两种:

  • 守护线程(Daemon)
  • 用户线程(User)。

任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on);true则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。

两者的区别:

唯一的区别是判断虚拟机(JVM)何时离开,Daemon是为其他线程提供服务,如果全部的User Thread已经结束,Daemon 没有可服务的线程,JVM关闭。

扩展:Thread Dump打印出来的线程信息,含有daemon字样的线程即为守护进程

2.线程与进程的区别

​ 进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。

​ 一个程序至少有一个进程,一个进程至少有一个线程。

3.什么是多线程中的上下文切换

多线程会共同使用一组计算机上的CPU,而线程数大于给程序分配的CPU数量时,为了让各个线程都有执行的机会,就需要轮转使用CPU。不同的线程切换使用CPU发生的切换数据等就是上下文切换。

4.死锁与活锁的区别,死锁与饥饿的区别?

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

产生死锁的必要条件:

  • 互斥条件:所谓互斥就是进程在某一时间内独占资源。

  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  • 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。

  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

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

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

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

5.synchronized底层实现原理

​ synchronized (this)原理:涉及两条指令:monitorentermonitorexit;再说同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。

​ JVM就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

注意,这个问题可能会接着追问,java对象头信息,偏向锁,轻量锁,重量级锁及其他们相互间转化。

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

ThreadGroup类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。

  • 线程组ThreadGroup对象中比较有用的方法是stop、resume、suspend等方法,由于这几个方法会导致线程的安全问题(主要是死锁问题),已经被官方废弃掉了,所以线程组本身的应用价值就大打折扣了。

  • 线程组ThreadGroup不是线程安全的,这在使用过程中获取的信息并不全是及时有效的,这就降低了它的统计使用价值。

7.什么是Executors框架?为什么使用Executor框架?

  • Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。

  • 每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。

  • 调用 new Thread()创建的线程缺乏管理,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。

  • 直接使用new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。

8.在Java中Executor和Executors的区别?

  • Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。

  • Executor 接口对象能执行我们的线程任务。

  • ExecutorService接口继承了Executor接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。

  • 使用ThreadPoolExecutor 可以创建自定义线程池。

9.什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?

原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。

处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。

在Java中可以通过锁和循环CAS的方式来实现原子操作。

CAS操作——Compare And Set,或是 Compare And Swap,现在几乎所有的CPU指令都支持CAS的原子操作。java.util.concurrent.atomic下提供了大量的原子操作类,比如原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference ,原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray ,原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

10.Java Concurrency API中的Lock接口(Lock interface)是什么?对比synchronized它有什么优势?

  • Lock接口比同步方法和同步块提供了更具扩展性的锁操作。

  • 他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

它的优势有:可以使锁更公平,可以使线程在等待锁的时候响应中断,可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间,可以在不同的范围,以不同的顺序获取和释放锁。

整体上来说Lock是synchronized的扩展版,Lock提供了无条件的、可轮询的(tryLock方法)、定时的(tryLock带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition方法)锁操作。另外Lock的实现类基本都支持非公平锁(默认)和公平锁,synchronized只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。

11.什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。

这两个附加的操作是:

  • 在队列为空时,获取元素的线程会等待队列变为非空。
  • 当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

JDK7提供了7个阻塞队列。在实现上,主要是利用了Condition和Lock的等待通知模式。

12.什么是Callable和Future?

Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。

可以认为是带有回调的Runnable。

Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。

13.什么是FutureTask?

​ 在Java并发程序中FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。

14.什么是并发容器的实现?

何为同步容器:可以简单地理解为通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。

比如Vector,Hashtable,以及Collections.synchronizedSet,synchronizedList等方法返回的容器。

并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问map,并且执行读操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程并发地修改map,所以它可以在并发环境下实现更高的吞吐量。

15.多线程同步和互斥有几种实现方法,都是什么?

线程同步:是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。

线程互斥:是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。

线程间的同步方法大体可分为两类:

用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。

用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。

16.什么是竞争条件?

​ 当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件(race condition)。

17.为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?

当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。

但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码,只会把run方法当作普通方法去执行。

18.在Java中CycliBarriar和CountdownLatch有什么区别?

CyclicBarrier可以重复使用,而CountdownLatch不能重复使用。

19.什么是不可变对象,它对写并发应用有什么帮助?

不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。

不可变对象的类即为不可变类(Immutable Class)。Java平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和BigDecimal等。

不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然它们的状态无法修改,这些常量永远不会变。

不可变对象永远是线程安全的。

只有满足如下状态,一个对象才是不可变的:

  • 它的状态不能在创建后再被修改;

  • 所有域都是final类型;并且, 它被正确创建

20.notify()和notifyAll()有什么区别?

当一个线程进入wait之后,就必须等其他线程notify/notifyall,使用notifyall,可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。

如果没把握,建议notifyAll,防止notigy因为信号丢失而造成程序异常。

21.什么是可重入锁(ReentrantLock)?谈谈它的实现。

​ 线程可以重复进入任何一个它已经拥有的锁所同步着的代码块,synchronized、ReentrantLock都是可重入的锁。在实现上,就是线程每次获取锁时判定如果获得锁的线程是它自己时,简单将计数器累积即可,每 释放一次锁,进行计数器累减,直到计算器归零,表示线程已经彻底释放锁。

22.当一个线程进入某个对象的一个synchronized的实例方法后,其它线程是否可进入此对象的其它方法?

如果其他方法没有synchronized的话,其他线程是可以进入的。

所以要开放一个线程安全的对象时,得保证每个方法都是线程安全的。

23.乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java里面的同步原语synchronized关键字的实现是悲观锁。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。在Java中j原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁的实现方式:

  • 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。

  • java中的Compare and Swap即CAS ,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

24.什么是CAS操作,缺点是什么?

CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。

CAS缺点:

  • ABA问题:

    ​ 比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。

  • 循环时间长开销大:

    ​ 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

  • 只能保证一个共享变量的原子操作:

    ​ 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

25.SynchronizedMap和ConcurrentHashMap有什么区别?

  • SynchronizedMap一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为map。

  • ConcurrentHashMap使用分段锁来保证在多线程下的性能。

26.写时复制容器可以用于什么应用场景?

CopyOnWrite并发容器用于对于绝大部分访问都是读,且只是偶尔写的并发场景。比如白名单,黑名单,商品类目的访问和更新场景。

透露的思想:

  • 读写分离,读和写分开

  • 最终一致性

  • 使用另外开辟空间的思路,来解决并发冲突

27.volatile有什么用?能否用一句话说明下volatile的应用场景?

  • volatile保证内存可见性和禁止指令重排。

  • volatile用于多线程环境下的一写多读,或者无关联的多写。

28.为什么代码会重排序?

​ 在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:

  • 在单线程环境下不能改变程序运行的结果;

  • 存在数据依赖关系的不允许重排序

29.在java中wait和sleep方法的不同?

​ 最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。

30.一个线程运行时发生异常会怎样?

​ 如果异常没有被捕获该线程将会停止执行。hread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。

31.为什么wait, notify 和 notifyAll这些方法不在thread类里面?

​ JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

32.什么是ThreadLocal变量?

​ ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。

33.Java中interrupted 和 isInterrupted方法的区别?

interrupted() 和 **isInterrupted()**的主要区别是前者会将中断状态清除而后者不会。

​ Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。

34.为什么wait和notify方法要在同步块中调用?

​ 主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。

35.为什么你应该在循环中检查等待条件?

​ 处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用wait()方法效果更好的原因

36.怎么检测一个线程是否拥有锁?

​ 在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。

37.你如何在Java中获取线程堆栈?

kill -3 [java pid]

不会在当前终端输出,它会输出到代码执行的或指定的地方去。比如,kill -3 tomcat pid, 输出堆栈到log目录下。

Jstack [java pid]

这个比较简单,在当前终端显示,也可以重定向到指定文件中。

或者使用Java提供的拟机线程系统的管理接口ManagementFactory.getThreadMXBean()。

38.Java线程池中submit() 和 execute()方法有什么区别?

​ 两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中。

而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口

39.你对线程优先级的理解是什么?

​ 每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个int变量(从1-10),1代表最低优先级,10代表最高优先级。

​ java的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。

40.你如何确保main()方法所在的线程是Java 程序最后结束的线程?

​ 可以使用Thread类的join()方法(或者CountDownLatch工具类)来确保所有程序创建的线程在main()方法退出前结束。

41.为什么Thread类的sleep()和yield ()方法是静态的?

​ Thread类的sleep()和yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。

现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

​ 可以用join方法实现。

42.你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?

​ volatile关键字,读写锁,写时复制等等都可以实现。

43.用Java实现阻塞队列

​ 适用Lock或者synchronize,队列空的时候进行阻塞,有新的入队的时候唤醒阻塞。

44.用Java写代码来解决生产者——消费者问题。

​ 阻塞队列实现即可,也可以用wait和notify来解决这个问题,或者用Semaphore

45.用Java编程一个会导致死锁的程序,你将怎么解决?

参照 JAVA中的死锁一章

46.Java中如何停止一个线程?

  • 使用共享变量的方式

    ​ 在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程用来作为是否中断的信号,通知中断线程的执行。

  • 使用interrupt方法终止线程

    ​ 如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?比如当一个线程由于需要等候键盘输入而被阻塞,或者调用Thread.join()方法,或者Thread.sleep()方法,在网络中调用ServerSocket.accept()方法,或者调用了DatagramSocket.receive()方法时,都有可能导致线程阻塞,使线程处于处于不可运行状态时,即使主程序中将该线程的共享变量设置为true,但该线程此时根本无法检查循环标志,当然也就无法立即中断。所以应该尽量使用Thread提供的interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态。

47.JVM中哪个参数是用来控制线程的栈堆栈大小的

-Xss

48.如果同步块内的线程抛出异常锁会释放吗?

49.单例模式的双重检查实现是什么?为什么并不安全?如何在Java中创建线程安全的Singleton?

​ 不安全的根本原因是重排序会导致未初始化完成的对象可以被其他线程看见而导致错误。创建安全的单例模式有:延迟占位模式、在声明的时候就new这个类的实例、枚举

50.写出3条你遵循的多线程最佳实践

  • 给你的线程起个有意义的名字。 这样可以方便找bug或追踪。OrderProcessor, QuoteProcessor or TradeProcessor 这种名字比 Thread-1. Thread-2 and Thread-3 好多了,给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至JDK都遵循这个最佳实践。
  • 避免锁定和缩小同步的范围 锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。
  • 多用同步类少用wait 和 notify 首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
  • 多用并发集合少用同步集合 这是另外一个容易遵循且受益巨大的最佳实践,并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。
  • 比如并发编程的黄金原则,尽量无锁化编程等等……..

51.合理地配置线程池

  • CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。

  • IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。

  • 混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

    对于IO型的任务的最佳线程数,有个公式可以计算 Nthreads = NCPU * UCPU * (1 + W/C)

52.请概述锁的公平和非公平,JDK内部是如何实现的。

  • 公平锁:是指所有试图获得锁的线程按照获取锁的顺序依次获得锁,而非公平锁则是当前的锁状态没有被占用时,当前线程可以直接占用,而不需要等待。在实现上,非公平锁逻辑基本跟公平锁一致,唯一的区别是,当前线程不需要判断同步队列中是否有等待线程。

  • 非公平锁:性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。

​ 使用场景的话呢,其实还是和他们的属性一一相关,比如:如果业务中线程占用(处理)时间要远长于线程等待,那用非公平锁其实效率并不明显,但是用公平锁可以保证不会有线程被饿死。

53.请概述AQS

​ 是用来构建锁或者其他同步组件的基础框架,比如ReentrantLock、ReentrantReadWriteLock和CountDownLatch就是基于AQS实现的。它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。它是CLH队列锁的一种变体实现。它可以实现2种同步方式:独占式,共享式。

​ AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,同步器的设计基于模板方法模式,所以如果要实现我们自己的同步工具类就需要覆盖其中几个可重写的方法,如tryAcquire、tryReleaseShared等等。

​ 这样设计的目的是同步组件(比如锁)是面向使用者的,它定义了使用者与同步组件交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。这样就很好地隔离了使用者和实现者所需关注的领域。

​ 在内部,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点,构成一个双端双向链表。

同时与Condition相关的等待队列,节点类型也是Node,构成一个单向链表。

54.请概述volatile

volatile关键字的作用主要有两点:

​ 多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据。但是volatile不能保证操作的原子,对任意单个volatile变量的读/写具有原子性,但类似于++这种复合操作不具有原子性。。

​ 代码底层在执行时为了获取更好的性能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止重排序,当然这也一定程度上降低了代码执行效率。

​ 同时在内存语义上,当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存,当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

​ 在Java中对于volatile修饰的变量,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序问题、强制刷新和读取。

​ 在具体实现上,volatile关键字修饰的变量会存在一个“lock:”的前缀。它不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。

​ 同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效。

评论