Java并发多线程高频面试题


并发知识无论在学习、面试仍是工做过程当中都很是很是重要,看完本文相信绝对能助你一臂之力。java

一、线程和进程有什么区别?

线程是进程的子集,一个进程能够有不少线程。每一个进程都有本身的内存空间,可执行代码和惟一进程标识符(PID)。程序员

每条线程并行执行不一样的任务。不一样的进程使用不一样的内存空间(线程本身的堆栈),而全部的线程共享一片相同的内存空间(进程主内存)。别把它和栈内存搞混,每一个线程都拥有单独的栈内存用来存储本地数据。web

二、实现多线程的方式有哪些?

  • 继承Thread类:Java单继承,不推荐;面试

  • 实现Runnable接口:Thread类也是继承Runnable接口,推荐;算法

  • 实现Callable接口:实现Callable接口,配合FutureTask使用,有返回值;数据库

  • 使用线程池:复用,节约资源;编程

  • 更多方式能够参考个人文章使用Java Executor框架实现多线程windows

三、用Runnable仍是Thread?

这个问题是上题的后续,你们都知道咱们能够经过继承Thread类或者调用Runnable接口来实现线程,问题是,那个方法更好呢?什么状况下使用它?这个问题很容易回答,若是你知道Java不支持类的多重继承,但容许你调用多个接口。因此若是你要继承其余类,固然是调用Runnable接口好了。数组

  • Runnable和Thread二者最大的区别是Thread是类而Runnable是接口,至于用类仍是用接口,取决于继承上的实际须要。Java类是单继承的,实现多个接口能够实现相似多继承的操做。缓存

  • 其次, Runnable就至关于一个做业,而Thread才是真正的处理线程,咱们须要的只是定义这个做业,而后将做业交给线程去处理,这样就达到了松耦合,也符合面向对象里面组合的使用,另外也节省了函数开销,继承Thread的同时,不只拥有了做业的方法run(),还继承了其余全部的方法。

  • 当须要建立大量线程的时候,有如下不足:①线程生命周期的开销很是高;②资源消耗;③稳定性。

  • 若是两者均可以选择不用,那就不用。由于Java这门语言发展到今天,在语言层面提供的多线程机制已经比较丰富且高级,彻底不用在线程层面操做。直接使用Thread和Runnable这样的“裸线程”元素比较容易出错,还须要额外关注线程数等问题。建议:简单的多线程程序,使用Executor。复杂的多线程程序,使用一个Actor库,首推Akka。

  • 若是必定要在Runnable和Thread中选择一个使用,选择Runnable。

四、Thread 类中的start() 和 run() 方法有什么区别?

这个问题常常被问到,但仍是能今后区分出面试者对Java线程模型的理解程度。start()方法被用来启动新建立的线程,并且start()内部调用了run()方法,JDK 1.8源码中start方法的注释这样写到:Causes this thread to begin execution; the Java Virtual Machine calls the <code>run</code> method of this thread.这和直接调用run()方法的效果不同。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程,JDK 1.8源码中注释这样写:The result is that two threads are running concurrently: the current thread (which returns from the call to the <code>start</code> method) and the other thread (which executes its <code>run</code> method).。

new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就能够开始运行了。start() 会执行线程的相应准备工做,而后自动执行 run() 方法的内容,这是真正的多线程工做。而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,因此这并非多线程工做。

总结:调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,仍是在主线程里执行。

五、说说 sleep() 方法和 wait() 方法区别和共同点?

  • 二者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。

  • 二者均可以暂停线程的执行。

  • Wait 一般被用于线程间交互/通讯,sleep 一般被用于暂停执行。

  • wait() 方法被调用后,线程不会自动苏醒,须要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。

六、说说并发与并行的区别?

  • 并发:同一时间段,多个任务都在执行 (单位时间内不必定同时执行);

  • 并行:单位时间内,多个任务同时执行。

七、说说线程的生命周期和状态?

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不一样状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。

线程在生命周期中并非固定处于某一个状态而是随着代码的执行在不一样状态之间切换。Java 线程状态变迁以下图所示(图源《Java 并发编程艺术》4.1.4 节):

由上图能够看出:线程建立以后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程得到了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。

操做系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJava:Java Thread Life Cycle and Thread States),因此 Java 系统通常将这两个状态统称为 RUNNABLE(运行中) 状态 。

当线程执行 wait()方法以后,线程进入 WAITING(等待)状态。进入等待状态的线程须要依靠其余线程的通知才可以返回到运行状态,而 TIME_WAITING(超时等待) 状态至关于在等待状态的基础上增长了超时限制,好比经过 sleep(long millis)方法或 wait(long millis)方法能够将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的状况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()方法以后将会进入到 TERMINATED(终止) 状态。

八、什么是线程死锁?

多个线程同时被阻塞,它们中的一个或者所有都在等待某个资源被释放。因为线程被无限期地阻塞,所以程序不可能正常终止。

以下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,因此这两个线程就会互相等待而进入死锁状态。

下面经过一个例子来讲明线程死锁,代码模拟了上图的死锁的状况 (代码来源于《并发编程之美》):

public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2

public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();

new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}

输出:

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

线程 A 经过 synchronized (resource1) 得到 resource1 的监视器锁,而后经过 Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 获得执行而后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,而后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。

学过操做系统的朋友都知道产生死锁必须具有如下四个条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。

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

  3. 不剥夺条件:线程已得到的资源在末使用完以前不能被其余线程强行剥夺,只有本身使用完毕后才释放资源。

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

九、如何避免线程死锁?

咱们只要破坏产生死锁的四个条件中的其中一个就能够了。

  • 破坏互斥条件:这个条件咱们没有办法破坏,由于咱们用锁原本就是想让他们互斥的(临界资源须要互斥访问)。

  • 破坏请求与保持条件:一次性申请全部的资源。

  • 破坏不剥夺条件:占用部分资源的线程进一步申请其余资源时,若是申请不到,能够主动释放它占有的资源。

  • 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

