Java多线程面试题

线程基础

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

这个线程问题目的是检测你对”join”方法是否熟悉。这个多线程问题比较简单,可以用join方法实现。

2、在java中 wait() 和 sleep()方法的不同?

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

3、怎么实现一个阻塞队列?

用wait()和notify()方法来实现阻塞队列。或者实现 BlockingQueue 的接口

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

当然有很多解决方法,我已经分享了一种用阻塞队列实现的方法。有些时候他们甚至会问怎么实现哲学家进餐问题。

5在java程序中怎么保证多线程的运行安全?

线程安全在三个方面体现:

原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);

可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);

有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。

6多线程锁的升级原理是什么?

在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。

锁升级的图示过程:

 Java多线程面试题_第1张图片

7、什么是死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称。最早在1965年由Dijkstra在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。

8怎么防止死锁?

死锁的四个必要条件:

互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源

请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放

不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放

环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。

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

这是我最喜欢的Java线程面试问题,因为即使死锁问题在写多线程并发程序时非常普遍,但是很多侯选者并不能写deadlockfreecode(无死锁代码?),他们很挣扎。只要告诉他们,你有N个资源和N个线程,并且你需要所有的资源来完成一个操作。为了简单这里的n可以替换为2,越大的数据会使问题看起来更复杂。通过避免Java中的死锁来得到关于死锁的更多信息。

10、说一下atomic的原理?

Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。

11、什么是竞争条件?你怎样发现和解决竞争?

这是一道出现在多线程面试的高级阶段的问题。大多数的面试官会问最近你遇到的竞争条件,以及你是怎么解决的。有些时间他们会写简单的代码,然后让你检测出代码的竞争条件。可以参考我之前发布的关于Java竞争条件的文章。在我看来这是最好的java线程面试问题之一,它可以确切的检测候选者解决竞争条件的经验,or writing code which is free of data race or anyother race condition。关于这方面最好的书是《Concurrency practices in Java》。

12、你将如何使用threaddump?你将如何分析Threaddump?

在UNIX中你可以使用kill-3,然后threaddump将会打印日志,在windows中你可以使用”CTRL+Break”。非常简单和专业的线程面试问题,但是如果他问你怎样分析它,就会很棘手。

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

当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码。

14、Java中你怎样唤醒一个阻塞的线程?

这是个关于线程和阻塞的棘手的问题,它有很多解决方法。如果线程遇到了IO阻塞,我并且不认为有一种方法可以中止线程。如果线程因为调用wait()、sleep()、或者join()方法而导致的阻塞,你可以中断线程,并且通过抛出InterruptedException来唤醒它。

Synchronized

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

这是一道Java面试中几乎百分百会问到的问题,因为没有任何写过并发程序的开发者会没听说或者没接触过Synchronized。

Synchronized是由JVM实现的一种实现互斥同步的一种方式,如果你查看被Synchronized修饰过的程序块编译后的字节码会发现,被Synchronized修饰过的程序块,在编译前后被编译器生成了Monitor Enter和Monitor Exit两个字节码指令。

这两个指令是什么意思呢?

在虚拟机执行到Monitor Enter指令时,首先要尝试获取对象的锁:

如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+1;当执行Monitor Exit指令时将锁计数器-1;当计数器为0时,锁就被释放了。

如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

Java中Synchronize通过在对象头设置标记,达到了获取锁和释放锁的目的。

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

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

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

2.如果没有明确指定:

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

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

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

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

可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。比如下面的伪代码,一个类中的同步方法调用另一个同步方法,假如Synchronized不支持重入,进入method2方法时当前线程获得锁,method2方法里面执行method1时当前线程又要去尝试获取锁,这时如果不支持重入,它就要等释放,把自己阻塞,导致自己锁死自己。

对Synchronized来说,可重入性是显而易见的,刚才提到,在执行Monitor Enter指令时,如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁(而不是已拥有了锁则不能继续获取),就把锁的计数器+1,其实本质上就通过这种方式实现了可重入性。

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

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

由于Java层面的线程与操作系统的原生线程有映射关系,如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代JDK中做了大量的优化。

一种优化是使用自旋锁,即在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程执行阻塞操作,避免了用户态到内核态的切换。

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

·偏向锁(BiasedLocking)

·轻量级锁

·重量级锁

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

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

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

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

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

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

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

锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。程序员怎么会在明知道不存在数据竞争的情况下使用同步呢?很多不是程序员自己加入的。

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

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

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

Synchronized显然是一个悲观锁,因为它的并发策略是悲观的:不管是否会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略。

