导图社区 并发编程
系统的梳理并发编程相关概念,助力面试!一种温柔的关闭线程池的方式,不会接收新的任务,但在关闭前将之前提交的任务处理完毕、一种粗暴的方式关闭线程池,既不会接收新的任务,也不会处理已经提交的任务,但会返回队列中未处理的任务。
编辑于2023-02-11 17:32:49 河南java后端面试必备的mysql知识点梳理、InnoDB存储引擎是一种兼顾高可靠性和高性能的通用存储引擎,也是MySQL5.5之后默认的存储引擎。
java虚拟机相关知识点梳理、1、通过类的全限定名获取定义此类的二进制字节流;2、将二进制字节流所代表的静、态存储结构转化为方法区的运行时数据结构;3、在内存中生成该类的Class对象放入元空间中,作为方法区这些数据的访问入口。
系统的梳理并发编程相关概念,助力面试!一种温柔的关闭线程池的方式,不会接收新的任务,但在关闭前将之前提交的任务处理完毕、一种粗暴的方式关闭线程池,既不会接收新的任务,也不会处理已经提交的任务,但会返回队列中未处理的任务。
社区模板帮助中心,点此进入>>
java后端面试必备的mysql知识点梳理、InnoDB存储引擎是一种兼顾高可靠性和高性能的通用存储引擎,也是MySQL5.5之后默认的存储引擎。
java虚拟机相关知识点梳理、1、通过类的全限定名获取定义此类的二进制字节流;2、将二进制字节流所代表的静、态存储结构转化为方法区的运行时数据结构;3、在内存中生成该类的Class对象放入元空间中,作为方法区这些数据的访问入口。
系统的梳理并发编程相关概念,助力面试!一种温柔的关闭线程池的方式,不会接收新的任务,但在关闭前将之前提交的任务处理完毕、一种粗暴的方式关闭线程池,既不会接收新的任务,也不会处理已经提交的任务,但会返回队列中未处理的任务。
并发编程
进程与线程的区别
进程是指计算机中已经执行的程序,是系统进行资源分配的基本单位,进程之间的资源是相互独立的
线程包含在进程中,是CPU调度执行的基本单位,同一个进程中的线程之间的资源是共享的
创建线程的四种方式
package com.work.qq_system_springboot.demo1; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; //创建线程的方式一:继承Thread类 class MyThread extends Thread{ public void run(){ System.out.println("myThread is running..."); } } //创建线程的方式二:实现Runnable接口 class MyRunnable implements Runnable{ public void run(){ System.out.println("myRunnable is running..."); } } //创建线程的方式三:实现Callable接口 class MyCallable implements Callable{ @Override public Object call() { System.out.println("myCallable is running..."); return 100; } } public class MyTest { public static void main(String[] args) throws ExecutionException, InterruptedException { //继承Thread MyThread myThread = new MyThread(); myThread.start(); //实现Runnable接口 Thread thread = new Thread(new MyRunnable()); thread.start(); //实现Callable接口 //创建未来任务 FutureTask futureTask = new FutureTask(new MyCallable()); thread = new Thread(futureTask); thread.start(); Object o = futureTask.get(); System.out.println(o); } }
继承Thread类
定义MyThread,继承Thread类
重写MyThread里面的run()方法
创建一个MyThread的实例
调用实例的start()方法启动一个线程
实现Runnable接口
定义MyRunnable类,实现Runnable接口
重写run()方法
创建这个类的对象
将该对象作为Thread类构造方法的参数,创建Thread类的对象
调用Thread对象的start方法
实现Callable接口
FutureTask类有两个方法: get()方法//获取线程执行结果,一直等下去 get(3,TimeUnit.SECONDS)方法//获取线程执行结果,如果超时,抛出异常,执行接下来的语句
定义MyCallable类,实现Callable接口
重写call()方法
创建MyCallable类的对象
将该对象作为FutureTask类构造方法的参数,创建FutureTask类的实例
将FutureTask对象作为Thread类的构造方法的参数,创建Thread的对象
调用Thread对象的start方法
使用线程池
几种方式的对比
继承Thread类
编程简单,可以直接使用Thread类中的方法
扩展性差,不能再继承其他的类
实现Runnable或Callable接口
扩展性好,可以继承其他的类
适合多个线程处理同一份资源的情况
编程稍微复杂,如果要访问当前线程,需要调用Thread.currentThread()方法
Runnable和Callable的区别
Runnable重写的是run()方法,Callable重写的是call()方法
Callable的任务有返回值,Runnable的任务没有返回值
call()方法可以抛出异常,run()方法不可以
线程优先级
同级别的线程,先到先服务,轮流抢占CPU的时间片
优先级高的线程,抢占CPU时间片的概率更高,不是一定
setPriority()设置线程优先级
getPriority()获取线程优先级
默认优先级是5,最小是1,最大是10
sleep()和wait()方法的区别?
sleep()是Thread类的静态方法,wait()是Object类的成员方法
sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码快中使用
sleep()不会释放锁,wait()会释放锁,需要notify()或notifyAll()重新获取锁
sleep()不需要被唤醒,wait()需要被唤醒
sleep()和yield()方法的区别
线程执行sleep()方法后进入超时等待状态,执行yield()方法后进入就绪状态
线程调用sleep()方法后会给低优先级的线程运行机会,而yield()只会给高优先级或同级的线程运行机会
synchronized和lock的区别
总的来说,Lock比synchronized功能更加丰富
(层面)synchronized是一个关键字,是jvm层面实现加锁和解锁,Lock是一个接口,在代码层面实现加锁和解锁
(释放锁)synchronized在发生异常时,会自动释放线程所占有的锁,因此不会发生死锁;而Lock在发生异常时,如果没有主动调用unLock来释放锁,则很可能发生死锁现象,因此需要在finally语句块中主动调用unLock来主动释放锁
(中断获取锁)synchronized会导致线程拿不到锁一直处于等待状态,而Lock可以设置线程获取锁失败的超时时间来中断获取锁
(判断加锁成功)synchronized无法得知线程是否成功获取锁,而Lock可以通过tryLock方法来判断是否加锁成功
(公平锁)synchronized是非公平锁,Lock默认也是非公平锁,但可以通过构造方法来让其变成公平锁
(使用场景)在资源竞争不是很激烈的情况下,两者性能差不多,而在资源竞争非常激烈的情况下,Lock的性能要远远大于synchronized的性能,Lock可以提高多个线程进行读操作的效率
线程的虚假唤醒问题
wait方法的特点是在哪里睡,就在哪里醒
集合的线程安全问题
(问题)例如,多线程环境下操作集合的add方法会发生并发修改异常
ArrayList线程安全问题
解决方案
(古老的方法)使用线程安全的vector
(古老的方法)使用Collections工具包
List<String> list = Collections.synchronizedList(new ArrayList<String>());
(常用)使用CopyOnWriteArrayList(JUC包下的类)
这是一种写时覆盖技术 就是读集合中的内容时不加锁,允许有多个线程同时读内容。 但写的时候,先复制一份集合内容,往新的集合中添加数据,添加完之后再覆盖掉旧的集合内容
HashSet线程安全问题
解决方案
使用CopyOnWriteArraySet(JUC包下的类)
HashMap线程安全问题
解决方案
使用ConcurrentHashMap(JUC包下的类)
多线程锁
jvm锁(synchronized、Lock)
概念
JVM 锁只能作用于单个 JVM,可以简单理解为就是单台服务器(容器),而对于多台服务器之间,JVM 锁则没法解决,这时候就需要引入分布式锁。
分类
synchronized的锁对象
修饰普通方法
锁是当前实例对象
修饰静态方法
锁是当前类的Class对象
修饰代码块
锁是synchronized括号里面配置的对象
死锁
死锁产生的条件
互斥条件:一个资源只能被一个线程独占
(请求和保持)占有和等待条件:已经获取某一个资源的线程还可以获取其他线程
不剥夺条件:已经分配给某一个资源的线程不可以被其他线程抢占
环路等待条件:
有两个或以上的线程形成一个环路,该环路中每一个线程都在等待下一个线程释放资源
预防死锁的方式
破坏死锁产生的条件
破坏互斥条件:一个资源可以被多个线程共享
破坏占有和等待条件
事先分配资源,在进程执行之前分配所需的全部资源,进程在执行过程中不会出现新的分配请求,便不会产生死锁
每当一个线程在请求新的线程之前,必须先释放它先前所拥有的资源
破坏不可剥夺条件·
进程一旦因为申请资源而出现阻塞,必须释放已经拥有的全部资源
破坏环路等待条件
系统将所有资源按类型进行线性排队,并赋予不同的序号,所有进程对资源的请求必须严格按照资源序号递增的次序提出,这样在所形成的资源分配图中,不可能出现环路,因此也就摒弃了“环路等待”条件。
模拟死锁的代码
package com.work.qq_system_springboot.demo1; public class DeathLockThread { //资源a public static Object a = new Object(); //资源b public static Object b = new Object(); //main函数 public static void main(String[] args) { //线程A拥有资源a,在尝试获取资源b new Thread(()->{ synchronized (a){ System.out.println(Thread.currentThread()+",持有资源:a,尝试获取资源b"); //尝试获取资源b try { Thread.sleep(1000);//等待一秒,先让线程b只有资源b } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b){ System.out.println(Thread.currentThread()+",持有资源:b"); } } }).start(); //线程B拥有资源b,在尝试获取资源a new Thread(()->{ synchronized (b){ System.out.println(Thread.currentThread()+",持有资源:b,尝试获取资源a"); //尝试获取资源b synchronized (a){ System.out.println(Thread.currentThread()+",持有资源:a"); } } }).start(); } }
公平锁与非公平锁
公平锁
多个线程按照先到先得的策略获取锁 优点是:线程不会饿死 缺点是:效率低
非公平锁
所有线程拼运气,运气好的就可以获取锁 这样就可以减少唤醒线程的开销,效率高 缺点是:会导致线程饿死
行级锁与表级锁
行级锁
对当前行进行加锁,加锁慢,开销大,锁粒度最小,锁冲突概率也最小,并发度高,但容易发生死锁
表级锁
对整张表进行加锁,加锁快,开销小,锁粒度最大,锁冲突概率也最大,并发度低,但不会出现死锁
乐观锁与悲观锁
悲观锁
乐观锁
读锁与写锁
读锁,又叫共享锁
会发生死锁
线程A读取记录r,在更新r之前,需要线程B释放锁 线程B读取记录r,在更新r之前,需要线程A释放锁
写锁,又叫独占锁
会发生死锁
线程A读取记录R1,在修改记录R2之前,需要线程B释放R2的锁 线程B读取记录R2,在修改记录R1之前,需要线程A释放R1的锁
分布式锁
基于数据库实现分布式锁
方式
在一个表中设置name字段为唯一索引,如果有多个请求插入数据的话,只有一个操作会成功
在表中增加一个state字段表示锁的状态,如果有多个请求更新数据,最终只有一个线程操作的表的影响行数为1、其余的都为0
缺点
这种加锁的方式完全依赖于数据库,如果数据库是单点的,数据库一旦挂掉,会导致整个业务系统不可用
这种锁没有失效时间,一旦解锁失败,会导致其他线程无法获取锁
这种锁只能是非阻塞的,因为其他线程如果插入失败,不会进入等待队列中,要想再次获取锁,就需要重新请求
这种锁是非重入的,线程在释放锁之前无法再次获取锁,因为数据库中字段的唯一性
相应的解决方案
数据库是单点?使用两个数据库,数据之间双相同步,挂掉其中一个,还会有另外一个
没有失效时间?使用定时任务,每隔一段时间就把数据库中超时数据清理一遍
非阻塞的?可以使用while死循环,直到插入数据成功就跳出循环
非重入的?在数据库中新增一个字段,用于记录当前获取锁的主机信息和线程信息,下次再获取锁的时候,先查询数据库,如果能够查询到主机信息和线程信息的话,就直接把锁分配给该线程即可
基于缓存redis实现分布式锁
加锁方式
set key value px 3000 nx
解锁方式
先判断当前锁是否还是我们持有,因为如果锁过期,有可能其他线程获取了锁
如果当前锁依然是我们持有,则执行解锁操作,就是删除对应的key
由于当前 Redis 还没有原子命令直接支持这两步操作,所以当前通常是使用 Lua 脚本来执行解锁操作,Redis 会保证脚本里的内容执行是一个原子操作。
redis分布式锁过期了,还没处理完怎么办?
额外起一个守护线程,定期检查当前线程是否还持有锁,如果有则延长过期时间
如果解锁时发现锁已经被别人获取了,此时需要进行回滚,并返回失败
基于Zookeper实现分布式锁
JUC三大辅助类
CountDownLatch减少计数
import java.util.concurrent.CountDownLatch; public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(15);//计数初始值 for(int i=0;i<15;i++){ new Thread(()->{ countDownLatch.countDown();//每执行一个线程,就减少计数 System.out.println(Thread.currentThread().getName()+"running..."); }).start(); } countDownLatch.await();//一直等待到技术为零,才唤醒 System.out.println(Thread.currentThread().getName()+"running..."); } }
CyclicBarrier循环栅栏
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class CyclicBarrierDemo { public static void main(String[] args) { //参数:线程个数,一个Runnable实例 CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{ System.out.println(Thread.currentThread().getName()+" 集齐7个龙珠,召唤神龙"); }); for(int i=0;i<7;i++){ new Thread(()->{ try { System.out.println(Thread.currentThread().getName()+" 收集一个龙珠"); cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }).start(); } System.out.println(Thread.currentThread().getName()+" is over!"); } }
SemaPhore信号灯
import java.util.Random; import java.util.concurrent.Semaphore; public class SemaPhoreDemo { public static void main(String[] args) { Semaphore semaphore = new Semaphore(3);//三个停车位 for(int i=0;i<7;i++){ new Thread(()->{ try { semaphore.acquire(); System.out.println(Thread.currentThread().getName()+"获得了停车位"); Thread.sleep(new Random().nextInt(10)); System.out.println(Thread.currentThread().getName()+"-----释放了停车位"); } catch (InterruptedException e) { e.printStackTrace(); }finally { semaphore.release(); } }).start(); } } }
线程池(Pool)
线程池的好处
降低资源的消耗
通过复用已经创建的线程,降低线程创建和销毁造成的消耗
提高响应速度
当任务到达时,任务可以不必等线程创建就能立即执行
增加线程的可管理性
线程是稀缺资源,使用线程池可以统一进行分配、调优和监控
常用参数
corePoolSize核心线程数
默认情况下,核心线程会一直存活。但可以通过allowCoreThreadTimeOut=true,使得核心线程也会超时回收
maximumPoolSize最大线程数
如果活跃线程操作该数值后,后续任务将会阻塞(阻塞队列必须是有界的,否则无意义)
keepAliveTime
非核心线程的存活时长
unit
指定KeepAliveTime参数的时间单位,常用的有毫秒、秒、分钟
workQueue
任务队列,线程池执行execute()方法所提交的Runnable对象存放在任务队列中,是一种阻塞队列
threadFactory
线程工厂,指定线程池创建新线程的方式
handler
拒绝策略
当调用shutdown()等方法关闭线程池之后,提交的任务会被拒绝
当提交的任务过多不能及时处理时,需要使用拒绝策略
线程池工作过程
当线程池执行execute()方法添加一个任务时,线程池会做如下判断
当正在运行的线程数量小于核心线程数量时,会创建核心线程来执行这个任务
当正在运行的线程数量大于等于核心线程数量时
如果此时任务队列没有满,会将这个任务放入任务队列
如果此时任务队列已满
当正在运行的线程数量小于最大线程数量时,会创建非核心线程来执行任务
当正在运行的线程数量大于等于最大线程数量时,通过handler指定的拒绝策略来处理任务
线程池的5种状态
RUNNING
在running状态下,线程池可以接收新的任务和执行已添加的任务
线程池一旦被创建,就处于running状态
SHUTDOWN
在shutdown状态下,线程池不会接收新的任务,但会处理已添加的任务
在running状态下,当线程池调用shutdown()方法时,会变为shutdown状态
STOP
在stop状态下,线程池不接收新的任务,不处理已添加的任务,并且会中断正在执行的任务
在running或shutdown状态下,当线程池调用shutdownNow()方法时,会变为stop状态
TIDYING
当所有任务已终止,记录的“任务数量”为零时,就进入tidying状态
在shutdown状态下,当阻塞队列为空并且线程池中执行的任务也为空时,会变为tidying状态
在stop状态下,当线程池中执行的任务为空时,会变为tidying状态
TERMINATED
当钩子函数terminated()执行完之后,线程池彻底终止,就变成了terminated状态了
7种阻塞队列
分类
ArrayBlockingQueue
一个由数组组成的一个有界的阻塞队列
LinkedBlockingQueue
一个由链表组成的一个有界阻塞队列,在未指明容量时,容量默认为Integer.MAX_VALUE
PriorityBlockingQueue
一个具有优先级的无界阻塞队列
DelayQueue
类似于PriorityBlockingQueue,一个由二叉堆实现的具有优先级的无界阻塞队列,要求元素必须实现Delayed接口,队列中的元素按照到期时间排序,而非入队顺序,当队首元素到期时才能取到元素,否则处于阻塞状态
SynchronousQueue
一个不存储元素的阻塞队列。消费者消费元素时会发生阻塞,直到有生产者产生了一个元素,生产者生产元素时发生阻塞,直到消费者消费元素
LinkedBlockingDeque
使用双端队列实现的有界双端阻塞队列,既可以先进先出,又可以先进后出
LinkTransferQueue
相当于LinkedBlockingQueue和SynchronousQueue的结合体,是一个无界阻塞队列
使用阻塞队列需要注意什么
使用有界队列时,需要注意当线程池满了之后,如何处理被拒绝的任务
使用无界队列时,需要注意如果任务提交的速度大于线程池处理的速度时,会导致内存溢出
生产者消费者模式
三种实现方式
使用synchronized和wait和notify
使用Lock和
阿斯顿发送到发大家水电费爱神的箭覅撒旦法为及阿斯顿发斯蒂芬及赛飞机撒豆副驾驶的第三方氨基酸地方吉安市地方加大福建水电费及阿斯顿发斯蒂芬及时代峰峻阿萨德覅及
4种拒绝策略
AbortPolicy(默认)
丢弃任务,并抛出拒绝执行异常:RejectExecuteException
DiscardPolicy
丢弃任务,不抛异常
DiscardOldestPolicy
丢弃队列中最早未处理的任务,然后重新提交被拒绝的任务
CallerRunsPolicy
由调用线程处理该任务
杂项
线程只有在任务到达时才会启动吗?
默认情况下,即使是核心线程也只能在新任务到达时才创建和启动
但可以通过使用prestartCoreThread()来提前启动一个核心线程或prestartAllCoreThreads()方法来提前启动所有的核心线程
如何终止线程池?
调用shutdown()方法
一种温柔的关闭线程池的方式,不会接收新的任务,但在关闭前将之前提交的任务处理完毕
调用shutdownNow()方法
一种粗暴的方式关闭线程池,既不会接收新的任务,也不会处理已经提交的任务,但会返回队列中未处理的任务
Executors提供的创建线程池的方法(功能线程池)
定长线程池FixedThreadPool
线程数量固定,且只有核心线程
工作队列为LinkedBlockingQueue
适用于需要控制线程最大并发数的场景
定时线程池ScheduledThreadPool
核心线程数量固定,非核心线程数量无限
工作队列为DelayedWorkQueue
适用于需要执行定时任务的场景
可缓存线程池CachedThreadPool
无核心线程,非核心线程数量无限
工作队列为不存储元素的SynchronousQueue
适用于执行大量,且耗时少的任务
单线程化线程池SingleThreadPool
只有一个核心线程,没有非核心线程
任务队列为LinkedBlockingQueue
单线程的应用场景,不适合并发的场景
可窃取的线程池WorkStealingPool
是jdk1.8新增的