精华内容
下载资源
问答
  • 多线程面试题(值得收藏)

    万次阅读 多人点赞 2019-08-16 09:41:18
    史上最强多线程面试47题(含答案),建议收藏 金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试47题和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~ 1、并发编程三要素?...

    史上最强多线程面试47题(含答案),建议收藏

    金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试47题和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~

    1、并发编程三要素?

    1)原子性

    原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

    2)可见性

    可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

    3)有序性

    有序性,即程序的执行顺序按照代码的先后顺序来执行。

    2、实现可见性的方法有哪些?

    synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。

    3、多线程的价值?

    1)发挥多核CPU的优势

    多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的,采用多线程的方式去同时完成几件事情而不互相干扰。

    2)防止阻塞

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

    3)便于建模

    这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

    4、创建线程的有哪些方式?

    1)继承Thread类创建线程类

    2)通过Runnable接口创建线程类

    3)通过Callable和Future创建线程

    4)通过线程池创建

    5、创建线程的三种方式的对比?

    1)采用实现Runnable、Callable接口的方式创建多线程。

    优势是:

    线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

    在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

    劣势是:

    编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

    2)使用继承Thread类的方式创建多线程

    优势是:

    编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

    劣势是:

    线程类已经继承了Thread类,所以不能再继承其他父类。

    3)Runnable和Callable的区别

    Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
    Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
    Call方法可以抛出异常,run方法不可以。
    运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

    6、线程的状态流转图

    线程的生命周期及五种基本状态:

    7、Java线程具有五中基本状态

    1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

    2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

    3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就
    绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

    4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。

    根据阻塞产生的原因不同,阻塞状态又可以分为三种:

    a.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

    b.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

    c.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

    5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    8、什么是线程池?有哪几种创建方式?

    线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。

    java 提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池。

    9、四种线程池的创建:

    1)newCachedThreadPool创建一个可缓存线程池

    2)newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。

    3)newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

    4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。

    10、线程池的优点?

    1)重用存在的线程,减少对象创建销毁的开销。

    2)可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

    3)提供定时执行、定期执行、单线程、并发数控制等功能。

    11、常用的并发工具类有哪些?

    CountDownLatch
    CyclicBarrier
    Semaphore
    Exchanger

    12、CyclicBarrier和CountDownLatch的区别

    1)CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行。

    2)cyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行!

    3)CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

    4)CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。如果被中断返回true,否则返回false。

    13、synchronized的作用?

    在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。

    synchronized既可以加在一段代码上,也可以加在方法上。

    14、volatile关键字的作用

    对于可见性,Java提供了volatile关键字来保证可见性。

    当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

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

    15、什么是CAS

    CAS是compare and swap的缩写,即我们所说的比较交换。

    cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

    CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

    java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的( AtomicInteger,AtomicBoolean,AtomicLong)。

    16、CAS的问题

    1)CAS容易造成ABA问题

    一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference来解决问题。

    2) 不能保证代码块的原子性

    CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

    3)CAS造成CPU利用率增加

    之前说过了CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。

    17、什么是Future?

    在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。

    Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。

    18、什么是AQS

    AQS是AbustactQueuedSynchronizer的简称,它是一个Java提高的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。

    AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

    19、AQS支持两种同步方式:

    1)独占式

    2)共享式

    这样方便使用者实现不同类型的同步组件,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如ReentrantReadWriteLock。总之,AQS为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。

    20、ReadWriteLock是什么

    首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

    因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

    21、FutureTask是什么

    这个其实前面有提到过,FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

    22、synchronized和ReentrantLock的区别

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

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

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

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

    另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。

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

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

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

    24、线程B怎么知道线程A修改了变量

    volatile修饰变量
    synchronized修饰修改变量的方法
    wait/notify
    while轮询

    25、synchronized、volatile、CAS比较

    synchronized是悲观锁,属于抢占式,会引起其他线程阻塞。
    volatile提供多线程共享变量可见性和禁止指令重排序优化。
    CAS是基于冲突检测的乐观锁(非阻塞)

    26、sleep方法和wait方法有什么区别?

    这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器

    27、ThreadLocal是什么?有什么用?

    ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

    简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

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

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

    29、多线程同步有哪几种方法?

    Synchronized关键字,Lock锁实现,分布式锁等。

    30、线程的调度策略

    线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

    1)线程体中调用了yield方法让出了对cpu的占用权利

    2)线程体中调用了sleep方法使线程进入睡眠状态

    3)线程由于IO操作受到阻塞

    4)另外一个更高优先级线程出现

    5)在支持时间片的系统中,该线程的时间片用完

    31、ConcurrentHashMap的并发度是什么

    ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?

    32、Linux环境下如何查找哪个线程使用CPU最长

    1)获取项目的pid,jps或者ps -ef | grep java,这个前面有讲过

    2)top -H -p pid,顺序不能改变

    33、Java死锁以及如何避免?

    Java中的死锁是一种编程情况,其中两个或多个线程被永久阻塞,Java死锁情况出现至少两个线程和两个或更多资源。

    Java发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。

    34、死锁的原因

    1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环。

    例如:线程在获得了锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。

    2)默认的锁申请操作是阻塞的。

    所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。

    35、怎么唤醒一个阻塞的线程

    如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

    36、不可变对象对多线程有什么帮助

    前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

    37、什么是多线程的上下文切换

    多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

    38、如果你提交任务时,线程池队列已满,这时会发生什么

    这里区分一下:

    1)如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务

    2)如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy

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

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

    40、什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?

    线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。

    41、什么是自旋

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

    42、Java
    Concurrency API中的Lock接口(Lock
    interface)是什么?对比同步它有什么优势?

    Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

    它的优势有:

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

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

    老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

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

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

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

    44、Semaphore有什么作用

    Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

    45、Executors类是什么?

    Executors为Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable类提供了一些工具方法。

    Executors可以用于方便的创建线程池

    46、线程类的构造方法、静态块是被哪个线程调用的

    这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

    如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

    1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

    2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

    47、同步方法和同步块,哪个是更好的选择?

    同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。

    48、Java线程数过多会造成什么异常?

    1)线程的生命周期开销非常高

    2)消耗过多的CPU资源

    如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。

    3)降低稳定性

    JVM在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括JVM的启动参数、Thread构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError异常。

    展开全文
  • Java多线程面试

    万次阅读 2020-10-25 15:56:40
    sleep 方法: 是 Thread 类的静态方法,当前线程将睡眠 n 毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进行可运行状态,等待 CPU 的到来。睡眠不释放锁(如果有的话); wait 方法: 是 Object 的方法...

    1、sleep( ) 和 wait( ) 的区别?

    sleep 方法: 是 Thread 类的静态方法,当前线程将睡眠 n 毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进行可运行状态,等待 CPU 的到来。睡眠不释放锁(如果有的话);

     

    wait 方法: 是 Object 的方法,必须与 synchronized 关键字一起使用,线程进入阻塞状态,当 notify 或者 notifyall 被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,释放互斥锁。

    2、synchronized 关键字?

    底层实现:

    进入时,执行 monitorenter,将计数器 +1,释放锁 monitorexit 时,计数器-1;

    当一个线程判断到计数器为 0 时,则当前锁空闲,可以占用;反之,当前线程进入等待状态。

     

    含义:(monitor 机制)

    Synchronized 是在加锁,加对象锁。对象锁是一种重量锁(monitor),synchronized 的锁机制会根据线程竞争情况在运行时会有偏向锁(单一线程)、轻量锁(多个线程访问 synchronized 区域)、对象锁(重量锁,多个线程存在竞争的情况)、自旋锁等。

    该关键字是一个几种锁的封装。

    3、volatile 关键字?

    该关键字可以保证可见性不保证原子性。

    功能:

    · 主内存和工作内存,直接与主内存产生交互,进行读写操作,保证可见性;

    · 禁止 JVM 进行的指令重排序。

    解析:关于指令重排序的问题,可以查阅 DCL 双检锁失效相关资料。

    4、volatile 能使得一个非原子操作变成原子操作吗?

    能。

    一个典型的例子是在类中有一个 long 类型的成员变量。如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile。为什么?因为 Java 中读取 long 类型变量不是原子的,需要分成两步,如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。但是对一个 volatile 型的 long 或 double 变量的读写是原子。

     

    面试官:volatile 修饰符的有过什么实践?

     

    一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是64位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中 volatile 型的 long 或 double 变量的读写是原子的。

    volatile 修复符的另一个作用是提供内存屏障(memory barrier),例如在分布式框架中的应用。简单的说,就是当你写一个 volatile 变量之前,Java 内存模型会插入一个写屏障(write barrier),读一个 volatile 变量之前,会插入一个读屏障(read barrier)。意思就是说,在你写一个 volatile 域时,能保证任何线程都能看到你写的值,同时,在写之前,也能保证任何数值的更新对所有线程是可见的,因为内存屏障会将其他所有写的值更新到缓存。

    5、ThreadLocal(线程局部变量)关键字?

    当使用 ThreadLocal 维护变量时,其为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程对应的副本。

    ThreadLocal 内部实现机制:

    每个线程内部都会维护一个类似 HashMap 的对象,称为 ThreadLocalMap,里边会包含若干了 Entry(K-V 键值对),相应的线程被称为这些 Entry 的属主线程;

    Entry 的 Key 是一个 ThreadLocal 实例,Value 是一个线程特有对象。Entry 的作用即是:为其属主线程建立起一个 ThreadLocal 实例与一个线程特有对象之间的对应关系;

    Entry 对 Key 的引用是弱引用;Entry 对 Value 的引用是强引用。

    7.程序,进程和线程的区别

    1)程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

    2)进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个程序从创建、运行到消亡的过程。简单地说,一个进程就是一个执行中地程序,它在计算机中一个指令接着一个指令地执行,同时,每个进程还占有某些系统资源,如CPU时间、内存空间、文件、输入输出设备地使用权等等。

    3)线程:其实与进程相似,也是一个执行中地程序,但是线程是一个比进程更小地执行单位。一个进程在执行过程中可以产生多个线程,形成多条执行执行路径。但是与进程不同的是,同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换的工作时,负担要比进程小得多,也正因为如此,也正因为如此,线程也被称为轻量级进程。

    8.线程有哪几种状态?

    新建状态、就绪状态、运行状态、阻塞状态、消亡状态这五种状态

    9.线程的互斥与同步的区别

    互斥是指两个或多个线程不能同时运行,而同步则是两个或多个线程的运行有先后次序的约束。

    10.线程的同步与共享数据的区别?

    共享是指线程之间对内存数据的共享,因为线程共同拥有对内存空间中数据的处理权力,这样会导致因为多个线程同时处理数据而使数据出现不一致,所以提出同步解决此问题,即同步是在共享的基础上,是针对多个线程共享会导致数据不一致而提出来的。

    同步指的是处理数据的线程不能处理其他线程当前还没处理完的数据,但是可以处理其他数据。

    11.线程同步与异步区别

    线程同步是多个线程同时访问同一资源,等待资源访问结束,浪费时间,效率低;线程同步:访问资源时在空闲等待时同时访问其他资源,实现多线程机制。

    1、并发编程三要素?

    (1)原子性

    原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

     

    (2)可见性

    可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

     

    (3)有序性

    有序性,即程序的执行顺序按照代码的先后顺序来执行。

    2、实现可见性的方法有哪些?

    synchronized 或者 Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。

    3、多线程的价值?

    (1)发挥多核 CPU 的优势

    多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的,采用多线程的方式去同时完成几件事情而不互相干扰。

     

    (2)防止阻塞

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

     

    (3)便于建模

    这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务 A 分解成几个小任务,任务 B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

    4、创建线程的有哪些方式?

    (1)继承 Thread 类创建线程类

     

    (2)通过 Runnable 接口创建线程类

     

    (3)通过 Callable 和 Future 创建线程

     

    (4)通过线程池创建

    5、创建线程的三种方式的对比?

    (1)采用实现 Runnable、Callable 接口的方式创建多线程。

    优势是:

     

    线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

     

    劣势是:

     

    编程稍微复杂,如果要访问当前线程,则必须使用 Thread.currentThread()方法。

     

    (2)使用继承 Thread 类的方式创建多线程

    优势是:

     

    编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread()方法,直接使用 this 即可获得当前线程。

     

    劣势是:

     

    线程类已经继承了 Thread 类,所以不能再继承其他父类。

     

    (3)Runnable 和 Callable 的区别

    1、Callable 规定(重写)的方法是 call(),Runnable 规定(重写)的方法是 run()。

     

    2、Callable 的任务执行后可返回值,而 Runnable 的任务是不能返回值的。

     

    3、Call 方法可以抛出异常,run 方法不可以。

     

    4、运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

    6、线程的状态流转图

    线程的生命周期及五种基本状态:

    image.png

    7、Java 线程具有五中基本状态

    (1)新建状态(New):

    当线程对象对创建后,即进入了新建状态,如:Thread t= new MyThread();

     

    (2)就绪状态(Runnable):

    当调用线程对象的 start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待 CPU 调度执行,并不是说执行了t.start()此线程立即就会执行;

     

    (3)运行状态(Running):

    当 CPU 开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

     

    (4)阻塞状态(Blocked):

    处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。

     

    根据阻塞产生的原因不同,阻塞状态又可以分为三种:

     

    1)等待阻塞:运行状态中的线程执行 wait()方法,使本线程进入到等待阻塞状态;

     

    2)同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),

     

    它会进入同步阻塞状态;

     

    3)其他阻塞:通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。

     

    (5)死亡状态(Dead):

    线程执行完了或者因异常退出了 run()方法,该线程结束生命周期。

    8、什么是线程池?有哪几种创建方式?

    线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。

     

    java 提供了一个 java.util.concurrent.Executor 接口的实现用于创建线程池。

    9、四种线程池的创建:

    (1)newCachedThreadPool 创建一个可缓存线程池

     

    (2)newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。

     

    (3)newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

     

    (4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。

    10、线程池的优点?

    (1)重用存在的线程,减少对象创建销毁的开销。

     

    (2)可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

     

    (3)提供定时执行、定期执行、单线程、并发数控制等功能。

    11、常用的并发工具类有哪些?

    (1)CountDownLatch

     

    (2)CyclicBarrier

     

    (3)Semaphore

     

    (4)Exchanger

    12、CyclicBarrier 和 CountDownLatch 的区别

    (1)CountDownLatch 简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用 countDown()方法发出通知后,当前线程才可以继续执行。

     

    (2)cyclicBarrier 是所有线程都进行等待,直到所有线程都准备好进入 await()方法之后,所有线程同时开始执行!

     

    (3)CountDownLatch 的计数器只能使用一次。而 CyclicBarrier 的计数器可以使用 reset() 方法重置。所以 CyclicBarrier 能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

     

    (4)CyclicBarrier 还提供其他有用的方法,比如 getNumberWaiting 方法可以获得 CyclicBarrier 阻塞的线程数量。isBroken 方法用来知道阻塞的线程是否被中断。如果被中断返回 true,否则返回 false。

    13、synchronized 的作用?

    在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 既可以加在一段代码上,也可以加在方法上。

    14、volatile 关键字的作用

    对于可见性,Java 提供了 volatile 关键字来保证可见性。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。

    15、什么是 CAS

    CAS 是 compare and swap 的缩写,即我们所说的比较交换。

     

    cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。

     

    CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。

     

    java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)。

    16、CAS 的问题

    (1)CAS 容易造成 ABA 问题

    一个线程 a 将数值改成了 b,接着又改成了 a,此时 CAS 认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version 加 1。在 java5 中,已经提供了 AtomicStampedReference 来解决问题。

     

    (2)不能保证代码块的原子性

    CAS 机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证 3 个变量共同进行原子性的更新,就不得不使用 synchronized 了。

     

    (3)CAS 造成 CPU 利用率增加

    之前说过了 CAS 里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。

    17、什么是 Future?

    在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承 thread 类还是实现 runnable 接口,都无法保证获取到之前的执行结果。通过实现 Callback 接口,并用 Future 可以来接收多线程的执行结果。

     

    Future 表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback 以便在任务执行成功或失败后作出相应的操作。

    18、什么是 AQS

    AQS 是 AbustactQueuedSynchronizer 的简称,它是一个 Java 提高的底层同步工具类,用一个 int 类型的变量表示同步状态,并提供了一系列的 CAS 操作来管理这个同步状态。

     

    AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于AQS 的。

    19、AQS 支持两种同步方式:

    (1)独占式

    (2)共享式

    这样方便使用者实现不同类型的同步组件,独占式如 ReentrantLock,共享式如Semaphore,CountDownLatch,组 合 式 的 如 ReentrantReadWriteLock。总之,AQS 为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。

    20、ReadWriteLock 是什么

    首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁 ReadWriteLock。ReadWriteLock 是一个读写锁接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

    21、FutureTask 是什么

    这个其实前面有提到过,FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。

    22、synchronized 和 ReentrantLock 的区别

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

     

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

     

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

     

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

     

    另外,二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word,这点我不能确定。

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

    (1)乐观锁:

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

     

    (2)悲观锁:

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

    24、线程 B 怎么知道线程 A 修改了变量

    (1)volatile 修饰变量

     

    (2)synchronized 修饰修改变量的方法

     

    (3)wait/notify

     

    (4)while 轮询

    25、synchronized、volatile、CAS 比较

    (1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。

     

    (2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。

     

    (3)CAS 是基于冲突检测的乐观锁(非阻塞)

    26、sleep 方法和 wait 方法有什么区别?

    这个问题常问,sleep 方法和 wait 方法都可以用来放弃 CPU 一定的时间,不同点在于如果线程持有某个对象的监视器,sleep 方法不会放弃这个对象的监视器,wait 方法会放弃这个对象的监视器

    27、ThreadLocal 是什么?有什么用?

    ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。简单说 ThreadLocal 就是一种以空间换时间的做法,在每个 Thread 里面维护了一个以开地址法实现的 ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

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

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

    29、多线程同步有哪几种方法?

    Synchronized 关键字,Lock 锁实现,分布式锁等。

    30、线程的调度策略

    线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

     

    (1)线程体中调用了 yield 方法让出了对 cpu 的占用权利

     

    (2)线程体中调用了 sleep 方法使线程进入睡眠状态

     

    (3)线程由于 IO 操作受到阻塞

     

    (4)另外一个更高优先级线程出现

     

    (5)在支持时间片的系统中,该线程的时间片用完

    31、ConcurrentHashMap 的并发度是什么

    ConcurrentHashMap 的并发度就是 segment 的大小,默认为 16,这意味着最多同时可以有 16 条线程操作 ConcurrentHashMap,这也是ConcurrentHashMap 对 Hashtable 的最大优势,任何情况下,Hashtable 能同时有两条线程获取 Hashtable 中的数据吗?

    32、Linux 环境下如何查找哪个线程使用 CPU 最长

    (1)获取项目的 pid,jps 或者 ps -ef | grep java

     

    (2)top -H -p pid,顺序不能改变

    33、Java 死锁以及如何避免?

    Java 中的死锁是一种编程情况,其中两个或多个线程被永久阻塞,Java 死锁情况出现至少两个线程和两个或更多资源。

     

    Java 发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。

    34、死锁的原因

    (1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环。

    例如:线程在获得了锁 A 并且没有释放的情况下去申请锁 B,这时,另一个线程已经获得了锁 B,在释放锁 B 之前又要先获得锁 A,因此闭环发生,陷入死锁循环。

     

    (2)默认的锁申请操作是阻塞的。

    所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。

    35、怎么唤醒一个阻塞的线程

    如果线程是因为调用了 wait()、sleep()或 者 join()方法而导致的阻塞,可以中断线程,并且通过抛出 InterruptedException 来唤醒它;如果线程遇到了 IO 阻塞,无能为力,因为 IO 是操作系统实现的,Java 代码并没有办法直接接触到操作系统。

    36、不可变对象对多线程有什么帮助

    前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

    37、什么是多线程的上下文切换

    多线程的上下文切换是指 CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程。

    38、如果你提交任务时,线程池队列已满,这时会发生什么

    这里区分一下:

     

    (1)如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务

     

    (2)如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy

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

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

    40、什么是线程调度器(Thread Scheduler)和时间分片(TimeSlicing)?

    线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。

    41、什么是自旋

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

    42、Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?

    Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

     

    它的优势有:

     

    (1)可以使锁更公平

     

    (2)可以使线程在等待锁的时候响应中断

     

    (3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间

     

    (4)可以在不同的范围,以不同的顺序获取和释放锁

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

    老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

     

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

     

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

     

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

    44、Semaphore 有什么作用

    Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。

    45、Executors 类是什么?

    Executors 为 Executor,ExecutorService,ScheduledExecutorService,ThreadFactory 和 Callable 类提供了一些工具方法。Executors 可以用于方便的创建线程池

    46、线程类的构造方法、静态块是被哪个线程调用的

    这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被 new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。

     

    如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么:

     

    (1)Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的

     

    (2)Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的

    47、同步方法和同步块,哪个是更好的选择?

    同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。

    48、Java 线程数过多会造成什么异常?

    (1)线程的生命周期开销非常高

     

    (2)消耗过多的 CPU 资源

    如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销。

     

    (3)降低稳定性

    JVM 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。

     1、Synchronized用过吗,其原理是什么?

    这是一道Java面试中几乎百分百会问到的问题,因为没有任何写过并发程 序的开发者会没听说或者没接触过Synchronizedo Synchronized是由JVM 实现的一种实现互斥同步的一种方式,如果你查看被Synchronized修饰过 的程序块编译后的字节码,会发现,被Synchronized修饰过的程序块,在 编译前后被编译器生成了 monitorenter和monitorexit两个字节码指令。这两 个指令是什么意思呢?在虚拟机执行到monitorenter指令时,首先要尝试 获取对象的锁:如果这个对象没有锁定,或者当前线程已经拥有了这个对 象的锁,把锁的计数器+ 1 ;当执行monitorexit指令时将锁计数器-1 ;当计 数器为0时,锁就被释放了。如果获取对象失败了,那当前线程就要阻塞 等待,直到对象锁被另外一个线程释放为止。Java中Synchronize通过在对 象头设置标记,达到了获取锁和释放锁的目的。

    2、你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的 锁?

    "锁"的本质其实是monitorenter和monitorexit字节码指令的一个Reference 类型的参数,即要锁定和解锁的对象。我们知道,使用Synchronized可以 修饰不同的对象,因此,对应的对象锁可以这么确定。

     

    1. 如果Synchronized明确指定了锁对象,比如Synchronized(变量名)、 Synchronized(this)等,说明加解锁对象为该对象。

     

    2. 如果没有明确指定:

     

    若Synchronized修饰的方法为非静态方法,表示此方法对应的对象为锁对象

     

    若Synchronized修饰的方法为静态方法,则表示此方法对应的类对象为锁对象。

     

    注意,当一个对象被锁住时,对象里面所有用Synchronized修饰的方法都将产生堵塞,而对象里非Synchronized修饰的方法可正常被调用,不受锁影响。

    3、什么是可重入性,为什么说Synchronized是可重入锁?

    可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。 比如下面的伪代码,一个类中的同步方法调用另一个同步方法,假如 Synchronized不支持重入,进入method2方法时当前线程获得锁, method2方法里面执行method1时当前线程又要去尝试获取锁,这时如 果不支持重入,它就要等释放,把自己阻塞,导致自己锁死自己。 对Synchronized来说,可重入性是显而易见的,刚才提到,在执行 monitorenter指令时,如果这个对象没有锁定,或者当前线程已经拥有了 这个对象的锁(而不是已拥有了锁则不能继续获取),就把锁的计数器+1, 其实本质上就通过这种方式实现了可重入性。

    4、JVM对Java的原生锁做了哪些优化?

    在Java 6之前,Monitor的实现完全依赖底层操作系统的互斥锁来实现, 也就是我们刚才在问题二中所阐述的获取/释放锁的逻辑。

    由于Java层面的线程与操作系统的原生线程有映射关系,如果要将一个 线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内 核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代JDK中做 了大量的优化。一种优化是使用自旋锁,即在把线程进行阻塞操作之前先 让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无 需再让线程执行阻塞操作,避免了用户态到内核态的切换。

    现代JDK中还提供了三种不同的Monitor实现,也就是三种不同的锁:

     

    •偏向锁(Biased Locking)

     

    •轻量级锁

     

    •重量级锁

     

    这三种锁使得JDK得以优化Synchronized的运行,当JVM检测到不同 的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级、降级。

     

    •当没有竞争出现时,默认会使用偏向锁。

    JVM会利用CAS操作,在对象头上的Mark Word部分设置线程ID,以表 示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应 用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可 以降低无竞争开销。

     

    •如果有另一线程试图锁定某个被偏斜过的对象,JVM就撤销偏斜锁,切 换到轻量级锁实现。

     

    •轻量级锁依赖CAS操作Mark Word来试图获取锁,如果重试成功,就 使用普通的轻量级锁;否则,进一步升级为重量级锁。

    5、为什么说Synchronized是非公平锁?

    非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待 线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样 做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。

    6、什么是锁消除和锁粗化?

    •锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检 测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。

    程序员怎么会在明知道不存在数据竞争的情况下使用同步呢?很多不是程 序员自己加入的。

     

    •锁粗化:原则上,同步块的作用范围要尽量小。但是如果一系列的连续操 作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进 行互斥同步操作也会导致不必要的性能损耗。

    锁粗化就是增大锁的作用域。

    7、为什么说Synchronized是一个悲观锁?乐观锁的实现原理又是什么?什么是CAS,它有什么特性?

    Synchronized显然是一个悲观锁,因为它的并发策略是悲观的:不管是否 会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换、维护锁 计数器和检查是否有被阻塞的线程需要被唤醒等操作。随着硬件指令集的 发展,我们可以使用基于冲突检测的乐观并发策略。先进行操作,如果没 有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲 突,那就再进行其他的补偿措施。这种乐观的并发策略的许多实现不需要 线程挂起,所以被称为非阻塞同步。乐观锁的核心算法是 CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值、预 期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。 这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样, 如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否 则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。 CAS具有原子性,它的原子性由CPU硬件指令实现保证,即使用JNI调 用Native方法调用由C++编写的硬件级别指令,JDK中提供了 Unsafe 类执行这些操作。

    8、乐观锁一定就是好的吗?

    乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:

     

    1. 乐观锁只能保证一个共享变量的原子操作。如果多一个或几个变量,乐 观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗 粒度大小。

     

    2. 长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会 给CPU带来很大的开销。

     

    3. ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判 断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后 来被一条线程改为B,最后又被改成了 A,则CAS认为此内存值并没有发 生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景 的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本 号加一。

    9、跟Synchronized相比,可重入锁ReentrantLock其实现原理有什 么不同?

    其实,锁的实现原理基本是为了达到一个目的:让所有的线程都能看到某 种标记。

    Synchronized通过在对象头中设置标记实现了这一目的,是一种JVM原 生的锁实现方式,而ReentrantLock以及所有的基于Lock接口的实现 类,都是通过用一个volitile修饰的int型变量,并保证每个线程都能拥有 对该int的可见性和原子修改,其本质是基于所谓的AQS框架。

    10、那么请谈谈AQS框架是怎么回事儿?

    AQS(AbstractQueuedSynchronizer类)是一个用来构建锁和同步器的框架, 各种Lock包中的锁(常用的有ReentrantLock、ReadWriteLock),以及其 他如 Semaphorex CountDownLatch,甚至是早期的 FutureTask 等,都是 基于AQS来构建。

     

    1. AQS在内部定义了一个volatile int state变量,表示同步状态:当线程调 用lock方法时,如果state=O,说明没有任何线程占有共享资源的 锁,可以获得锁并将state=1;如果state=1,则说明有线程目前正在使 用共享变量,其他线程必须加入同步队列进行等待。

     

    2. AQS通过Node内部类构成的一个双向链表结构的同步队列,来完成线 程获取锁的排队工作,当有线程获取锁失败后,就被添加到队列末尾。

     

    • Node类是对要访问同步代码的线程的封装,包含了线程本身及其状 态叫

    waitStatus(有五种不同取值,分别表駅是否被阻塞,是否等待唤醒,是 否已经被取消等),每个Node结点关联其prev结点和next结点,方便 线程释放锁后快速唤醒下一个在等待的线程,是一个FIFO的过程。

     

    • Node类有两个常量,SHARED和EXCLUSIVE,分别代表共享模式和 独占模式。所谓共享模式是一个锁允许多条线程同时操作(信号量 Semaphore就是基于AQS的共享模式实现的),独占模式是同一个时 间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等 待(如 ReentranLock)。

     

    3. AQS通过内部类ConditionObject构建等待队列(可有多个),当 Condition调用wait。方法后,线程将会加入等待队列中,而当 Condition调用signal。方法后,线程将从等待队列转移动同步队列中进 行锁竞争。

     

    4. AQS和Condition各自维护了不同的队列,在使用Lock和Condition 的时候,其实就是两个队列的互相移动。

    11、请尽可能详尽地对比下Synchronized和ReentrantLock的异同。

    ReentrantLock是Lock的实现类,是一个互斥的同步锁。从功能角度, ReentrantLock比Synchronized的同步操作更精细(因为可以像普通对象一 样使用),甚至实现Synchronized没有的高级功能,如:

     

    •等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以 选择放弃等待,对处理执行时间非常长的同步块很有用。

     

    •带超时的获取锁尝试:在指定的时间范围内获取锁,如果时间到了仍然无 法获取则返回。

     

    •可以判断是否有线程在排队等待获取锁。

     

    •可以响应中断请求:与Synchronized不同,当获取到锁的线程被中断

    时,能够响应中断,中断异常将会被抛出,同时锁会被释放。

     

    •可以实现公平锁。

     

    从锁释放角度,Synchronized在JVM层面上实现的,不但可以通过一些 监控工具监控Synchronized的锁定,而且在代码执行出现异常时,JVM 会自动释放锁定;但是使用Lock则不行,Lock是通过代码实现的,要保证 锁定一定会被释放,就必须将unLock。放到finally。中。

    从性能角度,Synchronized早期实现比较低效,对比ReentrantLock,大 多数场景性能都相差较大。

     

    但是在Java 6中对其进行了非常多的改进,在竞争不激烈时, Synchronized的性能要优于ReetrantLock;在高竞争情况下, Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常 态。

    12、ReentrantLock是如何实现可重入性的?

    ReentrantLock内部自定义了同步器Sync(Sync既实现了 AQS,又实现了 AOS,而AOS提供了一种互斥锁持有的方式),其实就是加锁的时候通过 CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,看下 当前维护的那个线程ID和当前请求的线程ID是否一样,一样就可重入 了。

    13、除了 ReetrantLock,你还接触过JUC中的哪些并发工具?

    通常所说的并发包(JUC)也就是java.util.concurrent及其子包,集中了 Java 并发的各种基础工具类,具体主要包括几个方面:

     

    •提供了 CountDownLatch、CyclicBarrier、Semaphore等,比 Synchronized更加高级,可以实现更加丰富多线程操作的同步结构。

     

    •提供了 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者通 过类似快照机制实现线程安全的动态数组CopyOnWriteArrayList等各种线

    程安全的容器。

     

    •提供了 ArrayBlockingQueue、SynchorousQueue 或针对特定场景的 PriorityBlockingQueue等,各种并发队列实现。

     

    •强大的Executor框架,可以创建各种不同类型的线程池,调度任务运行 等。

    14、请谈谈 ReadWriteLock 和 StampedLock。

    虽然ReentrantLock和Synchronized简单实用,但是行为上有一定局限 性,要么不占,要么独占。实际应用场景中,有时候不需要大量竞争的写 操作,而是以并发读取为主,为了进一步优化并发操作的粒度,Java提 供了读写锁。读写锁基于的原理是多个读操作不需要互斥,如果读锁试图 锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作 结束,这样就可以自动保证不会读取到有争议的数据。

     

    ReadWriteLock代表了一对锁,下面是一个基于读写锁实现的数据结构, 当数据量较大,并发读多、并发写少的时候,能够比纯同步版本凸显出优势

    读写锁看起来比Synchronized的粒度似乎细一些,但在实际应用中,其 表现也并不尽如人意,主要还是因为相对比较大的开销。所以,JDK在后 期引入了 StampedLock,在提供类似读写锁的同时,还支持优化读模式。 优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先 试着修改,然后通过validate方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。

    15、如何让Java的线程彼此同步?

    你了解过哪些同步器?请分别介绍下。 JUC中的同步器三个主要的成员:CountDownLatch、CyclicBarrier和 Semaphore,通过它们可以方便地实现很多线程之间协作的功能。

     

    CountDownLatch叫倒计数,允许一个或多个线程等待某些操作完成。看 几个场景:

     

    •跑步比赛,裁判需要等到所有的运动员(“其他线程")都跑到终点(达到目 标),才能去算排名和颁奖。

     

    •模拟并发,我需要启动100个线程去同时访问某一个地址,我希望它们 能同时并发,而不是一个一个的去执行。

     

    CyclicBarrier叫循环栅栏,它实现让一组线程等待至某个状态之后再全部 同时执行,而且当所有等待线程被释放后,CyclicBarrier可以被重复使 用。CyclicBarrier的典型应用场景是用来等待并发线程结束。CyclicBarrier 的主要方法是await(),await。每被调用一次,计数便会减少1,并阻塞住 当前线程。当计数减至0时,阻塞解除,所有在此CyclicBarrier上面阻塞 的线程开始运行。

     

    在这之后,如果再次调用await。,计数就又会变成N-1,新一轮重新开 始,这便是Cyclic的含义所在。CyclicBarrier.await。带有返回值,用来表 示当前线程是第几个到达这个Barrier的线程。

     

    Semaphore, Java版本的信号量实现,用于控制同时访问的线程个数,来 达到限制通用资源访问的目的,其原理是通过acquire。获取一个许可,如 果没有就等待,而release。释放一个许可。

     

    如果Semaphore的数值被初始化为1,那么一个线程就可以通过acquire 进入互斥状态,本质上和互斥锁是非常相似的。但是区别也非常明显,比 如互斥锁是有持有者的,而对于Semaphore这种计数器结构,虽然有类 似功能,但其实不存在真正意义的持有者,除非我们进行扩展包装。

    16、CyclicBarrier和CountDownLatch看起来很相似,请对比下呢?

    它们的行为有一定相似度,区别主要在于:

     

    • CountDownLatch是不可以重置的,所以无法重用,CyclicBarrier没有这 种限制,可以重用。

     

    • CountDownLatch的基本操作组合是countDown/await,调用await的线 程阻塞等待countDown足够的次数,不管你是在一个线程还是多个线程 里countDown,只要次数足够即可。CyclicBarrier的基本操作组合就是 await,当所有的伙伴都调用了 await,才会继续进行任务,并自动进行重置。

    CountDownLatch目的是让一个线程等待其他N个线程达到某个条件后, 自己再去做某个事(通过CyclicBarrier的第二个构造方法public

     

    CyclicBarrier(int parties, Runnable barrierAction),在新线程里做事可以达 到同样的效果)。而CyclicBarrier的目的是让N多线程互相等待直到所有 的都达到某个状态,然后这N个线程再继续执行各自后续(通过 CountDownLatch在某些场合也能完成类似的效果)。

    17、Java中的线程池是如何实现的?

    •在Java中,所谓的线程池中的“线程",其实是被抽象为了一个静态内部 类Worker,它基于AQS实现,存放在线程池的

    HashSet workers成员变量中;

     

    •而需要执行的任务则存放在成员变量workQueue(BlockingQueue workQueue)中。

    这样,整个线程池实现的基本思想就是:从workQueue中不断取出需要执 行的任务,放在Workers中进行处理。

    18、创建线程池的几个核心构造参数?

    Java中的线程池的创建其实非常灵活,我们可以通过配置不同的参数, 创建出行为不同的线程池,这几个参数包括:

     

    • corePoolSize:线程池的核心线程数。

     

    • maximumPoolSize:线程池允许的最大线程数。

     

    • keepAliveTime:超过核心线程数时闲置线程的存活时间。

     

    • workQueue :任务执行前保存任务的队列,保存由execute方法提交的 Runnable 任务。

    19、线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好 的吗?

    显然不是的。线程池默认初始化后不启动Worker,等待有请求时才启 动。

     

    每当我们调用execute()方法添加一个任务时,线程池会做如下判断: •如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务

     

    •如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放 入队列;

     

    •如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务

     

    •如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException o 当一个线程完成任务时,它会从队列中取下一个任务来执行。当一个线程 无事可做,超过一定的时间(keepAliveTime )时,线程池会判断。

    如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以 线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

    20、既然提到可以通过配置不同参数创建出不同的线程池,那么Java中 默认实现好的线程池又有哪些呢?请比较它们的异同。

    1. SingleThreadExecutor 线程池

    这个线程池只有一个核心线程在工作,也就是相当于单线程串行执行所有 任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代 它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

    • corePoolSize:'只有一个核心线程在工作。

    • maximumPoolSize: 1。

    • keepAliveTime: 0L。

    • workQueue:new LinkedBlockingQueue<Runnable>(),其缓冲队列 是无界的。

     

    2. FixedThreadPool 线程池

    FixedThreadPool是固定大小的线程池,只有核心线程。每次提交一个任 务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦 达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程 池会补充一个新线程。

    FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服

    务器。

    • corePoolSize: nThreads

    • maximumPoolSize: nThreads

    • keepAliveTime: 0L

    • workQueue:new LinkedBlockingQueue<Runnable>(),其缓冲队列 是无界的。

     

    3. CachedThreadPool 线程池

    CachedThreadPool是无界线程池,如果线程池的大小超过了处理任务所 需要的线程,那么就会回收部分空闲(60秒不执行任务)线程,当任务数增 加时,此线程池又可以智能的添加新线程来处理任务。线程池大小完全依 赖于操作系统(或者说JVM)能够创建的最大线程大小。SynchronousQueue 是一个是缓冲区为1的阻塞队列。缓存型池子通常用于执行一些生存期很 短的异步型任务,因此在一些面向连接的daemon型SERVER中用得不 多。但对于生存期短的异步任务,它是Executor的首选。

    • corePoolSize: 0

    • maximumPoolSize: Integer.MAX_VALUE

    • keepAliveTime: 60L

    • workQueue:new SynchronousQueue<Runnable>(), —个是缓冲区为 1的阻塞队列。

     

    4. ScheduledThreadPool 线程池

    ScheduledThreadPool :核心线程池固定,大小无限的线程池。此线程池 支持定时以及周期性执行任务的需求。创建一个周期性执行任务的线程 池。如果闲置,非核心线程池会在DEFAULT_KEEPALIVEMILLIS时间内 回收。

    • corePoolSize: corePoolSize

    • maximumPoolSize: Integer.MAX_VALUE

    • keepAliveTime: DEFAULT_KEEPALIVE_MILLIS

    • workQueue:new DelayedWorkQueue()

    21、如何在Java线程池中提交线程?

    线程池最常用的提交任务的方法有两种:

    1. execute。: ExecutorService.execute 方法接收一个例,它用来执行一个任务

     

    2. submit。: ExecutorService.submit。方法返回的是 Future 对象。可以用 isDone()来查询Future是否已经完成,当任务完成时,它具有一个结果, 可以调用get。来获取结果。也可以不用isDone。进行检查就直接调用 get,在这种情况下,get将阻塞,直至结果准备就绪。

    22、什么是Java的内存模型,Java中各个线程是怎么彼此看到对方的 变量的?

    Java的内存模型定义了程序中各个变量的访问规则,即在虚拟机中将变 量存储到内存和从内存中取出这样的底层细节。此处的变量包括实例字 段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数, 因为这些是线程私有的,不会被共享,所以不存在竞争问题。

     

    Java中各个线程是怎么彼此看到对方的变量的呢?Java中定义了主内存与 工作内存的概念:

     

    所有的变量都存储在主内存,每条线程还有自己的工作内存,保存了被该 线程使用到的变量的主内存副本拷贝。

     

    线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接 读写主内存的变量。不同的线程之间也无法直接访问对方工作内存的变 量,线程间变量值的传递需要通过主内存。

    23、请谈谈volatile有什么特点,为什么它能保证变量对所有线程的可见 性?

    关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量被 定义成volatile之后,具备两种特性:

     

    1. 保证此变量对所有线程的可见性。当一条线程修改了这个变量的值,新 值对于其他线程是可以立即得知的。而普通变量做不到这一点。

     

    2. 禁止指令重排序优化。普通变量仅仅能保证在该方法执行过程中,得到 正确结果,但是不保证程序代码的执行顺序。

     

    Java的内存模型定义了 8种内存间操作:

     

    lock 和 unlock

     

    •把一个变量标识为一条线程独占的状态。

    •把一个处于锁定状态的变量释放出来,释放之后的变量才能被其他线程 锁定。

     

    read 和 write

     

    •把一个变量值从主内存传输到线程的工作内存,以便load。

    •把store操作从工作内存得到的变量的值,放入主内存的变量中。

     

    load 和 store

     

    •把read操作从主内存得到的变量值放入工作内存的变量副本中。•把工 作内存的变量值传送到主内存,以便write。

     

    use 和 assgin

     

    •把工作内存变量值传递给执行引擎。

    •将执行引擎值传递给工作内存变量值。

     

    volatile的实现基于这8种内存间操作,保证了一个线程对某个volatile变 量的修改,一定会被另一个线程看见,即保证了可见性。

    24、既然volatile能够保证线程间的变量可见性,是不是就意味着基于 volatile变量的运算就是并发安全的?

    显然不是的。基于volatile变量的运算在并发下不一定是安全的。volatile 变量在各个线程的工作内存,不存在一致性问题(各个线程的工作内存中 volatile变量,每次使用前都要刷新到主内存)。

     

    但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一 样是不安全的。

    25、请对比下volatile对比Synchronized的异同。

    Synchronized既能保证可见性,又能保证原子性,而volatile只能保证可 见性,无法保证原子性。

     

    ThreadLoca l和Synchonized都用于解决多线程并发访问,防止任务在共 享资源上产生冲突。但是ThreadLocal与Synchronized有本质的区别。 Synchronized用于实现同步机制,是利用锁的机制使变量或代码块在某一 时该只能被一个线程访问,是一种“以时间换空间''的方式。

     

    而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一 时间访问到的并不是同一个对象,根除了对变量的共享,是一种“以空间 换时间”的方式。

    26、请谈谈ThreadLocal是怎么解决并发安全的?

    ThreadLocal这是Java提供的一种保存线程私有信息的机制,因为其在 整个线程生命周期内有效,所以可以方便地在一个线程关联的不同业务模 块之间传递信息,比如事务ID、Cookie等上下文相关信息。

     

    ThreadLocal为每一个线程维护变量的副本,把共享数据的可见范围限制 在同一个线程之内,其实现原理是,在ThreadLocal类中有一个Map,用 于存储每一个线程的变量的副本。

    27、很多人都说要慎用ThreadLocal,谈谈你的理解,使用ThreadLocal需要注意些什么?

    使用 ThreadLocal 要注意 remove!

     

    ThreadLocal的实现是基于一个所谓的ThreadLocalMap,在 ThreadLocalMap中,它的key是一个弱引用。

     

    通常弱引用都会和引用队列配合清理机制使用,但是ThreadLocal是个例 外,它并没有这么做。

     

    这意味着,废弃项目的回收依赖于显式地触发,否则就要等待线程结束, 进而回收相应ThreadLocalMap!这就是很多00M的来源,所以通常都会 建议,应用一定要自己负责remove,并且不要和线程池配合,因为 worker线程往往是不会退出的。

    11.进程和线程的区别是什么?

    进程是执行着的应用程序,而线程是进程内部的一个执行序列。一个进程可以有多个线程。线程又叫做轻量级进程。

    12.创建线程有几种不同的方式?你喜欢哪一种?为什么?

    有三种方式可以用来创建线程:

    ●继承Thread类

    ●实现Runnable接口

    ●应用程序可以使用Executor框架来创建线程池

    实现Runnable接口这种方式更受欢迎,因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而Java不支持多继承),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。

    13.概括的解释下线程的几种可用状态。

    线程在执行过程中,可以处于下面几种状态:

    ●就绪(Runnable):线程准备运行,不一定立马就能开始执行。

    ●运行中(Running):进程正在执行线程的代码。

    ●等待中(Waiting):线程处于阻塞的状态,等待外部的处理结束。

    ●睡眠中(Sleeping):线程被强制睡眠。

    ●I/O阻塞(Blocked on I/O):等待I/O操作完成。

    ●同步阻塞(Blocked on Synchronization):等待获取锁。

    ●死亡(Dead):线程完成了执行。

    14.同步方法和同步代码块的区别是什么?

    在Java语言中,每一个对象有一把锁。线程可以使用synchronized关键字来获取对象上的锁。synchronized关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁)。

    15.在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?

    监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。

    16.什么是死锁(deadlock)?

    两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是两个进程都陷入了无限的等待中。

    17.如何确保N个线程可以访问N个资源同时又不导致死锁?

    使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。

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

    只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。

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

    Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

     

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

    5、CyclicBarrier和CountDownLatch的区别

    两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:

     

    1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。

     

    2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务。

     

    3) CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。

    6、volatile关键字的作用

    一个非常重要的问题,是每个学习、应用多线程的Java程序员都必须掌握的。理解volatile关键字的作用的前提是要理解Java内存模型,这里就不讲Java内存模型了,可以参见第31点,volatile关键字的作用主要有两个:

     

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

     

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

    从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见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等都是线程非安全的类,点击这里了解为什么不安全。

    8、Java中如何获取到线程dump文件

    死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:

     

    1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java

     

    2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid

     

    另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈。

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

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

    10、如何在两个线程之间共享数据

    通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的

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

    这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器

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

    这个问题很理论,但是很重要:

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

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

    13、ThreadLocal有什么用

    简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了

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

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

    15、wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别

    wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于: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可以灵活地实现多路通知

     

    另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。

    19、ConcurrentHashMap的并发度是什么

    ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?

    20、ReadWriteLock是什么

    首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

     

    因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

    21、FutureTask是什么

    这个其实前面有提到过,FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

    22、Linux环境下如何查找哪个线程使用CPU最长

    这是一个比较偏实践的问题,这种问题我觉得挺有意义的。可以这么做:

     

    (1)获取项目的pid,jps或者ps -ef | grep java,这个前面有讲过

     

    (2)top -H -p pid,顺序不能改变

     

    这样就可以打印出当前的项目,每条线程占用CPU时间的百分比。注意这里打出的是LWP,也就是操作系统原生线程的线程号,我笔记本山没有部署Linux环境下的Java工程,因此没有办法截图演示,网友朋友们如果公司是使用Linux环境部署项目的话,可以尝试一下。

     

    使用"top -H -p pid"+"jps pid"可以很容易地找到某条占用CPU高的线程的线程堆栈,从而定位占用CPU高的原因,一般是因为不当的代码操作导致了死循环。

     

    最后提一点,"top -H -p pid"打出来的LWP是十进制的,"jps pid"打出来的本地线程号是十六进制的,转换一下,就能定位到占用CPU高的线程的当前线程堆栈了。

    23、Java编程写一个会导致死锁的程序

    1)两个线程里面分别持有两个Object对象:lock1和lock2。这两个lock作为同步代码块的锁;

     

    2)线程1的run()方法中同步代码块先获取lock1的对象锁,Thread.sleep(xxx),时间不需要太多,50毫秒差不多了,然后接着获取lock2的对象锁。这么做主要是为了防止线程1启动一下子就连续获得了lock1和lock2两个对象的对象锁

     

    3)线程2的run)(方法中同步代码块先获取lock2的对象锁,接着获取lock1的对象锁,当然这时lock1的对象锁已经被线程1锁持有,线程2肯定是要等待线程1释放lock1的对象锁的

     

    这样,线程1"睡觉"睡完,线程2已经获取了lock2的对象锁了,线程1此时尝试获取lock2的对象锁,便被阻塞,此时一个死锁就形成了。

    24、怎么唤醒一个阻塞的线程

    如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

    25、不可变对象对多线程有什么帮助

    前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

    26、什么是多线程的上下文切换

    多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

    27、如果你提交任务时,线程池队列已满,这时会发生什么

    1)如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务

     

    2)如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy

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

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

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

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

    30、什么是自旋

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

    31、什么是Java内存模型

    Java内存模型定义了一种多线程访问Java内存的规范。Java内存模型要完整讲不是这里几句话能说清楚的,我简单总结一下Java内存模型的几部分内容:

     

    1)Java内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去

     

    2)定义了几个原子操作,用于操作主内存和工作内存中的变量

     

    3)定义了volatile变量的使用规则

     

    4)happens-before,即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的happens-before规则,则这段代码一定是线程非安全的

    32、什么是CAS

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

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

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

     

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

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

    老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

     

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

     

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

     

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

    36、Semaphore有什么作用

    Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

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

    这是我之前的一个困惑,不知道大家有没有想过这个问题。某个方法中如果有多条语句,并且都在操作同一个类变量,那么在多线程环境下不加锁,势必会引发线程安全问题,这很好理解,但是size()方法明明只有一条语句,为什么还要加锁?

    关于这个问题,在慢慢地工作、学习中,有了理解,主要原因有两点:

     

    1)同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性

     

    2)CPU执行代码,执行的不是Java代码,这点很关键,一定得记住。Java代码最终是被翻译成机器码执行的,机器码才是真正可以和硬件电路交互的代码。即使你看到Java代码只有一行,甚至你看到Java代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句"return count"假设被翻译成了三句汇编语句执行,一句汇编语句和其机器码做对应,完全可能执行完第一句,线程就切换了。

    38、线程类的构造方法、静态块是被哪个线程调用的

    这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

     

    如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

     

    1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

     

    2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

    39、同步方法和同步块,哪个是更好的选择

    同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。

     

    借着这一条,我额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁-->解锁的次数,有效地提升了代码执行的效率。

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

    关于这个问题,个人看法是:

     

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

     

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

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

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

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

     

    展开全文
  • 多线程面试

    千次阅读 2012-04-06 13:51:57
    第一题:线程的基本概念、线程的基本状态及状态之间的关系? 进程概念   进程是表示资源分配的基本单位,又是调度运行的基本单位。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格...
    
    

    一.概念性问答题

    第一题:线程的基本概念、线程的基本状态及状态之间的关系?

    进程概念 
      进程是表示资源分配的基本单位,又是调度运行的基本单位。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、磁盘空间、I/O设备等。然后,把该进程放人进程的就绪队列。进程调度程序选中它,为它分配CPU以及其它有关资源,该进程才真正运行。所以,进程是系统中的并发执行的单位。 
      在Mac、Windows NT等采用微内核结构的操作系统中,进程的功能发生了变化:它只是资源分配的单位,而不再是调度运行的单位。在微内核系统中,真正调度运行的基本单位是线程。因此,实现并发功能的单位是线程。

     线程是进程中执行运算的最小单位,亦即执行处理机调度的基本单位。如果把进程理解为在逻辑上操作系统所完成的任务,那么线程表示完成该任务的许多可能的子任务之一。例如,假设用户启动了一个窗口中的数据库应用程序,操作系统就将对数据库的调用表示为一个进程。假设用户要从数据库中产生一份工资单报表,并传到一个文件中,这是一个子任务;在产生工资单报表的过程中,用户又可以输人数据库查询请求,这又是一个子任务。这样,操作系统则把每一个请求――工资单报表和新输人的数据查询表示为数据库进程中的独立的线程。线程可以在处理器上独立调度执行,这样,在多处理器环境下就允许几个线程各自在单独处理器上进行。操作系统提供线程就是为了方便而有效地实现这种并发性 

    引入线程的好处 
    (1)易于调度。 
    (2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。 
    (3)开销少。创建线程比创建进程要快,所需开销很少。。 
    (4)利于充分发挥多处理器的功能。通过创建多线程进程(即一个进程可具有两个或更多个线程),每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。 

    第二题:线程与进程的区别?

           这个题目问到的概率相当大,计算机专业考研中也常常考到。要想全部答出比较难。

    进程和线程的关系 
    (1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。 
    (2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。 
    (3)处理机分给线程,即真正在处理机上运行的是线程。 
    (4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

     

    第三题:多线程有几种实现方法,都是什么?

     

    第四题:多线程同步和互斥有几种实现方法,都是什么?

           我在参加2011年迅雷校园招聘时的一面和二面都被问到这个题目,回答的好将会给面试成绩加不少分。

     

    第五题:多线程同步和互斥有何异同,在什么情况下分别使用他们?举例说明。

    展开全文
  • 多线程面试

    2020-05-02 09:06:39
    面试官问题:多线程一般有哪些状态(生命周期),举例下场景? 二、多线程使用场景 三、多线程的难点、容易出错的地方和解决方法 面试官问题:比如线程怎么是现实同步? 面试官问题:synchronized 和 lock 的区别是...

    文章正文

    前言
    一、什么是多线程
    面试官问题:Java 线程调用 start() 后会立即执行 run() 方法吗?
    面试官问题:多线程一般有哪些状态(生命周期),举例下场景?
    二、多线程使用场景
    三、多线程的难点、容易出错的地方和解决方法
    面试官问题:比如线程怎么是现实同步?
    面试官问题:synchronized 和 lock 的区别是什么?优点和缺点哪些?
    面试问题:什么情况会发送死锁,解决方法有哪些?
    前言

    为什么面试官喜欢问多线程?

    很简单,因为这能刷选出优质员工的指标之一。相信各位在面试中有体会,你要回答了 SSM/SSH 框架的问题,面试官只会“嗯”。如果你能接的住多线程的问题,面试官会眼前一亮,其实面试官只是在一堆的简历,找到最满意的人,不需要你的技术有多匹配他们公司的技术,只有你比其他面试者优秀就行。一般的问题大家都会,所以分数就靠《多线程》来拉开差距。

    多线程为什么重要:

    在关键的技术难点上可以解决问题;
    是因为一般在项目中用到的地方很少,容易出错,不如不用,换其他方案,绕过多线程去解决。
    一、什么是多线程

    我们先要理解什么是线程,去掉多字。一般在思考这个问题的时候,我们可以再往前想,线程存在的含义是什么,为什么要有这个东西。

    在上古时代,大概 60 年代,是没有线程的,那时候跟线程功能类似的是进程,但是进程出现了很多弊端。一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。

    这是官方的解释,通俗讲就是:进程等于是米,单位是当时的最小单位,太大了,我要生产一支笔,结果最小的笔长 1 米。所以需要更小的单位,比如厘米。因此在 80 年代,出现了能独立运行的基本单位——线程(Threads)。

    这里我们说到进程(Process),我相信大家都对进程有所了解。原因一是进程我们在任务管理器可以直接看到,不管 Liunx 和 Win 系统中每一个进程有一个 pid。进程是多个线程的容器。那么线程我们也可以在任务管理器中看到。如图,我们可以看到 IDEA 的 pid 是 26504,线程数是 58 个。看到这个 58 应该能理解了什么是多线程了。

    任务管理器的线程

    最简单的例子就是 Java HelloWorld 的 main 方法,就是一个线程,并且是一个主线程。

    多线程就看多个方法在同时执行,是一个并行的概念,和多线程对应的就是单线程,等于串行的概念。

    下面我来往深点讲原理。多线程原理是什么呢?就是 CPU 快速地在多条线程之间调度(切换),等于这个线程方法执行下,马上换到下个方法去,如果 CPU 调度线程的时间足够快,就造成了多线程并发执行的假象。类似七龙珠的残像拳。

    那么 CPU 切换回来的时候,它怎么知道我上次执行到哪里呢?这里需要一个东西存下来记录,我回忆下 JVM 的内存模型,里面有个程序计数器的东西,在 JVM 中,通过程序计数器来记录某个线程的字节码执行位置。它也具有隔离性,每个线程单独有一个程序计数器。JVM 不明白的同学可以关注我下篇文章《来问 JVM 啊,我跟你聊一小时》。

    如果 CPU 调度线程的时间足够快,就造成了多线程并发执行的假象。那么在 Java 代码里面怎么实现呢,这是一个老生常谈的话题了,这里就不再复述了,网上资料很多,最精简的是这个。大家可以花 20 分钟读完。看的过程中一定要自己运行下里面代码:

    https://www.runoob.com/java/java-multithreading.html

    看完后,应该有几个疑问,run() 方法和 start() 方法。run 是需要重写的,就是这个线程的执行内容,start() 就是启动方法,会执行 run 方法里面的内容。

    面试官问题:Java 线程调用 start() 后会立即执行 run() 方法吗?

    class Test {
    public static void main(String[] args) {
    System.out.println(hello https://tool.lu/);
    Runnable runnable = new Runnable(){
    @Override
    public void run() {
    System.out.println("–run()–");
    }
    };

    Thread thread = new Thread(runnable);
    thread.start();
    System.out.println("--after start()--");
    

    }}
    这段代码很简单,就 2 个打印,但有时结果会出现先打印“–after start()–”然后再是“–run()-”(没出现的同学可以多运行几次)。

    从结果来看,运行 start 的时候,就开启的一个新的线程,等于 21 点时候,获得 2 张一样的牌,可以分出 2 副派的。我们之前的 main 方法里面的不会按照顺序继续执行 run() 方法里面的,而是又新的线程执行,但新的线程什么时候开始真正执行 run() 方法,这需要又 jvm 来决定。所以 start() 方法只是告诉 JVM 这里来新客人了,快来接客了,JVM 什么时候接客,就什么时候执行 run() 方法。这是不是有点像 Ajax 的异步调用了。

    面试官问题:多线程一般有哪些状态(生命周期),举例下场景?

    在问这类问题时候,无脑地给出 2 个——创建和结束。不管是类、持久化对象、Serlvet 等,肯定有这 2 个基本的。任何事物必须有开始和结束嘛。然后就是这个图,来源:https://www.runoob.com/java/java-multithreading.html。

    在这里插入图片描述

    这里唯一有难度的就是这个阻塞状态。其他的状态都说了对应的方法,但阻塞的方法是什么呢?

    下面我根据阻塞分类列举出来。

    等待阻塞:运行的线程执行 wait() 方法,JVM 会把该线程放入等待池中。(只能在同步方法或同步块中调用 wait() 方法。进入 wait() 方法后,当前线程释放锁) 通过 notify() 方法来“唤醒”。
    同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池中。
    其他阻塞:运行的线程执行 sleep() 或 join() 方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep() 状态超时、join() 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。(注意,sleep 是不会释放持有的锁)
    阻塞很多情况是因为同步。同步也面试中常常问的部分。

    好了,基本概念总算完了,下面我讲一般项目中使用场景。

    二、多线程使用场景

    1. 创建线程

    都不会自己去创建,而是使用 JDK 自带的线程池去管理线程。

    java.util.concurrent.Executor。为什么要用呢,因为自己写不好,之前讲了线程那多问题。有工具不用为什么要自己写呢。

    1. 多线程使用场景

    常见的就是异步操作。我讲下在系统注册用户或者完成重要的业务变更,比如修改密码,会发送手机短信通知用户是否本人操作。实现这个功能,需要调用短信接口发送短信,问题在这里,这是人家的接口,不受你控制,如果出现请求超时,或者性能很慢,那你这个业务就需要很长的时间。用户会以为这个功能是坏的。但实际不是你的锅。这时,我们应该分离出来,你注册的业务照样执行,调用它短信接口交给另外一个线程去做,就不用等待它。

    三、多线程的难点、容易出错的地方和解决方法

    面试官问题:比如线程怎么是现实同步?

    回答这个问题之前,我们先了解,我为什么要同步?同步到底能解决什么问题?

    同步是为了解决线程安全,资源互斥的问题。比如网络电影票的销售问题。

    下面代码来源于:

    https://www.cnblogs.com/aademeng/articles/10890654.html

    /**

    • 售票问题
      */
      public class Test1 {
      static int tickets=10;
      class SellTickets implements Runnable{
      @Override
      public void run() {
      // 未加同步时,产生脏数据
      while(tickets>0){
      System.out.println(Thread.currentThread().getName()+" -->售出第 “+tickets+” 张票");
      tickets–;
      try {
      Thread.sleep(100);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      }
      if(tickets<=0){
      System.out.println(Thread.currentThread().getName()+" -->售票结束!");
      }
      }
      }
      public static void main(String[] args) {
      SellTickets sell=new Test1().new SellTickets();
      Thread t1=new Thread(sell, “1号窗口”);
      Thread t2=new Thread(sell, “2号窗口”);
      Thread t3=new Thread(sell, “3号窗口”);
      Thread t4=new Thread(sell, “4号窗口”);
      t1.start();
      t2.start();
      t3.start();
      t4.start();
      }
      }
      上述代码运行后,效果如下:

    1号窗口 -->售出第 10 张票
    3号窗口 -->售出第 10 张票
    2号窗口 -->售出第 10 张票
    4号窗口 -->售出第 10 张票
    3号窗口 -->售出第 6 张票
    2号窗口 -->售出第 6 张票
    1号窗口 -->售出第 5 张票
    4号窗口 -->售出第 3 张票
    3号窗口 -->售出第 2 张票
    2号窗口 -->售出第 2 张票
    1号窗口 -->售出第 2 张票
    4号窗口 -->售票结束!
    3号窗口 -->售票结束!
    1号窗口 -->售票结束!
    2号窗口 -->售票结束!
    如果结果正常,请多执行几次。

    上述运行结果中,第 10 张票被售出多次,显然不符合实际应用中的逻辑。出现这个情况是多个线程同时操作票这个资源。这样是线程不安全的。 所以我们必须一次能让一个线程操作,同步就是解决线程安全的。

    常用的方法 3 种,面试中说出这 3 种就可以了。

    第一种,同步代码块。

    用 synchronized 关键字修饰的语句块,即为同步代码块。同步代码块会被 JVM 自动加上内置锁,从而实现同步。synchronized{} 把业务逻辑写到括号里面。

    第二种,同步方法 。

    和第一种其实道理一样,即有 synchronized 关键字修饰的方法。

    第三种,Lock 锁机制。

    使用

    Lock lock=new ReentrantLock();
    再加锁实现同步:

    lock.lock();
    释放锁结束同步。

    lock.unlock();
    如果你说到这里面试官喜欢问区别是什么?

    面试官问题:synchronized 和 lock 的区别是什么?优点和缺点哪些?

    由于 synchronized 是在 JVM 层面实现的,因此系统可以监控锁的释放与否;而 ReentrantLock 是使用代码实现的,系统无法自动释放锁,需要在代码中的 finally 子句中显式释放锁 lock.unlock()。

    另外,在并发量比较小的情况下,使用 synchronized 是个不错的选择;但是在并发量比较高的情况下,其性能下降会很严重,此时 ReentrantLock 是个不错的方案。

    到这多线程的基本概念和常见问题就讲完了,下面我们来说再项目中的应用。

    不对,我们还漏掉了一个重要点——锁。

    之前讲同步,其实就是加个锁,锁会带来性能问题,还会发送死锁。这对系统来说是灾难性的。

    面试问题:什么情况会发送死锁,解决方法有哪些?

    死锁就是我锁住这个资源,释放条件又是另一个被锁住的资源,一般是彼此互相等待。

    解决方法很多,我们这有一种最常用的——无锁算法(我不锁就行了吧),业内术语 CAS。这是什么意思,我忘记了,但你说出来肯定说明你业内认识了。它是在无阻塞的情况下实现同步。代码也只有几行:

    public class SimulatedCAS {
    private int value;

    public synchronized int getValue() {
        return value;
    }
    
    public synchronized int compareAndSwap(int expectedValue, int newValue) {
        if (value == expectedValue)
            value = newValue;
        return value;
    }
    

    }
    class CasCounter {
    private SimulatedCAS value;

    public int getValue() {
        return value.getValue();
    }
    
    public int decrement() {
        int oldValue = value.getValue();
        while (value.compareAndSwap(oldValue, oldValue + 1) != oldValue)
            oldValue = value.getValue();
        return oldValue + 1;
    }
    

    }
    看懂就看懂了,看不懂就背下来下面这句。

    CAS 的语义是:

    “我认为 V 的值应该为 A,如果是,那么将 V 的值更新为 B,否则不修改并告诉 V 的值实际为多少。”

    展开全文
  • JAVA多线程面试

    千次阅读 2017-03-12 00:53:00
    Java线程面试题 Top 50 15个顶级多线程面试题及答案 java多线程--几个多线程面试题小结 Java线程经典面试题
  • Java面试——多线程面试

    万次阅读 2016-09-02 10:22:35
    0.前言在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分,本文汇总了常见的一些多线程面试题。一些问题,比如volatile关键词的作用,synchronized和ReentrantLock的区别,wait()和sleep()的区别等等...
  • 必定也能助你在面试中秒杀所有多线程面试题。 1.《秒杀多线程第一篇 多线程笔试面试题汇总》 2.《秒杀多线程第二篇 多线程第一次亲密接触 CreateThread与_beginthreadex本质区别》 3.《秒杀多...
  • 个人总结40个Java多线程面试问题和答案,很全面,让你不再担心多线程的面试问题。
  • 多线程面试问题解析

    2018-06-12 16:48:34
    首先贴一下自己整理的一些并发知识,基于这些我可以说能解决你大部分的多线程面试题了。并发大纲0.什么是线程安全理论问题,每个人理解不一样,我的理解就是:多线程下执行的结果和单线程下一致,就是线程安全的。1....
  • Java多线程面试题-可能学了个寂寞?

    千次阅读 多人点赞 2021-03-29 21:13:44
    Java多线程面试题总结,以前可能学了个寂寞,才发现啥都不会。 文章目录 进程和线程 并发和并行 上下文切换 sleep()和wait() start()和run() synchronized关键字 volatile关键字 ThreadLocal 线程池 Runnable和...
  • JAVA 多线程面试题 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一...
  • Java多线程面试问题

    2016-11-14 21:30:32
    多线程和并发问题是Java技术面试中面试官比较...Java多线程面试问题 1. 进程和线程之间有什么不同? 一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的
  • 多线程面试题——两个线程交替打印 //一个线程打印1 2 3 ... //一个线程打印a b c ... //交替打印 1 a 2 b 3 c ... 直到所有字母打印完毕 public class Thread1 { private static int index = 1; private ...
  • Java多线程面试题整理及答案

    千次阅读 2019-08-19 16:18:38
    Java多线程面试题整理及答案 1) 什么是线程? 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对 运算密集型任务提速...
  • 50个多线程面试题,你会多少?

    万次阅读 多人点赞 2018-03-19 14:06:29
    下面是Java线程相关的热门面试题,你可以用它来好好准备面试。 什么是线程? 什么是线程安全和线程不安全? 什么是自旋锁? 什么是Java内存模型? 什么是CAS? 什么是乐观锁和悲观锁? 什么是AQS? 什么是...
  • Google C++多线程面试

    2019-03-07 14:56:15
    Google C++多线程面试题 有四个线程1、2、3、4。线程1的功能就是输出1,线程2的功能就是输出2,以此类推………现在有四个文件ABCD。初始都为空。现要让四个文件呈如下格式: A:1 2 3 4 1 2…. B:2 3 4 1 2 3…....
  • 2、15个高级Java多线程面试题及回答 3、GC线程是否为守护线程? 4、HashMap 是线程安全的吗,为什么不是线程安全的 5、Java中如何获取到线程dump文件 6、Java中实现线程通信的三个方法的作用是什么? 7、java中有几...
  • 多线程面试题和答案:线程锁+线程池+线程同步1、并发编程三要素?2、多线程的价值?3、创建线程的有哪些方式?区别是什么?4、创建线程的三种方式的对比?4、线程的生命周期及五种基本状态及转换条件1、Java线程具有...
  • JAVA 多线程面试

    2014-09-02 08:36:43
    多线程和并发问题是Java技术面试中面试官比较...Java多线程面试问题 1. 进程和线程之间有什么不同? 一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的
  • java多线程面试

    2014-01-15 12:28:22
    今天开始打算学习java多线程,看到了这个java多线程面试题,很不错,希望作为多线程与我的见面礼吧。   1.什么是多线程编程?什么时候使用? 多线程一般用于当一个程序需要同时做一个以上的任务。多线程通常用于...
  • 多线程面试题整理(1)

    千次阅读 2018-03-21 19:44:16
    自己整理的一些多线程面试题,持续更新多线程编程的好处程序中启用多个线程并发执行以提高程序的效率,多个线程共享heap memory,创建多个线程充分利用CPU资源,创建多个线程执行任务比创建多个进程要好用户线程和...
  • Java 多线程面试问题

    2014-10-09 11:59:40
    Java 多线程面试问题 1.进程和线程之间有什么不同? 一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。Java运行环境是一个包含了不同的类...
  • 多线程和并发问题是...1 Java多线程面试问题 1. 进程和线程之间有什么不同?  一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。Java运行...
  • 多线程面试题: 1.什么是线程,什么是进程,它们有什么区别和联系,一个进程里面是否必须有个线程 (先讲进程) 答案 进程本质上是一个执行的程序,一个进程可以有多个线程。它允许计算机同时运行两个或多个程序。...
  • java多线程面试40题

    2017-06-16 11:22:53
    多线程面试40个问题汇总 1、多线程有什么用? 一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回答更扯淡。所谓"知其然知其所以然","会用"只是"知其然","为什么用...
  • 转载自 史上最全Java多线程面试题及答案多线程并发编程是Java编程中重要的一块内容,也是面试重点覆盖区域。所以,学好多线程并发编程对Java程序员来来说极其重要的。下面小编整理了60道最常见的Java多线程面试题及...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 25,308
精华内容 10,123
关键字:

多线程面试