咱们对线程 2 的代码修改为下面这样就不会产生死锁了。

new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 2").start();

输出:

Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2

Process finished with exit code 0

咱们分析一下上面的代码为何避免了死锁的发生?

线程 1 首先得到到 resource1 的监视器锁,这时候线程 2 就获取不到了。而后线程 1 再去获取 resource2 的监视器锁,能够获取到。而后线程 1 释放了对 resource一、resource2 的监视器锁的占用,线程 2 获取到就能够执行了。这样就破坏了破坏循环等待条件,所以避免了死锁。

十、什么是死锁,活锁?

  • 死锁:多个线程都没法得到资源继续执行。能够经过避免一个线程获取多个锁;一个锁占用一个资源;使用定时锁;数据库加解锁在一个链接中。

  • 死锁的必要条件:环路等待,不可剥夺,请求保持,互斥条件

  • 活锁:线程之间相互谦让资源,都没法获取全部资源继续执行。

十一、Java中CyclicBarrier 和 CountDownLatch有什么不一样?

CyclicBarrier 和 CountDownLatch 均可以用来让一组线程等待其它线程。与 CyclicBarrier 不一样的是,CountdownLatch 不能从新使用。

  • CountDownLatch是一种灵活的闭锁实现,能够使一个或者多个线程等待一组事件发生。闭锁状态包括一个计数器,改计数器初始化为一个正数,表示须要等待的事件数量。countDown方法递减计数器,表示有一个事件发生了,而await方法等待计数器到达0,表示全部须要等待的事情都已经发生。若是计数器的值非零,那么await就会一直阻塞知道计数器的值为0,或者等待的线程中断,或者等待超时。

  • CyclicBarrier适用于这样的状况:你但愿建立一组任务,他们并行地执行工做,而后在进行下一个步骤以前等待,直至全部任务都完成。它使得全部的并行任务都将在栅栏出列队,所以能够一致的向前移动。这很是像CountDownLatch,只是CountDownLatch是只触发一次的事件,而CyclicBarrier能够屡次重用。

十二、Java中的同步集合与并发集合有什么区别?

  • 同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在Java1.5以前程序员们只有同步集合来用且在多线程并发的时候会致使争用,阻碍了系统的扩展性。Java5介绍了并发集合像ConcurrentHashMap,不只提供线程安全还用锁分离和内部分区等现代技术提升了可扩展性。

  • 同步容器是线程安全的。同步容器将全部对容器状态的访问都串行化,以实现他们的线程安全性。这种方法的代价是严重下降并发性,当多个线程竞争容器的锁时,吞吐量将严重下降。并发容器是针对多个线程并发访问设计的,改进了同步容器的性能。经过并发容器来代替同步容器,能够极大地提升伸缩性并下降风险。

1三、你如何在Java中获取线程堆栈?

对于不一样的操做系统,有多种方法来得到Java进程的线程堆栈。当你获取线程堆栈时,JVM会把全部线程的状态存到日志文件或者输出到控制台。在Windows你能够使用Ctrl + Break组合键来获取线程堆栈,Linux下用kill -3命令。你也能够用jstack这个工具来获取,它对线程id进行操做,你能够用jps这个工具找到id。

1四、Java中ConcurrentHashMap的并发度是什么?

  • ConcurrentHashMap把实际map划分红若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度得到的,它是ConcurrentHashMap类构造函数的一个可选参数,默认值为16,这样在多线程状况下就能避免争用。

  • 并发度能够理解为程序运行时可以同时更新ConccurentHashMap且不产生锁竞争的最大线程数,实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度。ConcurrentHashMap默认的并发度为16,但用户也能够在构造函数中设置并发度。当用户设置并发度时,ConcurrentHashMap会使用大于等于该值的最小2幂指数做为实际并发度(假如用户设置并发度为17,实际并发度则为32)。运行时经过将key的高n位(n = 32 – segmentShift)和并发度减1(segmentMask)作位与运算定位到所在的Segment。segmentShift与segmentMask都是在构造过程当中根据concurrency level被相应的计算出来。

  • 若是并发度设置的太小,会带来严重的锁竞争问题;若是并发度设置的过大,本来位于同一个Segment内的访问会扩散到不一样的Segment中,CPU cache命中率会降低,从而引发程序性能降低。

1五、Java中的同步集合与并发集合有什么区别?

  • 同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在Java1.5以前程序员们只有同步集合来用且在多线程并发的时候会致使争用,阻碍了系统的扩展性。Java5介绍了并发集合像ConcurrentHashMap,不只提供线程安全还用锁分离和内部分区等现代技术提升了可扩展性。

  • 同步容器是线程安全的。同步容器将全部对容器状态的访问都串行化,以实现他们的线程安全性。这种方法的代价是严重下降并发性,当多个线程竞争容器的锁时,吞吐量将严重下降。并发容器是针对多个线程并发访问设计的,改进了同步容器的性能。经过并发容器来代替同步容器,能够极大地提升伸缩性并下降风险。

1六、Thread类中的yield方法有什么做用?

  • Yield方法能够暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法并且只保证当前线程放弃CPU占用而不能保证使其它线程必定能占用CPU,执行yield()的线程有可能在进入到暂停状态后立刻又被执行。

  • 线程让步:若是知道已经完成了在run()方法的循环的一次迭代过程当中所需的工做,就能够给线程调度机制一个暗示:你的工做已经作得差很少了,可让别的线程使用CPU了。这个暗示将经过调用yield()方法来作出(不过这只是一个暗示,没有任何机制保证它将会被采纳)。当调用yield()时,也是在建议具备相同优先级的其余线程能够运行。

  • yield()的做用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具备相同优先级的等待线程获取执行权;可是,并不能保证在当前线程调用yield()以后,其它具备相同优先级的线程就必定能得到执行权;也有多是当前线程又进入到“运行状态”继续运行!

1七、什么是ThreadLocal变量?

