目录
1.5、线程有哪几种状态? 分别说明从一种状态到另一种状态转变有哪些方式?
2.2、Synchronized本质上是通过什么保证线程安全的?
2.3、Synchronized使得同时只有一个线程可以执行,性能比较差,有什么提升的方法?
2.4、Synchronized由什么样的缺陷? Java Lock是怎么弥补这些缺陷的?
2.7、Synchronized修饰的方法在抛出异常时,会释放锁吗?
2.8、多个线程等待同一个Synchronized锁的时候,JVM如何选择下一个获取锁的线程?
2.12、32位机器上共享的long和double变量的为什么要用volatile?
2.17、如何理解private所修饰的方法是隐式的final?
4.3、CAS使用示例,结合AtomicInteger给出示例?
4.8、AtomicStampedReference是怎么解决ABA的?
5.3、通过LockSupport的park/unpark实现同步?
5.4、Thread.sleep()、Object.wait()、Condition.await()、LockSupport.park()的区别? 重点
5.5、如果在wait()之前执行了notify()会怎样?
5.6、如果在park()之前执行了unpark()会怎样?
5.12、什么是可重入,什么是可重入锁? 它用来解决什么问题?
5.13、ReentrantLock的核心是AQS,那么它怎么来实现的,继承吗?
5.16、ReentrantLock默认实现的是公平还是非公平锁?
5.17、为了有了ReentrantLock还需要ReentrantReadWriteLock?
5.18、ReentrantReadWriteLock底层实现原理?
5.19、ReentrantReadWriteLock底层读写状态如何设计的?
5.21、本地线程计数器ThreadLocalHoldCounter是用来做什么的?
6.1、为什么HashTable慢? 它的并发度是什么? 那么ConcurrentHashMap并发度是什么?
6.2、ConcurrentHashMap在JDK1.7和JDK1.8中实现有什么差别? JDK1.8解決了JDK1.7中什么问题
6.3、ConcurrentHashMap JDK1.7实现的原理是什么?
6.4、ConcurrentHashMap JDK1.7中Segment数(concurrencyLevel)默认值是多少? 为何一旦初始化就不可再扩容?
6.5、ConcurrentHashMap JDK1.7说说其put的机制?
6.6、ConcurrentHashMap JDK1.7是如何扩容的?
6.7、ConcurrentHashMap JDK1.8实现的原理是什么?
6.8、ConcurrentHashMap JDK1.8是如何扩容的?
6.9、ConcurrentHashMap JDK1.8链表转红黑树的时机是什么? 临界值为什么是8?
6.10、ConcurrentHashMap JDK1.8是如何进行数据迁移的?
6.12、CopyOnWriteArrayList的实现原理?
6.14、CopyOnWriteArrayList为什么并发安全且性能比Vector好?
6.15、CopyOnWriteArrayList有何缺陷,说说其应用场景?
6.17、ConcurrentLinkedQueue实现的数据结构?
6.18、ConcurrentLinkedQueue底层原理?
6.19、ConcurrentLinkedQueue的核心方法有哪些?
6.20、说说ConcurrentLinkedQueue的HOPS(延迟更新的策略)的设计?
6.21、ConcurrentLinkedQueue适合什么样的使用场景?
6.22、什么是BlockingDeque? 适合用在什么样的场景?
6.26、什么是BlockingDeque? 适合用在什么样的场景?
6.27、BlockingDeque 与BlockingQueue有何关系,请对比下它们的方法?
7.1、FutureTask用来解决什么问题的? 为什么会出现?
7.6、Java是实现和管理线程池有哪些方式? 请简单举例如何使用。
7.8、ThreadPoolExecutor有哪些核心的配置参数? 请简要说明
7.9、ThreadPoolExecutor可以创建哪是哪三种线程池呢?
7.10、当队列满了并且worker的数量达到maxSize的时候,会怎么样?
7.11、说说ThreadPoolExecutor有哪些RejectedExecutionHandler策略? 默认是什么策略?
7.17、为什么很多公司不允许使用Executors去创建线程池? 那么推荐怎么使用呢?
7.18、ScheduledThreadPoolExecutor要解决什么样的问题?
7.19、ScheduledThreadPoolExecutor相比ThreadPoolExecutor有哪些特性?
7.20、ScheduledThreadPoolExecutor有什么样的数据结构,核心内部类和抽象类?
7.21、ScheduledThreadPoolExecutor有哪两个关闭策略? 区别是什么?
7.22、ScheduledThreadPoolExecutor中scheduleAtFixedRate 和 scheduleWithFixedDelay区别是什么?
7.23、为什么ThreadPoolExecutor 的调整策略却不适用于 ScheduledThreadPoolExecutor?
7.24、Executors 提供了几种方法来构造 ScheduledThreadPoolExecutor?
7.26、Fork/Join框架是在哪个JDK版本中引入的?
7.27、Fork/Join框架主要包含哪三个模块? 模块之间的关系是怎么样的?
7.30、整个Fork/Join 框架的执行流程/运行机制是怎么样的?
7.31、具体阐述Fork/Join的分治思想和work-stealing 实现方式?
7.33、如何使用Executors工具类创建ForkJoinPool?
7.34、写一个例子: 用ForkJoin方式实现1+2+3+...+100000?
7.35、Fork/Join在使用时有哪些注意事项? 结合JDK中的斐波那契数列实例具体说明。
8.5、写道题:实现一个容器,提供两个方法,add,size 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束?
8.7、CountDownLatch和CyclicBarrier对比?
8.10、Semaphore常用方法有哪些? 如何实现线程同步和互斥的?
8.11、单独使用Semaphore是不会使用到AQS的条件队列?
8.12、Semaphore初始化有10个令牌,11个线程同时各调用1次acquire方法,会发生什么?
8.13、Semaphore初始化有10个令牌,一个线程重复调用11次acquire方法,会发生什么?
8.14、Semaphore初始化有1个令牌,1个线程调用一次acquire方法,然后调用两次release方法,之后另外一个线程调用acquire(2)方法,此线程能够获取到足够的令牌并继续运行吗?
8.15、Semaphore初始化有2个令牌,一个线程调用1次release方法,然后一次性获取3个令牌,会获取到吗?
8.17、Phaser与CyclicBarrier和CountDownLatch的区别是什么?
8.21、对比SynchronousQueue,为什么说Exchanger可被视为 SynchronousQueue 的双向形式?
8.22、Exchanger在不同的JDK版本中实现有什么差别?
8.24、什么是ThreadLocal? 用来解决什么问题的?
8.27、为什么ThreadLocal会造成内存泄露? 如何解决
致力于一个专栏将Java面试说的清清楚楚,从工作实践角度出发,尽量涵盖Java主流知识点,全面讲述Java面试题。
本篇讲述Java并发和多线程,总共涵盖8个知识点,161道热点面试题。
1、并发基础
1.1、多线程的出现是要解决什么问题的? 本质什么?
CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
- CPU 增加了缓存,以均衡与内存的速度差异;// 导致可见性问题
- 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致原子性问题
- 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致有序性问题
1.2、Java是怎么解决并发问题的?
Java 内存模型是个很复杂的规范,具体看Java 内存模型详解。
理解的第一个维度:核心知识点
JMM本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括:
- volatile、synchronized 和 final 三个关键字
- Happens-Before 规则
理解的第二个维度:可见性,有序性,原子性
- 原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。 请分析以下哪些操作是原子性操作:
x = 10; //语句1: 直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中
y = x; //语句2: 包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
x++; //语句3: x++包括3个操作:读取x的值,进行加1操作,写入新的值。
x = x + 1; //语句4: 同语句3
上面4个语句只有语句1的操作具备原子性。
也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
- 可见性
Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
- 有序性
在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before 规则来保证有序性的。
1.3、线程安全有哪些实现思路?
1、互斥同步
synchronized 和 ReentrantLock。
2、非阻塞同步
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
- CAS
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略: 先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是: 比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
- AtomicInteger
J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。
3、无同步方案
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
- 栈封闭
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
- 线程本地存储(Thread Local Storage)
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
1.4、如何理解并发和并行的区别?
并发是指一个处理器同时处理多个任务。
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。
1.5、线程有哪几种状态? 分别说明从一种状态到另一种状态转变有哪些方式?
- 新建(New)
创建后尚未启动。
- 可运行(Runnable)
可能正在运行,也可能正在等待 CPU 时间片。
包含了操作系统线程状态中的 Running 和 Ready。
- 阻塞(Blocking)
等待获取一个排它锁,如果其线程释放了锁就会结束此状态。
- 无限期等待(Waiting)
等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
进入方法 | 退出方法 |
---|---|
没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
LockSupport.park() 方法 | - |
- 限期等待(Timed Waiting)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
进入方法 | 退出方法 |
---|---|
Thread.sleep() 方法 | 时间结束 |
设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
LockSupport.parkNanos() 方法 | - |
LockSupport.parkUntil() 方法 | - |
- 死亡(Terminated)
可以是线程结束任务之后自己结束,或者产生了异常而结束。
1.6、通常线程有哪几种使用方式?
有三种使用线程的方法:
- 实现 Runnable 接口;
- 实现 Callable 接口;
- 继承 Thread 类。
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
1.7、基础线程机制有哪些?
- Executor
Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。
主要有三种 Executor:
- CachedThreadPool: 一个任务创建一个线程;
- FixedThreadPool: 所有任务只能使用固定大小的线程;
- SingleThreadExecutor: 相当于大小为 1 的 FixedThreadPool。
- Daemon
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
main() 属于非守护线程。使用 setDaemon() 方法将一个线程设置为守护线程。
- sleep()
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。
sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
- yield()
对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
1.8、线程的中断方式有哪些?
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。
- InterruptedException
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。
public class InterruptExample {
private static class MyThread1 extends Thread {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new MyThread1();
thread1.start();
thread1.interrupt();
System.out.println("Main run");
}
}
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at InterruptExample.lambda$main$0(InterruptExample.java:5)
at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
- interrupted()
如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
- Executor 的中断操作
调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
1.9、线程的互斥同步方式有哪些? 如何比较和选择?
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
1. 锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
2. 性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
3. 等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。
4. 公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
5. 锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
1.10、线程之间有哪些协作方式?
当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。
- join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。
public class JoinExample {
private class A extends Thread {
@Override
public void run() {
System.out.println("A");
}
}
private class B extends Thread {
private A a;
B(A a) {
this.a = a;
}
@Override
public void run() {
try {
a.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B");
}
}
public void test() {
A a = new A();
B b = new B(a);
b.start();
a.start();
}
}
public static void main(String[] args) {
JoinExample example = new JoinExample();
example.test();
}
A
B
- wait() notify() notifyAll()
调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
它们都属于 Object 的一部分,而不属于 Thread。
只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。
使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
wait() 和 sleep() 的区别
- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
- wait() 会释放锁,sleep() 不会。
- await() signal() signalAll()
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
2、并发关键字
2.1、Synchronized可以作用在哪里?
- 对象锁
- 方法锁
- 类锁
2.2、Synchronized本质上是通过什么保证线程安全的?
- 加锁和释放锁的原理
深入JVM看字节码,创建如下的代码:
public class SynchronizedDemo2 {
Object object = new Object();
public void method1() {
synchronized (object) {
}
}
}
使用javac命令进行编译生成.class文件
>javac SynchronizedDemo2.java
使用javap命令反编译查看.class文件的信息
>javap -verbose SynchronizedDemo2.class
得到如下的信息:
关注红色方框里的monitorenter
和monitorexit
即可。
Monitorenter
和Monitorexit
指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:
- monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
- 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加
- 这把锁已经被别的线程获取了,等待锁释放
monitorexit指令
:释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。
下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系:
该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。
- 可重入原理:加锁次数计数器
看如下的例子:
public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) {
}
method2();
}
private synchronized static void method2() {
}
}
对应的字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class tech/pdai/test/synchronized/SynchronizedDemo
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
15: invokestatic #3 // Method method2:()V
Exception table:
from to target type
5 7 10 any
10 13 10 any
上面的SynchronizedDemo中在执行完同步代码块之后紧接着再会去执行一个静态同步方法,而这个方法锁的对象依然就这个类对象,那么这个正在执行的线程还需要获取该锁吗? 答案是不必的,从上图中就可以看出来,执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是锁的重入性,即在同一锁程中,线程不需要再次获取同一把锁。
Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
- 保证可见性的原理:内存模型和happens-before规则
Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。继续来看代码:
public class MonitorDemo {
private int a = 0;
public synchronized void writer() { // 1
a++; // 2
} // 3
public synchronized void reader() { // 4
int i = a; // 5
} // 6
}
该代码的happens-before关系如图所示:
在图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:线程A释放锁happens-before线程B加锁,蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。现在我们来重点关注2 happens-before 5,通过这个关系我们可以得出什么?
根据happens-before的定义中的一条:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B。线程A先对共享变量A进行加一,由2 happens-before 5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1。
2.3、Synchronized使得同时只有一个线程可以执行,性能比较差,有什么提升的方法?
简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的;然而在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境)如果每次都调用Mutex Lock那么将严重的影响程序的性能。不过在jdk1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销。
-
锁粗化(Lock Coarsening):也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。
-
锁消除(Lock Elimination):通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销)。
-
轻量级锁(Lightweight Locking):这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒。
-
偏向锁(Biased Locking):是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟。
-
适应性自旋(Adaptive Spinning):当线程在获取轻量级锁的过程中执行CAS操作失败时,在进入与monitor相关联的操作系统重量级锁(mutex semaphore)前会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁)进入到阻塞状态。
2.4、Synchronized由什么样的缺陷? Java Lock是怎么弥补这些缺陷的?
- synchronized的缺陷
- 效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时
- 不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活
- 无法知道是否成功获得锁,相对而言,Lock可以拿到状态
- Lock解决相应问题
Lock类这里不做过多解释,主要看里面的4个方法:
lock()
: 加锁unlock()
: 解锁tryLock()
: 尝试获取锁,返回一个boolean值tryLock(long,TimeUtil)
: 尝试获取锁,可以设置超时
Synchronized只有锁只与一个条件(是否获取锁)相关联,不灵活,后来Condition与Lock的结合解决了这个问题。
多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。
2.5、Synchronized和Lock的对比,和选择?
- 存在层次上
synchronized: Java的关键字,在jvm层面上
Lock: 是一个接口
- 锁的释放
synchronized: 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁
Lock: 在finally中必须释放锁,不然容易造成线程死锁
- 锁的获取
synchronized: 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待
Lock: 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待(可以通过tryLock判断有没有锁)
- 锁的释放(死锁产生)
synchronized: 在发生异常时候会自动释放占有的锁,因此不会出现死锁
Lock: 发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生
- 锁的状态
synchronized: 无法判断
Lock: 可以判断
- 锁的类型
synchronized: 可重入 不可中断 非公平
Lock: 可重入 可判断 可公平(两者皆可)
- 性能
synchronized: 少量同步
Lock: 大量同步
Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离) 在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
- 调度
synchronized: 使用Object对象本身的wait 、notify、notifyAll调度机制
Lock: 可以使用Condition进行线程之间的调度
- 用法
synchronized: 在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
Lock: 一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
- 底层实现
synchronized: 底层使用指令码方式来控制锁的,映射成字节码指令就是增加来两个指令:monitorenter和monitorexit。当线程执行遇到monitorenter指令时会尝试获取内置锁,如果获取锁则锁计数器+1,如果没有获取锁则阻塞;当遇到monitorexit指令时锁计数器-1,如果计数器为0则释放锁。
Lock: 底层是CAS乐观锁,依赖AbstractQueuedSynchronizer类,把所有的请求线程构成一个CLH队列。而对该队列的操作均通过Lock-Free(CAS)操作。
2.6、Synchronized在使用时有何注意事项?
- 锁对象不能为空,因为锁的信息都保存在对象头里
- 作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错
- 避免死锁
- 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错
2.7、Synchronized修饰的方法在抛出异常时,会释放锁吗?
会
2.8、多个线程等待同一个Synchronized锁的时候,JVM如何选择下一个获取锁的线程?
非公平锁,即抢占式
2.9、synchronized是公平锁吗?
synchronized实际上是非公平的,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待,这样有利于提高性能,但是也可能会导致饥饿现象。
2.10、volatile关键字的作用是什么?
- 防重排序 我们从一个最经典的例子来分析重排序问题。大家应该都很熟悉单例模式的实现,而在并发环境下的单例实现方式,我们通常可以采用双重检查加锁(DCL)的方式来实现。其源码如下:
public class Singleton {
public static volatile Singleton singleton;
/**
* 构造函数私有,禁止外部实例化
*/
private Singleton() {};
public static Singleton getInstance() {
if (singleton == null) {
synchronized (singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
现在我们分析一下为什么要在变量singleton之间加上volatile关键字。要理解这个问题,先要了解对象的构造过程,实例化一个对象其实可以分为三个步骤:
- 分配内存空间。
- 初始化对象。
- 将内存空间的地址赋值给对应的引用。
但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:
- 分配内存空间。
- 将内存空间的地址赋值给对应的引用。
- 初始化对象
如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。
- 实现可见性
可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。volatile关键字能有效的解决这个问题,我们看下下面的例子,就可以知道其作用:
public class TestVolatile {
private static boolean stop = false;
public static void main(String[] args) {
// Thread-A
new Thread("Thread A") {
@Override
public void run() {
while (!stop) {
}
System.out.println(Thread.currentThread() + " stopped");
}
}.start();
// Thread-main
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread() + " after 1 seconds");
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
}
}
执行输出如下
Thread[main,5,main] after 1 seconds
// Thread A一直在loop, 因为Thread A 由于可见性原因看不到Thread Main 已经修改stop的值
可以看到 Thread-main 休眠1秒之后,设置 stop = ture,但是Thread A根本没停下来,这就是可见性问题。如果通过在stop变量前面加上volatile关键字则会真正stop:
Thread[main,5,main] after 1 seconds
Thread[Thread A,5,main] stopped
Process finished with exit code 0
- 保证原子性:单次读/写
volatile不能保证完全的原子性,只能保证单次的读/写操作具有原子性。
2.11、volatile能保证原子性吗?
不能完全保证,只能保证单次的读/写操作具有原子性。
2.12、32位机器上共享的long和double变量的为什么要用volatile?
因为long和double两种数据类型的操作可分为高32位和低32位两部分,因此普通的long或double类型读/写可能不是原子的。因此,鼓励大家将共享的long和double变量设置为volatile类型,这样能保证任何情况下对long和double的单次读/写操作都具有原子性。
如下是JLS中的解释:
17.7 Non-Atomic Treatment of double and long
- For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write.
- Writes and reads of volatile long and double values are always atomic.
- Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.
- Some implementations may find it convenient to divide a single write action on a 64-bit long or double value into two write actions on adjacent 32-bit values. For efficiency’s sake, this behavior is implementation-specific; an implementation of the Java Virtual Machine is free to perform writes to long and double values atomically or in two parts.
- Implementations of the Java Virtual Machine are encouraged to avoid splitting 64-bit values where possible. Programmers are encouraged to declare shared 64-bit values as volatile or synchronize their programs correctly to avoid possible complications.
目前各种平台下的商用虚拟机都选择把 64 位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不把long 和 double 变量专门声明为 volatile多数情况下也是不会错的。
2.13、volatile是如何实现可见性的?
内存屏障。
2.14、volatile是如何实现有序性的?
happens-before等
2.15、说下volatile的应用场景?
使用 volatile 必须具备的条件
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
- 只有在状态真正独立于程序内其他内容时才能使用 volatile。
- 例子 1: 单例模式
单例模式的一种实现方式,但很多人会忽略 volatile 关键字,因为没有该关键字,程序也可以很好的运行,只不过代码的稳定性总不是 100%,说不定在未来的某个时刻,隐藏的 bug 就出来了。
class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 例子2: volatile bean
在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。
@ThreadSafe
public class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age;
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setAge(int age) {
this.age = age;
}
}
2.16、所有的final修饰的字段都是编译期常量吗?
不是
2.17、如何理解private所修饰的方法是隐式的final?
类中所有private方法都隐式地指定为final的,由于无法取用private方法,所以也就不能覆盖它。可以对private方法增添final关键字,但这样做并没有什么好处。看下下面的例子:
public class Base {
private void test() {
}
}
public class Son extends Base{
public void test() {
}
public static void main(String[] args) {
Son son = new Son();
Base father = son;
//father.test();
}
}
Base和Son都有方法test(),但是这并不是一种覆盖,因为private所修饰的方法是隐式的final,也就是无法被继承,所以更不用说是覆盖了,在Son中的test()方法不过是属于Son的新成员罢了,Son进行向上转型得到father,但是father.test()是不可执行的,因为Base中的test方法是private的,无法被访问到。
2.18、说说final类型的类如何拓展?
比如String是final类型,我们想写个MyString复用所有String中方法,同时增加一个新的toMyString()的方法,应该如何做?
外观模式:
/**
* @pdai
*/
class MyString{
private String innerString;
// ...init & other methods
// 支持老的方法
public int length(){
return innerString.length(); // 通过innerString调用老的方法
}
// 添加新方法
public String toMyString(){
//...
}
}
2.19、final方法可以被重载吗?
我们知道父类的final方法是不能够被子类重写的,那么final方法可以被重载吗? 答案是可以的,下面代码是正确的。
public class FinalExampleParent {
public final void test() {
}
public final void test(String str) {
}
}
2.20、父类的final方法能不能够被子类重写?
不可以
2.21、说说基本类型的final域重排序规则?
先看一段示例性的代码:
public class FinalDemo {
private int a; //普通域
private final int b; //final域
private static FinalDemo finalDemo;
public FinalDemo() {
a = 1; // 1. 写普通域
b = 2; // 2. 写final域
}
public static void writer() {
finalDemo = new FinalDemo();
}
public static void reader() {
FinalDemo demo = finalDemo; // 3.读对象引用
int a = demo.a; //4.读普通域
int b = demo.b; //5.读final域
}
}
假设线程A在执行writer()方法,线程B执行reader()方法。
- 写final域重排序规则
写final域的重排序规则禁止对final域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面:
- JMM禁止编译器把final域的写重排序到构造函数之外;
- 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。
我们再来分析writer方法,虽然只有一行代码,但实际上做了两件事情:
- 构造了一个FinalDemo对象;
- 把这个对象赋值给成员变量finalDemo。
我们来画下存在的一种可能执行时序图,如下:
由于a,b之间没有数据依赖性,普通域(普通变量)a可能会被重排序到构造函数之外,线程B就有可能读到的是普通变量a初始化之前的值(零值),这样就可能出现错误。而final域变量b,根据重排序规则,会禁止final修饰的变量b重排序到构造函数之外,从而b能够正确赋值,线程B就能够读到final变量初始化后的值。
因此,写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障。比如在上例,线程B有可能就是一个未正确初始化的对象finalDemo。
- 读final域重排序规则
读final域重排序规则为:在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这两个操作的重排序。(注意,这个规则仅仅是针对处理器),处理器会在读final域操作的前面插入一个LoadLoad屏障。实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。
read()方法主要包含了三个操作:
- 初次读引用变量finalDemo;
- 初次读引用变量finalDemo的普通域a;
- 初次读引用变量finalDemo的final域b;
假设线程A写过程没有重排序,那么线程A和线程B有一种的可能执行时序为下图:
读对象的普通域被重排序到了读对象引用的前面就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。而final域的读操作就“限定”了在读final域变量前已经读到了该对象的引用,从而就可以避免这种情况。
读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。
2.22、说说final的原理?
- 写final域会要求编译器在final域写之后,构造函数返回前插入一个StoreStore屏障。
- 读final域的重排序规则会要求编译器在读final域的操作前插入一个LoadLoad屏障。
PS:很有意思的是,如果以X86处理为例,X86不会对写-写重排序,所以StoreStore屏障可以省略。由于不会对有间接依赖性的操作重排序,所以在X86处理器中,读final域需要的LoadLoad屏障也会被省略掉。也就是说,以X86为例的话,对final域的读/写的内存屏障都会被省略!具体是否插入还是得看是什么处理器。
3、JUC全局观
3.1、JUC框架包含几个部分?
五个部分:
主要包含: (注意: 上图是网上找的图,无法表述一些继承关系,同时少了部分类;但是主体上可以看出其分类关系也够了)
- Lock框架和Tools类(把图中这两个放到一起理解)
- Collections: 并发集合
- Atomic: 原子类
- Executors: 线程池
3.2、Lock框架和Tools哪些核心的类?
- 接口: Condition, Condition为接口类型,它将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。可以通过await(),signal()来休眠/唤醒线程。
- 接口: Lock,Lock为接口类型,Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的Condition对象。
- 接口ReadWriteLock ReadWriteLock为接口类型, 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。
- 抽象类: AbstractOwnableSynchonizer AbstractOwnableSynchonizer为抽象类,可以由线程以独占方式拥有的同步器。此类为创建锁和相关同步器(伴随着所有权的概念)提供了基础。AbstractOwnableSynchronizer 类本身不管理或使用此信息。但是,子类和工具可以使用适当维护的值帮助控制和监视访问以及提供诊断。
- 抽象类(long): AbstractQueuedLongSynchronizer AbstractQueuedLongSynchronizer为抽象类,以 long 形式维护同步状态的一个 AbstractQueuedSynchronizer 版本。此类具有的结构、属性和方法与 AbstractQueuedSynchronizer 完全相同,但所有与状态相关的参数和结果都定义为 long 而不是 int。当创建需要 64 位状态的多级别锁和屏障等同步器时,此类很有用。
- 核心抽象类(int): AbstractQueuedSynchronizer AbstractQueuedSynchronizer为抽象类,其为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。
- 锁常用类: LockSupport LockSupport为常用类,用来创建锁和其他同步类的基本线程阻塞原语。LockSupport的功能和"Thread中的 Thread.suspend()和Thread.resume()有点类似",LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程。但是park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。
- 锁常用类: ReentrantLock ReentrantLock为常用类,它是一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
- 锁常用类: ReentrantReadWriteLock ReentrantReadWriteLock是读写锁接口ReadWriteLock的实现类,它包括Lock子类ReadLock和WriteLock。ReadLock是共享锁,WriteLock是独占锁。
- 锁常用类: StampedLock 它是java8在java.util.concurrent.locks新增的一个API。StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。
- 工具常用类: CountDownLatch CountDownLatch为常用类,它是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
- 工具常用类: CyclicBarrier CyclicBarrier为常用类,其是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。
- 工具常用类: Phaser Phaser是JDK 7新增的一个同步辅助类,它可以实现CyclicBarrier和CountDownLatch类似的功能,而且它支持对任务的动态调整,并支持分层结构来达到更高的吞吐量。
- 工具常用类: Semaphore Semaphore为常用类,其是一个计数信号量,从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。
- 工具常用类: Exchanger Exchanger是用于线程协作的工具类, 主要用于两个线程之间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange()方法交换数据,当一个线程先执行exchange()方法后,它会一直等待第二个线程也执行exchange()方法,当这两个线程到达同步点时,这两个线程就可以交换数据了。
3.3、JUC并发集合哪些核心的类?
- Queue: ArrayBlockingQueue 一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。
- Queue: LinkedBlockingQueue 一个基于已链接节点的、范围任意的 blocking queue。此队列按 FIFO(先进先出)排序元素。队列的头部 是在队列中时间最长的元素。队列的尾部 是在队列中时间最短的元素。新元素插入到队列的尾部,并且队列获取操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。
- Queue: LinkedBlockingDeque 一个基于已链接节点的、任选范围的阻塞双端队列。
- Queue: ConcurrentLinkedQueue 一个基于链接节点的无界线程安全队列。此队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部 是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。当多个线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。此队列不允许使用 null 元素。
- Queue: ConcurrentLinkedDeque 是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式。
- Queue: DelayQueue 延时无界阻塞队列,使用Lock机制实现并发访问。队列里只允许放可以“延期”的元素,队列中的head是最先“到期”的元素。如果队里中没有元素到“到期”,那么就算队列中有元素也不能获取到。
- Queue: PriorityBlockingQueue 无界优先级阻塞队列,使用Lock机制实现并发访问。priorityQueue的线程安全版,不允许存放null值,依赖于comparable的排序,不允许存放不可比较的对象类型。
- Queue: SynchronousQueue 没有容量的同步队列,通过CAS实现并发访问,支持FIFO和FILO。
- Queue: LinkedTransferQueue JDK 7新增,单向链表实现的无界阻塞队列,通过CAS实现并发访问,队列元素使用 FIFO(先进先出)方式。LinkedTransferQueue可以说是ConcurrentLinkedQueue、SynchronousQueue(公平模式)和LinkedBlockingQueue的超集, 它不仅仅综合了这几个类的功能,同时也提供了更高效的实现。
- List: CopyOnWriteArrayList ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。
- Set: CopyOnWriteArraySet 对其所有操作使用内部CopyOnWriteArrayList的Set。即将所有操作转发至CopyOnWriteArayList来进行操作,能够保证线程安全。在add时,会调用addIfAbsent,由于每次add时都要进行数组遍历,因此性能会略低于CopyOnWriteArrayList。
- Set: ConcurrentSkipListSet 一个基于ConcurrentSkipListMap 的可缩放并发 NavigableSet 实现。set 的元素可以根据它们的自然顺序进行排序,也可以根据创建 set 时所提供的 Comparator 进行排序,具体取决于使用的构造方法。
- Map: ConcurrentHashMap 是线程安全HashMap的。ConcurrentHashMap在JDK 7之前是通过Lock和segment(分段锁)实现,JDK 8 之后改为CAS+synchronized来保证并发安全。
- Map: ConcurrentSkipListMap 线程安全的有序的哈希表(相当于线程安全的TreeMap);映射可以根据键的自然顺序进行排序,也可以根据创建映射时所提供的 Comparator 进行排序,具体取决于使用的构造方法。
3.4、JUC原子类哪些核心的类?
其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。
- 原子更新基本类型
- AtomicBoolean: 原子更新布尔类型。
- AtomicInteger: 原子更新整型。
- AtomicLong: 原子更新长整型。
- 原子更新数组
- AtomicIntegerArray: 原子更新整型数组里的元素。
- AtomicLongArray: 原子更新长整型数组里的元素。
- AtomicReferenceArray: 原子更新引用类型数组里的元素。
- 原子更新引用类型
- AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
- AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
- AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
- AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述
- 原子更新字段类
- AtomicReference: 原子更新引用类型。
- AtomicStampedReference: 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。
- AtomicMarkableReferce: 原子更新带有标记位的引用类型。
3.5、JUC线程池哪些核心的类?
- 接口: Executor Executor接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法。通常使用 Executor 而不是显式地创建线程。
- ExecutorService ExecutorService继承自Executor接口,ExecutorService提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以关闭 ExecutorService,这将导致其停止接受新任务。关闭后,执行程序将最后终止,这时没有任务在执行,也没有任务在等待执行,并且无法提交新任务。
- ScheduledExecutorService ScheduledExecutorService继承自ExecutorService接口,可安排在给定的延迟后运行或定期执行的命令。
- AbstractExecutorService AbstractExecutorService继承自ExecutorService接口,其提供 ExecutorService 执行方法的默认实现。此类使用 newTaskFor 返回的 RunnableFuture 实现 submit、invokeAny 和 invokeAll 方法,默认情况下,RunnableFuture 是此包中提供的 FutureTask 类。
- FutureTask FutureTask 为 Future 提供了基础实现,如获取任务执行结果(get)和取消任务(cancel)等。如果任务尚未完成,获取任务执行结果时将会阻塞。一旦执行结束,任务就不能被重启或取消(除非使用runAndReset执行计算)。FutureTask 常用来封装 Callable 和 Runnable,也可以作为一个任务提交到线程池中执行。除了作为一个独立的类之外,此类也提供了一些功能性函数供我们创建自定义 task 类使用。FutureTask 的线程安全由CAS来保证。
- 核心: ThreadPoolExecutor ThreadPoolExecutor实现了AbstractExecutorService接口,也是一个 ExecutorService,它使用可能的几个池线程之一执行每个提交的任务,通常使用 Executors 工厂方法配置。 线程池可以解决两个不同问题: 由于减少了每个任务调用的开销,它们通常可以在执行大量异步任务时提供增强的性能,并且还可以提供绑定和管理资源(包括执行任务集时使用的线程)的方法。每个 ThreadPoolExecutor 还维护着一些基本的统计数据,如完成的任务数。
- 核心: ScheduledThreadExecutor ScheduledThreadPoolExecutor实现ScheduledExecutorService接口,可安排在给定的延迟后运行命令,或者定期执行命令。需要多个辅助线程时,或者要求 ThreadPoolExecutor 具有额外的灵活性或功能时,此类要优于 Timer。
- 核心: Fork/Join框架 ForkJoinPool 是JDK 7加入的一个线程池类。Fork/Join 技术是分治算法(Divide-and-Conquer)的并行实现,它是一项可以获得良好的并行性能的简单且高效的设计技术。目的是为了帮助我们更好地利用多处理器带来的好处,使用所有可用的运算能力来提升应用的性能。
- 工具类: Executors Executors是一个工具类,用其可以创建ExecutorService、ScheduledExecutorService、ThreadFactory、Callable等对象。它的使用融入到了ThreadPoolExecutor, ScheduledThreadExecutor和ForkJoinPool中。
4、JUC原子类
4.1、线程安全的实现方法有哪些?
线程安全的实现方法包含:
- 互斥同步: synchronized 和 ReentrantLock
- 非阻塞同步: CAS, AtomicXXXX
- 无同步方案: 栈封闭,Thread Local,可重入代码
4.2、什么是CAS?
CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。 简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁。JDK中大量使用了CAS来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新。
相信sql大家都熟悉,类似sql中的条件更新一样:update set id=3 from table where id=2。因为单条sql执行具有原子性,如果有多个线程同时执行此sql语句,只有一条能更新成功。
4.3、CAS使用示例,结合AtomicInteger给出示例?
如果不使用CAS,在高并发下,多线程同时修改一个变量的值我们需要synchronized加锁(可能有人说可以用Lock加锁,Lock底层的AQS也是基于CAS进行获取锁的)。
public class Test {
private int i=0;
public synchronized int add(){
return i++;
}
}
java中为我们提供了AtomicInteger 原子类(底层基于CAS进行更新数据的),不需要加锁就在多线程并发场景下实现数据的一致性。
public class Test {
private AtomicInteger i = new AtomicInteger(0);
public int add(){
return i.addAndGet(1);
}
}
4.4、CAS会有哪些问题?
CAS 方式为乐观锁,synchronized 为悲观锁。因此使用 CAS 解决并发问题通常情况下性能更优。
但使用 CAS 方式也会有几个问题:
- ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。
从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
- 循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行命令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。
- 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i = 2,j = a,合并一下ij = 2a,然后用CAS来操作ij。
从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
4.5、AtomicInteger底层实现?
- CAS+volatile
- volatile保证线程的可见性,多线程并发时,一个线程修改数据,可以保证其它线程立马看到修改后的值CAS 保证数据更新的原子性。
4.6、请阐述你对Unsafe类的理解?
UnSafe类总体功能:
如上图所示,Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类,下面将对其相关方法和应用场景进行详细介绍。
4.7、说说你对Java原子类的理解?
包含13个,4组分类,说说作用和使用场景。
- 原子更新基本类型
- AtomicBoolean: 原子更新布尔类型。
- AtomicInteger: 原子更新整型。
- AtomicLong: 原子更新长整型。
- 原子更新数组
- AtomicIntegerArray: 原子更新整型数组里的元素。
- AtomicLongArray: 原子更新长整型数组里的元素。
- AtomicReferenceArray: 原子更新引用类型数组里的元素。
- 原子更新引用类型
- AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
- AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
- AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
- AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述
- 原子更新字段类
- AtomicReference: 原子更新引用类型。
- AtomicStampedReference: 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。
- AtomicMarkableReferce: 原子更新带有标记位的引用类型。
4.8、AtomicStampedReference是怎么解决ABA的?
AtomicStampedReference主要维护包含一个对象引用以及一个可以自动更新的整数"stamp"的pair对象来解决ABA问题。
5、 JUC锁
5.1、为什么LockSupport也是核心基础类?
AQS框架借助于两个类:Unsafe(提供CAS操作)和LockSupport(提供park/unpark操作)
5.2、通过wait/notify实现同步?
class MyThread extends Thread {
public void run() {
synchronized (this) {
System.out.println("before notify");
notify();
System.out.println("after notify");
}
}
}
public class WaitAndNotifyDemo {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
synchronized (myThread) {
try {
myThread.start();
// 主线程睡眠3s
Thread.sleep(3000);
System.out.println("before wait");
// 阻塞主线程
myThread.wait();
System.out.println("after wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果
before wait
before notify
after notify
after wait
说明: 具体的流程图如下
使用wait/notify实现同步时,必须先调用wait,后调用notify,如果先调用notify,再调用wait,将起不了作用。具体代码如下
class MyThread extends Thread {
public void run() {
synchronized (this) {
System.out.println("before notify");
notify();
System.out.println("after notify");
}
}
}
public class WaitAndNotifyDemo {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
// 主线程睡眠3s
Thread.sleep(3000);
synchronized (myThread) {
try {
System.out.println("before wait");
// 阻塞主线程
myThread.wait();
System.out.println("after wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
before notify
after notify
before wait
说明: 由于先调用了notify,再调用的wait,此时主线程还是会一直阻塞。
5.3、通过LockSupport的park/unpark实现同步?
import java.util.concurrent.locks.LockSupport;
class MyThread extends Thread {
private Object object;
public MyThread(Object object) {
this.object = object;
}
public void run() {
System.out.println("before unpark");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取blocker
System.out.println("Blocker info " + LockSupport.getBlocker((Thread) object));
// 释放许可
LockSupport.unpark((Thread) object);
// 休眠500ms,保证先执行park中的setBlocker(t, null);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再次获取blocker
System.out.println("Blocker info " + LockSupport.getBlocker((Thread) object));
System.out.println("after unpark");
}
}
public class test {
public static void main(String[] args) {
MyThread myThread = new MyThread(Thread.currentThread());
myThread.start();
System.out.println("before park");
// 获取许可
LockSupport.park("ParkAndUnparkDemo");
System.out.println("after park");
}
}
运行结果:
before park
before unpark
Blocker info ParkAndUnparkDemo
after park
Blocker info null
after unpark
说明: 本程序先执行park,然后在执行unpark,进行同步,并且在unpark的前后都调用了getBlocker,可以看到两次的结果不一样,并且第二次调用的结果为null,这是因为在调用unpark之后,执行了Lock.park(Object blocker)函数中的setBlocker(t, null)函数,所以第二次调用getBlocker时为null。
上例是先调用park,然后调用unpark,现在修改程序,先调用unpark,然后调用park,看能不能正确同步。具体代码如下
import java.util.concurrent.locks.LockSupport;
class MyThread extends Thread {
private Object object;
public MyThread(Object object) {
this.object = object;
}
public void run() {
System.out.println("before unpark");
// 释放许可
LockSupport.unpark((Thread) object);
System.out.println("after unpark");
}
}
public class ParkAndUnparkDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread(Thread.currentThread());
myThread.start();
try {
// 主线程睡眠3s
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("before park");
// 获取许可
LockSupport.park("ParkAndUnparkDemo");
System.out.println("after park");
}
}
运行结果:
before unpark
after unpark
before park
after park
说明: 可以看到,在先调用unpark,再调用park时,仍能够正确实现同步,不会造成由wait/notify调用顺序不当所引起的阻塞。因此park/unpark相比wait/notify更加的灵活。
5.4、Thread.sleep()、Object.wait()、Condition.await()、LockSupport.park()的区别? 重点
- Thread.sleep()和Object.wait()的区别
首先,我们先来看看Thread.sleep()和Object.wait()的区别,这是一个烂大街的题目了,大家应该都能说上来两点。
- Thread.sleep()不会释放占有的锁,Object.wait()会释放占有的锁;
- Thread.sleep()必须传入时间,Object.wait()可传可不传,不传表示一直阻塞下去;
- Thread.sleep()到时间了会自动唤醒,然后继续执行;
- Object.wait()不带时间的,需要另一个线程使用Object.notify()唤醒;
- Object.wait()带时间的,假如没有被notify,到时间了会自动唤醒,这时又分好两种情况,一是立即获取到了锁,线程自然会继续执行;二是没有立即获取锁,线程进入同步队列等待获取锁;
其实,他们俩最大的区别就是Thread.sleep()不会释放锁资源,Object.wait()会释放锁资源。
- Object.wait()和Condition.await()的区别
Object.wait()和Condition.await()的原理是基本一致的,不同的是Condition.await()底层是调用LockSupport.park()来实现阻塞当前线程的。
实际上,它在阻塞当前线程之前还干了两件事,一是把当前线程添加到条件队列中,二是“完全”释放锁,也就是让state状态变量变为0,然后才是调用LockSupport.park()阻塞当前线程。
- Thread.sleep()和LockSupport.park()的区别 LockSupport.park()还有几个兄弟方法——parkNanos()、parkUtil()等,我们这里说的park()方法统称这一类方法。
- 从功能上来说,Thread.sleep()和LockSupport.park()方法类似,都是阻塞当前线程的执行,且都不会释放当前线程占有的锁资源;
- Thread.sleep()没法从外部唤醒,只能自己醒过来;
- LockSupport.park()方法可以被另一个线程调用LockSupport.unpark()方法唤醒;
- Thread.sleep()方法声明上抛出了InterruptedException中断异常,所以调用者需要捕获这个异常或者再抛出;
- LockSupport.park()方法不需要捕获中断异常;
- Thread.sleep()本身就是一个native方法;
- LockSupport.park()底层是调用的Unsafe的native方法;
- Object.wait()和LockSupport.park()的区别
二者都会阻塞当前线程的运行,他们有什么区别呢? 经过上面的分析相信你一定很清楚了,真的吗? 往下看!
- Object.wait()方法需要在synchronized块中执行;
- LockSupport.park()可以在任意地方执行;
- Object.wait()方法声明抛出了中断异常,调用者需要捕获或者再抛出;
- LockSupport.park()不需要捕获中断异常;
- Object.wait()不带超时的,需要另一个线程执行notify()来唤醒,但不一定继续执行后续内容;
- LockSupport.park()不带超时的,需要另一个线程执行unpark()来唤醒,一定会继续执行后续内容;
park()/unpark()底层的原理是“二元信号量”,你可以把它相像成只有一个许可证的Semaphore,只不过这个信号量在重复执行unpark()的时候也不会再增加许可证,最多只有一个许可证。
5.5、如果在wait()之前执行了notify()会怎样?
如果当前的线程不是此对象锁的所有者,却调用该对象的notify()或wait()方法时抛出IllegalMonitorStateException异常;
如果当前线程是此对象锁的所有者,wait()将一直阻塞,因为后续将没有其它notify()唤醒它。
5.6、如果在park()之前执行了unpark()会怎样?
线程不会被阻塞,直接跳过park(),继续执行后续内容
5.7、什么是AQS? 为什么它是核心?
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AbstractQueuedSynchronizer类底层的数据结构是使用CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。其中Sync queue,即同步队列,是双向链表,包括head结点和tail结点,head结点主要用作后续的调度。而Condition queue不是必须的,其是一个单向链表,只有当使用Condition时,才会存在此单向链表。并且可能会有多个Condition queue。
5.8、AQS的核心思想是什么?
底层数据结构: AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
5.9、AQS有哪些核心的方法?
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
5.10、AQS定义什么样的资源获取方式?
AQS定义了两种资源获取方式:
- 独占(只有一个线程能访问执行,又根据是否按队列的顺序分为公平锁和非公平锁,如
ReentrantLock
) - 共享(多个线程可同时访问执行,如
Semaphore
、CountDownLatch
、CyclicBarrier
)。ReentrantReadWriteLock
可以看成是组合式,允许多个线程同时对某一资源进行读。
5.11、AQS底层使用了什么样的设计模式?
模板, 共享锁和独占锁在一个接口类中。
5.12、什么是可重入,什么是可重入锁? 它用来解决什么问题?
可重入:(来源于维基百科)若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
5.13、ReentrantLock的核心是AQS,那么它怎么来实现的,继承吗?
ReentrantLock总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系。
说明: ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。下面逐个进行分析。
5.14、ReentrantLock是如何实现公平锁的?
FairSync
5.15、ReentrantLock是如何实现非公平锁的?
UnFairSync
5.16、ReentrantLock默认实现的是公平还是非公平锁?
非公平锁
5.17、为了有了ReentrantLock还需要ReentrantReadWriteLock?
读锁和写锁分离:ReentrantReadWriteLock表示可重入读写锁,ReentrantReadWriteLock中包含了两种锁,读锁ReadLock和写锁WriteLock,可以通过这两种锁实现线程间的同步。
5.18、ReentrantReadWriteLock底层实现原理?
ReentrantReadWriteLock有五个内部类,五个内部类之间也是相互关联的。内部类的关系如下图所示。
说明: 如上图所示,Sync继承自AQS、NonfairSync继承自Sync类、FairSync继承自Sync类;ReadLock实现了Lock接口、WriteLock也实现了Lock接口。
5.19、ReentrantReadWriteLock底层读写状态如何设计的?
高16位为读锁,低16位为写锁
5.20、读锁和写锁的最大数量是多少?
2的16次方-1
5.21、本地线程计数器ThreadLocalHoldCounter是用来做什么的?
本地线程计数器,与对象绑定(线程-》线程重入的次数)
5.22、写锁的获取与释放是怎么实现的?
tryAcquire/tryRelease
5.23、读锁的获取与释放是怎么实现的?
tryAcquireShared/tryReleaseShared
5.24、什么是锁的升降级?
RentrantReadWriteLock为什么不支持锁升级? RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。
6、JUC集合类
6.1、为什么HashTable慢? 它的并发度是什么? 那么ConcurrentHashMap并发度是什么?
Hashtable之所以效率低下主要是因为其实现使用了synchronized关键字对put等操作进行加锁,而synchronized关键字加锁是对整个对象进行加锁,也就是说在进行put等修改Hash表的操作时,锁住了整个Hash表,从而使得其表现的效率低下。
6.2、ConcurrentHashMap在JDK1.7和JDK1.8中实现有什么差别? JDK1.8解決了JDK1.7中什么问题
HashTable
: 使用了synchronized关键字对put等操作进行加锁;ConcurrentHashMap JDK1.7
: 使用分段锁机制实现;ConcurrentHashMap JDK1.8
: 则使用数组+链表+红黑树数据结构和CAS原子操作实现;
6.3、ConcurrentHashMap JDK1.7实现的原理是什么?
在JDK1.5~1.7版本,Java使用了分段锁机制实现ConcurrentHashMap.
简而言之,ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段;而每个Segment元素,它通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全;这样,在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁即可。因此,ConcurrentHashMap在多线程并发编程中可是实现多线程put操作。
concurrencyLevel
: Segment 数(并行级别、并发数)。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。
6.4、ConcurrentHashMap JDK1.7中Segment数(concurrencyLevel)默认值是多少? 为何一旦初始化就不可再扩容?
默认是 16
6.5、ConcurrentHashMap JDK1.7说说其put的机制?
整体流程还是比较简单的,由于有独占锁的保护,所以 segment 内部的操作并不复杂
- 计算 key 的 hash 值
- 根据 hash 值找到 Segment 数组中的位置 j; ensureSegment(j) 对 segment[j] 进行初始化(Segment 内部是由 数组+链表 组成的)
- 插入新值到 槽 s 中
6.6、ConcurrentHashMap JDK1.7是如何扩容的?
rehash(注:segment 数组不能扩容,扩容是 segment 数组某个位置内部的数组 HashEntry<K,V>[] 进行扩容)
6.7、ConcurrentHashMap JDK1.8实现的原理是什么?
在JDK1.7之前,ConcurrentHashMap是通过分段锁机制来实现的,所以其最大并发度受Segment的个数限制。因此,在JDK1.8中,ConcurrentHashMap的实现原理摒弃了这种设计,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现。
简而言之:数组+链表+红黑树,CAS
6.8、ConcurrentHashMap JDK1.8是如何扩容的?
tryPresize, 扩容也是做翻倍扩容的,扩容后数组容量为原来的 2 倍
6.9、ConcurrentHashMap JDK1.8链表转红黑树的时机是什么? 临界值为什么是8?
size = 8, log(N)
6.10、ConcurrentHashMap JDK1.8是如何进行数据迁移的?
transfer, 将原来的 tab 数组的元素迁移到新的 nextTab 数组中
6.11、先说说非并发集合中Fail-fast机制?
快速失败
6.12、CopyOnWriteArrayList的实现原理?
COW基于拷贝
// 将toCopyIn转化为Object[]类型数组,然后设置当前数组
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
属性中有一个可重入锁,用来保证线程安全访问,还有一个Object类型的数组,用来存放具体的元素。当然,也使用到了反射机制和CAS来保证原子性的修改lock域。
// 可重入锁
final transient ReentrantLock lock = new ReentrantLock();
// 对象数组,用于存放元素
private transient volatile Object[] array;
// 反射机制
private static final sun.misc.Unsafe UNSAFE;
// lock域的内存偏移量
private static final long lockOffset;
6.13、弱一致性的迭代器原理是怎么样的?
COWIterator<E>
COWIterator表示迭代器,其也有一个Object类型的数组作为CopyOnWriteArrayList数组的快照,这种快照风格的迭代器方法在创建迭代器时使用了对当时数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。在迭代器上进行的元素更改操作(remove、set 和 add)不受支持。这些方法将抛出 UnsupportedOperationException。
6.14、CopyOnWriteArrayList为什么并发安全且性能比Vector好?
Vector对单独的add,remove等方法都是在方法上加了synchronized; 并且如果一个线程A调用size时,另一个线程B 执行了remove,然后size的值就不是最新的,然后线程A调用remove就会越界(这时就需要再加一个Synchronized)。这样就导致有了双重锁,效率大大降低,何必呢。于是vector废弃了,要用就用CopyOnWriteArrayList 吧。
6.15、CopyOnWriteArrayList有何缺陷,说说其应用场景?
CopyOnWriteArrayList 有几个缺点:
-
由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc
-
不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;
CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用
因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
6.16、要想用线程安全的队列有哪些选择?
Vector,Collections.synchronizedList( List<T> list)
, ConcurrentLinkedQueue等
6.17、ConcurrentLinkedQueue实现的数据结构?
ConcurrentLinkedQueue的数据结构与LinkedBlockingQueue的数据结构相同,都是使用的链表结构。ConcurrentLinkedQueue的数据结构如下:
说明: ConcurrentLinkedQueue采用的链表结构,并且包含有一个头节点和一个尾结点。
6.18、ConcurrentLinkedQueue底层原理?
// 反射机制
private static final sun.misc.Unsafe UNSAFE;
// head域的偏移量
private static final long headOffset;
// tail域的偏移量
private static final long tailOffset;
说明: 属性中包含了head域和tail域,表示链表的头节点和尾结点,同时,ConcurrentLinkedQueue也使用了反射机制和CAS机制来更新头节点和尾结点,保证原子性。
6.19、ConcurrentLinkedQueue的核心方法有哪些?
offer(),poll(),peek(),isEmpty()等队列常用方法
6.20、说说ConcurrentLinkedQueue的HOPS(延迟更新的策略)的设计?
通过上面对offer和poll方法的分析,我们发现tail和head是延迟更新的,两者更新触发时机为:
-
tail更新触发时机:当tail指向的节点的下一个节点不为null的时候,会执行定位队列真正的队尾节点的操作,找到队尾节点后完成插入之后才会通过casTail进行tail更新;当tail指向的节点的下一个节点为null的时候,只插入节点不更新tail。
-
head更新触发时机:当head指向的节点的item域为null的时候,会执行定位队列真正的队头节点的操作,找到队头节点后完成删除之后才会通过updateHead进行head更新;当head指向的节点的item域不为null的时候,只删除节点不更新head。
并且在更新操作时,源码中会有注释为:hop two nodes at a time
。所以这种延迟更新的策略就被叫做HOPS的大概原因是这个(猜的