先进行操作,如果没有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施。这种乐观的并发策略的许多实现不需要线程挂起,所以被称为非阻塞同步。

乐观锁的核心算法是CAS(CompareandSwap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。

这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。

CAS具有原子性,它的原子性由CPU硬件指令实现保证,即使用JNI调用Native方法调用由C++编写的硬件级别指令,JDK中提供了Unsafe类执行这些操作。

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

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

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

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

3.ABA问题

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

9、Synchronized 和 volatile 的区别是什么?

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

volatile仅能使用在变量级别;Synchronized则可以使用在变量、方法、和类级别的。

volatile仅能实现变量的修改可见性,不能保证原子性;而Synchronized 则可以保证变量的修改可见性和原子性。

volatile不会造成线程的阻塞;Synchronized 可能会造成线程的阻塞。

volatile标记的变量不会被编译器优化;Synchronized 标记的变量可以被编译器优化。

10、Synchronized 和Lock有什么区别?

首先Synchronized 是java内置关键字,在jvm层面,Lock是个java类;

Synchronized 无法判断是否获取锁的状态,Lock可以判断是否获取到锁;

Synchronized 会自动释放锁(a 线程执行完同步代码会释放锁;b线程执行过程中发生异常会释放锁),

Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;

用Synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;

Synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平;

Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

ReentrantLock及其他显式锁

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

其实,锁的实现原理基本是为了达到一个目的:

让所有的线程都能看到某种标记。

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

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

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

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

2.AQS通过Node内部类构成的一个双向链表结构的同步队列,来完成线程获取锁的排队工作,当有线程获取锁失败后,就被添加到队列末尾。oNode类是对要访问同步代码的线程的封装,包含了线程本身及其状态叫waitStatus(有五种不同取值,分别表示是否被阻塞,是否等待唤醒,是否已经被取消等),每个Node结点关联其prev结点和next结点,方便线程释放锁后快速唤醒下一个在等待的线程,是一个FIFO的过程。

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

(如ReentranLock)。

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

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

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

Synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。

既然ReentrantLock是类,那么它就提供了比 Synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上: 

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

ReentrantLock可以获取各种锁的信息

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

另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,Synchronized 操作的应该是对象头中markword。

ReentrantLock是Lock的实现类,是一个互斥的同步锁。

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

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

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

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

可以响应中断请求:与Synchronized不同,当获取到锁的线程被中断时,能够响应中断,中断异常将会被抛出,同时锁会被释放。

可以实现公平锁。

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

从性能角度,Synchronized早期实现比较低效,对比ReentrantLock,大多数场景性能都相差较大。但是在Java6中对其进行了非常多的改进,在竞争不激烈时,Synchronized的性能要优于ReetrantLock;在高竞争情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。

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

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

一样,一样就可重入了。

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

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

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

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

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

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

6、请谈谈 ReadWriteLock 和 StampedLock。

虽然ReentrantLock和Synchronized简单实用,但是行为上有一定局限性,要么不占,要么独占。实际应用场景中,有时候不需要大量竞争的写操作,而是以并发读取为主,为了进一步优化并发操作的粒度,Java提供了读写锁。

读写锁基于的原理是多个读操作不需要互斥,如果读锁试图锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作结束,这样就可以自动保证不会读取到有争议的数据。

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

读写锁看起来比Synchronized的粒度似乎细一些,但在实际应用中,其表现也并不尽如人意,主要还是因为相对比较大的开销。

所以,JDK在后期引入了StampedLock,在提供类似读写锁的同时,还支持优化读模式。优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先试着修改,然后通过validate方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。

7、如何让Java的线程彼此同步?你了解过哪些同步器?请分别介绍下。

JUC中的同步器三个主要的成员:CountDownLatch、CyclicBarrier和Semaphore,通过它们可以方便地实现很多线程之间协作的功能。CountDownLatch叫倒计数,允许一个或多个线程等待某些操作完成。看几个场景:

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

模拟并发,我需要启动100个线程去同时访问某一个地址,我希望它们能同时并发,而不是一个一个的去执行。用法:CountDownLatch构造方法指明计数数量,被等待线程调用

countDown将计数器减1,等待线程使用await进行线程等待。一个简单的例子:

CyclicBarrier叫循环栅栏,它实现让一组线程等待至某个状态之后再全部同时执行,而且当所有等待线程被释放后,CyclicBarrier可以被重复使用。CyclicBarrier的典型应用场景是用来等待并发线程结束。

CyclicBarrier的主要方法是await(),await()每被调用一次,计数便会减少1,并阻塞住当前线程。当计数减至0时,阻塞解除,所有在此CyclicBarrier上面阻塞的线程开始运行。

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

Semaphore,Java版本的信号量实现,用于控制同时访问的线程个数,来达到限制通用资源访问的目的,其原理是通过acquire()获取一个许可,如果没有就等待,而release()释放一个许可。如果Semaphore的数值被初始化为1,那么一个线程就可以通过acquire进入互斥状态,本质上和互斥锁是非常相似的。但是区别也非常明显,比如互斥锁是有持有者的,而对于Semaphore这种计数器结构,虽然有类似功能,但其实不存在真正意义的持有者,除非我们进行扩展包装。

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

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

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

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

进行任务,并自动进行重置。

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

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

果)。