ThreadLocal是Java里一种特殊的变量。每一个线程都有一个ThreadLocal就是每一个线程都拥有了本身独立的一个变量,竞争条件被完全消除了。它是为建立代价高昂的对象获取线程安全的好方法,好比你能够用ThreadLocal让SimpleDateFormat变成线程安全的,由于那个类建立代价高昂且每次调用都须要建立不一样的实例因此不值得在局部范围使用它,若是为每一个线程提供一个本身独有的变量拷贝,将大大提升效率。首先,经过复用减小了代价高昂的对象的建立个数。其次,你在没有使用高代价的同步或者不变性的状况下得到了线程安全。线程局部变量的另外一个不错的例子是ThreadLocalRandom类,它在多线程环境中减小了建立代价高昂的Random对象的个数。

ThreadLocal是一种线程封闭技术。ThreadLocal提供了get和set等访问接口或方法,这些方法为每一个使用该变量的线程都存有一份独立的副本,所以get老是返回由当前执行线程在调用set时设置的最新值。

1八、Java内存模型是什么?

Java内存模型规定和指引Java程序在不一样的内存架构、CPU和操做系统间有肯定性地行为。它在多线程的状况下尤为重要。Java内存模型对一个线程所作的变更能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系定义了一些规则让程序员在并发编程时思路更清晰。好比,先行发生关系确保了:

  • 线程内的代码可以按前后顺序执行,这被称为程序次序规则。

  • 对于同一个锁,一个解锁操做必定要发生在时间上后发生的另外一个锁定操做以前,也叫作管程锁定规则。

  • 前一个对volatile的写操做在后一个volatile的读操做以前,也叫volatile变量规则。

  • 一个线程内的任何操做必需在这个线程的start()调用以后,也叫做线程启动规则。

  • 一个线程的全部操做都会在线程终止以前,线程终止规则。

  • 一个对象的终结操做必需在这个对象构造完成以后,也叫对象终结规则。

  • 可传递性

我强烈建议你们阅读《Java并发编程实践》第十六章来加深对Java内存模型的理解。

1九、Java中的volatile 变量是什么?

volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺乏同步类的状况下,多线程对成员变量的操做对其它线程是透明的。volatile变量能够保证下一个读取操做会在前一个写操做以后发生,就是上一题的volatile变量规则。

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操做通知到其余线程。当把变量声明为volatile类型后,编译器和运行时都会注意到这个变量是共享的,所以不会将变量上的操做和其余内存操做一块儿重排序。volatile变量不会被缓存在寄存器或者对其余处理器不可见的地方,所以在读取volatile类型的时候总会返回最新写入的值。

在访问volatile变量时不会执行加锁操做,所以也不会使执行线程阻塞,所以volatile变量是一种比synchronized关键字更轻量级的同步机制。

加锁机制既能够确保可见性又能够确保原子性,而volatile变量只能确保可见性。

20、volatile 变量和 atomic 变量有什么不一样?

这是个有趣的问题。首先,volatile 变量和 atomic 变量看起来很像,但功能却不同。Volatile变量能够确保先行关系,即写操做会发生在后续的读操做以前, 但它并不能保证原子性。例如用volatile修饰count变量那么 count++ 操做就不是原子性的。而AtomicInteger类提供的atomic方法可让这种操做具备原子性如getAndIncrement()方法会原子性的进行增量操做把当前值加一,其它数据类型和引用变量也能够进行类似操做。

2一、Java中Runnable和Callable有什么不一样?

  • Runnable和Callable都表明那些要在不一样的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在JDK1.5增长的。它们的主要区别是Callable的 call() 方法能够返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable能够返回装载有计算结果的Future对象。

  • Runnable是执行工做的独立任务,可是它不返回任何值。若是但愿任务在完成的时候可以返回一个值,那么能够实现Callable接口而不是Runnable接口。在Java SE5中引入的Callable是一种具备类型参数的泛型,它的类型参数表示的是从方法call()(而不是run())中返回的值,而且必须使用ExecutorService.submit()方法调用它。submit()方法会产生Future对象,它用Callable返回结果的特定类型进行了参数化。

2二、哪些操做释放锁,哪些不释放锁?

  • sleep(): 释放资源,不释放锁,进入阻塞状态,唤醒随机线程,Thread类方法。

  • wait(): 释放资源,释放锁,Object类方法。

  • yield(): 不释放锁,进入可执行状态,选择优先级高的线程执行,Thread类方法。

  • 若是线程产生的异常没有被捕获,会释放锁。

2三、如何正确的终止线程?

  • 使用共享变量,要用volatile关键字,保证可见性,可以及时终止。

  • 使用interrupt()和isInterrupted()配合使用。

2四、interrupt(), interrupted(), isInterrupted()的区别?

  • interrupt():设置中断标志;

  • interrupted():响应中断标志并复位中断标志;

  • isInterrupted():响应中断标志;

2五、synchronized的锁对象是哪些?

  • 普通方法是当前实例对象;

  • 同步方法快是括号中配置内容,能够是类Class对象,能够是实例对象;

  • 静态方法是当前类Class对象。

  • 只要不是同一个锁,就能够并行执行,同一个锁,只能串行执行。

  • 更多参考个人文章Java中Synchronized关键字简介(译)

2六、volatile和synchronized的区别是什么?

  1. volatile只能使用在变量上;而synchronized能够在类,变量,方法和代码块上。

  2. volatile至保证可见性;synchronized保证原子性与可见性。

  3. volatile禁用指令重排序;synchronized不会。

  4. volatile不会形成阻塞;synchronized会。

2七、什么是缓存一致性协议?

由于CPU是运算很快,而主存的读写很忙,因此在程序运行中,会复制一份数据到高速缓存,处理完成在将结果保存主存.

这样存在一些问题,在多核CPU中多个线程,多个线程拷贝多份的高速缓存数据,最后在计算完成,刷到主存的数据就会出现覆盖

