目录
5. ReentrantLock和ReentrantReadWriteLock
(3)tryLock(long time,TimeUnit unit)
一、多线程基础概念&三大特性
1. wait()方法的使用
- wait()方法是锁调用的,不是线程调用的,即应该是 锁对象.wait(); 而不是 线程对象.wait();
- 无参的wait()必须手动唤醒,有参时到了时间被唤醒。
/**
* 线程状态
*/
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (o) {
try {
o.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}, "t1");
System.out.println(t1.getState());//NEW
t1.start();
Thread.sleep(1000);
System.out.println(t1.getState());//WAITING
synchronized (o) {
o.notify();
}
System.out.println(t1.getState());//BLOCKED
Thread.sleep(1000);
System.out.println(t1.getState());//TERMINATED
}
}
2. sleep方法与wait方法的区别
3. volatile关键字
volatile可以解决并发编程中的可见性和有序性,并不能解决原子性。
使用场景:①标记位;②解决DCL懒汉式(单例设计模式)中指令重排的问题。
说明:双重检测锁模式,也叫DCL懒汉式(DCL:Double Check Lock)
设计模式之---单例模式(以及DCL懒汉式并发问题解决)_单例singletonholder-CSDN博客
4. CAS面试
(1)CAS是什么?
(2)CAS的缺点有哪些?
https://www.cnblogs.com/javazhizhe/p/17478838.html
5. ThreadLocal
6. 并发编程三大特性如何保证
(1)如何保证并发编程的原子性
- 加锁;——Lock锁,synchronized锁;
- CAS方式;——如基于CAS实现的AtomicInteger等原子类,AtomicStampedReference解决了ABA问题
- ThreadLocal;——ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据。
(2)如何保证并发编程的可见性
- 加锁;——Lock锁,synchronized锁;
- volatile;—— 使用该关键字之后,CPU不会使用三级缓存,从而确保每次读取数据都要从主内存读取;
- final关键字。
(3)如何保证并发编程的有序性
- volatile —— 在属性上加volatile后,通过内存屏障来禁止指令重排,从而解决了乱序问题。
5. 其他知识点回忆
- 线程状态有哪些?
- 线程的常用API有哪些?
- 创建线程、结束线程的方式分别有哪些?
- 并发编程中的三大特性指什么?
二、锁
1. 锁的分类有哪些
- synchronized是悲观锁,可重入锁,非公平锁,互斥锁。
- ReentrantLock是悲观锁,可重入锁,互斥锁,默认是非公平锁(也可实现公平锁)。
- ReentrantReadWriteLock是悲观锁,可重入锁,既可共享锁也可互斥锁(读读共享),既可公平锁也可以非公平锁。
- 乐观锁:Atomic原子性类中,就是基于CAS乐观锁实现的。
悲观锁:获取不到资源时会挂起(进入BLOCKED、WAITING),线程挂起会涉及到用户态和内核态的切换,而这种切换是比较消耗资源的。
乐观锁:获取不到锁资源,可以再次让CPU调度,重新尝试获取锁资源。
互斥锁:同一把锁在同一时刻只能有一个线程持有。
2. synchronized
2.1 synchronized的使用
(a)同步代码块时,锁对象就是小括号()中配置的对象;
(b)同步方法时,有两种情况:如果是静态方法,则锁是当前类的Class对象;如果是普通方法,则锁对象为当前实例对象。
2.2 synchronized的锁优化
JDK1.6进行了优化,优化之一就是锁升级。
Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级-CSDN博客
2.3 synchronized实现原理
首先需要了解Java中对象在堆内存的存储。
Mark Word详情:
MarkWord中标记着四种锁的信息:无锁、偏向锁、轻量级锁、重量级锁。 重量级锁底层是ObjectMonitor。
3. AQS面试
AQS的全称是 AbstractQueuedSynchronizer,主要是用来构造同步器和锁,Java的 Juc 包中很多锁如 ReentrangLock、Semaphore,它们都有一个共同的基类,就是AQS,因为 AQS 能十分便利的搭建锁或者同步器,所以在 Java 并发编程中得以大量使用,同样的,我们可以基于 AQS 来搭建自己的锁或者同步器。
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
https://www.cnblogs.com/kxxiaomutou/p/15980675.html
4. Atomic原子类
在 Java 1.5后引入了原子类操作类,这些操作的加入,让我们不用锁即可以实现原子操作,更加高效,简洁。
Atomic包下所有的原子类都只适用于单个元素,即只能保证一个基本数据类型、对象、或者数组的原子性。根据使用范围,可以将这些类分为四种类型,分别为原子更新基本类型、原子更新数组、原子更新引用、原子更新属性。
参考:https://www.cnblogs.com/kxxiaomutou/p/15980936.html
https://www.cnblogs.com/javazhizhe/p/17478841.html
Java原子类Atomic详解_java atomic-CSDN博客
Atomic原子类常用方法总结(包含四大类型)_原子类里的方法-CSDN博客
问:
- 什么是原子类?与锁有什么区别?
- 原子类底层是如何实现的?
- 原子更新基本类型有哪些?常用API有哪些?
5. ReentrantLock和ReentrantReadWriteLock
相比于 ReentrantLock 适用于一般场合,ReentrantReadWriteLock 适用于读多写少的情况,合理使用可以进一步提高并发效率。
5.1 Lock接口基本的方法
5.2 Lock 接口方法详解
(1)lock()
- 最普通的获取锁,如果所被其他线程获得了,进行等待
- Lock不会像synchronized一样在异常时自动释放锁
- 使用时,一定要在finally中释放锁
- lock()方法 不能被中断,一旦死锁,lock() 就会永久等待
(2)tryLock()
- tryLock()用来
尝试获取锁
,如果当前线程没有被其他线程占用,则获取成功,则返回true,否则返回false,代表获取锁失败 - 相比上面的lock(),他可以返回一个值,让我们知道是否成功获取到锁;进而
决定后续程序的行为
- 它会
立刻返回
,即便在拿不到锁时,不会一直等待
(3)tryLock(long time,TimeUnit unit)
可以设定超时时间的尝试获取锁
,一段时间内等待锁,超时就放弃。使用 tryLock(long time,TimeUnit unit) 来避免死锁的代码也略,详见下面链接文章中。
(4)LockInterruptibly
这个是lockInterruptibly和tryLock(time,unit)唯一的区别是:lockInterruptibly拿不到锁资源就死等,等到锁资源释放后被唤醒,或者是被中断唤醒。
5.3 Lock和synchronized的区别
略。
5.4 自旋锁&阻塞锁
5.5 好文:
https://www.cnblogs.com/javazhizhe/p/17478837.html ★★★★★
AQS源码分析之ConditionObject_conditionobject源码分析-CSDN博客
百度安全验证https://baijiahao.baidu.com/s?id=1710170107392167577&wfr=spider&for=pcCondition的await-signal流程详解_await signal-CSDN博客
5.6 ConditionObject
ConditionObject是AQS中定义的内部类,实现了Condition接口,在其内部通过链表来维护等待队列(条件队列)。Contidion必须在lock的同步控制块中使用,调用Condition的signal方法并不代表线程可以马上执行,signal方法的作用是将线程所在的节点从等待队列中移除,然后加入到同步队列中,线程的执行始终都需要根据同步状态(即线程是否占有锁)。
每个条件变量都会有两个方法,唤醒和等待。当条件满足时,我们就会通过唤醒方法将条件容器内的线程放入同步队列中;如果不满足条件,我们就会通过等待方法将线程阻塞然后放入条件队列中。
深入详解Condition条件队列、signal和await-腾讯云开发者社区-腾讯云
【多线程系列】JUC 中的另一重要大杀器 AQS 抽象队列同步器-腾讯云开发者社区-腾讯云
5.7 不熟悉的几道面试题
(1)为什么需要锁降级?
答:提高效率。某个线程执行过程中不同时间段的操作不同,一开始执行写操作,之后都进行读;一直使用写锁的话,后面的读操作不能和其他线程进行共享,就会浪费资源;如果将写锁释放掉然后去抢占读锁,不一定能抢到。所有就有了写锁降级,然后让其他线程也能获取到读锁。遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级为读锁。
(2)为什么不支持读锁的升级?
答:因为读锁升级需要等所有的读锁都释放了才能升级,容易造成死锁,比如两个线程都在等待升级的话,就会互相等待对方释放读锁,就成了死锁。
(3)什么是锁的可见性保证?
- lock符合happens-before规则,具有可见性
- 当线程解锁,下一个线程加锁后可以看到所有前一个线程解锁前发生的所有操作。
(4)锁饥饿是什么?
答:等待时间过长以至于给进程推进和响应带来明显影响,“饿而不死”。
5.8 死锁★★★
高频面试点。
什么是死锁?
死锁产生的条件是什么?
如何预防?
如何检测?
学习文章:
心得:感觉锁这一块是并发编程核心中的核心,原理、源码、流程等等,内容很多。
太耗时了,以下暂且只罗列大纲吧。
三、阻塞队列
3.1 概述
BlockingQueue是java.util.concurrent包下的接口,用于在多线程环境下实现线程安全的数据传输。
并发阻塞队列BlockingQueue解读-腾讯云开发者社区-腾讯云 ★★★★★
【面试题精讲】什么是 BlockingQueue?-腾讯云开发者社区-腾讯云
JUC之阻塞队列解读(BlockingQueue)-腾讯云开发者社区-腾讯云
Java 阻塞队列 BlockingQueue 介绍: put,add 和 offer 三个方法-腾讯云开发者社区-腾讯云
3.2 各实现类对比
ArrayBlockingQueue : 底层基于数组的,不支持读写同时操作,因为底层用的同一把锁。
LinkedBlockingQueue:底层基于链表。支持读写同时操作,因为底层两把锁。并发情况下,效率高。
ArrayBlockingQueue 和 LinkedBlockingQueue 是两个最普通也是最常用的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题时,使用这两个足以。
PriorityBlockingQueue :基于优先级排序的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定)。PriorityBlockingQueue是无界队列(默认初始长度11,而后动态扩容),基于数组,数据结构为二叉堆,数组第一个也是树的根节点总是最小值。PriorityBlockingQueue并不会阻塞生产者,而只会在没有可消费数据时,阻塞数据的消费者。因此,使用时须特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。
DelayQueue : 使用优先级队列实现的延迟无界阻塞队列。它里面的元素只有当其指定的延迟时间到了,才能从队列中取到该元素。DelayQueue无界,所以插入数据的操作(生产者)永远不会阻塞,只有获取数据的操作才会阻塞。
SynchronousQueue : 不存储元素的阻塞队列。
四、线程池
Java线程池实现原理及其在美团业务中的实践 - 美团技术团队 (meituan.com)
1. ThreadPoolExecutor的UML类图
2. 线程池的参数有哪些?如何设计?
动态化线程池的核心设计包括以下三个方面:
- 简化线程池配置:线程池构造参数有8个,但是最核心的是3个:corePoolSize、maximumPoolSize,workQueue,它们最大程度地决定了线程池的任务分配和线程分配策略。考虑到在实际应用中我们获取并发性的场景主要是两种:(1)并行执行子任务,提高响应速度。这种情况下,应该使用同步队列,没有什么任务应该被缓存下来,而是应该立即执行。(2)并行执行大批次任务,提升吞吐量。这种情况下,应该使用有界队列,使用队列去缓冲大批量的任务,队列容量必须声明,防止任务无限制堆积。所以线程池只需要提供这三个关键参数的配置,并且提供两种队列的选择,就可以满足绝大多数的业务需求,Less is More。
- 参数可动态修改:为了解决参数不好配,修改参数成本高等问题。在Java线程池留有高扩展性的基础上,封装线程池,允许线程池监听同步外部的消息,根据消息进行修改配置。将线程池的配置放置在平台侧,允许开发同学简单的查看、修改线程池配置。
- 增加线程池监控:对某事物缺乏状态的观测,就对其改进无从下手。在线程池执行任务的生命周期添加监控能力,帮助开发同学了解线程池状态。
3. ThreadPoolExecutor是如何运行的呢?
其运行机制如下图所示:
线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:
(1)直接申请线程执行该任务;
(2)缓冲到队列中等待线程执行;
(3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
4. 线程池的阻塞队列有哪些常用的实现?
5. JDK提供的拒绝策略有哪些?
6. 其他
线程池的钩子函数有哪些?
submit方法与execute方法的区别?
ScheduledThreadPoolExecutor是干什么的?
线程池的生命周期:
五、并发集合
1. 首先把我的那篇并发集合的笔记看完;
2. 下面这篇博文耐心读完,尤其关注以下几点:
(1)ConcurrentHashMap 的 put() 操作发生了什么?get操作呢?(面试)
(2)CopyOnWriteArrayList 相关部分
绝大多数
并发情况下,ConcurrentHashMap 和 CopyOnWriteArrayList的性能都很好;- CopyOnWriteArrayList 更适合
读多写少
的场景,如果经常有写操作, Collections.sychronizedList 比 CopyOnWriteArrayList 性能好;- 无论是读还是写操作,ConcurrentHashMap 都比 Hashtable、Collections.synchronizedMap() 方法的性能好。
六、并发工具&异步编程
看看我写的那篇文章即可。写不动了。