zoukankan      html  css  js  c++  java
  • java 多线程 面试

    1、多线程有什么用?

      (1)发挥多核CPU的优势:

      当前,应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。

           (2)防止阻塞:

      从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧(比如访问数据库要花很多时间),对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

    2、创建线程的方式:

      一般就是两种:

      (1)继承Thread类

      (2)实现Runnable接口

      至于哪个好,不用说肯定是后者好,因为实现接口的方式比继承类的方式更灵活(java可以实现多个接口,但是只能继承一个类),也能减少程序之间的耦合度,面向接口编程也是设计模式6大原则的核心。

    3、start()方法和run()方法的区别

      只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。

      如果只是调用run()方法,那么代码还是同步执行的。

    4、Runnable接口和Callable接口的区别

      Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;

      Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用

    5、CyclicBarrier和CountDownLatch的区别

    6、volatile关键字的作用

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

      (1)多线程主要围绕可见性原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据

      (2)代码底层执行不像我们看到的高级语言----Java程序这么简单,它的执行是Java代码-->字节码-->根据字节码执行对应的C/C++代码-->C/C++代码被编译成汇编语言-->和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率

      从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性(原子类的实现就是CAS(Compare and Swap),和volatile的结合,可以看一下源码),详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。

    7、什么是线程安全

      如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。

      这个问题有值得一提的地方,就是线程安全也是有几个级别的:

    (1)不可变

      像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用

    (2)绝对线程安全

      不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet

    (3)相对线程安全

      相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast(快速失败)机制。

    (4)线程非安全

      这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类。

    9、一个线程如果出现了运行时异常会怎么样

      如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放

    11、sleep方法和wait方法有什么区别

      sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器(也就是锁),sleep方法不会放弃这个对象的监视器(仍然持有锁),wait方法会放弃这个对象的监视器(释放锁)

    12、生产者消费者模型的作用是什么

    (1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用

    (2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约 

    14、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

      这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁

    16、为什么要使用线程池

      避免频繁地创建和销毁线程,达到线程对象的重用(重用)。

      另外,使用线程池还可以根据项目灵活地控制并发的数目(控制并发数量)。 

    17、怎么检测一个线程是否持有对象监视器

      Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着"某条线程"指的是当前线程

    18、synchronized和ReentrantLock的区别

      synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:

      (1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁

      (2)ReentrantLock可以获取各种锁的信息

      (3)ReentrantLock可以灵活地实现多路通知

        (4)  lock更灵活,可以自由定义多把锁的枷锁解锁顺序(synchronized要按照先加的后解顺序)

             (5) 提供多种加锁方案,lock 阻塞式, trylock 无阻塞式, lockInterruptily 可打断式, 还有trylock的带超时时间版本。

             (7) 能力越大,责任越大,必须控制好加锁和解锁,否则会导致灾难。synchronized是由JVM保证释放锁的,抛异常后会自动释放锁;但是ReentrantLock要在finally中unlock, 保证异常的情况也能释放锁。

             (8) 和Condition类的结合。

     19 BlockingQueue

      很有用的一个类,实现了生产者-消费者的功能。

    20 Synchronized, ReetarntLock性能对比

      在网上看到如下说法,但是自己没试验,不知道对错,留着以后验证:

      在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetarntLock,但是在很激烈的情况下,synchronized的性能会下降几十倍??

    21 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

     一般原则:

    • CPU密集型任务

      尽量使用较小的线程池,一般为CPU核心数+1。
      因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。

    • IO密集型任务

      可以使用稍大的线程池,一般为2*CPU核心数。
      IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。

    • 混合型任务

      可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。
      只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
      因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失


      最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
      因为很显然,线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
    下面举个例子:
      比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:
    最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

    (1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

    (2)并发不高、任务执行时间长的业务要区分开看:

      a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务

      b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

    (3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

    22、Hashtable的size()方法中明明只有一条语句"return count",为什么还要做同步?

      这个问题也挺有意思: 其实也是为了保证线程安全。表面上是一条语句,实际上可能CPU有多个指令,也需要同步。

    23 Semaphore有什么作用

    java.util.concurrent下面有个Semaphore, 信号量,表示同一时间最多有多少个线程访问临界资源,这个是对Synchronized的补充,Synchronized只能保证最多一个线程访问临界资源,但是Semaphore可以自己设置个数,当Semaphore = 1的时候,就相当于Synchronized了。

    24、单例模式的线程安全性

      (1)饿汉式单例模式的写法:线程安全

      (2)懒汉式单例模式的写法:非线程安全

      (3)双检锁单例模式的写法:线程安全

    25、什么是AQS

    33、什么是乐观锁和悲观锁

    (1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑

    (2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

    34、什么是CAS

      CAS,全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。

    35、什么是自旋

      很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

      在多线程并发编程中synchronized一直是元老级角色, 很多人都会称呼它为重量级锁. 但是, 随着Java SE 1.6对synchronized进行了各种优化之后, 有些情况下它就并不那么重了. Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁, 以及锁的存储结构和升级过程.

      优化后synchronized锁的分类级别从低到高依次是

        无锁状态:
        偏向锁状态:偏向锁是针对于一个线程而言的(在单线程状态上如果每次也获得锁/释放锁,开销大), 线程获得锁之后就不会再有解锁等操作了, 这样可以省略很多开销. 假如有两个线程来竞争该锁话, 那么偏向锁就失效了, 进而升级成轻量级锁了.为什么要这样做呢? 因为经验表明, 其实大部分情况下, 都会是同一个线程进入同一块同步代码块的. 这也是为什么会有偏向锁出现的原因。
        轻量级锁状态:当出现有两个线程来竞争锁的话, 那么偏向锁就失效了, 此时锁就会膨胀, 升级为轻量级锁.

                 轻量级锁采用的是自旋的方式。
        重量级锁状态:

      锁可以升级, 但不能降级. 即: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的.

    36、Thread.sleep(0)的作用是什么

      

    28、Java中用到的线程调度算法是什么

      java的线程调度采用抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

      由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作

  • 相关阅读:
    有关于iphone 音频 录制 播发
    iPhone开发之网络编程 AsyncSocket
    在.NET中使用Speex 音频数据编解码
    下面首先来看GCD的使用
    [已解决] AVAudioRecorder 录音,编码问题
    [转](让你少走十年弯路)四十以后才明白
    怎样才算读懂一本书?
    5000个知识点后怎样?
    DIKW体系 个人知识管理领域中最基础的概念
    个人竞争力 每个人必须悟透的概念
  • 原文地址:https://www.cnblogs.com/liufei1983/p/11809006.html
Copyright © 2011-2022 走看看