因此就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每一个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,若是发现操做的变量是共享变量,即在其余CPU中也存在该变量的副本,会发出信号通知其余CPU将该变量的缓存行置为无效状态,所以当其余CPU须要读取这个变量时,发现本身缓存中缓存该变量的缓存行是无效的,那么它就会从内存从新读取。

2八、Synchronized关键字、Lock,并解释它们之间的区别?

Synchronized 与Lock都是可重入锁,同一个线程再次进入同步代码的时候.能够使用本身已经获取到的锁

Synchronized是悲观锁机制,独占锁。而Locks.ReentrantLock是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。ReentrantLock适用场景

某个线程在等待一个锁的控制权的这段时间须要中断

须要分开处理一些wait-notify,ReentrantLock里面的Condition应用,可以控制notify哪一个线程,锁能够绑定多个条件。

具备公平锁功能,每一个到来的线程都将排队等候。

2九、Volatile如何保证内存可见性?

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。

  • 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

30、 Java中什么是竞态条件?

竞态条件会致使程序在并发状况下出现一些bugs。多线程对一些资源的竞争的时候就会产生竞态条件,若是首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不肯定的bugs。这种bugs很难发现并且会重复出现,由于线程间的随机竞争。

3一、为何wait, notify 和 notifyAll这些方法不在thread类里面?

明显的缘由是JAVA提供的锁是对象级的而不是线程级的,每一个对象都有锁,经过线程得到。若是线程须要等待某些锁那么调用对象中的wait()方法就有意义了。若是wait()方法定义在Thread类中,线程正在等待的是哪一个锁就不明显了。简单的说,因为wait,notify和notifyAll都是锁级别的操做,因此把他们定义在Object类中由于锁属于对象。

3二、Java中synchronized 和 ReentrantLock 有什么不一样?

类似点:

这两种同步方式有不少类似之处,它们都是加锁方式同步,并且都是阻塞式的同步,也就是说当若是一个线程得到了对象锁,进入了同步块,其余访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的.

区别:

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

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

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

  • 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程能够选择放弃等待,这至关于Synchronized来讲能够避免出现死锁的状况。

  • 公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序得到锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是建立的非公平锁,能够经过参数true设为公平锁,但公平锁表现的性能不是很好。

  • 锁绑定多个条件,一个ReentrantLock对象能够同时绑定对个对象。

3三、synchronized 用过吗,其原理是什么?

这是一道 Java 面试中几乎百分百会问到的问题,由于只要是程序员就必定会经过或者接触过 synchronized。

答:synchronized 是由 JVM 实现的一种实现互斥同步的一种方式,若是 你查看被 synchronized 修饰过的程序块编译后的字节码,会发现, 被 synchronized 修饰过的程序块,在编译先后被编译器生成了monitorenter 和 monitorexit 两 个 字 节 码 指 令 。

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

在虚拟机执行到 monitorenter 指令时,首先要尝试获取对象的锁: 若是这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁 的计数器 +1;当执行 monitorexit 指令时将锁计数器 -1;当计数器 为 0 时,锁就被释放了。若是获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另一 个线程释放为止。

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

3四、上面提到获取对象的锁,这个“锁”究竟是什么?如何肯定对象的锁?

答:“锁”的本质实际上是 monitorenter 和 monitorexit 字节码指令的一 个 Reference 类型的参数,即要锁定和解锁的对象。咱们知道,使用Synchronized 能够修饰不一样的对象,所以,对应的对象锁能够这么确 定:

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

  2. 若是没有明确指定:

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

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

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

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

先来看一下维基百科关于可重入锁的定义:

若一个程序或子程序能够“在任意时刻被中断而后操做系统调度执行另一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程能够再次进入并执行它,仍然得到符合设计时预期的结果。与多线程并发执行的线程安全不一样,可重入强调对单个线程执行时从新进入同一个子程序仍然是安全的。

通俗来讲:当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由本身持有的对象锁时,若是该锁是重入锁,请求就会成功,不然阻塞。

要证实synchronized是否是可重入锁,咱们先来看一段代码:

package com.mzc.common.concurrent.synchronize;

/**
* <p class="detail">
* 功能: 证实synchronized为何是可重入锁
* </p>
*
* @author Moore
* @ClassName Super class.
* @Version V1.0.
* @date 2020.02.07 15:34:12
*/

public class SuperClass {

public synchronized void doSomething(){
System.out.println("father is doing something,the thread name is:"+Thread.currentThread().getName());
}
}

package com.mzc.common.concurrent.synchronize;

/**
* <p class="detail">
* 功能: 证实synchronized为何是可重入锁
* </p>
*
* @author Moore
* @ClassName Sub class.
* @Version V1.0.
* @date 2020.02.07 15:34:41
*/

public class SubClass extends SuperClass {

public synchronized void doSomething() {
System.out.println("child is doing doSomething,the thread name is:" + Thread.currentThread().getName());
// 调用本身类中其余的synchronized方法
doAnotherThing();
}

private synchronized void doAnotherThing() {
// 调用父类的synchronized方法
super.doSomething();
System.out.println("child is doing anotherThing,the thread name is:" + Thread.currentThread().getName());
}

public static void main(String[] args) {
SubClass child = new SubClass();
child.doSomething();
}
}

经过运行main方法,先一下结果:

child is doing doSomething,the thread name is:main
father is doing something,the thread name is:main
child is doing anotherThing,the thread name is:main

由于这些方法输出了相同的线程名称,代表即便递归使用synchronized也没有发生死锁,证实其是可重入的。

还看不懂?那我就再解释下!

这里的对象锁只有一个,就是 child 对象的锁,当执行 child.doSomething 时,该线程得到 child 对象的锁,在 doSomething 方法内执行 doAnotherThing 时再次请求child对象的锁,由于synchronized 是重入锁,因此能够获得该锁,继续在 doAnotherThing 里执行父类的 doSomething 方法时第三次请求 child 对象的锁,一样可获得。若是不是重入锁的话,那这后面这两次请求锁将会被一直阻塞,从而致使死锁。

