并发知识不管在学习、面试还是工作过程中都非常非常重要,看完本文,相信绝对能助你一臂之力。
线程是进程的子集,一个进程可以有很多线程。每个进程都有自己的内存空间,可执行代码和唯一进程标识符(PID)。
每条线程并行执行不同的任务。不同的进程使用不同的内存空间(线程自己的堆栈),而所有的线程共享一片相同的内存空间(进程主内存)。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。
这个问题是上题的后续,大家都知道我们可以通过继承Thread类或者调用Runnable接口来实现线程,问题是,那个方法更好呢?什么情况下使用它?这个问题很容易回答,如果你知道Java不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是调用Runnable接口好了。
这个问题经常被问到,但还是能从此区分出面试者对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 的一个普通方法调用,还是在主线程里执行。
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 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
学过操作系统的朋友都知道产生死锁必须具备以下四个条件:
我们只要破坏产生死锁的四个条件中的其中一个就可以了。
我们对线程 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 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
CyclicBarrier 和 CountDownLatch 都可以用来让一组线程等待其它线程。与 CyclicBarrier 不同的是,CountdownLatch 不能重新使用。
对于不同的操作系统,有多种方法来获得Java进程的线程堆栈。当你获取线程堆栈时,JVM会把所有线程的状态存到日志文件或者输出到控制台。在Windows你可以使用Ctrl + Break组合键来获取线程堆栈,Linux下用kill -3命令。你也可以用jstack这个工具来获取,它对线程id进行操作,你可以用jps这个工具找到id。
15、Java中的同步集合与并发集合有什么区别?
17、什么是ThreadLocal变量?
ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数。
ThreadLocal是一种线程封闭技术。ThreadLocal提供了get和set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
Java内存模型规定和指引Java程序在不同的内存架构、CPU和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系定义了一些规则让程序员在并发编程时思路更清晰。比如,先行发生关系确保了:
我强烈建议大家阅读《Java并发编程实践》第十六章来加深对Java内存模型的理解。
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()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
21、Java中Runnable和Callable有什么不同?
22、哪些操作释放锁,哪些不释放锁?
23、如何正确的终止线程?
24、interrupt(), interrupted(), isInterrupted()的区别?
25、synchronized的锁对象是哪些?
26、volatile和synchronized的区别是什么?
27、什么是缓存一致性协议?
因为CPU是运算很快,而主存的读写很忙,所以在程序运行中,会复制一份数据到高速缓存,处理完成在将结果保存主存.
这样存在一些问题,在多核CPU中多个线程,多个线程拷贝多份的高速缓存数据,最后在计算完成,刷到主存的数据就会出现覆盖
所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
Synchronized 与Lock都是可重入锁,同一个线程再次进入同步代码的时候.可以使用自己已经获取到的锁
Synchronized是悲观锁机制,独占锁。而Locks.ReentrantLock是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。ReentrantLock适用场景
某个线程在等待一个锁的控制权的这段时间需要中断
需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程,锁可以绑定多个条件。
具有公平锁功能,每个到来的线程都将排队等候。
29、Volatile如何保证内存可见性?
竞态条件会导致程序在并发情况下出现一些bugs。多线程对一些资源的竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不确定的bugs。这种bugs很难发现而且会重复出现,因为线程间的随机竞争。
31、为什么wait, notify 和 notifyAll这些方法不在thread类里面?
明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的.
区别:
这两种方式最大区别就是对于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项:
33、Synchronized 用过吗,其原理是什么?
这是一道 Java 面试中几乎百分百会问到的问题,因为只要是程序员就一定会通过或者接触过Synchronized。
答:Synchronized 是由 JVM 实现的一种实现互斥同步的一种方式,如果 你查看被 Synchronized 修饰过的程序块编译后的字节码,会发现, 被 Synchronized 修饰过的程序块,在编译前后被编译器生成了monitorenter 和 monitorexit 两 个 字 节 码 指 令 。
这两个指令是什么意思呢?
在虚拟机执行到 monitorenter 指令时,首先要尝试获取对象的锁: 如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁 的计数器 +1;当执行 monitorexit 指令时将锁计数器 -1;当计数器 为 0 时,锁就被释放了。如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一 个线程释放为止。
Java 中 Synchronize 通过在对象头设置标记,达到了获取锁和释放 锁的目的。
34、上面提到获取对象的锁,这个“锁”到底是什么?如何确定对象的锁?
答:“锁”的本质其实是 monitorenter 和 monitorexit 字节码指令的一 个 Reference 类型的参数,即要锁定和解锁的对象。我们知道,使用Synchronized 可以修饰不同的对象,因此,对应的对象锁可以这么确 定:
注意,当一个对象被锁住时,对象里面所有用 Synchronized 修饰的 方法都将产生堵塞,而对象里非 Synchronized 修饰的方法可正常被 调用,不受锁影响。
35、什么是可重入性,为什么说 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,则释放该锁。
36、JVM 对 Java 的原生锁做了哪些优化?
在 Java 6 之前,Monitor 的实现完全依赖底层操作系统的互斥锁来 实现,也就是我们刚才在问题二中所阐述的获取/释放锁的逻辑。
由于 Java 层面的线程与操作系统的原生线程有映射关系,如果要将一 个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换 到内核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代 JDK中做了大量的优化。一种优化是使用自旋锁,即在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程 执行阻塞操作,避免了用户态到内核态的切换。
现代 JDK 中还提供了三种不同的 Monitor 实现,也就是三种不同的锁:
这三种锁使得 JDK 得以优化 Synchronized 的运行,当 JVM 检测 到不同的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级、 降级。
37、为什么说 Synchronized 是非公平锁?
答:非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等 待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁, 这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。
38、为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理 又是什么?什么是 CAS,它有什么特性?
答:Synchronized 显然是一个悲观锁,因为它的并发策略是悲观的:不管是否会产生竞争,任何的数据操作都必须要加锁、用户态核心态转 换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略。先进行操作,如果没有其他线程征用数据,那操作就成功了; 如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施。这种 乐观的并发策略的许多实现不需要线程挂起,所以被称为非阻塞同步。
乐观锁的核心算法是 CAS(Compareand Swap,比较并交换),它涉 及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相 等时才将内存值修改为新值。这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一 样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操 作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此 块内存。
CAS 具有原子性,它的原子性由CPU 硬件指令实现保证,即使用JNI 调用 Native 方法调用由 C++ 编写的硬件级别指令,JDK 中提 供了 Unsafe 类执行这些操作。
39、乐观锁一定就是好的吗?
答:乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也 有缺点:
40、谈一谈AQS框架。
AQS(AbstractQueuedSynchronizer 类)是一个用来构建锁和同步器 的框架,各种Lock 包中的锁(常用的有 ReentrantLock、 ReadWriteLock) , 以 及 其 他 如 Semaphore、 CountDownLatch, 甚 至是早期的 FutureTask 等,都是基于 AQS 来构建。
41、ReentrantLock 是如何实现可重入性的?
答:ReentrantLock 内部自定义了同步器 Sync(Sync 既实现了 AQS, 又实现了 AOS,而 AOS 提供了一种互斥锁持有的方式),其实就是 加锁的时候通过 CAS 算法,将线程对象放到一个双向链表中,每次获 取锁的时候,看下当前维护的那个线程 ID 和当前请求的线程 ID 是否 一样,一样就可重入了。
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();
}
}
}
43、Java 中的线程池是如何实现的?
44、线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?
答:显然不是的。线程池默认初始化后不启动 Worker,等待有请求时才启动。每当我们调用 execute() 方法添加一个任务时,线程池会做如下判 断:
当一个线程完成任务时,它会从队列中取下一个任务来执行。当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断。
如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
45、什么是竞争条件?如何发现和解决竞争?
两个线程同步操作同一个对象,使这个对象的最终状态不明——叫做竞争条件。竞争条件可以在任何应该由程序员保证原子操作的,而又忘记使用synchronized的地方。
唯一的解决方案就是加锁。
Java有两种锁可供选择:
46、很多人都说要慎用 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)算法的基础,这种算法可以不用锁和阻塞来达到同步的目的。
https://www.jianshu.com/p/e65f7b6c2a94 https://www.cnblogs.com/jxldjsn/p/10872154.html https://www.cnblogs.com/sgh1023/p/10297322.html https://blog.csdn.net/u011780616/article/details/95339236 https://www.jianshu.com/p/ca98ca34b47e