Java面试之JUC
引言:juc,java并发编程工具包,高频面试考点。
参考:https://www.bilibili.com/video/av48961087
代码:https://gitee.com/machine4869/note-code/tree/master/note_juc
该文章转自我的博客:https://machine4869.gitee.io/20200221192831555/
文章目录
- Juc框架概览
- Volatile
- volatile是什么?为什么引入了volatile
- JMM抽象结构图描述
- JMM如何保证同步的?
- JMM三大特性是什么?
- 可见性是什么意思?
- volatile保证可见性代码演示
- 原子性是什么意思?
- volatile不保证原子性代码演示
- volatile不保证原子性理论解释(num++为什么不安全)
- volatile不保证原子性问题怎么解决?
- AtomicInteger保证原子性代码演示
- * 有序性之Happens-before原则
- Volatile通过什么实现可见性?
- 什么是指令重排?造成什么问题?
- * 指令重排造成的不安全举例
- * 什么是内存屏障?
- 如何保证有序性?
- 你在哪些地方用到了volatile?
- 单例模式在多线程下不安全代码演示
- ! 单例模式在多线程下不安全解决方案?
- 为什么只用DCL不能保证线程安全?
- CAS
- ABA问题
- 简述ABA问题和解决方案?
- ABA问题描述?问题出在哪?
- 原子更新引用是啥?
- AtomicReference使用代码演示
- AtomicReference存在ABA问题代码验证
- AtomicStampReference解决ABA问题代码验证
- 集合类不安全
- ArrayList线程不安全演示-什么故障?什么原因?怎么解决?
- CopyOnWriteArrayList原理?它有什么好?
- CopyOnWriteArrayList 缺点&使用场合
- CopyOnWriteArrayList透露的思想
- 集合类不安全之Set-演示/故障/解决
- 集合类不安全之Map-演示/故障/解决
- HashMap底层实现原理-jdk7
- ConcurrentHashMap底层原理-jdk7
- HashMap底层实现原理-jdk8
- ! ConcurrentHashMap底层原理-jdk8
- Map类的各种对比
- Java锁
- Java 15锁,列举一些?
- 公平和非公平锁是什么?两者区别(优缺点)?两种锁举例?
- 可重入锁是什么?与不可重入的区别?可重入锁举例?作用?实现原理?
- 可重入锁代码验证
- 自旋锁是什么?优点?缺点?
- 手写一个自旋锁
- 独占锁和共享锁是什么?举例?优缺点比较?
- 验证读写锁ReentrantReadWriteLock
- 什么是乐观锁/悲观锁?举例?
- 什么是乐观读/悲观读?
- ReentrantReadWriteLock是乐观读还是悲观读?
- ! StempedLock作用?
- synchronized和Lock的区别是什么?
- AQS
- 什么是AQS?
- CountDownLatch -简述?应用场景?应用案例?常用方法?
- CyclicBarrier- 简述?应用场景?应用案例?常用方法?
- CyclicBarrier和CountDownLatch的使用区别?
- Semaphore- 简述?应用场景?应用案例?常用方法?
- Condition是什么?核心方法?使用案例?
- Condition分组唤醒是什么?锁绑定多个Condition使用案例?
- 阻塞队列
- 什么是阻塞队列?什么情况会导致阻塞是?主要应用场景?
- 为什么要用阻塞队列?有什么好?
- BlockingQueue的架构图?有哪些实现类?
- ! BlockingQueue的核心方法?
- SynchronousQueue使用案例?
- 阻塞队列的应用场景举例?
- 生产者消费者模式的传统版实现?(synchronized版和Lock版)
- 生产者消费者模式的阻塞队列版实现?
- 线程池
- 获得多线程的方式有几种?
- Runable Vs Callable
- Callable基本使用(demo)
- 线程池主要工作?特点?为什么用?优势?
- 线程池相关类架构图
- ! 线程池创建方式有多少种?
- 线程池3个常用的创建方式(demo)? 及底层实现?
- ThreadPoolExecutor 7参数介绍?
- 线程池底层工作原理?处理流程描述?
- 线程池拒绝策略时什么?有哪些?分别是什么意思?
- 线程池在实际生产中用哪一个?
- 为什么生产中不用jdk自带的线程池?
- 自定义线程池(demo)
- 合理配置线程池参数,如何考虑?
- 死锁是什么?产生死锁的主要原因?
- 死锁案例(demo)
- 怎么排查死锁问题?使用过哪些命令行工具?
Juc框架概览
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A9tlY24W-1584804080925)(20200221192831555/JUC框架.png)]
Volatile
volatile是什么?为什么引入了volatile
volatile是java虚拟机提供的一种轻量级同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排(保证了有序性)
高并发下不用synchronized,因为并发性不好;用volatile和juc下面的类;
JMM抽象结构图描述
关于JMM: blog/2018/07/27/15326929363424/#2-3-JAVA内存模型(JMM)
结构图主要描述:
- 主内存/工作内存/共享变量/拷贝副本
- JMM是一种抽象概念,不真实存在,它描述的是一组规则/规范
JMM如何保证同步的?
- 解锁前,把工作内存的值刷到主内存
- 加锁时,从主内存读取最新值到工作内存
- 加锁解锁是同一把锁
JMM三大特性是什么?
- 可见性
- 原子性
- 有序性
三大特性使线程安全性获得保证
可见性是什么意思?
某线程如果修改了主内存的共享变量,对其他线程是可见的。
volatile保证可见性代码演示
demo
package com.mxx.juc;
import java.util.concurrent.TimeUnit;
/**
* 可见性验证:
* - a线程修改变量值
* - 对b线程(main)是否可见
* 结论:
* - 不加volatile,a线程的修改对b线程不可见
* - 加volatile,a线程的修改对b线程可见
*/
public class VolatileDemo {
// int num = 1;
volatile int num = 1;
public void changeNum(){
this.num = 30;
}
public static void main(String[] args) {
// 线程 操作 资源类
VolatileDemo vd = new VolatileDemo();
new Thread(()->{
System.out.println("进入线程"+Thread.currentThread().getName()+",num="+vd.num);
// 休息,等待num被其他线程读取到
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
vd.changeNum();
System.out.println("线程"+Thread.currentThread().getName()+"修改num,num="+vd.num);
},"a").start();
while (vd.num==1){
// 一直等待循环
}
System.out.println(Thread.currentThread().getName()+"线程结束,num="+vd.num);
// 加volatile:
//进入线程a,num=1
//线程a修改num,num=30
//main线程结束,num=30
}
}
原子性是什么意思?
某个线程在做具体业务时,要么整体成功,要么整体失败,保证数据一致性
volatile不保证原子性代码演示
demo
package com.mxx.juc;
import java.util.concurrent.TimeUnit;
public class VolatileDemo {
// int num = 1;
volatile int num = 1;
public void addNum(){
num++;
}
/**
* 不保证原子性验证:
* - 20个线程,每个线程1000次,做+1操作
* - 20个线程全部跑完后,看main函数打印结果
* 结论:
* - 每次结果都不一样,小于20001
*
* @param args
*/
public static void main(String[] args) {
VolatileDemo vd = new VolatileDemo();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
vd.addNum();
}
},"线程"+i).start();
}
// 主线程+GC线程
while (Thread.activeCount()>2){
// 线程礼让
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"线程: num="+ vd.num);
}
}
volatile不保证原子性理论解释(num++为什么不安全)
num++出现问题底层逻辑(javap -c):
- 获得:各线程从主内存读num到工作内存
- 修改:各线程在各自工作内存做+1操作,工作内存中的操作互相不可见。
- 写回:线程在写回前被挂起了,写回的时候相互覆盖,造成数值丢失。
volatile不保证原子性问题怎么解决?
- 加synchronized,并发性能不好
- juc的atomic,比如AtomicInteger的getAndIncrement()
AtomicInteger保证原子性代码演示
demo
AtomicInteger num2 = new AtomicInteger(1);
public void addNumByAtomic(){
num2.getAndIncrement();
// num2.incrementAndGet();
}
/**
* volatile不保证原子性解决:
* - AtomicInteger
*/
public static void main(String[] args) {
// seeOkByVolatile();
VolatileDemo vd = new VolatileDemo();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
// 不安全
vd.addNum();
// 安全
vd.addNumByAtomic();
}
},"线程"+i).start();
}
// 主线程+GC线程
while (Thread.activeCount()>2){
// 线程礼让
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"线程: num="+ vd.num);
System.out.println(Thread.currentThread().getName()+"线程: num2="+ vd.num2);
// main线程: num=13808
// main线程: num2=20001
}
* 有序性之Happens-before原则
java内存模型一个列出了八种Happens-before规则,如果两个操作的次序不能从这八种规则中推倒出来,则不能保证有序性
更详细的:blog/2018/07/28/15327607157902/#4-4-有序性
Volatile通过什么实现可见性?
通过加入 内存屏障 和 禁止重排序优化来实现
什么是指令重排?造成什么问题?
- Java内存模型中,为提高性能,允许编译器和处理器对指令进行重排序。
- 重排时会考虑到指令间的数据依赖性
- 不会影响单线程环境下程序执行
- 多线程下,线程交替执行,由于优化重排的存在,两线程使用的变量能否保证一致性是无法确定的。结果无法预测。
* 指令重排造成的不安全举例
int a=0;
boolean flag = false;
public void m1(){
a=1; // 1
flag=true; // 2
}
public void m2() {
if(flag){
a = a+5; // 3
System.out.println("revalue="+a);
}
}
解释:
- 线程1执行m1,线程2执行m2
- 在不重排时,一定是按照123步骤执行,结果为6
- 如果发生重排,比如1和2交换了顺序,当m1执行完2时,线程切换,执行m1,这时可以进入if函数,a结果为5
* 什么是内存屏障?
详细:blog/2018/07/28/15327607157902/#4-3-可见性
关键字:
- load屏障/store屏障
- volatile读/volatile写
- 内存屏障还能强制刷出cpu缓存,保证数据最新
如何保证有序性?
保证有序性:volatile、synchronized、Lock
你在哪些地方用到了volatile?
- 单例模式在多线程下不安全
- 读写锁/手写缓存
- cas底层源码分析
单例模式在多线程下不安全代码演示
demo
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName()+"\t调用构造方法");
}
public static SingletonDemo getInstance() {
if(instance == null){
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
// 单线程下,单例模式正常。
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//main 调用构造方法
//true
//true
//true
// 多线程下,单例模式不行
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonDemo.getInstance();
},"线程"+i).start();
}
//线程0 调用构造方法
//线程4 调用构造方法
//线程3 调用构造方法
//线程2 调用构造方法
//线程1 调用构造方法
}
}
! 单例模式在多线程下不安全解决方案?
DCL双端检索机制+volatile
为什么只用DCL不能保证线程安全?
简答:在创建对象过程中发生指令重排。检测到intance不为null但对象却没有完全创建成功。
双端检索机制:在加锁前和加锁后都进行一次判断
demo:
public static SingletonDemo getInstance() {
if(instance == null){
synchronized (SingletonDemo.class){
if(instance == null){
instance = new SingletonDemo();
}
}
}
return instance;
}
为什么以上代码不一定安全?
- 因为有指令重排序的存在
- 原因在于:某线程读到instance不为nul时,instance的引用对象可能还没有完成初始化(new SingletonDemo()做到一半)
instance = new SingletonDemo()分为以下步骤:
1)分配对象内存空间
2)初始化对象
3)设置instance指向内存地址,此时instance != nul
其中步骤2 3可能发生重排, - 因此多线程下,当线程a访问instance!=null时,instance实例却未必初始化完成(还没做2);此时切到线程b,线程b直接取intance实例,这个实例是未完成初始化的实例。因此线程不安全。
如何解决?
private static volatile SingletonDemo instance = null;
// 告诉编译器禁止指令重排
CAS
提问线路:CAS—> Unsafe—> CAS底层原理 —> 原子引用更新 —> 如何规避ABA问题
compareAndSet怎么用?
比较并交换(compareAndSet)
/**
* boolean compareAndSet(int expect, int update)
* - 如果主内存的值=期待值expect,就将主内存值改为update
* - 该方法可以检测线程a的操作变量X没有被其他线程修改过
* - 保证了线程安全
*/
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 10)+ "\t" + atomicInteger);
System.out.println(atomicInteger.compareAndSet(5, 20)+ "\t" + atomicInteger);
//true 10
//false 10
}
CAS底层原理简述?
- Compare-And-Swap。是一条CPU并发原语。(原语:操作系统范畴,依赖硬件,不被中断。)
- 功能是判断内存某个位置的值是否为预期值(Compare),是就更新(Swap),这个过程是原子的。
- 功能描述:
- 判断内存某个位置的值是否为预期值(Compare),是就更新(Swap),这个过程是原子的。
- cas有三个操作数,内存值V,旧预期值A,要更新的值B。仅当预期值A=内存值V时,才将内存值V修改为B,否则什么都不做。
- 自旋:比较并交换,直到比较成功
- 底层靠Unsafe类保证原子性。
getAndIncrement() 源码解析(用了cas保证线程安全)
/**
* 参数说明:
* this: atomicInteger对象
* valueOffset:对象的内存地址
* unsafe:sun.misc.Unsafe类
* AtomicInteger中变量value使用volatile修饰,保证内存可见。
* 结论:底层依赖CAS操作/Unsafe类
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
* compareAndSwapInt:即CAS
* while: 如果修改失败,会一直尝试修改,直到成功。
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
简述:
- 调用了Unsafe类的getAndAddInt
- getAndAddInt使用cas一直循环尝试修改主内存
对Unsafe的理解?
Unsave类
- 该类所有方法都是native修饰,直接调用底层资源。sun.misc包中。
- 可以像C的指针一样直接操作内存。java的CAS操作依赖Unsafe类的方法。
! CAS有哪些缺点?
- 循环时间长,开销大
- 如果cas失败,就一直do while尝试。如果长时间不成功,可能给CPU带来很大开销。
- 只能保证一个共享变量的原子操作
- 如果时多个共享变量,cas无法保证原子性,只能加锁,锁住代码段。
- 引出来ABA问题。
ABA问题
简述ABA问题和解决方案?
简述版:
ABA问题描述: 线程1做CAS操作将A改为B再改为A,而线程2再做CAS时修改成功了,这不符合设计思想。
怎么解决:AtomicStampReference时间戳原子引用
ABA问题描述?问题出在哪?
ABA问题描述:
- 比如线程1从内存位置V中取出A,此时线程2也取出A。且线程2做了一次cas将值改为了B,然后又做了一次cas将值改回了A。此时线程1做cas发现内存中还是A,则线程1操作成功。这个时候实际上A值已经被其他线程改变过,这与设计思想是不符合的。
这个过程问题出在哪?
- 如果只在乎结果,ABA不介意B的存在,没什么问题
- 如果B的存在会造成影响,需要通过AtomicStampReference,加时间戳解决。
原子更新引用是啥?
AtomicStampReference,使用时间戳,解决cas中出现的ABA问题。
AtomicReference使用代码演示
demo
/**
* 如果希望原子操作的变量是User,Book,此时需要使用AtomicReference类
*/
public static void main(String[] args) {
User z3 = new User("z3",18);
User l4 = new User("l4",19);
AtomicReference<User> atomicReference = new AtomicReference<>(z3);
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
//[email protected]
//false [email protected]
}
AtomicReference存在ABA问题代码验证
demo
AtomicReference atomicReference = new AtomicReference<Integer>(100);
/**
* ABA问题验证:
* 1--ABA
* 2--A,C
* @param args
*/
public static void main(String[] args) {
ABADemo abaDemo = new ABADemo();
new Thread(()->{
abaDemo.atomicReference.compareAndSet(100,101);
abaDemo.atomicReference.compareAndSet(101,100);
},"1").start();
new Thread(()->{
// 睡1s等线程1执行完ABA
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { e.printStackTrace();}
System.out.println(abaDemo.atomicReference.compareAndSet(100, 2020)+"\t"+abaDemo.atomicReference.get());
//true 2020
},"2").start();
}
AtomicStampReference解决ABA问题代码验证
解决思路:每次变量更新的时候,把变量的版本号加一,这样只要变量被某一个线程修改过,该变量版本号就会发生递增操作,从而解决了ABA变化
AtomicStampedReference atomicStampedReference = new AtomicStampedReference<Integer>(100,1);
public static void main(String[] args) {
// ABAProblem();
ABADemo abaDemo = new ABADemo();
new Thread(()->{
// 等线程2读到初始版本号的值
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { e.printStackTrace();}
System.out.println("线程1在ABA前的版本号:"+abaDemo.atomicStampedReference.getStamp());
abaDemo.atomicStampedReference.compareAndSet(100,101,abaDemo.atomicStampedReference.getStamp(),abaDemo.atomicStampedReference.getStamp()+1);
abaDemo.atomicStampedReference.compareAndSet(101,100,abaDemo.atomicStampedReference.getStamp(),abaDemo.atomicStampedReference.getStamp()+1);
System.out.println("线程1在ABA后的版本号:"+abaDemo.atomicStampedReference.getStamp());
},"1").start();
new Thread(()->{
// 存一下修改前的版本号
int stamp = abaDemo.atomicStampedReference.getStamp();
System.out.println("线程2在修改操作前的版本号:"+stamp);
// 睡1s等线程1执行完ABA
try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) { e.printStackTrace();}
System.out.println(abaDemo.atomicStampedReference.compareAndSet(100,2020,stamp,abaDemo.atomicStampedReference.getStamp()+1)+ "\t" + abaDemo.atomicStampedReference.getReference());
//线程2在修改操作前的版本号:1
//线程1在ABA前的版本号:1
//线程1在ABA后的版本号:3
//false 100
},"2").start();
}
集合类不安全
ArrayList线程不安全演示-什么故障?什么原因?怎么解决?
ArrayList底层是一个数组,默认大小10,超过就扩容,扩原值的一半10+5=15
线程不安全,因为add方法没有加锁。
不安全案例:
public class ContainerNotSafeDemo {
/**
* 1. 故障
* java.util.ConcurrentModificationException 并发修改异常
*
* 2. 原因
* 线程并发修改导致,线程1正在写入,线程2抢占资源,导致数据不一致。
*
* 3. 解决
* 1 new Vector(); add方法加锁,线程安全,并发性差。
* 2 Collections.synchronizedList(new ArrayList<>()); 包装成安全的,还是加锁,并发性差。
* 3 new CopyOnWriteArrayList<>(); juc的类,写时复制
* 4. 优化
*/
public static void main(String[] args) {
// List<String> list = new ArrayList<>();
// List<String> list = new Vector<>();
List<String> list = Collections.synchronizedList(new ArrayList<>());
// List<String> list = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 30; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,4));
System.out.println(Thread.currentThread().getName()+"\t"+list);
},String.valueOf(i)).start();
}
}
}
CopyOnWriteArrayList原理?它有什么好?
参考:https://blog.csdn.net/linsongbin1/article/details/54581787
add源码:
public boolean add(E e) {
// 1. 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 2. 拷贝数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 3. 新增元素到新数组
newElements[len] = e;
// 4. 将array引用指向新数组
setArray(newElements);
return true;
} finally {
// 解锁
lock.unlock();
}
}
解释写时复制:
- 写操作时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。整个add操作都是在锁的保护下进行的。
- 读操作时,如果写完成且引用指向新数组,则读到的是最新数据;否则读到的是原始数组数据。可见读操作是不加锁的。
CopyOnWriteArrayList 缺点&使用场合
- 消耗内存。写操作,拷贝数组,消耗内存,数组大的话可能导致gc
- 不能实时读。拷贝新增需要时间,读到的可能是旧数据,能保证最终一致性,但不满足实时要求。
因此,适合读多写少的场景。
CopyOnWriteArrayList透露的思想
- 读写分离,提高并发
- 最终一致性
- 通过另辟空间,来解决并发冲突
集合类不安全之Set-演示/故障/解决
Set同理
HashSet > Collections.synchronizedSet() > CopyOnWriteArraySet
且CopyOnWriteArraySet底层还是用的CopyOnWriteArrayList
HashSet底层是HashMap, add(key,一个常量)
集合类不安全之Map-演示/故障/解决
Map类似
HashMap > Collections.synchronizedMap() > ConcurrentHashMap
HashMap底层实现原理-jdk7
可参考:
-
2018/10/30/20181030111752438/#10-4-HashMap与ConcurrentHashMap解析
-
HashMap源码分析:https://blog.csdn.net/weixin_36910300/article/details/79985197
map的存储结构是什么?
- Hash表(数组+链表)
- key-value构成一个entry对象
- 数组容量:默认16,始终保持 2^n(为了存取高效,减少碰撞,数据分配均匀)
new HashMap<>()底层发生了什么?
创建了长度=16的 Entry table。
源码分析
- capacity : 当前数组容量
- loadFactor:负载因子,默认为 0.75。
- threshold:扩容的阈值,等于 capacity * loadFactor。当元素个数超过这个值就触发扩容。
- MAXIMUM_CAPACITY = 1 << 30:最大的容量为 2 ^ 30
//调用无参构造器
public HashMap() {
//默认初始容量大小为16,默认的加载因子为0.75f
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
//初始容量不能小于0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始容量不能超过MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因子不能小于等于0,或者加载因子不能是非数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//设置加载因子
this.loadFactor = loadFactor;
//设置临界值
threshold = initialCapacity;
//伪构造,里面没有代码
init();
}
map.put(key1,value1)底层发生了什么?
- 求key1的hash值,调用hashCode(),该hash值用来计算Entry数组下标,得到存放位置。
- 如果该位置为空,则添加成功。
- 如果不为空,意味着该位置存在链表:
- 如果hash值与其他key不同,则添加成功。
- 如果key1的hash值有key与之相同,则调用equal(),继续比较:
- 如果equal返回false,则添加成功。
- 如果equal返回true,则使用value1替换旧值(修改操作)
map.get(key1)底层发生了什么?
- 根据key1计算hash,找到对应数组下标。
- 遍历该位置处的链表,找到key1.equal(key2)为true的entry,返回其value。
扩容的原理?
扩容后,数组大小为原来的 2 倍。
ConcurrentHashMap底层原理-jdk7
关键词:Segment数组/基于分段锁/提高并发
-
引入一个Segment数组。每个Segment单元都包含一个与HashMap结构差不多hash表
-
读取过程:
- 先取key的hash值,取高sshift位决定属于哪个Segment单元。
- 接着就是HashMap那一套
-
Segment继承jucReetrantLock,上锁方便,即分段锁。因此segment[1]锁了,不影响其他Segment单元并发。
HashMap底层实现原理-jdk8
与jdk7的不同的地方:
-
new HashMap()时,不创建长度为16的数组。
-
底层使用Node[], 而不是Entry[]
-
数组结构采用
数组+链表+红黑树
- 触发时机:当某索引位置链表长度>8 且 数组长度>64时,次索引位置的链表改为红黑树
- 红黑树的关键性质: 从根到叶子最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。
- 目的:加快检索速度
! ConcurrentHashMap底层原理-jdk8
参考:https://www.jianshu.com/p/5dbaa6707017
底层结构
- 和 1.8 HashMap 结构类似,也是数组+链表+红黑树的。取消了Segment 分段锁
那如何保证线程安全的?
- CAS + synchronized + volatile 来保证并发安全性,具体的如下
put方法逻辑
源代码解析:
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//1. 计算key的hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//2. 判断Node[]数组是否初始化,没有则进行初始化操作
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//3. 通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//4. 检查到内部正在扩容,就帮助它一块扩容。
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 如果该坐标Node不为null且没有正在扩容,就加锁,进行链表/红黑树 节点添加操作
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
//5. 当前为链表,在链表中插入新的键值对
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 6.当前为红黑树,将新的键值对插入到红黑树中
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 7.判断链表长度已经达到临界值8(默认值),当节点超过这个值就需要把链表转换为树结构
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//8.对当前容量大小进行检查,如果超过了临界值(实际大小*加载因子)就需要扩容
addCount(1L, binCount);
return null;
}
解析:对当前的table进行无条件自循环直到put成功(cas自旋)。
- 如果数组下标没有Node节点,就用CAS+自旋添加链表头节点。
- 如果有Node节点,就加synchronized,添加链表或红黑树节点。
get操作,由于数组被volatile修饰了,因此不用担心数组的可见性问题。
volatile Node<K,V>[] table;
Map类的各种对比
- HashMap和ConcurrentHashMap对比
- HashMap和HashTable的对比
- HashTable和ConcurrentHashMap对比
Java锁
Java 15锁,列举一些?
1.公平锁 / 非公平锁
2.可重入锁 / 不可重入锁
3.独享锁 / 共享锁
4.互斥锁 / 读写锁
5.乐观锁 / 悲观锁
6.分段锁
7.偏向锁 / 轻量级锁 / 重量级锁
8.自旋锁
公平和非公平锁是什么?两者区别(优缺点)?两种锁举例?
是什么?
- 公平锁:多个线程按照申请锁的顺序来获取锁。
- 非公平锁:多个线程获取锁的顺序不是按照申请舒顺序来的,有可能后申请的线程比先申请的线程优先获取锁。
高并发下,有可能造成优先级反转或者饥饿现象。
区别:
- 公平锁:保证顺序(队列,FIFO),性能下降。
- 非公平:先尝试直接占有锁,如果尝试失败,再采用类似公平锁的方式。优点在于吞吐量比公平锁大。
举例:
- ReentrantLock可以指定创建公平锁或非公平锁,无参构造默认创建非公平锁。
- synchronized是非公平的。
可重入锁是什么?与不可重入的区别?可重入锁举例?作用?实现原理?
是什么?
- 也叫递归锁
- 当一个线程获取某个对象锁后,可以再次获取同一把对象锁。
- 即,线程可进入任何他所拥有的对象锁所同步着的代码块。
区别?
public synchronized void m1(){
m1();
}
- 可重入锁:某线程进入外层m1后,可以再次进入递归m1方法。也叫递归锁。
- 不可重入锁:某线程进入外部m1后,不可再进如内部m1,必须等待锁释放。这里就造成了死锁。
举例:
- ReentrantLock/synchronized就是典型的可重入锁
作用:
- 避免死锁。案例:递归
实现原理:
- 计数器:进入最外层计数器=1,每递归一次,计数器+1,每退出一层,计数器-1,直到计数器=0,说明退出了最外层,此时该线程释放锁对象,其他线程才能获取该锁。
可重入锁代码验证
synchronized
public class SynchronizedLockDemo {
public synchronized void m1(){
System.out.println(Thread.currentThread().getName() + "----m1----");
m2();
}
public synchronized void m2(){
System.out.println(Thread.currentThread().getName() + "----m2----");
}
/**
* 可以看出,m1,m2是同一把锁,只有线程释放最外层锁,其他线程才能占用该锁。
* @param args
*/
public static void main(String[] args) {
SynchronizedLockDemo rd = new SynchronizedLockDemo();
for (int i = 0; i < 5; i++) {
new Thread(()->{
rd.m1();
},String.valueOf(i)).start();
}
}
//0----m1----
//0----m2----
//4----m1----
//4----m2----
//3----m1----
//3----m2----
//2----m1----
//2----m2----
//1----m1----
//1----m2----
}
ReentrantLock
public class ReentrantLockDemo {
Lock lock = new ReentrantLock();
public void m1(){
lock.lock();
// lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "----m1----");
m2();
}finally {
lock.unlock();
// lock.unlock();
}
}
public void m2(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "----m2----");
}finally {
lock.unlock();
}
}
/**
* 锁要成对出现,否则:
* - 多一个lock.lock()会造成锁无法释放,程序卡住
* - 多一个lock.unlock()直接报错
*/
public static void main(String[] args) {
ReentrantLockDemo rd = new ReentrantLockDemo();
for (int i = 0; i < 5; i++) {
new Thread(()->{
rd.m1();
},String.valueOf(i)).start();
}
//0----m1----
//0----m2----
//2----m1----
//2----m2----
//1----m1----
//1----m2----
//3----m1----
//3----m2----
//4----m1----
//4----m2----
}
}
自旋锁是什么?优点?缺点?
- 自旋锁(spinlock)是指尝试获取锁的对象不会立即阻塞,而是采用循环的方式取尝试获取锁。好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
手写一个自旋锁
public class SpinLockDemo {
/**
* 手写自旋锁
* 自旋锁的核心:while+cas+原子引用线程
*
* A线程加锁,一顿操作5秒钟,解锁。B线程一直自旋等待A线程释放锁,然后获取锁。
* 打印结果:
* A尝试获取锁
* A成功获取锁
* A一顿操作5秒...
* B尝试获取锁
* A释放锁
* B成功获取锁
* B一顿操作
* B释放锁
*/
public static void main(String[] args) {
SpinLockDemo sDemo = new SpinLockDemo();
new Thread(()->{
sDemo.myLock();
System.out.println(Thread.currentThread().getName()+"一顿操作5秒...");
try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
sDemo.myUnLock();
},"A").start();
// 保证线程A先上的锁
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
sDemo.myLock();
System.out.println(Thread.currentThread().getName()+"一顿操作");
sDemo.myUnLock();
},"B").start();
}
AtomicReference atomicThreadRef = new AtomicReference<Thread>();
// 加锁
public void myLock(){
// 线程A进来,发现是null值,成功操作cas把自己放进去
// 线程B进来,发现不是null值,一直自旋等待线程A释放锁
System.out.println(Thread.currentThread().getName()+"尝试获取锁");
while (!atomicThreadRef.compareAndSet(null,Thread.currentThread())){
}
System.out.println(Thread.currentThread().getName()+"成功获取锁");
}
// 释放锁
public void myUnLock(){
// 线程A发现原子引用是自己,于是cas成功修改为null值,即释放锁
atomicThreadRef.compareAndSet(Thread.currentThread(),null);
System.out.println(Thread.currentThread().getName()+"释放锁");
}
}
独占锁和共享锁是什么?举例?优缺点比较?
是什么?
- 独占锁:写锁,该锁只能被一个线程所持有。
- 共享锁:读锁,该锁可被多个线程所持有。
举例
- ReentrantLock和sychronized都是独占锁。
- ReentrantReadWriteLock,其读锁是共享锁,写锁是独占锁。
优缺:
- 共享锁保证并发读是非常高效的;读写、写读、写写过程是互斥的。
验证读写锁ReentrantReadWriteLock
并发读写不安全演示
public class ReadWriteUnsafeDemo {
Map<Integer,String> map = new HashMap<>();
public void myPut(Integer key, String value){
System.out.println(Thread.currentThread().getName() + "\t" + "正在写入:"+key);
map.put(key ,value);
System.out.println(Thread.currentThread().getName() + "\t" + "写入完成:"+key);
}
public void myGet(Integer key){
System.out.println(Thread.currentThread().getName() + "\t" + "正在读取:"+key);
String value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t" + "读取完成:"+key+","+value);
}
/**
* 并发读写不安全演示
* 打印:
* 0 正在写入:0
* 2 正在写入:2
* 4 正在写入:4
* 3 正在写入:3
* 1 正在写入:1
* 3 写入完成:3
* 0 正在读取:0
* 4 写入完成:4
* 0 读取完成:0,0
* 2 写入完成:2
* 0 写入完成:0
* ...
* 结论:写入不是原子操作,线程不安全
*
* 只锁put不锁get会发生什么?
* 2 正在写入
* 0 正在读取
* 2 写入完成:2
* 造成写时读,不安全。没有保证写的原子性。
*/
public static void main(String[] args) {
ReadWriteUnsafeDemo demo = new ReadWriteUnsafeDemo();
for(int i=0; i<5; ++i){
final int tmp = i;
new Thread(()->{
demo.myPut(tmp,String.valueOf(tmp));
},String.valueOf(i)).start();
}
for(int i=0; i<5; ++i){
final int tmp = i;
new Thread(()->{
demo.myGet(tmp);
},String.valueOf(i)).start();
}
}
}
验证读写锁ReentrantReadWriteLock
public class ReadWriteLockDemo {
// 缓存资源都加一下volatile,保证线程间可见
volatile Map<Integer,String> map = new HashMap<>();
ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void myPut(Integer key, String value){
rwLock.writeLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t" + "正在写入");
map.put(key ,value);
System.out.println(Thread.currentThread().getName() + "\t" + "写入完成:"+key);
}catch (Exception e){
e.printStackTrace();
}finally {
rwLock.writeLock().unlock();
}
}
public void myGet(Integer key){
rwLock.readLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t" + "正在读取");
String value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t" + "读取完成:"+value);
}catch (Exception e){
e.printStackTrace();
}finally {
rwLock.readLock().unlock();
}
}
/**
* 要求:
* - 读可并发
* - 写和任何操作互斥
* 核心:ReentrantReadWriteLock + volatile
* 打印:
* 2 正在写入
* 2 写入完成:2
* 3 正在写入
* 3 写入完成:3
* 4 正在写入
* 4 写入完成:4
* 4 正在读取
* 2 正在读取
* 1 正在读取
* 1 读取完成:1
* 3 正在读取
* 结论:读写锁保证了写原子性,读并发
*/
public static void main(String[] args) {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
for(int i=0; i<5; ++i){
final int tmp = i;
new Thread(()->{
demo.myPut(tmp,String.valueOf(tmp));
},String.valueOf(i)).start();
}
for(int i=0; i<5; ++i){
final int tmp = i;
new Thread(()->{
demo.myGet(tmp);
},String.valueOf(i)).start();
}
}
}
什么是乐观锁/悲观锁?举例?
悲观锁
- 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁
- 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,采用版本号+cas的方式去保证线程安全。乐观锁适用于多读写少的应用类型,这样可以提高吞吐量。atomic包的类就是基于cas实现乐观锁的。
什么是乐观读/悲观读?
- 悲观读:在没有任何读写锁的时候才能取得写入的锁,可用于实现悲观读取(读优先,没有读时才能写),读多写少的场景下可能会出现线程饥饿。
- 乐观读:如果读多写少,就乐观的认为读写同时发生的情况少,因此不采用完全锁定的方式,而是采用cas实现乐观锁。
ReentrantReadWriteLock是乐观读还是悲观读?
读优先:在没有任何读写锁的时候才能取得写入的锁,可用于实现悲观读取(读优先,没有读时才能写),读多写少的场景下可能会出现线程饥饿。
! StempedLock作用?
参考:blog/2018/10/27/20181027234153307/#7-5-ReentrantLock与锁
它控制锁有三种模式(写、悲观读、乐观读)。
核心代码:
private final StampedLock sl = new StampedLock();
long stamp = sl.tryOptimisticRead(); // 获得一个乐观读锁
// stamp=0表示没有写锁入侵,
long stamp = sl.readLock(); // 获取悲观读锁
long stamp = lock.writeLock();// 获得写锁
ReentrantReadWriteLock是悲观读,读优先,而StempedLock可以指定。
synchronized和Lock的区别是什么?
- 原始构成
synchronized是关键字,属于JVM层面(底层通过monitor对象完成,monitorenter\monitorexit)
Lock是juc里具体的类,是API层面 - 使用方法
synchronized不需要手动释放锁,代码块执行完就自动释放了
ReentrantLock必须手动释放锁,否则可能导致死锁 - 等待是否可中断
synchronized不可中断,除非抛异常或正常执行完毕
ReentrantLock可中断
1 lock.tryLock(long timeout, TimeUnit unit)
2 lock.lockInterruptibly() 直接中断锁 - 加锁是否公平
synchronized非公平
ReentrantLock可以指定,默认非公平。 - 锁能否绑定多个条件(Condition)
synchronized没有,要么随机唤醒一个,要么唤醒全部
ReentrantLock实现分组唤醒,可精确唤醒。(详见AQS的Condition小节)
Condition condition_pro = lock.newCondition();
Condition condition_con = lock.newCondition();
AQS
什么是AQS?
-
AbstractQueuedSynchronizer类 - AQS
-
是juc实现锁或其他同步工具的基础框架,数据结构是队列
-
基于AQS的同步组件:CountDownLatch、Semaphore、CyclicBarrier、ReentrantLock、Condition
CountDownLatch -简述?应用场景?应用案例?常用方法?
public class CountDownLatchDemo {
CountDownLatch countDownLatch = new CountDownLatch(6);
/**
* 一个需求:某线程要在前面线程都执行完任务后才能执行。
* CountLatch应用场景:阻塞某线程直到前面所有线程执行完才被唤醒。
* demo.countDownLatch.await();
* 等待前面线程执行完
* countDownLatch.await(10, TimeUnit.MILLISECONDS);
* 只等待指定时间,超过就不继续等待。
* 打印:
* 0 执行任务
* 4 执行任务
* 3 执行任务
* 2 执行任务
* 1 执行任务
* 5 执行任务
* main 最后执行执行任务
*/
public static void main(String[] args) throws InterruptedException {
CountDownLatchDemo demo = new CountDownLatchDemo();
for(int i=0; i<6; ++i){
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t"+"执行任务");
// 一个线程执行完任务就-1
demo.countDownLatch.countDown();
},String.valueOf(i)).start();
}
// 阻塞,等待countDownLatch变为0,即所有任务线程执行完。
demo.countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t"+"最后执行执行任务");
}
}
CyclicBarrier- 简述?应用场景?应用案例?常用方法?
/**
* CyclicBarrier
* - 功能描述:使一组线程到达一个屏障(Barrier)时被阻塞,直到最后一个线程到达屏障时,所有被屏障拦截的线程才能全部被唤醒继续操作。
* - 使用方法:线程调用await(),会进入等待状态,且计数器-1,直到计数器=0时,所有线程被唤醒。
* - 应用场景:5个线程执行完ready任务后都相互等待,直到5个线程的ready任务都执行完毕再执行continue任务。
* - 常用方法:
* barrier.await();
* 等待其他线程
* barrier.await(2000, TimeUnit.MILLISECONDS);
* 等待指定时间后会抛异常。
* new CyclicBarrier(5, () -> {})
* 在ready和continue之间会执行的代码放在此处
* - 计数器重复使用(Cyclic):cyclicBarrier.reset();
*
* 打印:
* 0 执行ready
* 2 执行ready
* 3 执行ready
* 1 执行ready
* 4 执行ready
* 集中点,此时所有线程完成ready操作
* 4 执行continue
* 3 执行continue
* 0 执行continue
* 1 执行continue
* 2 执行continue
*/
public class CyclicBarrierDemo {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
System.out.println("集中点,此时所有线程完成ready操作");
});
public static void main(String[] args) {
CyclicBarrierDemo demo = new CyclicBarrierDemo();
for(int i=0; i<5; ++i){
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t"+"执行ready");
try {
demo.cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"执行continue");
},String.valueOf(i)).start();
}
// 循环使用计数器
demo.cyclicBarrier.reset();
// 下一波操作
}
}
CyclicBarrier和CountDownLatch的使用区别?
想它们的代码案例
Semaphore- 简述?应用场景?应用案例?常用方法?
/**
* Semaphore
* - 使用场景:
* demo1-共享资源互斥(吃海底捞,座位有限,但是可以等待别人吃完,再进去,不用离开)
* demo2-控制并发数(线程池,超过并发数,就抛弃连接)
* demo1-打印:
* 0 占到座位
* 1 占到座位
* 2 占到座位
* 0 释放座位
* 2 释放座位
* 3 占到座位
* 4 占到座位
* 1 释放座位
* 5 占到座位
* 3 释放座位
* 5 释放座位
* 4 释放座位
* demo2-打印:
* 0 尝试获取连接
* 2 尝试获取连接
* 3 尝试获取连接
* 1 尝试获取连接
* 5 尝试获取连接
* 4 尝试获取连接
* 5 连接成功
* 1 连接成功
* 4 连接成功
* 3 连接失败
* 0 连接失败
* 2 连接失败
*/
public class SemaphoreDemo {
public static void main(String[] args) {
// demo1
haidilao();
// demo2
xianchengchi();
}
// 线程池:最大连接数为3,超过就抛弃连接。
private static void xianchengchi() {
Semaphore semaphore = new Semaphore(3);
for(int i=0; i<6; ++i){
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"\t"+"尝试获取连接");
// 只等200毫秒,等不到就抛弃,连接失败
if(semaphore.tryAcquire(200, TimeUnit.MILLISECONDS)){
System.out.println(Thread.currentThread().getName()+"\t"+"连接成功");
TimeUnit.SECONDS.sleep(1);
}else {
System.out.println(Thread.currentThread().getName()+"\t"+"连接失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
},String.valueOf(i)).start();
}
}
// 海底捞:3个座位6个人
private static void haidilao() {
Semaphore semaphore = new Semaphore(3);
for(int i=0; i<6; ++i){
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"\t"+"占到座位");
// 吃一会儿
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"\t"+"释放座位");
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
Condition是什么?核心方法?使用案例?
Condition:多线程间协调通信的工具类
核心:
Condition condition = reentrantLock.newCondition();
condition.await();
condition.signalAll();
使用案例:
2018/10/27/20181027234153307/#7-5-ReentrantLock与锁
Condition分组唤醒是什么?锁绑定多个Condition使用案例?
核心代码:
// 生产者组
Condition condition_pro = lock.newCondition();
// 消费者组
Condition condition_con = lock.newCondition();
// 阻塞当前生产者线程
condition_pro.await();
// 唤醒所有生产者
condition_pro.signalAll();
// 阻塞当前消费者线程
condition_con.await();
// 唤醒所有消费者
condition_con.signalAll();
锁绑定多个Condition使用案例:
/**
* 锁绑定多个条件(Condition) 案例
* 要求:A打印3次,通知B打印4次,通知C打印5次;循环5轮;
*
* 线程 操作 资源类
* 判断 干活 通知
* 防止假唤醒while
*/
public class MultiConditionDemo {
public static void main(String[] args) {
Resources rs = new Resources();
new Thread(()->{
for (int i = 0; i < 5; i++) {
rs.pa3();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
rs.pb4();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
rs.pc5();
}
},"C").start();
}
}
class Resources{
Lock lock = new ReentrantLock();
String thread = "A"; // 当前线程
Condition cA = lock.newCondition();
Condition cB = lock.newCondition();
Condition cC = lock.newCondition();
public void pa3(){
lock.lock();
try{
while(!thread.equals("A")){
cA.await();
}
for (int i = 0; i < 3; i++) {
System.out.println("A打印");
}
// A做完了通知B
thread="B";
cB.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void pb4(){
lock.lock();
try{
while(!thread.equals("B")){
cB.await();
}
for (int i = 0; i < 4; i++) {
System.out.println("B打印");
}
// B做完了通知C
thread="C";
cC.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void pc5(){
lock.lock();
try{
while(!thread.equals("C")){
cC.await();
}
for (int i = 0; i < 5; i++) {
System.out.println("C打印");
}
// C做完了通知A
thread="A";
cA.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
阻塞队列
什么是阻塞队列?什么情况会导致阻塞是?主要应用场景?
队列:FILO
以下情况发生阻塞:
- 队满时入队
- 队空时出队
应用场景:生产者消费者模式
为什么要用阻塞队列?有什么好?
阻塞就是线程挂起,当满足条件后,又被唤醒。
为什么需要BlockingQueue?
- 不再需要关心线程阻塞和唤醒的时机,因为BlockingQueue包办了这个细节
- 在Juc出现前,程序员必须手动控制这些细节,兼顾效率和线程安全,增大了开发难度。
BlockingQueue的架构图?有哪些实现类?
架构图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KCUsapF3-1584804080927)(20200221192831555/BlockingQueue架构图.png)]
实现类:
- *ArrayBlockingQueue: 由数组结构组成的有界阻塞队列
- *LinkedBlockingQueue: 由链表结构组成的有界阻塞队列(但默认大小为Integer.MAX_VALUE,大小配置可选)
- PriorityBlockingQueue: 支持优先级排序的*阻塞队列
- DelayQueue: 使用优先级队列实现的延迟*阻塞队列(内部采用PriorityQueue(排序)与ReentrantLock(锁)实现)
- *SynchronousQueue: 队列只插入一个元素,同步队列
- LinkedTransferQueue: 由链表结构组成的*阻塞队列
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列
带*是重要的,是线程池的底层实现。
! BlockingQueue的核心方法?
2018/10/29/20181029185403603/#7-3-BlockingQueue
三套操作:插入、移除、检查。
四套处理:抛异常,返回特殊值,阻塞,超时
一个图表
SynchronousQueue使用案例?
/**
* SynchronousQueue案例
* 打印:
* 消费者 take a
* 生产者 put a
* 消费者 take b
* 生产者 put b
* 消费者 take c
* 生产者 put c
*/
public class BlockingQueueDemo {
public static void main(String[] args) {
BlockingQueue blockingQueue = new SynchronousQueue();
new Thread(()->{
try {
blockingQueue.put("a");
System.out.println(Thread.currentThread().getName()+"\tput a");
blockingQueue.put("b");
System.out.println(Thread.currentThread().getName()+"\tput b");
blockingQueue.put("c");
System.out.println(Thread.currentThread().getName()+"\tput c");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"生产者").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"\ttake "+blockingQueue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"\ttake "+blockingQueue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"\ttake "+blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"消费者").start();
}
}
阻塞队列的应用场景举例?
1、生产者消费者模式
2、线程池底层原理
3、消息中间件
生产者消费者模式的传统版实现?(synchronized版和Lock版)
1、sync+wait+notify
/**
* 传统版1:sync+wait+notify
* 题目:变量初始值为0,两个线程交替操作,一个加1一个减1,进行5轮。(线程1,线程2,线程1,线程2...)
* 打印:
* A plus num=1
* B reduce num=0
* A plus num=1
* B reduce num=0
* A plus num=1
* B reduce num=0
* A plus num=1
* B reduce num=0
* A plus num=1
* B reduce num=0
*/
public class ProdConsumer_TDemo1 {
public static void main(String[] args) {
Number1 number = new Number1();
new Thread(()->{
for (int i = 0; i < 5; i++) {
number.plus();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
number.reduce();
}
},"B").start();
}
}
class Number1{
private int num = 0;
public synchronized void plus(){
// 使用while不用if,防止线程虚假唤醒。当前线程被唤醒后,会重新走验证程序,不会直接往下执行。
while( num!=0 ){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.println(Thread.currentThread().getName()+"\tplus"+"\tnum="+num);
this.notify();
}
public synchronized void reduce(){
// 使用while不用if,防止线程虚假唤醒。当前线程被唤醒后,会重新走验证程序,不会直接往下执行。
while( num==0 ){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
System.out.println(Thread.currentThread().getName()+"\treduce"+"\tnum="+num);
this.notify();
}
}
2、lock+await+signalAll(Condition)
/**
* 传统版2:lock+await+signalAll(Condition)
* 题目:变量初始值为0,两个线程交替操作,一个加1一个减1,进行5轮。(线程1,线程2,线程1,线程2...)
* 打印:
* A plus num=1
* B reduce num=0
* A plus num=1
* B reduce num=0
* A plus num=1
* B reduce num=0
* A plus num=1
* B reduce num=0
* A plus num=1
* B reduce num=0
*/
public class ProdConsumer_TDemo2 {
public static void main(String[] args) {
Number2 number = new Number2();
new Thread(()->{
for (int i = 0; i < 5; i++) {
number.plus();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
number.reduce();
}
},"B").start();
}
}
class Number2{
private int num = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void plus(){
// 线程1调用了lock方法,加入到了AQS的等待队里里面去
lock.lock();
try{
// 使用while不用if,防止线程虚假唤醒。当前线程被唤醒后,会重新走验证程序,不会直接往下执行。
while( num!=0 ){
// 调用await方法后,从AQS队列里移除了,进入到了condition队列里面去,等待一个信号
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName()+"\tplus"+"\tnum="+num);
// 调用signalAll发送信号的方法,Condition节点的线程1节点元素被取出,放在了AQS等待队列里(注意并没有被唤醒)
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
// 线程1释放锁,这时候AQS队列中只剩下线程2,线程2开始执行
lock.unlock();
}
}
public void reduce(){
lock.lock();
try{
// 使用while不用if,防止线程虚假唤醒。当前线程被唤醒后,会重新走验证程序,不会直接往下执行。
while( num==0 ){
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName()+"\treduce"+"\tnum="+num);
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
生产者消费者模式的阻塞队列版实现?
/**
* 使用阻塞队列实现生产者消费者(不使用Lock/Condition相关的东西)。
* 题目:当flag=true时,启动生产者和消费者线程,一直循环做生产消费,当flag=false时,停止所有线程,程序运行结束。
* 测试:开启一个生产者和一个消费者,5秒钟修改flag,看执行效果。
* 核心:
* num++操作改成 AtomicInteger;
* BlockingQueue不写具体类,留接口给外部;
* flag标记对所有线程可见,所以需要volatile。
* 打印:
* 启动生产者
* 启动消费者
* 生产者 成功入队 蛋糕1
* 消费者 成功出队 蛋糕1
* 生产者 成功入队 蛋糕2
* 消费者 成功出队 蛋糕2
* 生产者 成功入队 蛋糕3
* 消费者 成功出队 蛋糕3
* 生产者 成功入队 蛋糕4
* 消费者 成功出队 蛋糕4
* 生产者 成功入队 蛋糕5
* 消费者 成功出队 蛋糕5
* 消费者 消费等待超过2秒,消费失败
*/
public class ProdConsumer_BDemo {
public static void main(String[] args) {
// 如果生产速度快,就把容量弄大点,免得经常入队失败
MyResource rs = new MyResource(new ArrayBlockingQueue<String>(3));
new Thread(()->{
System.out.println("启动"+Thread.currentThread().getName());
try {
rs.myProducer();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"生产者").start();
new Thread(()->{
System.out.println("启动"+Thread.currentThread().getName());
try {
rs.myConsumer();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"消费者").start();
// 5s后修改flag,结束程序
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
rs.STOP();
}
}
class MyResource{
private AtomicInteger atomicInteger = new AtomicInteger(0);
private BlockingQueue<String> blockingQueue = null;
private volatile boolean FLAG=true;
public MyResource(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
}
public void STOP(){
FLAG=false;
}
public void myProducer() throws InterruptedException {
int id;
String cake;
boolean result;
while (FLAG){
// 创建蛋糕
id = atomicInteger.incrementAndGet();// 蛋糕id
cake = "蛋糕"+id;
// 蛋糕入队
result = blockingQueue.offer(cake, 2, TimeUnit.SECONDS);
if(result){
System.out.println(Thread.currentThread().getName()+"\t成功入队\t"+cake);
}else {
// 说明队列满了
System.out.println(Thread.currentThread().getName()+"\t入队失败\t"+cake);
}
// 生产者产的慢,产一个消费者就吃一个。
// 生产者产的快,就会入队失败,等消费者来吃。
TimeUnit.SECONDS.sleep(1);
}
}
public void myConsumer() throws InterruptedException {
while (FLAG){
// 消费蛋糕
String cake = blockingQueue.poll(2, TimeUnit.SECONDS);
if(cake==null || cake.equals("")){
System.out.println(Thread.currentThread().getName()+"\t消费等待超过2秒,消费失败");
// 通知生产者也退出,不然生产者一直循环尝试入队。
// FLAG=false;
// return;
}else{
System.out.println(Thread.currentThread().getName()+"\t成功出队\t"+cake);
}
// TimeUnit.SECONDS.sleep(2);
}
}
}
线程池
获得多线程的方式有几种?
1、new Thread()
2、实现Runable接口
3、实现Callable接口
4、线程池
Runable Vs Callable
- Callabla有返回值
- Callable会抛异常
- Callable是call()方法
Callable基本使用(demo)
class MyThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"\t调用call方法");
TimeUnit.SECONDS.sleep(1);
return 1024;
}
}
/**
* Callable的基本使用
* 打印:
* main 调用
* A 调用call方法
* call返回:1024
*/
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//FutureTask(Callable<V> callable)
//FutureTask<V> implements RunnableFuture<V>
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
new Thread(futureTask,"A").start();
System.out.println(Thread.currentThread().getName()+"\t调用1");
// 如果没有计算完成,就不能执行下面代码
while (!futureTask.isDone()){
}
// get方法建议放在最后调用:因为
// get方法会要求返回Callable计算结果,如果没有完成,会导致其他线程阻塞,直到计算出结果返回。
Integer result = futureTask.get();
System.out.println("call返回:"+result);
System.out.println(Thread.currentThread().getName()+"\t调用2");
}
}
线程池主要工作?特点?为什么用?优势?
主要工作:控制运行线程的数量,将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过规定的最大数量,超出的线程将排队等候,等其他线程执行完毕,再从队列中取出任务执行。
主要特点:线程复用;控制最大并发数;管理线程;
优势:
- 降低资源消耗。重复利用已创建的线程。
- 提高响应速度。任务到达时,不需要等线程创建,直接从池里拿。
- 提高线程客观理性。统一分配、监控、调优。
线程池相关类架构图
! 线程池创建方式有多少种?
- Executors.newScheduledThreadPool() // 和调度相关
- Executors.newWorkStealingPool() // java8新增,可用处理器作为它的并行级别
三个常用实现方式: - Executors.newFixedThreadPool() // 固定个数线程。应用场景:执行长期任务,性能好。
- Executors.newSingleThreadExecutor(); // 线程池就一个线程。应用场景:任务需要一个个执行。
- Executors.newCachedThreadPool(); // 可扩容的,怎么扩由内部机制决定。应用场景:执行很多短期异步小程序 或 负载较轻的服务器。
线程池3个常用的创建方式(demo)? 及底层实现?
demo
public class ExecutorDemo {
/**
* 模拟10个用户交给线程池的5个线程处理
*
* newFixedThreadPool打印:
* pool-1-thread-1 办理业务
* pool-1-thread-5 办理业务
* pool-1-thread-1 办理业务
* pool-1-thread-4 办理业务
* pool-1-thread-2 办理业务
* pool-1-thread-3 办理业务
* pool-1-thread-2 办理业务
* pool-1-thread-4 办理业务
* pool-1-thread-1 办理业务
* pool-1-thread-5 办理业务
*
* newSingleThreadExecutor打印:
* pool-1-thread-1 办理业务
* pool-1-thread-1 办理业务
* pool-1-thread-1 办理业务
* pool-1-thread-1 办理业务
* pool-1-thread-1 办理业务
* pool-1-thread-1 办理业务
* pool-1-thread-1 办理业务
* pool-1-thread-1 办理业务
* pool-1-thread-1 办理业务
* pool-1-thread-1 办理业务
*
* newCachedThreadPool打印:
* pool-1-thread-1 办理业务
* pool-1-thread-4 办理业务
* pool-1-thread-3 办理业务
* pool-1-thread-2 办理业务
* pool-1-thread-6 办理业务
* pool-1-thread-5 办理业务
* pool-1-thread-8 办理业务
* pool-1-thread-7 办理业务
* pool-1-thread-9 办理业务
* pool-1-thread-2 办理业务
*/
public static void main(String[] args) {
// ExecutorService threadPool = Executors.newFixedThreadPool(5);
// ExecutorService threadPool = Executors.newSingleThreadExecutor();
// ExecutorService threadPool = Executors.newCachedThreadPool();
// Executors.newScheduledThreadPool()
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2,
5,
1,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy() // 如果是CallerRunsPolicy,打印 main 办理业务,说明将任务回退给了调用者main线程
);
try{
for (int i = 0; i < 11; i++) {
// 执行
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t办理业务");
});
}
}catch(Exception e){
e.printStackTrace();
}finally{
// 关闭
threadPool.shutdown();
}
}
}
底层:
底层实现:
newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
ThreadPoolExecutor 7参数介绍?
7大参数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
介绍:
- corePoolSize: 线程池中的常驻核心线程数。
1)任务来了,会安排线程池中的线程去执行任务
2)当线程池中线程数到达corePoolSize,就会把到达的任务放到缓存队列中。 - maximumPoolSize:线程池能容纳同时执行的最大线程数(>=1)。
- keepAliveTime:多余空闲线程的存活时间。
当前线程池数>corePoolSize 且 空闲时间>keepAliveTime时,多余空闲线程会被销毁直到剩下corePoolSize个线程。 - unit:keepAliveTime单位
- workQueue:任务队列,被提交但未被执行的任务。
- threadFactory:线程工厂,用于创建线程,一般用默认的即可
- handler:拒绝策略。当队满 且 工作线程>maximumPoolSize时,如何拒绝请求执行的runnable策略。
线程池底层工作原理?处理流程描述?
核心:
1、ThreadPoolExecutor 7参数
2、阻塞队列
线程池工作流程图:
线程池拒绝策略时什么?有哪些?分别是什么意思?
是什么?
- 等待队列满了,池中max线程也到了,这时就调用拒绝策略处理这个问题。
jdk内置4中拒绝策略:
RejectedExecutionHandler
- AbortPolicy:默认。直接抛出RejectedExecutionException异常阻止系统正常运行。
- CallerRunsPolicy:“调用者运行”。不抛弃任务也不抛异常,而是将任务回退给调用者,从而降低新任务流量。
- DiscardOldestPolicy:丢弃等待最久的任务。然后将当前任务加入队列,尝试再次提交当前任务。
- DiscardPolicy:直接丢弃任务。不做处理,不抛异常。如果允许任务丢弃,这是最好的策略。
线程池在实际生产中用哪一个?
jdk提供了内置的(Executors返回的),一般生产环境不用的,只能使用自定义的。
为什么生产中不用jdk自带的线程池?
(答案来自阿里开发手册)
首先线程不允许显式创建,必须使用线程池。其次,不用jdk自带的线程池,原因如下:
1)FixedThreadPool和SingleThreadExecutor
底层是LinkedBlockingQueue,允许请求队列长度为Integer.MAX_VALUE,可能造成大量请求堆积,导致OOM
2)CachedThreadPool和ScheduledThreadPool
maximumPoolSize设置的是Integer.MAX_VALUE,可能造成大量请求堆积,导致OOM
自定义线程池(demo)
demo
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2,
5,
1,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy() // 如果是CallerRunsPolicy,打印 main 办理业务,说明将任务回退给了调用者main线程
);
合理配置线程池参数,如何考虑?
分情况讨论:
- cpu密集型。
- 即大量计算,没有阻塞。
- 一般公式:CPU核数+1 个线程的线程池
- io密集型
- 并不是一直在跑计算,很可能有很多阻塞等待。
- 尽可能多开,公式:cpu核数*2。
- 公式2:cpu核数/(1-阻塞系数),阻塞系数在0.8~0.9之间。8核就是8/(1-0.9) = 80个线程。
死锁是什么?产生死锁的主要原因?
多线程抢资源造成互相等待
原因:
- 系统资源不足
- 进程推荐顺序不合适
- 资源分配不当
死锁案例(demo)
class HoldLockThread implements Runnable{
private String lockA;
private String lockB;
public HoldLockThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+"\t持有"+lockA+"\t等待"+lockB);
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace(); }
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+"\t持有"+lockB+"\t等待"+lockA);
}
}
}
}
/**
* 死锁案例
* 打印:
* A 持有钥匙1 等待钥匙2
* B 持有钥匙2 等待钥匙1
*/
public class DeadLockDemo {
public static void main(String[] args) {
new Thread(new HoldLockThread("钥匙1","钥匙2"),"A").start();
new Thread(new HoldLockThread("钥匙2","钥匙1"),"B").start();
}
}
怎么排查死锁问题?使用过哪些命令行工具?
解决:
jps -l // 查看java进程
jstack 进程号 // 查看堆栈信息
打印:Found 1 deadlock.
推荐阅读
-
Java面试之JUC
-
java基础面试题(一)
-
java基础面试相关
-
Java面试资料
-
【java之Undertow介绍】 博客分类: Undertow 【java之Undertow介绍】
-
JAVA NIO之Direct Buffer 与 Heap Buffer的区别? 博客分类: IO and NIO nio direct buffer heap
-
Java基础CleanCode之lombok&try-with-resources
-
重温java知识(二十、面向对象多态性之二:对象的多态性之对象向上转型)
-
java泛型总结2-2 面试题总结 博客分类: java泛型 泛型面试题协变逆变java限定通配符和非限定通配符
-
Java自学之路-Java基础教程-19:Java四大特性之抽象性以及abstract