因此在 java 内部,同一线程在调用本身类中其余 synchronized 方法/块或调用父类的 synchronized 方法/块都不会阻碍该线程的执行。就是说同一线程对同一个对象锁是可重入的,并且同一个线程能够获取同一把锁屡次,也就是能够屡次重入。由于java线程是基于“每线程(per-thread)”,而不是基于“每调用(per-invocation)”的(java中线程得到对象锁的操做是以线程为粒度的,per-invocation 互斥体得到对象锁的操做是以每调用做为粒度的)。

重入锁实现可重入性原理或机制是:每个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程均可能得到该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,而且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程若是再次请求这个锁,就能够再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,若是计数器为 0,则释放该锁。

3六、JVM 对 Java 的原生锁作了哪些优化?

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

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

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

  • 偏向锁(Biased Locking)

  • 轻量级锁

  • 重量级锁

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

  • 当没有竞争出现时,默认会使用偏向锁。JVM 会利用 CAS 操做,在对象头上的 Mark Word 部分设置线程ID,以表示这个对象偏向于当前线程,因此并不涉及真正的互斥锁,因 为在不少应用场景中,大部分对象生命周期中最多会被一个线程锁定, 使用偏斜锁能够下降无竞争开销。

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

  • 轻量级锁依赖 CAS 操做 Mark Word 来试图获取锁,若是重试成功, 就使用普通的轻量级锁;不然,进一步升级为重量级锁。

3七、为何说 Synchronized 是非公平锁?

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

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

答:Synchronized 显然是一个悲观锁,由于它的并发策略是悲观的:无论是否会产生竞争,任何的数据操做都必需要加锁、用户态核心态转 换、维护锁计数器和检查是否有被阻塞的线程须要被唤醒等操做。

随着硬件指令集的发展,咱们能够使用基于冲突检测的乐观并发策略。先进行操做,若是没有其余线程征用数据,那操做就成功了; 若是共享数据有征用,产生了冲突,那就再进行其余的补偿措施。这种 乐观的并发策略的许多实现不须要线程挂起,因此被称为非阻塞同步。

乐观锁的核心算法是 CAS(Compareand Swap,比较并交换),它涉 及到三个操做数:内存值、预期值、新值。当且仅当预期值和内存值相 等时才将内存值修改成新值。这样处理的逻辑是,首先检查某块内存的值是否跟以前我读取时的一 样,如不同则表示期间此内存值已经被别的线程更改过,舍弃本次操 做,不然说明期间没有其余线程对此内存值操做,能够把新值设置给此 块内存。

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

3九、乐观锁必定就是好的吗?

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

  • 乐观锁只能保证一个共享变量的原子操做。若是多一个或几个变量,乐 观锁将变得力不从心,但互斥锁能轻易解决,无论对象数量多少及对象颗粒度大小。

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

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

40、谈一谈AQS框架。

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

  1. AQS 在内部定义了一个 volatile int state 变量,表示同步状态:当线 程调用 lock 方法时 ,若是 state=0,说明没有任何线程占有共享资源 的锁,能够得到锁并将 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 的时候,其实就是两个队列的互相移动。

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

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

4二、Java中Semaphore是什么?

Java中的Semaphore是一种新的同步类,它是一个计数信号。从概念上讲,从概念上讲,信号量维护了一个许可集合。若有必要,在许可可用前会阻塞每个 acquire(),而后再获取该许可。每一个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。可是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采起相应的行动。信号量经常用于多线程的代码中,好比数据库链接池。

package com.mzc.common.concurrent;

import java.util.concurrent.Semaphore;

/**
* <p class="detail">
* 功能: Semaphore Test
* </p>
*
* @author Moore
* @ClassName Test semaphore.
* @Version V1.0.
* @date 2020.02.07 20:11:00
*/

public class TestSemaphore {

static class Worker extends Thread{
private int num;
private Semaphore semaphore;
public Worker(int num,Semaphore semaphore){
this.num = num;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
// 抢许可
semaphore.acquire();
Thread.sleep(2000);
// 释放许可
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
// 机器数目,即5个许可
Semaphore semaphore = new Semaphore(5);
// 8个线程去抢许可
for (int i = 0; i < 8; i++){
new Worker(i,semaphore).start();
}
}
}

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

  • 在 Java 中,所谓的线程池中的“线程”,实际上是被抽象为了一个静态内部类 Worker,它基于 AQS 实现,存放在线程池的HashSet<Worker> workers 成员变量中;

  • 而须要执行的任务则存放在成员变量 workQueue(BlockingQueue<Runnable> workQueue)中。这样,整个线程池实现的基本思想就是: 从 workQueue 中不断取出须要执行的任务,放在 Workers 中进行处理。

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

答:显然不是的。线程池默认初始化后不启动 Worker,等待有请求时才启动。每当咱们调用 execute() 方法添加一个任务时,线程池会作以下判断:

  • 若是正在运行的线程数量小于 corePoolSize,那么立刻建立线程运行这个任务;

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

  • 若是这时候队列满了,并且正在运行的线程数量小于maximumPoolSize,那么仍是要建立非核心线程马上运行这个任务;

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

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

若是当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。因此线程池的全部任务完成后,它最终会收缩到 corePoolSize 的大小。

4五、什么是竞争条件?如何发现和解决竞争?

两个线程同步操做同一个对象,使这个对象的最终状态不明——叫作竞争条件。竞争条件能够在任何应该由程序员保证原子操做的,而又忘记使用synchronized的地方。

惟一的解决方案就是加锁。

Java有两种锁可供选择:

