温故而知新-JUC篇【面试复习】
- 前言
- 版权
- 推荐
- 温故而知新-JUC篇
- 多线程
- Java语言中的线程安全
- 线程安全的实现方法
- 线程的创建
- 线程的状态
- wait和sleep的区别
- ThreadLocal
- synchronize的优化
- synchronize和Reentrant的对比
- AQS
- 线程池
- ThreadPoolExecutor
- ThreadPoolExecutor源码
- ConcurrentHashMap
- CopyOnWriteArrayList
- 最后
前言
2023-8-1 09:07:12
以下内容源自《【面试复习】》
仅供学习交流使用
版权
禁止其他平台发布时删除以下此话
本文首次发布于CSDN平台
作者是CSDN@日星月云
博客主页是https://blog.csdn.net/qq_51625007
禁止其他平台发布时删除以上此话
推荐
无
温故而知新-JUC篇
多线程
单个进程中同时运行多个线程
好处:提高CPU利用率,避免等待网络IO或磁盘IO
举例:Tomcat并行处理多个请求
局限:太多的线程导致CPU上下文切换的开销增大
线程安全性问题:操作临界资源,数据不一致;涉及到锁可能会引起死锁。
Java语言中的线程安全
不可变类:String、Integer
绝对线程安全
相对线程安全
线程兼容
线程对立
ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据,Map<Thread,资源>
线程安全的实现方法
互斥同步:
synchronize,ReentrantLock
非阻塞同步:
CAS
争用共享资源,成功;失败就不停重试;
避免了线程的阻塞和唤醒带来的开销
无同步方案:
可重入代码
线程本地存储
线程的创建
继承Thread,重写run()方法
实现Runnable,重写run()方法
实现Callable,重写call()方法
Thread FutureTask Callable
线程池创建:submit、execute
线程的状态
操作系统中:
NEW Ready Running BLOCKED TERMINATED
新建 就绪 运行 阻塞 结束
Java中:
NEW RUNNABLE WAIT TIME-WAIT BLOCKED TERMINATED
新建 运行 无限期等待 限期等待 阻塞 结束
wait和sleep的区别
wait是Object类中的方法,sleep是Thread类中的静态方法
sleep属于TIMED_WAITING,自动被唤醒、wait属于WAITING,需要手动唤醒。
wait会放弃锁资源,sleep仍占用锁锁资源
sleep可以在持有锁或者不持有锁时,执行。 wait方法必须在持有锁时(同步代码块中|临界区)才可以执行。
抛出llegalMonitorStateException
wait方法会将持有锁的线程从owner扔到WaitSet集合中,这个操作是在修改ObjectMonitor对象,如果没有持有synchronized锁的话,是无法操作ObjectMonitor对象的。
ThreadLocal
Java中的四种引用类型
Java中的使用引用类型分别是强,软,弱,虚。
User user = new User();
在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它始终处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
SoftReference
其次是软引用,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中,作为缓存使用。
然后是弱引用,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。可以解决内存泄漏问题,ThreadLocal就是基于弱引用解决内存泄漏的问题。
最后是虚引用,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。不过在开发中,我们用的更多的还是强引用。
ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据
代码实现
static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();
public static void main(String[] args) {
tl1.set("123");
tl2.set("456");
Thread t1 = new Thread(() -> {
System.out.println("t1:" + tl1.get());
System.out.println("t1:" + tl2.get());
});
t1.start();
System.out.println("main:" + tl1.get());
System.out.println("main:" + tl2.get());
}
ThreadLocal实现原理:
- 每个Thread中都存储着一个成员变量,ThreadLocalMap
- ThreadLocal本身不存储数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap
- ThreadLocalMap本身就是基于Entry[]实现的,因为一个线程可以绑定多个ThreadLocal,这样一来,可能需要存储多个数据,所以采用Entry[]的形式实现。
- 每一个现有都自己独立的ThreadLocalMap,再基于ThreadLocal对象本身作为key,对value进行存取
- ThreadLocalMap的key是一个弱引用,弱引用的特点是,即便有弱引用,在GC时,也必须被回收。这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法被回收
ThreadLocal内存泄漏问题:
- 如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法被获取到。
- 只需要在使用完毕ThreadLocal对象之后,及时的调用remove方法,移除Entry即可
synchronize的优化
锁消除:没有操作临界资源
锁膨胀:循环内获取锁->循环外获取锁
锁升级:无锁->偏向锁->轻量级锁->重量级锁
- 无锁、匿名偏向:当前对象没有作为锁存在。
- 偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断,当前指向的线程是否是当前线程 。
- 如果是,直接拿着锁资源走。
- 如果当前线程不是我,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争的情况)
- 轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁)
- 如果成功获取到,拿着锁资源走
- 如果自旋了一定次数,没拿到锁资源,锁升级。
- 重量级锁:就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程。(用户态&内核态)
synchronize和Reentrant的对比
JVM语义,API层面
monitorenter monitorexit
基于AQS实现,有一个基于CAS维护的state变量来实现锁的操作。
都是可重入锁
1.6优化synchronize,可以锁升级
额外功能
等待可中断:ReentrantLock可以指定等待锁资源的时间。
public boolean tryLock(long timeout, TimeUnit unit)
公平锁:ReentrantLock支持公平锁和非公平锁
public ReentrantLock(boolean fair)
锁绑定多个条件
public Condition newCondition()
ReentrantLock和synchronized的区别
废话区别:单词不一样。。。
核心区别:
- ReentrantLock是个类,synchronized是关键字,当然都是在JVM层面实现互斥锁的方式
效率区别:
- 如果竞争比较激烈,推荐ReentrantLock去实现,不存在锁升级概念。而synchronized是存在锁升级概念的,如果升级到重量级锁,是不存在锁降级的。
底层实现区别:
- 实现原理是不一样,ReentrantLock基于AQS实现的,synchronized是基于ObjectMonitor
功能向的区别:
- ReentrantLock的功能比synchronized更全面。
- ReentrantLock支持公平锁和非公平锁
- ReentrantLock可以指定等待锁资源的时间。
选择哪个:如果你对并发编程特别熟练,推荐使用ReentrantLock,功能更丰富。如果掌握的一般般,使用synchronized会更好
AQS
说说你对AQS的理解
它是抽象的队列同步器,
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且
将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒
时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实
例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的
一个结点(Node)来实现锁的分配
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。
AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
状态信息通过 protected 类型的 getState , setState , compareAndSetState 进行操作
模板方法模式实现架构
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;
正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独
占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即
释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的
(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state
是能回到零态的。
再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个
数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会
CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然
后主调用线程就会从 await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现 tryAcquiretryRelease 、 tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器同
时实现独占和共享两种方式,如 ReentrantReadWriteLock 。
Semaphore与CountDownLatch一样,也是共享锁的一种实现。它默认构造AQS的state为
permits。当执行任务的线程数量超出permits,那么多余的线程将会被放入阻塞队列Park,并自旋判断
state是否大于0。只有当state大于0的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执
行release方法,release方法使得state的变量会加1,那么自旋的线程便会判断成功。
如此,每次只有最多不超过permits数量的线程能自旋成功,便限制了执行任务线程的数量。
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到
达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障
拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties) ,其参数
表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前
线程被阻塞。
再来看一下它的构造函数:
其中,parties 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所
有线程通过。
总结:CyclicBarrier 内部通过一个 count 变量作为计数器,cout 的初始值为 parties 属性的初始化
值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后
一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。
高频面试题-Java 并发【java面试】
ReentrantReadWtiteLock
实现
state使用高低位区分读锁 写锁的状态
//因为读锁和写锁使用的是同一个Sync,但是只有一个state,所以应该如何区分
//高16位存读锁 低16位存写锁
//统计读锁的个数 无符号右移16位 取出state高16位
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
//统计写锁的个数 按位与16个1 取出state低16位
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
static final int SHARED_SHIFT = 16; //16位划分读锁和写锁的状态
static final int SHARED_UNIT = (1 << SHARED_SHIFT); //读锁的单位 状态+-
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;//锁最大的数量
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//取写锁的数量
ReentrantLock的特性:
绑定多个条件:newCondition
举例:ArrayBlockingQueue源码
目标:生产者通知消费者,消费者通知生产者。
避免:生产者通知生产者,消费者通知消费者。
//两个队列 两个Condition
private final Condition notEmpty;
private final Condition notFull;
如果是传统的synchronize,必须signalAll()
2023-7-26 09:27:15
线程池
固定线程池 n,n
单例线程池 1,1
可缓存线程池:0 max
定时任务线程池:DelayQueue
ThreadPoolExecutor
查看一下ThreadPoolExecutor提供的七个核心参数
public ThreadPoolExecutor(
int corePoolSize, // 核心工作线程(当前任务执行结束后,不会被销毁)
int maximumPoolSize, // 最大工作线程(代表当前线程池中,一共可以有多少个工作线程)
long keepAliveTime, // 非核心工作线程在阻塞队列位置等待的时间
TimeUnit unit, // 非核心工作线程在阻塞队列位置等待时间的单位
BlockingQueue<Runnable> workQueue, // 任务在没有核心工作线程处理时,任务先扔到阻塞队列中
ThreadFactory threadFactory, // 构建线程的线程工作,可以设置thread的一些信息
RejectedExecutionHandler handler) { // 当线程池无法处理投递过来的任务时,执行当前的拒绝策略
// 初始化线程池的操作
}
JDK提供的几种拒绝策略:
- AbortPolicy:当前拒绝策略会在无法处理任务时,直接抛出一个异常
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
- CallerRunsPolicy:当前拒绝策略会在线程池无法处理任务时,将任务交给调用者处理
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
- DiscardPolicy:当前拒绝策略会在线程池无法处理任务时,直接将任务丢弃掉
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
- DiscardOldestPolicy:当前拒绝策略会在线程池无法处理任务时,将队列中最早的任务丢弃掉,将当前任务再次尝试交给线程池处理
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
- 自定义Policy:根据自己的业务,可以将任务扔到数据库,也可以做其他操作。
private static class MyRejectedExecution implements RejectedExecutionHandler{
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("根据自己的业务情况,决定编写的代码!");
}
}
ThreadPoolExecutor源码
ctl变量
高三位:代表线程池的5种状态
低29位:工作线程的个数
ConcurrentHashMap
ConcurrentHashMap是线程安全的HashMap
ConcurrentHashMap在JDK1.8中是以CAS+synchronized实现的线程安全
CAS:在没有hash冲突时(Node要放在数组上时)
synchronized:在出现hash冲突时(Node存放的位置已经有数据了)
存储的结构:数组+链表+红黑树
sizeCtl:是数组在初始化和扩容操作时的一个控制变量
-1:代表当前数组正在初始化
小于-1:低16位代表当前数组正在扩容的线程个数(如果1个线程扩容,值为-2,如果2个线程扩容,值为-3)
0:代表数组还没初始化
大于0:代表当前数组的扩容阈值,或者是当前数组的初始化大小
CopyOnWriteArrayList
CopyOnWriteArrayList是一个线程安全的ArrayList。
CopyOnWriteArrayList是基于lock锁和数组副本的形式去保证线程安全。
在写数据时,需要先获取lock锁,需要复制一个副本数组,将数据插入到副本数组中,将副本数组赋值给CopyOnWriteArrayList中的array。
因为CopyOnWriteArrayList每次写数据都要构建一个副本,如果你的业务是写多,并且数组中的数据量比较大,尽量避免去使用CopyOnWriteArrayList,因为这里会构建大量的数组副本,比较占用内存资源。
CopyOnWriteArrayList是弱一致性的,写操作先执行,但是副本还有落到CopyOnWriteArrayList的array属性中,此时读操作是无法查询到的。
最后
我们都有光明的未来
祝大家考研上岸
祝大家工作顺利
祝大家得偿所愿
祝大家如愿以偿
点赞收藏关注哦