Java 分布式并发编程
如想了解更多更全面的Java必备内容可以阅读:所有JAVA必备知识点面试题文章目录:
文章目录
- Java 分布式并发编程
- 1、结合源码说说线程状态的枚举值都有哪些?
- 2、结合源码分析线程的启动原理?
- 3、结合源码分析interrupt线程的终止原理?
- 4、请说说Thread中interrupted()和isInterrupted()分别有什么作用?
- 5、为什么Object.wait、Thread.sleep和Thread.join都会抛出InterruptedException?
- 7、synchronized 锁是如何存储的呢?
- 8、偏向锁的基本原理?
- 9、偏向锁的获取和撤销逻辑?
- 10、轻量级锁的基本原理
- 11、重量级锁的基本原理?
- 12、wait/notify的基本原理?
- 13、了解哪些常见的缓存一致性协议?
- 14、什么是内存屏障?
- 15、谈谈你对JMM的了解?
- 16、什么是happens-before(先行发生)规则?
- 17、volatile实现可见性的实现原理?
- 18、volatile实现有序性的实现原理?
- 19、那么禁止指令重排序又是如何实现的呢?
- 20、对Lock接口的了解?
- 21、说说对AQS的了解?
- 22、请结合ReentrantLock说说Sync、FairSync和NonfairSync的区别?
- 23、结合源码分析AQS中acquire(int arg)的实现原理?
- 24、结合源码分析AQS中tryAcquire(int arg)的实现原理?
- 25、结合源码分析AQS中addWaiter(Node mode)的实现原理?
- 26、结合源码分析AQS中enq(final Node node)的实现原理?
- 27、结合源码分析AQS中acquireQueued(final Node node, int arg)的实现原理?
- 28、在AQS中Node的状态(waitStatus)分别有哪些?
- 29、说说对Unsafe类的了解?
- 30、为什么在释放锁的时候是从尾节点(tail)进行扫描?
- 31、对(JUC:java.util.concurrent)内的Condition接口了解多少?
- 32、结合源码分析Condition接口中await()的实现原理?
- 33、ThreadA 这个节点是否存在于AQS队列中呢?
- 34、结合源码分析Condition接口中signal()的实现原理?
- 35、基于源码分析Condition接口中signal()和signalAll()的的区别?
- 36、对Condition接口中await()和signal()/signalAll()的总结
- 37、对CountDownLatch的了解?
- 38、结合源码分析CountDownLatch中await()的实现原理?
- 39、结合源码分析CountDownLatch中countDown()的实现原理?
- 40、对Semaphore的了解有多少?
- 41、对CyclicBarrier的了解有多少?
- 42、讲一下ConcurrentHashMap的实现原理?
- 43、JDK1.8 中ConcurrentHashMap为什么使用synchronized锁替换 可重入锁 ReentrantLock?
- 44、ConcurrentHashMap 迭代器是强一致性还是弱一致性?
- 45、ConcurrentHashMap 的并发度是什么?
- 46、ConcurrentHashMap不支持key或者value为null的原因?
- 47、为什么ConcurrentHashMap的get方法不需要加锁?
- 48、ConcurrentHashMap 的 size()方法原理分析?
- 49、结合jdk1.8源码分析一下ConcurrentHashMap的addCount() 添加元素个数的实现原理?
- 50、在jdk1.8中ConcurrentHashMap什么时候会触发扩容?
- 51、在jdk1.8中ConcurrentHashMap扩容的原理是什么?
- 52、jdk并发包中提供的阻塞队列有哪些?
- 53、在ArrayBlockingQueue阻塞队列中,添加元素操作add(e)、offer(e)、put(e)和offer(e,time,unit)的区别?
- 54、在ArrayBlockingQueue阻塞队列中,移除操作remove()、poll()、take()和poll(time,unit)的区别?
- 55、Executors中提供哪些方式来创建线程池?
- 56、讲一下创建线程池时ThreadPoolExecutor参数的含义?
- 57、线程池的实现原理?
- 58、在线程池中,ctl变量的作用?
- 59、线程池的状态有哪些?
- 60、线程池有哪些常见的拒绝策略?
- 61、阿里开发手册不建议使用Executors 去创建线程池?
- 62、如何合理配置线程池的大小 ?
- 63、线程池是如何关闭的?
- 64、线程池容量能动态调整吗?
- 65、线程池中submit与execute区别?
- 66、ThreadLocal工作原理是什么?
- 67、ThreadLocal如何解决Hash冲突?
- 68、ThreadLocal的内存泄露是怎么回事?
- 69、为什么ThreadLocalMap的key是弱引用?
1、结合源码说说线程状态的枚举值都有哪些?
###jdk1.8 Thread类部分源码如下:
public enum State {
/**
* 尚未启动的线程的线程状态。线程被构建,但是还没有调用start方法。
*/
NEW,
/**
* 可运行线程的线程状态。处于可运行状态的线程正在Java虚拟机中执行,但它可能正在等待来自操作系统(如处理器)的其他资源。
*/
RUNNABLE,
/**
* 等待监视器锁的阻塞线程的线程状态。
*
* 表示线程进入等待状态,也就是线程因为某种原因放弃了 CPU 使用权,阻塞也分为几种情况:
* ➢ 等待阻塞:运行的线程执行 wait 方法,jvm 会把当前线程放入到等待队列。
* ➢ 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么jvm 会把当前的线程放入到锁池中。
* ➢ 其他阻塞:运行的线程执行 Thread.sleep 或者 t.join 方法,或者发出了I/O请求时,JVM 会把当前线程设置为阻塞状态,当sleep结束、join线程终止、io 处理完毕则线程恢复。
*/
BLOCKED,
/**
* 等待线程的线程状态。线程由于调用以下方法之一而处于等待状态:
* @link Object#wait()
* @link Thread#join()
* @link LockSupport#park()
* 处于等待状态的线程正在等待另一个线程执行特定的操作。
*
* 例如,在一个对象上调用了【object .wait()】的线程正在等待另一个线程调用【object .notify()】或【object . notifyall()】。
* 在这个对象。调用了【thread .join()】的线程正在等待指定的线程终止。
*/
WAITING,
/**
* 具有指定等待时间的等待线程的线程状态。线程由于调用了以下方法中的一种,并且指定了正的等待时间,所以处于定时等待状态:
* @link Thread.#sleep(long)
* @link Object#wait(long)
* @link Thread.#join(long)
* @link LockSupport#parkNanos
* @link LockSupport#parkUntil
*/
TIMED_WAITING,
/**
* 终止线程的线程状态。线程已完成执行。
*/
TERMINATED;
}
2、结合源码分析线程的启动原理?
启动一个线程为什么是调用 start 方法,而不是 run 方法,这做一个简单的分析,先简单看一下 start 方法的定义:
###jdk1.8 Thread类部分源码如下:
class Thread implements Runnable {
private static native void registerNatives();
static {
//第一件事就是:注册Native方法
registerNatives();
}
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();//调用 start 方法实际上是调用一个 native 方法start0()来启动一个线程
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void stat0();
}
registerNatives的本地方法的定义在文件: Thread.c 源码链接。 所以:start0()实际会执行JVM_StartThread方法。
找到 jvm.cpp 这个文件;这个文件需要下载 hotspot 的源码才能找到:
#### jvm.cpp 文件部分hotspot源码如下:
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_StartThread");
.....
//创建线程
native_thread = new JavaThread(&thread_entry, sz);
.....
//启动线程
Thread::start(native_thread);
JVM_END
JavaThread 的定义在 hotspot 的源码中 thread.cpp 文件代码:
#### Thread.cpp 文件部分hotspot源码如下:
JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
·····
//调用对应的操作系统创建线程
os::create_thread(this, thr_type, stack_sz);
_safepoint_visible = false;
}
void Thread::start(Thread* thread) {
trace("start", thread);
·····
//调用对应的操作系统启动线程
os::start_thread(thread);
}
}
3、结合源码分析interrupt线程的终止原理?
当其他线程通过调用当前线程的 interrupt 方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。
interrupt通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是类似stop方式武断地将线程停止。
###jdk1.8 Thread类部分源码如下:
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
private native void interrupt0();
调用链:Thread#interrupt0() - - -> jvm.cpp#JVM_Interrupt - - -> Thread.cpp#Thread::interrupt(thr) - - -> os_各种操作系统.cpp#os::interrupt。以 os_linux.cpp 文件为例:
void os::interrupt(Thread* thread) {
assert(Thread::current() == thread || Threads_lock->owned_by_self(),
"possibility of dangling Thread pointer");
OSThread* osthread = thread->osthread();/获取本地线程对象
if (!osthread->interrupted()) {
osthread->set_interrupted(true);//*********设置中断线程为true
//内存屏障,作用:是修改的值对其他线程可见,原理类似volatile
OrderAccess::fence();
//如果线程调用的sleep方法,调用unpark()将其唤醒。
ParkEvent * const slp = thread->_SleepEvent ;
if (slp != NULL) slp->unpark() ;
}
// For JSR166. Unpark even if interrupt status already was set
if (thread->is_Java_thread())
((JavaThread*)thread)->parker()->unpark();
ParkEvent * ev = thread->_ParkEvent ;
if (ev != NULL) ev->unpark() ;
}
thread.interrupt()方法实际就是设置一个interrupted状态标识为true、并且通过ParkEvent的unpark方法来唤醒线程。
- 对于 synchronized 阻塞的线程,被唤醒以后会继续尝试获取锁,如果失败仍然可能被park。
- 在调用ParkEvent的park方法之前,会先判断线程的中断状态,如果为true,会清除当前线程的中断标识。
4、请说说Thread中interrupted()和isInterrupted()分别有什么作用?
- interrupted:设置中断标识的线程复位【通过thread.interrupt()将中断标志被设置成true,也就是通过isInterrupted()获得的值是true,interrupted将这个值改成false】,并返回复位是否成功。是属于当前线程的,是当前线程对外界中断信号的一个响应,表示自己已经得到了中断信号。【当抛出InterruptedException异常时也会复位】
- isInterrupted:线程是否被中断。默认情况下没有被中断返回 false、通过thread.interrupt中断后变成了true。
### 简单使用的例子如下,不考虑性能及其业务等。
public class ThreadInterruptdDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println("before:"+Thread.currentThread().isInterrupted());//true
System.out.println("interrupted1:"+Thread.interrupted());//第一次将true改成false,返回复位是否成功-true成功
System.out.println("interrupted2:"+Thread.interrupted());//已经是false,不需要复位,返回复位是否成功-false失败
System.out.println("interrupted3:"+Thread.interrupted());//已经是false,不需要复位,返回复位是否成功-false失败
System.out.println("after:"+Thread.currentThread().isInterrupted());//false
}
}
},"ThreadInterruptdDemo");
thread.start();
TimeUnit.SECONDS.sleep(1);
//主线程对thread做中断操作,将中断标志由false改成true
thread.interrupt();
}
}
5、为什么Object.wait、Thread.sleep和Thread.join都会抛出InterruptedException?
这几个方法有一个共同点,都是属于阻塞的方法而阻塞方法的释放会取决于一些外部的事件,但是阻塞方法可能因为等不到外部的触发事件而导致无法终止,所以它允许一个线程请求自己来停止它正在做的事情。
由于线程调用了interrupt()中断方法,那么Object.wait、Thread.sleep等被阻塞的线程被唤醒以后会通过is_interrupted方法判断中断标识的状态变化,如果发现中断标识为true,则先清除中断标识,然后抛出InterruptedException
需要注意的是,InterruptedException异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理取决于线程本身,比如
- 直接捕获异常不做任何处理
- 将异常往外抛出
- 停止当前线程,并打印异常信息
7、synchronized 锁是如何存储的呢?
观察synchronized的整个语法发现,synchronized(lock)是基于lock这个对象的生命周期来控制锁粒度的。
对象在内存中的布局:对象头(Hander)、实例数据(Instance Data)、对其填充(Padding)。
- 对象头(Hander):包含运行时元数据(包含:哈希值、GC分代年龄/阙值、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳)和类型指针(指向方法区元数据的Class信息)。
- 实例数据(Instance Data):对象存储的有效信息,包含程序代码中定义的各种类型的字段以及父类的。(规则:相同宽度的字段总被分配在一起;父类定义的变量会出现在子类之前)。
- 对其填充(Padding):不是必须得,起到占位符的作用。
当我们在 Java 代码中,使用 new 创建一个对象实例的时候,(hotspot 虚拟机)JVM 层面实际上会创建一个instanceOopDesc 对象。
instanceOopDesc的定义在Hotspot源码中的instanceOop.hpp文件中。instanceOopDesc继承自oopDesc。
oopDesc 的定义载 Hotspot 源码中的oop.hpp 文件中,oopDesc 的定义包含两个成员,分别是 _mark 和 _metadata。
#### oop.hpp 文件部分源码:
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;//mark 表示对象标记、属于 markOop 类型,也就是接下来看看Mark World
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;//_metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中 Klass 表示普通指针
在 Hotspot 中,markOop 的定义在 markOop.hpp 文件中,部分源代码如下
class markOopDesc: public oopDesc {
private:
// Conversion
uintptr_t value() const { return (uintptr_t) this; }
public:
// Constants
enum { age_bits = 4,//分代年龄
lock_bits = 2,//锁标识
biased_lock_bits = 1,//是否为偏向锁
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,//对象的hash值
cms_bits = LP64_ONLY(1) NOT_LP64(0),
epoch_bits = 2//偏向锁的时间戳
};
Mark word 记录了对象和锁有关的信息,当某个对象被synchronized 关键字当成同步锁时,那么围绕这个锁的一系列操作都和 Mark word 有关系。
Mark Word 在 32 位虚拟机的长度是 32bit、在 64 位虚拟机的长度是 64bit。
Mark Word 里面存储的数据会随着锁标志位的变化而变化,Mark Word 可能变化为存储以下 5 中情况:
8、偏向锁的基本原理?
当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。
而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了。
绝大部分情况下一定会存在 2 个以上的线程竞争,那么如果开启偏向锁,反而会提升获取锁的资源消耗。
【UseBiasedLocking】来设置开启或关闭偏向锁。
9、偏向锁的获取和撤销逻辑?
获取和撤销逻辑:
- 检查对象头中是否存储当前线程
- 如果没有,则尝试通过 CAS 操作,把当前线程的 ID写入到 MarkWord
- 如果 CAS 成功。表示已经获得了锁对象的偏向锁,接着执行同步代码块。
- 如果 CAS 失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁。
- 如果有,不需要再次获得锁,可直接执行同步代码块。
偏向锁的撤销: 并不是把对象恢复到无锁可偏向状态,而是在获取偏向锁的过程中,发现 cas 失败也就是存在线程竞争时,直接把被偏向的锁对象升级到被加了轻量级锁的状态。
对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程有两种情况:
- 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于 CAS 重新偏向但前线程。
- 如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块。
简单流程图分析如下:
10、轻量级锁的基本原理
锁升级为轻量级锁之后,对象的 Markword 也会进行相应的的变化。升级为轻量级锁的过程:
- 线程在自己的栈桢中创建锁记录(LockRecord)。
- 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。
- 将锁记录中的Owner指针指向锁对象。
- 将锁对象的对象头的 MarkWord替换为指向锁记录的指针。
轻量级锁在加锁过程中,用到了自旋锁:就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。
注意:锁在原地循环的时候,是会消耗 cpu 的,就相当于在执行一个啥也没有的 for 循环。
所以,轻量级锁适用于那些同步代码块执行的很快的场景。
自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源。默认情况下自旋的次数是 10 次,可以通【preBlockSpin】来修改。
在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。
11、重量级锁的基本原理?
当轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起阻塞来等待被唤醒了。
每一个 JAVA 对象都会与一个监视器 monitor 关联,当一个线程想要执行一段被synchronized 修饰的同步方法或者代码块时,该线程得先获取到 synchronized 修饰的对象对应的 monitor。
- monitorenter:表示去获得一个对象监视器。
- monitorexit :表示释放 monitor 监视器的所有权。
monitor 依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。
重量级锁的加锁的基本流程:
12、wait/notify的基本原理?
- wait:表示持有对象锁的线程A准备释放对象锁和cpu资源,进入等待状态。
- notify:表示通知jvm唤醒某个竞争该对象锁的线程。其他竞争线程继续等待。
- notifyAll:唤醒所有竞争同一个对象锁的所有线程。
注意:三个方法都必须在synchronized同步关键字所限定的作用域中调用,否则会报错java.lang.IllegalMonitorStateException。
意思是因为没有同步,所以线程对对象锁的状态是不确定的,不能调用这些方法。
13、了解哪些常见的缓存一致性协议?
为了达到数据访问的一致,需要各个处理器在访问缓存时遵循一些协议,在读写时根据协议来操作,最常见的就是 MESI 协议。
在 MESI 协议中,每个缓存的缓存控制器不仅知道自己的读写操作,而且也监听其它缓存的读写操作。MESI表示缓存行的四种状态,分别是:
- M(Modify) 表示共享数据只缓存在当前CPU缓存中,并且是被修改状态,也就是缓存的数据和主内存中的数据不一致。
- E(Exclusive) 表示缓存的独占状态,数据只缓存在当前CPU 缓存中,并且没有被修改。
- S(Shared) 表示数据可能被多个 CPU 缓存,并且各个缓存中的数据和主内存数据一致。
- I(Invalid) 表示缓存已经失效。
各个 CPU 缓存行的状态是通过消息传递来进行的。如果CPU0 要对一个在缓存中共享的变量进行写入,首先需要发送一个失效的消息给到其他缓存了该数据的 CPU。并且要等到他们的确认回执。CPU0 在这段时间内都会处于阻塞状态。为了避免阻塞带来的资源浪费。在 cpu 中引入了 Store Bufferes。
CPU0 只需要在写入共享数据时,直接把数据写入到 store bufferes 中,同时发送数据无效消息,然后继续去处理其他指令。当收到其他所有CPU发送了告知收到了数据无效的消息时,再将 store bufferes 中的数据数据存储至cache line中。最后再从缓存行同步到主内存。
14、什么是内存屏障?
内存屏障:是将 store bufferes 中的指令写入到内存,从而使得其他访问同一共享内存的线程的可见性。
主要包括:lfence(读屏障)、sfence(写屏障)、mfence(全屏障)。
- 写屏障(Store Memory Barrier):告诉处理器在写屏障之前将存储缓存(store bufferes)中的数据同步到主内存。
- 读屏障(Load Memory Barrier):处理器在读屏障之后的读读取内存中的数据,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的。
- 全屏障(Full Memory Barrier):将屏障前的内存读写操作的结果提交到内存后,再执行屏障后的读写操作。
15、谈谈你对JMM的了解?
Java内存模型(Java Memory Model:JMM):不是真实存在的,而是一套规范,分为主内存、工作内存。
- 主内存:所有线程共享的,一般是实例对象、静态字段、数组对象等存储在堆内存中的变量。
- 工作内存:是每个线程独占的,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量,线程之间的共享变量值的传递都是基于主内存来完成。
JMM 提供了一些禁用缓存以及禁止重排序的方法,来解决可见性和有序性问题。如:volatile、synchronized、final;
为了保证内存可见性,Java 编译器在生成指令序列的适当位置会插入内存屏障来禁止特定类型的处理器的重排序。
16、什么是happens-before(先行发生)规则?
表示的是前一个操作的结果对于后续操作是可见的规则,所以它是一种表达多个线程之间对于内存的可见性。
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在happens-before关系。
- 程序次序规则(Program Order Rule):在一个线程内,书写在前面的操作先行发生于后面的操作。准确的说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支和循环等结构。
- volatile变量规则(Volatile Lock Rule):对于 volatile 修饰的变量的写的操作一定 happen-before 后续对于volatile变量的读操作。
- 传递性规则(Transitivity Rule):1 happens-before 2;2 happens-before 3;故而根据传递性规则可以推导出:1 happens-before 3。
- 线程启动规则(Thread Start Rule):Thread对象的start()方法,先行发生于对此线程的每一个操作动作。
- 线程join 规则(Thread join Rule):如果线程 A 执行操作ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 于线程A 从 ThreadB.join()操作成功返回。
- 监视器锁定规则(Monitor Lock Rule):对一个锁的解锁,happens-before 于随后对这个锁的加锁。
- 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,我们可以通过Thread.interrupted()方法检测到是否有中断发生。
- 对象终结规则(Finalizer Rule):一个对象的初始化完成先行发生于它的finalize()方法。
17、volatile实现可见性的实现原理?
#### 例子:
public class VolatileTest {
private static volatile VolatileTest instance = null;
private VolatileTest(){}
}
并将代码生成的处理器的汇编指令打印出来,如下:
putstatic的含义是给一个静态变量设置值,在上述代码中也就是给静态变量instance赋值,当有volatile修饰时,会发现在add前面都会加个一个lock指令。
lock指令在多核处理器下会引发下面的事件:将当前处理器的缓存行的数据写回到系统内存,同时使其他CPU里缓存了该内存地址的数据置为无效。
在一个处理器将自己缓存行的数据写回到系统内存后,其他的每个处理器就会通过嗅探在总线上传播的数据来检查自己缓存的数据是否已过期,当处理器发现自己缓存行对应的内存地址的数据被修改后,就会将自己缓存行缓存的数据设置为无效,当处理器要对这个数据进行修改操作的时候,会重新从系统内存中把数据读取到自己的缓存行,重新缓存。
总结:volatile可见性的实现就是借助了CPU的lock指令,通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:
- 写volatile时处理器会将缓存写回到主内存。
- 一个处理器的缓存写回到内存会导致其他处理器的缓存失效,从而其他处理器会重新从系统内存中把数据读取到自己的缓存行,重新缓存,实现了其他线程对对修改的数据可见。
18、volatile实现有序性的实现原理?
volatile有序性的保证就是通过禁止指令重排序来实现的。指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。
19、那么禁止指令重排序又是如何实现的呢?
加内存屏障。JMM为volatile加内存屏障有以下4种情况:
- 在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。
- 在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。
- 在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。
- 在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。
上述内存屏障的插入策略是非常保守的,比如一个volatile的写操作后面需要加上StoreStore和StoreLoad屏障,但这个写volatile后面可能并没有读操作,因此理论上只加上StoreStore屏障就可以,的确,有的处理器就是这么做的。但JMM这种保守的内存屏障插入策略能够保证在任意的处理器平台,volatile变量都是有序的。
20、对Lock接口的了解?
#### jdk1.8 Lock源码如下:
public interface Lock {
//获得锁
void lock();
//获取锁,除非当前线程是{@linkplain Thread#interrupt interrupted}
void lockInterruptibly() throws InterruptedException;
//只有在调用时锁是空闲的时,才获取锁
boolean tryLock();
//如果锁在给定的等待时间内是空闲的,并且当前线程还没有空闲,则获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
void unlock();
//返回绑定到这个{@code Lock}实例的 新的{@link Condition}实例。
Condition newCondition();
}
Lock 的类关系图:
21、说说对AQS的了解?
AQS,全称AbstractQueuedSynchronizer,它是一个同步队列也是Lock用来实现线程同步的核心组件。
AQS 是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备业务功能,所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能。
AQS的功能分为两种:独占锁(每次只能有一个线程持有锁,如:ReentrantLock)和共享锁(允许多个线程同时获取锁,如:ReadWriteLock)。
内部实现:
每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个Node其实是由线程封装,当线程争抢锁失败后会封装成Node加入到ASQ队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
添加节点:
- 新的线程封装成Node节点追加到同步队列的尾部,设置prev指针指向前一个节点,将前置节点的next指针指向当前节点。
- 通过CAS将tail重新指向新的尾部节点。
释放节点:
- 修改head节点指向下一个获得锁的节点。
注意:设置head节点不需要用CAS,原因是设置head节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要CAS保证。 - 新的获得锁的节点,将prev的指针指向null。
22、请结合ReentrantLock说说Sync、FairSync和NonfairSync的区别?
Sync、FairSync和NonfairSync都是ReentrantLock的静态内部类。
Sync继承自AbstractQueuedSynchronizer同步队列。
FairSync和NonfairSync都继承Sync抽象静态内部类。
#### jdk1.8 ReentrantLock类部分源码如下:
public class ReentrantLock implements Lock, java.io.Serializable {
······
abstract static class Sync extends AbstractQueuedSynchronizer{···}
static final class FairSync extends Sync{···}
static final class NonfairSync extends Sync{···}
······
}
FairSync和NonfairSync的区别:
-
NonfairSync(非公平锁):表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁。
#### jdk1.8 部分源码如下: final void lock() { //会先通过 CAS 进行抢占 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
-
FairSync(公平锁): 表示锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
#### jdk1.8 部分源码如下: final void lock() { acquire(1); }
23、结合源码分析AQS中acquire(int arg)的实现原理?
#### jdk1.8 部分源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
主要逻辑:
- 通过tryAcquire尝试获取独占锁,如果成功返回true,失败返回false。
- 如果tryAcquire失败,则会通过addWaiter方法,将当前线程封装成Node 添加到AQS 队列尾部。
- acquireQueued,将Node 作为参数,通过自旋去尝试获取锁。
- 如果通过tryAcquire失败并且acquireQueued(addWaiter(Node.EXCLUSIVE), arg)失败,则执行
- Thread.currentThread().interrupt()将当前线程中断。
24、结合源码分析AQS中tryAcquire(int arg)的实现原理?
tryAcquire方法具体的实现是由子类公平锁(FairSync)和非公平锁(NonfairSync)来实现的。
- 公平锁(FairSync)的tryAcquire方法:
#### jdk1.8 部分源码如下: protected final boolean tryAcquire(int acquires) { //获取当前执行的线程 final Thread current = Thread.currentThread(); //获得 state 的值 int c = getState(); //当 state=0 时,表示无锁状态 //当state>0 时,表示已经有线程获得了锁 //所以同一个线程多次获得同步锁的时候,state 会递增,比如重入 5 次,那么 state=5。而在释放锁的时候,同样需要释放 5 次直到 state=0其他线程才有资格获得锁。 if (c == 0) { if (!hasQueuedPredecessors() &&//查询是否有线程等待获取的时间超过当前线程 compareAndSetState(0, acquires)) {//cas替换state 的值,cas 成功表示获取锁成功 setExclusiveOwnerThread(current);//保存当前获得锁的线程,下次再来的时候不要再尝试竞争锁 return true; } } else if (current == getExclusiveOwnerThread()) {//如果同一个线程来获得锁,直接增加重入次数 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
- 非公平锁(NonfairSync)的tryAcquire方法:
#### jdk1.8 部分源码如下: final boolean nonfairTryAcquire(int acquires) { //获取当前执行的线程 final Thread current = Thread.currentThread(); //获得 state 的值 int c = getState(); 当 state=0 时,表示无锁状态 if (c == 0) { if (compareAndSetState(0, acquires)) {//cas替换state的值,cas成功表示获取锁成功 setExclusiveOwnerThread(current);//保存当前获得锁的线程,下次再来的时候不要再尝试竞争锁 return true; } } else if (current == getExclusiveOwnerThread()) {//如果同一个线程来获得锁,直接增加重入次数 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
25、结合源码分析AQS中addWaiter(Node mode)的实现原理?
根据AQS中acquire方法可知,当tryAcquire方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成Node。
#### jdk1.8 部分源码如下:
private Node addWaiter(Node mode) {//入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了 AQS 的独占锁功能
//把当前线程封装为Node
Node node = new Node(Thread.currentThread(), mode);
//tail是AQS中表示同步队列的队尾属性,默认是 null
Node pred = tail;
if (pred != null) {//tail 不为空的情况下,说明队列中存在节点
node.prev = pred;//把当前线程的Node的prev指向原来的尾节点
if (compareAndSetTail(pred, node)) {//通过cas把node加入到AQS队列尾部,也就是设置为tail尾节点。
pred.next = node;//设置成功以后,把原来尾节点的next指向当前节点
return node;
}
}
enq(node);//tail=null,把node添加到同步队列
return node;
}
主要逻辑:
- 把当前线程封装为Node
- 判断当前链表中的尾节点是否为空,如果不为空,则通过cas把当前线程的节点添加到AQS队列尾部。
- 如果为空或者cas失败,调用enq方法将节点添加到AQS队列。
26、结合源码分析AQS中enq(final Node node)的实现原理?
//enq 就是通过自旋操作把当前节点加入到队列中
#### jdk1.8 部分源码如下:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
27、结合源码分析AQS中acquireQueued(final Node node, int arg)的实现原理?
通过addWaiter方法把线程添加到链表后,会接着把Node作为参数传递给acquireQueued方法,去竞争锁。
#### jdk1.8 部分源码如下:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取当前节点的前一个节点
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {//如果是头节点,说明有资格去争抢锁
setHead(node);//获取锁成功,也就是比如:ThreadA 已经释放了锁,然后设置head为 ThreadB 获得执行权限
p.next = null; //把原 head 节点从链表中移除
failed = false;
return interrupted;
}
//ThreadA可能还没释放锁,使得ThreadB在执行tryAcquire时会返回false
if (shouldParkAfterFailedAcquire(p, node) &&//通过Node的状态来判断,ThreadA竞争锁失败以后是否应该被挂起。false-不需要,true-需要。
parkAndCheckInterrupt())//使用LockSupport.park 挂起当前线程编程 WATING 状态
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
28、在AQS中Node的状态(waitStatus)分别有哪些?
- SIGNAL:只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程
- CANCELLED:在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该 Node 的结点。过循环扫描链表把 CANCELLED 状态的节点移除。
- CONDITION:该节点当前位于条件队列上。在传输之前,它不会被用作同步队列节点,此时状态将设置为0。
- PROPAGATE:共享模式下,PROPAGATE 状态的线程处于可运行状态。是共享锁模式下的节点状态,处于这个状态下的节点,会对线程的唤醒进行传播。
- 0:初始状态。
#### jdk1.8 部分源码如下:
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
/**
* 状态字段,仅接受值:SIGNA、CANCELLED、CONDITION、PROPAGATE、0
**/
volatile int waitStatus;
········
}
29、说说对Unsafe类的了解?
Unsafe 类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Hadoop、Kafka等;
它提供了一些低层次操作,如直接内存访问、线程的挂起和恢复、CAS、线程同步、内存屏障。
30、为什么在释放锁的时候是从尾节点(tail)进行扫描?
存在其他线程调用 unlock 方法从 head开始往后遍历,由于 t.next=node 还没执行,意味着链表的关系还没有建立完整。就会导致遍历到某个节点的时候被中断。所以从后往前遍历,一定不会存在这个问题。
31、对(JUC:java.util.concurrent)内的Condition接口了解多少?
Condition:是J.U.C里面提供的多线程协调通信的工具类,它可以让某些线程一起等待某个条件(condition),只有满足条件时,线程才会被唤醒。
#### jdk1.8实现源码,如下:
public interface Condition {
//使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。直到收到信号或Thread#interrupt中断。把当前线程阻塞挂起。
void await() throws InterruptedException;
//使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。直到收到通知为止
void awaitUninterruptibly();
//使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。直到收到信号或被中断,或超过指定的等待时间
long awaitNanos(long nanosTimeout) throws InterruptedException;
//使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。直到收到信号或被中断,或超过指定的等待时间,这种方法在行为上等同于【awaitNanos(unit.toNanos(time)) > 0】
boolean await(long time, TimeUnit unit) throws InterruptedException;
//使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。直到收到信号或被中断,或超过指定的最后期限
boolean awaitUntil(Date deadline) throws InterruptedException;
//唤醒一个等待的线程
void signal();
//唤醒所有等待的线程
void signalAll();
}
32、结合源码分析Condition接口中await()的实现原理?
调用 Condition,需要获得 Lock 锁,所以意味着会存在一个 AQS 同步队列。
#### await()在AbstractQueuedSynchronizer中的jdk1.8实现源码,如下:
public final void await() throws InterruptedException {
//如果线程中断,则抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
//把当前线程封装成Node,节点状态为 condition,添加到等待队列,采用单向链表
Node node = addConditionWaiter();
//彻底的释放锁(当前锁存在多次重入,那么在这个方法中只需要释放一次就会把所有的重入次数归零),
//得到锁的状态,并唤醒AQS队列中的一个线程
int savedState = fullyRelease(node);
int interruptMode = 0;
//isOnSyncQueue判断当前节点是否在同步队列中,false-不在;true-在。
//如果不在,说明当前节点没有唤醒去争抢同步锁,所以需要把当前线程阻塞起来,直到其他的线程调用 signal 唤醒
//如果在,意味着它需要去竞争同步锁去获得执行程序执行权限
while (!isOnSyncQueue(node)) {
LockSupport.park(this);//通过 park 挂起当前线程
//当前线程是否在park的时候被中断唤醒。判断后续的处理应该是抛出InterruptedException还是重新中断
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) //当这个线程醒来,会尝试拿锁, 当 acquireQueued返回 false 就是拿到锁了
&& interruptMode != THROW_IE)//表示这个线程没有成功将node入队,但signal执行了enq方法让其入同步对队了
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) //如果 node 的下一个等待者不是 null, 则进行清理。
unlinkCancelledWaiters();
if (interruptMode != 0)// 如果线程被中断了,需要抛出异常.或者什么都不做
reportInterruptAfterWait(interruptMode);
}
33、ThreadA 这个节点是否存在于AQS队列中呢?
- 如果ThreadA的waitStatus的状态为CONDITION,说明它存在于condition队列中,不在AQS队列。因为AQS队列的状态一定不可能有CONDITION
- 如果node.prev为空,说明也不存在于AQS队列,原因是prev=null在AQS队列中只有一种可能性,就是它是head节点,head节点意味着它是获得锁的节点。
- 如果node.next不等于空,说明一定存在于AQS队列中,因为只有AQS队列才会存在next和prev的关系。
- findNodeFromTail,表示从tail节点往前扫描AQS队列,一旦发现AQS队列的节点和当前节点相等,说明节点一定存在于AQS队列中。
34、结合源码分析Condition接口中signal()的实现原理?
#### jdk1.8实现源码,如下:
public final void signal() {
if (!isHeldExclusively())//先判断当前线程是否获得了锁,这个判断比较简单,直接用获得锁的线程和当前线程相比即可.
throw new IllegalMonitorStateException();
Node first = firstWaiter;// 拿到 Condition队列上第一个节点
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
//CAS 修改了节点状态,如果成功,就将这个节点放到 AQS 队列中,然后唤醒这个节点上的线程。
final boolean transferForSignal(Node node) {
//更新节点的状态为 0,如果更新失败,只有一种可能就是节点被 CANCELLED 了
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//调用enq,把当前节点添加到AQS队列。并且返回返回按当前节点的上一个节点,也就是原来的尾节点
Node p = enq(node);
int ws = p.waitStatus;
//如果上一个节点的状态被取消了, 或者尝试设置上一个节点的状态为 SIGNAL失败了
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
//唤醒节点上的线程
LockSupport.unpark(node.thread);
return true;
}
35、基于源码分析Condition接口中signal()和signalAll()的的区别?
#### jdk1.8实现源码,如下:
private void doSignal(Node first) {//只唤醒当前节点
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
private void doSignalAll(Node first) {//一次遍历,并唤醒所有节点。
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
36、对Condition接口中await()和signal()/signalAll()的总结
阻塞:await()方法,在线程释放锁资源之后,如果节点不在 AQS 等待队列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁。
释放:signal()/signalAll()方法,节点会从 condition 队列移动到 AQS等待队列,则进入正常锁的获取流程。
37、对CountDownLatch的了解?
CountDownLatch:是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完毕再执行。
在内部写了一个Sync并且继承了AQS。并重写了AQS中的共享锁方法。使用的是共享锁机制。
提供了3个比较重要的方法:
#### jdk1.8实现源码,如下:
public class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
······
}
//构造方法
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
//阻塞
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//计数器减一
public void countDown() {
sync.releaseShared(1);
}
······
}
38、结合源码分析CountDownLatch中await()的实现原理?
await()可以被多个线程调用。是一个阻塞方法。
所有调用了await()方法的线程都阻塞在AQS的阻塞队列中。
当等待条件满足(state == 0),将线程从队列中一个个唤醒过来。
39、结合源码分析CountDownLatch中countDown()的实现原理?
线程被await方法阻塞了,所以只有等到countDown()使得state=0的时候才会被唤醒。
#### jdk1.8实现源码,如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//只有当state减为0的时候,才返回 true, 否则只是简单做state=state-1操作。
doReleaseShared();//唤醒处于 await 状态下的线程
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);
}
else if (ws == 0 &&
//标识为 PROPAGATE 状态的节点,是共享锁模式下的节点状态,处于这个状态下的节点,会对线程的唤醒进行传播
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
40、对Semaphore的了解有多少?
Semaphore可以控制同时访问的线程个数。基于AQS的共享锁的实现。具有公平策略和非公平策略。
acquire方法获取一个许可,如果没有资源就等待;通过release方法释放一个许可。
基本原理:
创建Semaphore实例的时候,需要一个参数 permits,并设置给AQS的state属性。
- 当调用acquire():如果state=0,说明没有资源了,需要将当前线程添加到AQS中;反之获得Semaphore令牌执行state=state-1。
- 当调用release():释放Semaphore令牌,并执行state=state+1。
#### jdk1.8实现源码,如下:
public class Semaphore implements java.io.Serializable {
//实现Sync抽象静态内部类,并实现AQS同步队列
abstract static class Sync extends AbstractQueuedSynchronizer {···}
//非公平锁
static final class NonfairSync extends Sync {···}
//公平锁
static final class FairSync extends Sync {···}
//构造方法
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
//从这个信号量获取一个许可,会阻塞直到可用,或者Thread#interrupt中断线程可用
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//释放一个许可,将它返回给信号量
public void release() {
sync.releaseShared(1);
}
//是否公平锁
public boolean isFair() {
return sync instanceof FairSync;
}
//返回AQS队列的长度
public final int getQueueLength() {
return sync.getQueueLength();
}
//返回AQS队列的线程信息集合
protected Collection<Thread> getQueuedThreads() {
return sync.getQueuedThreads();
}
······
}
41、对CyclicBarrier的了解有多少?
CyclicBarrier:是可循环使用(Cyclic)的屏障(Barrier)。
作用是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门(循环),所有被屏障拦截的线程才会继续工作。
源码实现:基于 ReentrantLock 和 Condition 的组合使用。
注意:
- 通过构造方法指定计数值parties,如果由于某种原因,没有足够的线程到达这个屏障(也可以叫同步点),则所有调用await的线程都会被阻塞;可以调用 await(timeout, unit),设置超时时间,在设定时间内,如果没有足够线程到达,则解除阻塞状态,继续工作;
- 通过reset重置计数,会使得进入await的线程出现BrokenBarrierException;
42、讲一下ConcurrentHashMap的实现原理?
JDK1.7:Segment 数组结构和 HashEntry 数组结构组成,Segment数组默认长度是16,Segment通过继承ReentrantLock来进行加锁。
数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问。理论情况下上可以同时支持 16 个线程的并发写入。
#### jdk1.7实现源码,如下:
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
final Segment<K,V>[] segments;
//Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁
static final class Segment<K,V> extends ReentrantLock implements Serializable {
······
final V put(K key, int hash, V value, boolean onlyIfAbsent) {···}
private void rehash(HashEntry<K,V> node) {···}
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {···}
private void scanAndLock(Object key, int hash) {···}
final V remove(Object key, int hash, Object value) {···}
final boolean replace(K key, int hash, V oldValue, V newValue) {···}
final V replace(K key, int hash, V value) {···}
final void clear() {···}
}
//用 volatile 修饰了 HashEntry 的数据 value 和 下一个节点 next,保证了多线程环境下数据获取时的可见性
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
···
}
······
}
数组+单向链表的数据结构:
JDK1.8:取消segment分段设计。与 HashMap 相同的Node数组+链表+红黑树结构。采用CAS + synchronized实现更加细粒度的锁。
#### jdk1.8实现源码,如下:
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
static class Node<K,V> implements Map.Entry<K,V> {···}
transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
private transient volatile int sizeCtl;
···
}
数组+单向链表+红黑树的结构:
43、JDK1.8 中ConcurrentHashMap为什么使用synchronized锁替换 可重入锁 ReentrantLock?
首先:在 JDK1.6 中,对 synchronized 锁的实现进行大量的优化,通过实现锁的升级(无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁)提高性能。
其次:假设使用ReentrantLock锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。
44、ConcurrentHashMap 迭代器是强一致性还是弱一致性?
HashMap/HashTable迭代器是强一致性;ConcurrentHashMap 迭代器是弱一致性。
ConcurrentHashMap 的迭代器创建后,就会按照哈希表结构遍历每个元素,但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反映出来,而如果变化发生在未遍历过的部分,迭代器就会发现并反映出来。
ConcurrentHashMap的get,clear,iterator 都是弱一致性。
45、ConcurrentHashMap 的并发度是什么?
JDK1.7中:默认是egment[]的数组长度(默认16)作为并发度;如果自己设置了并发度,会使用大于等于该值的最小的2的幂指数作为实际并发度。
JDK1.8中:选择了Node数组+链表+红黑树结构,并发度大小依赖于数组的大小。
46、ConcurrentHashMap不支持key或者value为null的原因?
- 源码不支持:会报空指针。
if (key == null || value == null) throw new NullPointerException();
- 二义性:假设 ConcurrentHashMap 允许存放值为 null 的 value,这时有A、B两个线程,线程A调用ConcurrentHashMap.get(key)方法,返回为 null ,我们不知道这个 null 是没有映射的 null ,还是存的值就是 null 。
假设此时,返回为 null 的真实情况是没有找到对应的 key。那么,我们可以用 ConcurrentHashMap.containsKey(key)来验证我们的假设是否成立,我们期望的结果是返回 false。
但是在我们调用 ConcurrentHashMap.get(key)方法之后,containsKey方法之前,线程B执行了ConcurrentHashMap.put(key, null)的操作。那么我们调用containsKey方法返回的就是 true 了,这就与我们的假设的真实情况不符合了,这就有了二义性。
47、为什么ConcurrentHashMap的get方法不需要加锁?
- jdk1.7中,HashEntry的元素value和next都用volatile 关键字修饰。在多线程环境下线程A修改节点的 value 或者新增节点的时候是对线程B可见的。
####jdk1.7 部分源码如下: static final class HashEntry<K,V> { final int hash; final K key; volatile V value; volatile HashEntry<K,V> next; ····· }
- jdk1.8中, Node 的元素val和指针next是用 volatile 修饰的,在多线程环境下线程A修改节点的 val或者新增节点的时候是对线程B可见的。
####jdk1.8 部分源码如下: static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; ····· }
48、ConcurrentHashMap 的 size()方法原理分析?
- JDK1.7 中:给每一段加锁,遍历每一段上的元素做累加操作,在finally中调用.unlock()释放每一段锁,返回size大小。
#### jdk1.7源码如下: public int size() { final Segment<K,V>[] segments = this.segments; int size; boolean overflow; // true if size overflows 32 bits long sum; // sum of modCounts long last = 0L; // previous sum int retries = -1; // first iteration isn't retry try { for (;;) { if (retries++ == RETRIES_BEFORE_LOCK) { for (int j = 0; j < segments.length; ++j) ensureSegment(j).lock(); // *************给每一段加锁 } sum = 0L; size = 0; overflow = false; for (int j = 0; j < segments.length; ++j) {// *************遍历每一段上的元素做累加操作 Segment<K,V> seg = segmentAt(segments, j); if (seg != null) { sum += seg.modCount; int c = seg.count; if (c < 0 || (size += c) < 0) overflow = true; } } if (sum == last) break; last = sum; } } finally { if (retries > RETRIES_BEFORE_LOCK) {// *************释放锁 for (int j = 0; j < segments.length; ++j) segmentAt(segments, j).unlock(); } } return overflow ? Integer.MAX_VALUE : size;// *************返回size的值 }
- JDK1.8 中:jdk1.8中,提供了mappingCount()和size()查询集合元素个数,最终都是调用计算大小的核心方法sumCount()。
size = baseCount + counterCells的所有值之和。#### jdk1.8源码如下: public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable { ······ //baseCount值有两种情况: // 1). counterCells 数组未初始化,在没有线程争用时,将 size 的变化写入此字段 // 2). 初始化 counterCells 数组时,没有获取到 cellsBusy 锁,会再次尝试将 size 的变化写入此字段 private transient volatile long baseCount; //用于同步 counterCells 数组结构修改的乐观锁资源 private transient volatile int cellsBusy; //counterCells数组一旦初始化,size的变化将不再尝试写入baseCount //可以将size 的变化写入数组中的任意元素 //可扩容,长度保持为2的幂 private transient volatile CounterCell[] counterCells; @sun.misc.Contended static final class CounterCell { volatile long value; CounterCell(long x) { value = x; } } public long mappingCount() { long n = sumCount(); return (n < 0L) ? 0L : n; } public int size() { long n = sumCount(); return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n); } final long sumCount() { CounterCell[] as = counterCells; CounterCell a; long sum = baseCount; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; } ······ }
49、结合jdk1.8源码分析一下ConcurrentHashMap的addCount() 添加元素个数的实现原理?
addCount方法记录size变化的过程:
-
counterCells 数组未初始化
- CAS 一次 baseCount
- 如果 CAS 失败,则调用 fullAddCount 方法
-
counterCells 数组已初始化
- CAS 一次当前线程探针哈希到的数组元素
- 如果 CAS 失败,则调用 fullAddCount 方法
#### jdk1.8源码如下:
// 参数 x 表示键值对个数的变化值,如果为正,表示新增了元素,如果为负,表示删除了元素
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 如果 counterCells 为空,则直接尝试通过 CAS 将 x 累加到 baseCount 中
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
// counterCells 非空
// 或 counterCells 为空,但 CAS baseCount 失败都会来到这里
CounterCell a; long v; int m;
boolean uncontended = true; // CAS 数组元素时,有没有发生线程争用的标志
// 如果当前线程探针哈希到的数组元素非空,则尝试将 x 累加到对应数组元素
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// counterCells 为空,或其长度小于1
// 或当前线程探针哈希到的数组元素为空
// 或当前线程探针哈希到的数组元素非空,但 CAS 数组元素失败
// 都会调用 fullAddCount 方法来完成 x 的写入
fullAddCount(x, uncontended);
return; // 如果调用过 fullAddCount,则当前线程一定不会协助扩容
}
// 走到这说明,CAS 数组元素成功
// 此时如果 check <= 1,也不协助可能会发生的扩容
if (check <= 1)
return;
// 如果 check 大于 1,则计算当前 map 的 size,为判断是否需要扩容做准备
s = sumCount();
}
// size 的变化已经写入完成
// 后面如果 check >= 0,则判断当前的 size 是否会触发扩容
if (check >= 0) {
// 扩容相关的逻辑
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
fullAddCount()方法的主要步骤:
- 线程探针哈希值的初始化。
- counterCells 数组的初始化和扩容。
- counterCells 元素的初始化。
- 将size的变化,写入counterCells 中的某一个元素。(如果 counterCells 初始化时,获取锁失败,则还会尝试将 size 的变化,写入 baseCount。)
#### jdk1.8源码如下:
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
// 判断线程探针哈希值是否初始化
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true; // 重新假设未发生争用
}
boolean collide = false; // 是否要给 counterCells 扩容的标志
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
// 数组不为空且长度大于 0
if ((a = as[(n - 1) & h]) == null) {
// 尝试初始化线程探针哈希到的数组元素
if (cellsBusy == 0) { // Try to attach new Cell
// 注意,这里已经把 x 放入对象
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 && // 准备初始化数组元素,要求 cellsBusy 为 0,并尝试将其置 1
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
// 获得 cellsBusy 锁
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
// 判断有没有被其它线程初始化
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0; // 释放 cellsBusy 锁
}
if (created) // 初始化元素成功,直接退出循环
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash(指的是更改当前线程的探针哈希值)
// wasUncontended 为 true 执行到这
// 尝试将 x 累加进数组元素
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
// CAS 失败
// 判断 counterCells 是否正在扩容,或数组长度是否大于等于处理器数
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
// 如果数组没有在扩容,且数组长度小于处理器数
// 此时,如果 collide 为 false,则把它变成 true
// 在下一轮循环中,如果 CAS 数组元素继续失败,就会触发 counterCells 扩容
else if (!collide)
collide = true;
// 如果 collide 为 true,则尝试给 counterCells 数组扩容
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h); // 更改当前线程的探针哈希值
}
// counterCells 数组为空或长度为 0
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
// 获取 cellsBusy 锁
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2]; // 初始长度为 2
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
// counterCells 数组为空或长度为 0,并且获取 cellsBusy 锁失败
// 则会再次尝试将 x 累加到 baseCount
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
} // end for
}
50、在jdk1.8中ConcurrentHashMap什么时候会触发扩容?
- 如果新增节点之后,所在链表的元素个数达到了阈值 8,则会调用treeifyBin方法把链表转换成红黑树,不过在结构转换之前,会对数组长度进行判断,实现如下:
如果数组长度n小于阈值MIN_TREEIFY_CAPACITY,默认是64,则会调用tryPresize方法把数组长度扩大到原来的两倍,并触发transfer方法,重新调整节点的位置。#### jdk1.8源码如下: //链表转成红黑树的阈值 static final int TREEIFY_THRESHOLD = 8; final V putVal(K key, V value, boolean onlyIfAbsent) { ······ if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); ······ } private final void treeifyBin(Node<K,V>[] tab, int index) { ······ if ((n = tab.length) < MIN_TREEIFY_CAPACITY) tryPresize(n << 1); ······ }
- 新增节点之后,会调用addCount方法记录元素个数,并检查是否需要进行扩容,当数组元素个数达到阈值时,会触发transfer方法,重新调整节点的位置。
51、在jdk1.8中ConcurrentHashMap扩容的原理是什么?
transfer()方法是用来扩容的,扩容为原来的两倍。扩容的核心操作在于数据的转移,在做数据转移时,会用低位(原来大小的链表)和高位(新扩容大小的链表)来实现。
ConcurrentHashMap 并没有直接加锁,而是采用 CAS 实现无锁的并发同步策略,最精华的部分是它可以利用多线程来进行协同扩容。
简单来说,它把 Node 数组当作多个线程之间共享的任务队列,然后通过维护一个指针来划分每个线程锁负责的区间,每个线程通过区间逆向遍历来实现扩容,一个已经迁移完的bucket 会被替换为一个 ForwardingNode 节点,标记当前 bucket 已经被其他线程迁移完了。
52、jdk并发包中提供的阻塞队列有哪些?
阻塞队列 - (jdk1.8) | 说明 |
---|---|
ArrayBlockingQueue | 数组实现的有界阻塞队列, 此队列按照先进先出(FIFO)的原则对元素进行排序。 |
LinkedBlockingQueue | 链表实现的有界阻塞队列, 此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。 |
PriorityBlockingQueue | 支持优先级排序的无界阻塞队列, 默认情况下元素采取自然顺序升序排列。也可以自定义类实现 compareTo()方法来指定元素排序规则,或者初始化 PriorityBlockingQueue 时,指定构造参数 Comparator 来对元素进行排序。 |
DelayQueue | 优先级队列实现的无界阻塞队列。 |
SynchronousQueue | 不存储元素的阻塞队列, 每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。 |
LinkedTransferQueue | 链表实现的无界阻塞队列。 |
LinkedBlockingDeque | 链表实现的双向阻塞队列。 |
53、在ArrayBlockingQueue阻塞队列中,添加元素操作add(e)、offer(e)、put(e)和offer(e,time,unit)的区别?
- add(e) :实际调用offer(e),如果队列满了,继续插入元素会throw new IllegalStateException(“Queue full”)。
#### jdk1.8源码如下: public boolean add(E e) { return super.add(e); } ## AbstractQueue类 public boolean add(E e) { if (offer(e)) return true; else throw new IllegalStateException("Queue full"); }
- offer(e) : 使用ReentrantLock锁,会返回元素是否插入成功的状态,如果成功则返回 true。
#### jdk1.8源码如下: public boolean offer(E e) { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lock(); try { if (count == items.length) return false; else { enqueue(e); return true; } } finally { lock.unlock(); } }
- put(e) :使用ReentrantLock锁,当阻塞队列满了,生产者线程会被阻塞(Condition.await() ),直到队列可用。
#### jdk1.8源码如下: public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) notFull.await(); enqueue(e); } finally { lock.unlock(); } }
- offer(e,time,unit) :使用ReentrantLock锁,当阻塞队列满了,生产者线程会被阻塞指定时间(Condition.awaitNanos(nanos) ),如果超时,则线程直接退出。
#### jdk1.8源码如下: public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { checkNotNull(e); long nanos = unit.toNanos(timeout); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) { if (nanos <= 0) return false; nanos = notFull.awaitNanos(nanos); } enqueue(e); return true; } finally { lock.unlock(); } }
54、在ArrayBlockingQueue阻塞队列中,移除操作remove()、poll()、take()和poll(time,unit)的区别?
- remove():使用ReentrantLock锁,当队列为空时,调用 remove会返回false,如果元素移除成功,则返回 true。
#### jdk1.8源码如下: public boolean remove(Object o) { if (o == null) return false; final Object[] items = this.items; final ReentrantLock lock = this.lock; lock.lock(); try { if (count > 0) { final int putIndex = this.putIndex; int i = takeIndex; do { if (o.equals(items[i])) { removeAt(i); return true; } if (++i == items.length) i = 0; } while (i != putIndex); } return false; } finally { lock.unlock(); } }
- poll(): 使用ReentrantLock锁,当队列中存在元素,则从队列中取出一个元素,如果队列为空,则直接返回null。
#### jdk1.8源码如下: public E poll() { final ReentrantLock lock = this.lock; lock.lock(); try { return (count == 0) ? null : dequeue(); } finally { lock.unlock(); } }
- take():使用ReentrantLock锁,基于阻塞的方式获取队列中的元素,如果队列为空,则take方法会一直阻塞(Condition.await() ),直到队列中有新的数据可以消费。
#### jdk1.8源码如下: public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) notEmpty.await(); return dequeue(); } finally { lock.unlock(); } }
- poll(time,unit):使用ReentrantLock锁,带超时机制的获取数据,如果队列为空,消费者线程会被阻塞指定时间(Condition.awaitNanos(nanos) ),等待指定的时间再去获取元素返回。
#### jdk1.8源码如下: public E poll(long timeout, TimeUnit unit) throws InterruptedException { long nanos = unit.toNanos(timeout); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) { if (nanos <= 0) return null; nanos = notEmpty.awaitNanos(nanos); } return dequeue(); } finally { lock.unlock(); } }
55、Executors中提供哪些方式来创建线程池?
//返回nThreads个固定数量的线程池,线程数不变。
//当有一个任务提交时,若线程池中空闲,则立即执行,
//若线程都不空闲,则会被暂缓在LinkedBlockingQueue队列中,等待有空闲的线程去执行。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在LinkedBlockingQueue队列中。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
//返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若用空闲的线程则执行任务,若没有空闲线程则暂缓在SynchronousQueue队列中。
//并且每一个空闲线程会在60秒后自动回收
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//创建一个可以指定线程的数量的线程池,但是这个线程池还带有延迟和周期性执行任务的功能。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
56、讲一下创建线程池时ThreadPoolExecutor参数的含义?
通过Executors对四种线程池的创建,都是基于ThreadpoolExecutor来构建的。
public ThreadPoolExecutor(int corePoolSize, //核心线程数量
int maximumPoolSize, //最大线程数量
long keepAliveTime, //超时时间,超出核心线程数量以外的线程空余存活时间
TimeUnit unit, //存活时间单位
BlockingQueue<Runnable> workQueue, //保存执行任务的队列
ThreadFactory threadFactory, //创建新线程使用的工厂,默认:Executors.defaultThreadFactory()
RejectedExecutionHandler handler) //当任务无法执行的时候的处理方式(策略),默认:new AbortPolicy()--终止政策
{···}
57、线程池的实现原理?
ThreadPoolExecutor 是线程池的核心,提供了线程池的实现。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();//获取 ctl 的值,是AtomicInteger类型。
if (workerCountOf(c) < corePoolSize) {//1.当前池中线程比核心数要少
if (addWorker(command, true))//新建一个线程执行任务
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {//2.核心池已满,通过offer将任务尝试添加到队列中,true-添加成功。
int recheck = ctl.get();//次获取 ctl 的值
if (! isRunning(recheck) && remove(command))///如果线程池处于非运行状态,并且把当前的任务从任务队列中移除成功
reject(command);//拒绝该任务
else if (workerCountOf(recheck) == 0)//如果之前的线程已被销毁完
addWorker(null, false);//新建一个线程
}
else if (!addWorker(command, false))///3.核心池已满,队列已满,试着创建一个新线程
reject(command);//创建新线程失败了,说明线程池被关闭或者线程池完全满了,拒绝任务
}
58、在线程池中,ctl变量的作用?
一个int数值是32个bit位,这里采用高3位来保存运行状态,低29位来保存线程数量。
#### jdk1.8源码如下:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
ctl 贯穿在线程池的整个生命周期中,是一个原子类。作用是保存:线程的有效数量(workerCount) 和 线程池的状态(runState)
一个int数值是32个bit位,这里采用高3位来保存运行状态,低29位来保存线程数量。
59、线程池的状态有哪些?
运行状态保存在ctl 的高3位 (所有数值左移 29 位)
#### jdk1.8源码如下:
private static final int RUNNING = -1 << COUNT_BITS;// 接收新任务,并执行队列中的任务
private static final int SHUTDOWN = 0 << COUNT_BITS;// 不接收新任务,但是执行队列中的任务
private static final int STOP = 1 << COUNT_BITS;// 不接收新任务,不执行队列中的任务,中断正在执行中的任务
private static final int TIDYING = 2 << COUNT_BITS; //所有的任务都已结束,线程数量为 0,处于该状态的线程池即将调用 terminated()方法
private static final int TERMINATED = 3 << COUNT_BITS;// terminated()方法执行完成
60、线程池有哪些常见的拒绝策略?
- AbortPolicy:直接抛出异常,默认策略;
- CallerRunsPolicy:用调用者所在的线程来执行任务;
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
- DiscardPolicy:直接丢弃任务;
也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义策略,如记录日志或持久化存储不能处理的任务。
61、阿里开发手册不建议使用Executors 去创建线程池?
线程池的构建不建议使用Executors去创建,而是推荐通过 ThreadPoolExecutor的方式。
Executors 使得用户不需要关心线程池的参数配置。
比如:newFixedThreadPool或者newSingleThreadExecutor使用的LinkedBlockingQueue队列。允许的队列长度为Integer.MAX_VALUE,如果使用不当会导致大量请求堆积到队列中导致OOM 的风险。
而 newCachedThreadPool,允许创建线程数量为 Integer.MAX_VALUE,也可能会导致大量线程的创建出现 CPU 使用过高或者 OOM 的问题。
而如果我们通过 ThreadPoolExecutor 来构造线程池的话,我们势必要了解线程池构造中每个
参数的具体含义,使得开发者在配置参数的时候能够更加谨慎。
62、如何合理配置线程池的大小 ?
首先需要分析:
- 线程池执行的任务的特性: CPU密集型 / IO密集型
- 每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系。
- CPU 密集型:线程池的最大线程数可以配置为:CPU核心数+1
- IO密集型:一般可以配置cpu核心数的2倍。
线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程CPU时间)/ 线程CPU时间 )* CPU数目
63、线程池是如何关闭的?
- shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
- shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
64、线程池容量能动态调整吗?
能
- setCorePoolSize():设置核心池大小
- setMaximumPoolSize():设置线程池最大能创建的线程数目大小
65、线程池中submit与execute区别?
- 由不同的接口提供
public interface Executor { void execute(Runnable command); } public interface ExecutorService extends Executor { <T> Future<T> submit(Callable<T> task); <T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task); ···· }
- 入参和返回值不一样
- execute如果出现异常会抛出;submit 方法调用不会抛异常,除非调用 Future.get,get会阻塞的原理是LockSupport的park(this)、parkNanos(this, nanos)。
66、ThreadLocal工作原理是什么?
ThreadLocal是一个本地线程副本变量工具类。每个线程的内部都维护了一个ThreadLocalMap,ThreadLocalMap的数据结构数组,它是一个Map(key,value)数据格式,key是一个弱引用,也就是ThreadLocal本身,而value强引用存的是线程变量的值。
67、ThreadLocal如何解决Hash冲突?
与HashMap不同,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式。所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经被其他的key值占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
68、ThreadLocal的内存泄露是怎么回事?
ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候Entry中的key已经被回收,但是value又是一强引用不会被垃圾收集器回收,这样ThreadLocal的线程如果一直持续运行,value就一直得不到回收,这样就会发生内存泄露。
69、为什么ThreadLocalMap的key是弱引用?
我们知道ThreadLocalMap中的key是弱引用,而value是强引用才会导致内存泄露的问题,至于为什么要这样设计,这样分为两种情况来讨论:
- key使用强引用:这样会导致一个问题,引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,则会导致内存泄漏。
- key使用弱引用:这样的话,引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。
====================================================================
······
帮助他人,快乐自己,最后,感谢您的阅读!
所以如有纰漏或者建议,还请读者朋友们在评论区不吝指出!
个人网站…知识是一种宝贵的资源和财富,益发掘,更益分享…