  • 对象或者类(class)的锁。每个对象或者类都有一个锁。使用synchronized关键字获取。synchronized加到static方法上面就使用类锁,加到普通方法上面就用对象锁。除此以外synchronized还能够用于锁定关键区域块(Critical Section)。synchronized以后要制定一个对象(锁的携带者),并把关键区域用大括号包裹起来。synchronized(this){// critical code}。

  • 显示构建的锁(java.util.concurrent.locks.Lock),调用lock的lock方法锁定关键代码。

4六、不少人都说要慎用 ThreadLocal,谈谈你的理解,使用ThreadLocal 须要注意些什么?

答:使 用 ThreadLocal 要 注 意 remove!

ThreadLocal 的实现是基于一个所谓的 ThreadLocalMap,在ThreadLocalMap 中,它的 key 是一个弱引用。一般弱引用都会和引用队列配合清理机制使用,可是 ThreadLocal 是 个例外,它并无这么作。这意味着,废弃项目的回收依赖于显式地触发,不然就要等待线程结 束,进而回收相应 ThreadLocalMap! 这就是不少 OOM 的来源,因此一般都会建议,应用必定要本身负责 remove,而且不要和线程池配 合,由于 worker 线程每每是不会退出的。

线程与锁

哲学家问题
问题描述:五位哲学家围绕一个圆桌就作,桌上在每两位哲学家之间摆着一支筷子。哲学家的状态多是“思考”或者“饥饿”。若是饥饿,哲学家将拿起他两边的筷子就餐一段时间。进餐结束后,哲学家就会放回筷子。

代码实现:

public class Philosopher extends Thread {
private Chopstick left;
private Chopstick right;
private Random random;

public Philosopher(Chopstick left, Chopstick right) {
this.left = left;
this.right = right;
random = new Random();
}

@Override
public void run() {
try {
while (true) {
Thread.sleep(random.nextInt(1000)); // 思考一下子
synchronized (left) { // 拿起左手的筷子
synchronized (right) { // 拿起右手的筷子
Thread.sleep(random.nextInt(1000)); // 进餐
}
}
}
} catch (InterruptedException e) {
// handle exception
}
}
}

规避方法:
一个线程使用多把锁时,就须要考虑死锁的可能。幸运的是,若是老是按照一个全局的固定的顺序得到多把锁,就能够避开死锁。

public class Philosopher2 extends Thread {
private Chopstick first;
private Chopstick second;
private Random random;

public Philosopher2(Chopstick left, Chopstick right) {
if (left.getId() < right.getId()) {
first = left;
second = right;
} else {
first = right;
second = left;
}
random = new Random();
}

@Override
public void run() {
try {
while (true) {
Thread.sleep(random.nextInt(1000)); // 思考一下子
synchronized (first) { // 拿起左手的筷子
synchronized (second) { // 拿起右手的筷子
Thread.sleep(random.nextInt(1000)); // 进餐
}
}
}
} catch (InterruptedException e) {
// handle exception
}
}
}

外星方法
定义:调用这类方法时,调用者对方法的实现细节并不了解。

public class Downloader extends Thread {
private InputStream in;
private OutputStream out;
private ArrayList<ProgressListener> listeners;

public Downloader(URL url, String outputFilename) throws IOException {
in = url.openConnection().getInputStream();
out = new FileOutputStream(outputFilename);
listeners = new ArrayList<>();
}

public synchronized void addListener(ProgressListener listener) {
listeners.add(listener);
}

public synchronized void removeListener(ProgressListener listener) {
listeners.remove(listener);
}

private synchronized void updateProgress(int n) {
for (ProgressListener listener : listeners) {
listener.onProgress(n);
}
}

@Override
public void run() {
// ...
}
}

这里 updateProgress(n) 方法调用了一个外星方法,这个外星方法可能作任何事,好比持有另一把锁。

能够这样来修改:

private  void updateProgress(int n) {
ArrayList<ProgressListener> listenersCopy;
synchronized (this) {
listenersCopy = (ArrayList<ProgressListener>) listeners.clone();
}

for (ProgressListener listener : listenersCopy) {
listener.onProgress(n);
}
}

线程与锁模型带来的三个主要危害:

竞态条件
死锁
内存可见性


规避原则:

对共享变量的全部访问都须要同步化
读线程和写线程都须要同步化
按照约定的全局顺序来获取多把锁
当持有锁时避免调用外星方法
持有锁的时间应尽量短


内置锁
内置锁限制:

没法中断 一个线程由于等待内置锁而进入阻塞以后,就没法中断该线程了;
没法超时 尝试获取内置锁时,没法设置超时;
不灵活 得到内置锁,必须使用 synchronized 块。

synchronized( object ) {
<<使用共享资源>>
}

ReentrantLock
其提供了显式的lock和unlock, 能够突破以上内置锁的几个限制。

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
<<使用共享资源>>
} finally {
lock.unlock()
}

可中断
使用内置锁时,因为阻塞的线程没法被中断,程序不可能从死锁中恢复。

内置锁:制造一个死锁:

public class Uninterruptible {

public static void main(String[] args) throws InterruptedException {
final Object o1 = new Object();
final Object o2 = new Object();

Thread t1 = new Thread(){
@Override
public void run() {
try {
synchronized (o1) {
Thread.sleep(1000);
synchronized (o2) {}
}
} catch (InterruptedException e) {
System.out.println("Thread-1 interrupted");
}
}
};

Thread t2 = new Thread(){
@Override
public void run() {
try {
synchronized (o2) {
Thread.sleep(1000);
synchronized (o1) {}
}
} catch (InterruptedException e) {
System.out.println("Thread-2 interrupted");
}
}
};

t1.start();
t2.start();
Thread.sleep(2000);
t1.interrupt();
t2.interrupt();
t1.join();
t2.join();
}

}

ReentrantLock 替代内置锁:

public class Interruptible {

public static void main(String[] args) {
final ReentrantLock lock1 = new ReentrantLock();
final ReentrantLock lock2 = new ReentrantLock();

Thread t1 = new Thread(){
@Override
public void run() {
try {
lock1.lockInterruptibly();
Thread.sleep(1000);
lock2.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("Thread-1 interrupted");
}
}
};

// ...
}
}

可超时
利用 ReentrantLock 超时设置解决哲学家问题:

public class Philosopher3 extends Thread {
private ReentrantLock leftChopstick;
private ReentrantLock rightChopstick;
private Random random;

public Philosopher3(ReentrantLock leftChopstick, ReentrantLock rightChopstick) {
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
random = new Random();
}

@Override
public void run() {
try {
while (true) {
Thread.sleep(random.nextInt(1000)); // 思考一下子
leftChopstick.lock();
try {
// 获取右手边的筷子
if (rightChopstick.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
Thread.sleep(random.nextInt(1000));
} finally {
rightChopstick.unlock();
}
} else {
// 没有获取到右手边的筷子,放弃并继续思考
}
} finally {
leftChopstick.unlock();
}
}
} catch (InterruptedException e) {
// ...
}
}
}