Java线程池

1线程池都有哪些状态?

线程池有5种状态:Running、Shut Down、Stop、Tidying、Terminated。线程池各个状态切换框架图:

Java多线程面试题_第2张图片

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

接收的参数不一样

submit有返回值,而execute没有submit方便Exception处理

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

在Java中,所谓的线程池中的“线程”,其实是被抽象为了一个静态内部类Worker,它基于AQS实现,存放在线程池的HashSetworkers成员变量中;而需要执行的任务则存放在成员变量workQueue(BlockingQueueworkQueue)中。

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

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

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

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

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

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

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

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

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

每当我们调用execute()方法添加一个任务时,线程池会做如下判断:

如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

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

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

如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常RejectExecutionException。

当一个线程完成任务时,它会从队列中取下一个任务来执行。当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断。

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

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

1.SingleThreadExecutor线程池

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

corePoolSize:1,只有一个核心线程在工作。

maximumPoolSize:1。

keepAliveTime:0L。

workQueue:newLinkedBlockingQueue(),其缓冲队列是无界的。

2.NewFixedThreadPool线程池

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

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

corePoolSize:nThreads

maximumPoolSize:nThreads

keepAliveTime:0L

workQueue:newLinkedBlockingQueue(),其缓冲队列是无界的。

3.CachedThreadPool线程池

CachedThreadPool是无界线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。

线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。SynchronousQueue是一个是缓冲区为1的阻塞队列。

缓存型池子通常用于执行一些生存期很短的异步型任务,因此在一些面向连接的daemon型SERVER中用得不多。但对于生存期短的异步任务,它是Executor的首选。

corePoolSize:0

maximumPoolSize:Integer.MAX_VALUE

keepAliveTime:60L

workQueue:newSynchronousQueue(),一个是缓冲区为1的阻塞队列。(有一个必须要消费完才能插入另外一个)

4.ScheduledThreadPool线程池

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

corePoolSize:corePoolSize

maximumPoolSize:Integer.MAX_VALUE

keepAliveTime:DEFAULT_KEEPALIVE_MILLIS

workQueue:newDelayedWorkQueue()

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

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

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

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

Java内存模型

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

Java的内存模型定义了程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。

此处的变量包括实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为这些是线程私有的,不会被共享,所以不存在竞争问题。

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

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

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

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

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

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

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

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

lock和unlock

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

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

read和write

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

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

·把read操作从主内存得到的变量值放入工作内存的变量副本中。

·把工作内存的变量值传送到主内存,以便write。

use和assgin

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

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

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

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

显然不是的。基于volatile变量的运算在并发下不一定是安全的。

volatile变量在各个线程的工作内存,不存在一致性问题(各个线程的工作内存中volatile变量,每次使用前都要刷新到主内存)。

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

4、ThreadLocal是什么?有哪些使用场景?

线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如web服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java应用就存在内存泄露的风险。

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

ThreadLocal这是Java提供的一种保存线程私有信息的机制,因为其在整个线程生命周期内有效,所以可以方便地在一个线程关联的不同业务模块之间传递信息,比如事务ID、Cookie等上下文相关信息。ThreadLocal为每一个线程维护变量的副本,把共享数据的可见范围限制在同一个线程之内,其实现原理是,在ThreadLocal类中有一个

Map,用于存储每一个线程的变量的副本。

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

使用ThreadLocal要注意remove!

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

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

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

7、请对比下 ThreadLocal 对比 Synchronized 的异同。

ThreadLocal和Synchonized都用于解决多线程并发访问,防止任务在共享资源上产生冲突。但是ThreadLocal与Synchronized有本质的区别。

Synchronized用于实现同步机制,是利用锁的机制使变量或代码块在某一时该只能被一个线程访问,是一种“以时间换空间”的方式。

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

你可能感兴趣的:(java,jvm,面试)