交替锁

场景:在链表中插入一个节点时,使用交替锁只锁住链表的一部分,而不是用锁保护整个链表。

线程安全链表:

public class ConcurrentSortedList {  // 降序有序链表

private class Node {
int value;
Node pre;
Node next;

ReentrantLock lock = new ReentrantLock();

Node() {}

Node(int value, Node pre, Node next) {
this.value = value;
this.pre = pre;
this.next = next;
}
}

private final Node head;
private final Node tail;

public ConcurrentSortedList() {
this.head = new Node();
this.tail = new Node();
this.head.next = tail;
this.tail.pre = head;
}

public void insert(int value) {
Node current = this.head;
current.lock.lock();
Node next = current.next;
try {
while (true) {
next.lock.lock();
try {
if (next == tail || next.value < value) {
Node newNode = new Node(value, current, next);
next.pre = newNode;
current.next = newNode;
return;
}
} finally {
current.lock.unlock();
}
current = next;
next = current.next;

}
} finally {
next.lock.unlock();
}
}

public int size() {
Node current = tail; // 这里为何要是从尾部开始遍历呢?由于插入是从头部开始遍历的
int count = 0;
while (current != head) {
ReentrantLock lock = current.lock;
lock.lock();
try {
++count;
current = current.pre;
} finally {
lock.unlock();
}
}
return count;
}
}

条件变量

并发编程常常要等待某个条件知足。好比从队列删除元素必须等待队列不为空、向缓存添加数据前须要等待缓存有足够的空间。

条件变量模式:

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newConditiion();

lock.lock();
try {
while(!<<条件为真>>) { // 条件不为真时
condition.await();
}
<<使用共享资源>>
} finnally {
lock.unlock();
}

一个条件变量须要与一把锁关联,线程在开始等待条件以前必须得到锁。获取锁后,线程检查等待的条件是否为真。

若是为真,线程将继续执行并解锁;
若是不为真,线程会调用 await(),它将原子的解锁并阻塞等待条件。
当另外一个线程调用 signal() 或 signalAll(),意味着对应的条件可能变为真, await() 将原子的恢复运行并从新加锁。

条件变量解决哲学家就餐问题:

public class Philosopher4 extends Thread {

private boolean eating;
private Philosopher4 left;
private Philosopher4 right;
private ReentrantLock table;
private Condition condition;
private Random random;

public Philosopher4(ReentrantLock table) {
this.eating = false;
this.table = table;
this.condition = table.newCondition();
this.random = new Random();
}

public void setLeft(Philosopher4 left) {
this.left = left;
}

public void setRight(Philosopher4 right) {
this.right = right;
}

@Override
public void run() {
try {
while (true) {
think();
eat();
}
} catch (InterruptedException e) {
// ...
}
}

private void think() throws InterruptedException {
this.table.lock();
try {
this.eating = false;
this.left.condition.signal();
this.right.condition.signal();
} finally {
table.unlock();
}
Thread.sleep(1000);
}

private void eat() throws InterruptedException {
this.table.lock();
try {
while (left.eating || right.eating) {
this.condition.await();
}
this.eating = true;
} finally {
this.table.unlock();
}
Thread.sleep(1000);
}
}

原子变量

原子变量是无锁(lock-free) 非阻塞(non-blocking)算法的基础,这种算法能够不用锁和阻塞来达到同步的目的。



15个Java多线程面试题及回答

1.如今有T一、T二、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

这个线程问题一般会在第一轮或电话面试阶段被问到,目的是检测你对”join”方法是否熟悉。这个多线程问题比较简单,能够用join方法实现。

2.在Java中Lock接口比synchronized块的优点是什么?你须要实现一个高效的缓存,它容许多个用户读,但只容许一个用户写,以此来保持它的完整性,你会怎样去实现它?

lock接口在多线程和并发编程中最大的优点是它们为读和写分别提供了锁,它能知足你写像ConcurrentHashMap这样的高性能数据结构和有条件的阻塞。Java线程面试的问题愈来愈会根据面试者的回答来提问。我强烈建议在你去参加多线程的面试以前认真读一下Locks,由于当前其大量用于构建电子交易终统的客户端缓存和交易链接空间。

3.在java中wait和sleep方法的不一样?

一般会在电话面试中常常被问到的Java线程面试问题。最大的不一样是在等待时wait会释放锁,而sleep一直持有锁。Wait一般被用于线程间交互,sleep一般被用于暂停执行。

4.用Java实现阻塞队列。

这是一个相对艰难的多线程面试问题,它能达到不少的目的。第一,它能够检测侯选者是否能实际的用Java线程写程序;第二,能够检测侯选者对并发场景的理解,而且你能够根据这个问不少问题。若是他用wait()和notify()方法来实现阻塞队列,你能够要求他用最新的Java 5中的并发类来再写一次。

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

与上面的问题很相似,但这个问题更经典,有些时候面试都会问下面的问题。在Java中怎么解决生产者——消费者问题,固然有不少解决方法,我已经分享了一种用阻塞队列实现的方法。有些时候他们甚至会问怎么实现哲学家进餐问题。

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

这是我最喜欢的Java线程面试问题,由于即便死锁问题在写多线程并发程序时很是广泛,可是不少侯选者并不能写deadlock free code(无死锁代码?),他们很挣扎。只要告诉他们,你有N个资源和N个线程,而且你须要全部的资源来完成一个操做。为了简单这里的n能够替换为2,越大的数据会使问题看起来更复杂。经过避免Java中的死锁来获得关于死锁的更多信息。

7.什么是原子操做,Java中的原子操做是什么?

很是简单的java线程面试问题,接下来的问题是你须要同步一个原子操做。

8.Java中的volatile关键是什么做用?怎样使用它?在Java中它跟synchronized方法有什么不一样?

自从Java 5和Java内存模型改变之后,基于volatile关键字的线程问题愈来愈流行。应该准备好回答关于volatile变量怎样在并发环境中确保可见性、顺序性和一致性。

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

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

10.你将如何使用thread dump?你将如何分析Thread dump?

在UNIX中你能够使用kill -3,而后thread dump将会打印日志,在windows中你能够使用”CTRL+Break”。很是简单和专业的线程面试问题,可是若是他问你怎样分析它,就会很棘手。

11.为何咱们调用start()方法时会执行run()方法,为何咱们不能直接调用run()方法?

这是另外一个很是经典的java多线程面试问题。这也是我刚开始写线程程序时候的困惑。如今这个问题一般在电话面试或者是在初中级Java面试的第一轮被问到。这个问题的回答应该是这样的,当你调用start()方法时你将建立新的线程,而且执行在run()方法里的代码。可是若是你直接调用run()方法,它不会建立新的线程也不会执行调用线程的代码。阅读我以前写的《start与run方法的区别》这篇文章来得到更多信息。

12.Java中你怎样唤醒一个阻塞的线程?

这是个关于线程和阻塞的棘手的问题,它有不少解决方法。若是线程遇到了IO阻塞,我而且不认为有一种方法能够停止线程。若是线程由于调用wait()、sleep()、或者join()方法而致使的阻塞,你能够中断线程,而且经过抛出InterruptedException来唤醒它。我以前写的《How to deal with blocking methods in java》有不少关于处理线程阻塞的信息。

13)在Java中CycliBarriar和CountdownLatch有什么区别?

这个线程问题主要用来检测你是否熟悉JDK5中的并发包。这两个的区别是CyclicBarrier能够重复使用已经经过的障碍,而CountdownLatch不能重复使用。

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

另外一个多线程经典面试问题,并不直接跟线程有关,但间接帮助不少。这个java面试问题能够变的很是棘手,若是他要求你写一个不可变对象,或者问你为何String是不可变的。

15.你在多线程环境中遇到的共同的问题是什么?你是怎么解决它的?

多线程和并发程序中常遇到的有Memory-interface、竞争条件、死锁、活锁和饥饿。问题是没有止境的,若是你弄错了,将很难发现和调试。这是大多数基于面试的,而不是基于实际应用的Java线程问题。


60道最多见的Java多线程面试题


  1. 多线程有什么用?

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

  3. ava实现线程有哪几种方式?

  4. 启动线程方法start()和run()有什么区别?

  5. 怎么终止一个线程?如何优雅地终止线程?

  6. 一个线程的生命周期有哪几种状态?它们之间如何流转的?

  7. 线程中的wait()和sleep()方法有什么区别?

  8. 多线程同步有哪几种方法?

  9. 什么是死锁?如何避免死锁?

  10. 多线程之间如何进行通讯?

  11. 线程怎样拿到返回结果?

  12. violatile关键字的做用?

  13. 新建T一、T二、T3三个线程,如何保证它们按顺序执行?

  14. 怎么控制同一时间只有3个线程运行?

  15. 为何要使用线程池?

  16. 经常使用的几种线程池并讲讲其中的工做原理。

  17. 线程池启动线程submit()和execute()方法有什么不一样?

  18. CyclicBarrier和CountDownLatch的区别?

  19. 什么是活锁、饥饿、无锁、死锁?

  20. 什么是原子性、可见性、有序性?

  21. 什么是守护线程?有什么用?

  22. 怎么中断一个线程?如何保证中断业务不影响?

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

  24. 什么是重入锁?

  25. Synchronized有哪几种用法?

  26. Fork/Join框架是干什么的?

  27. 线程数过多会形成什么异常?

  28. 说说线程安全的和不安全的集合。

  29. 什么是CAS算法?在多线程中有哪些应用。

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

  31. Jdk中排查多线程问题用什么命令?

  32. 线程同步须要注意什么?

  33. 线程wait()方法使用有什么前提?

  34. Fork/Join框架使用有哪些要注意的地方?

  35. 线程之间如何传递数据?

  36. 保证"可见性"有哪几种方式?

  37. 说几个经常使用的Lock接口实现锁。

  38. ThreadLocal是什么?有什么应用场景?

  39. ReadWriteLock有什么用?

  40. FutureTask是什么?

  41. 怎么唤醒一个阻塞的线程?

  42. 不可变对象对多线程有什么帮助?

  43. 多线程上下文切换是什么意思?

  44. Java中用到了什么线程调度算法?

  45. Thread.sleep(0)的做用是什么?

  46. Java内存模型是什么,哪些区域是线程共享的,哪些是不共享的

  47. 什么是乐观锁和悲观锁?

  48. Hashtable的size()方法为何要作同步?

  49. 同步方法和同步块,哪一种更好?

  50. 什么是自旋锁?

  51. Runnable和Thread用哪一个好?

  52. Java中notify和notifyAll有什么区别?

  53. 为何wait/notify/notifyAll这些方法不在thread类里面?

  54. 为何wait和notify方法要在同步块中调用?

  55. 为何你应该在循环中检查等待条件?

  56. Java中堆和栈有什么不一样?

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

  58. 如何建立线程安全的单例模式?

  59. 什么是阻塞式方法?

  60. 提交任务时线程池队列已满会时发会生什么?

本文分享自微信公众号 - JAVA高级架构(gaojijiagou)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索