中国建设银行分行网站,微信商城怎么弄,怎么注册微信号,微信会员卡系统怎么做这一块知识#xff0c;那真是有的啃了。
直接先看速成基础#xff0c;再直接吃掉高频考点。
每个小知识点#xff0c;直接看短视频#xff0c;浅浅了解#xff0c;在写下来就是自己的资料。 # 基础 一个进程有多个线程#xff0c;多个线程共享进程的堆和方法区#xf…这一块知识那真是有的啃了。
直接先看速成基础再直接吃掉高频考点。
每个小知识点直接看短视频浅浅了解在写下来就是自己的资料。 # 基础 一个进程有多个线程多个线程共享进程的堆和方法区每个线程独有PC、VM Stack、NM Stack
## 为什么程序计数器是线程私有的
程序计数器主要有俩个作用
字节码解释器通过改变程序计数器来依次读取指令从而实现代码的流程控制记录当前线程执行的位置。
所以程序计数器私有主要是为了线程切换后能正确恢复到原来·的执行位置。
## 虚拟机栈和本地方法栈为什么是线程私有的
虚拟机栈每个Java方法在执行前会创建一个栈帧用来存储局部变量、操作数栈、常量池引用等信息。方法调用-完成对应着一个栈帧在Java虚拟机栈中出栈入栈。本地方法栈与虚拟机栈作用类似虚拟机栈为虚拟机执行Java方法服务而本地方法栈为虚拟机使用到的Native方法服务。在HotSpot中虚拟机栈与本地方法栈合二为一。
所以虚拟机栈与本地方法栈私有是为了保证线程中的局部变量不被其他线程访问到。
一个线程可以调用多个方法而一个方法又可以被多个线程调用 堆进程中最大的一块内存主要用于存放新创建的对象方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码。 进程操作系统分配资源的最小单元。线程操作系统调度的最小单元 并发两个及以上任务在同一时间段执行并行两个及以上任务在同一时刻执行 同步发出一个调用在没有得到结果之前该调用就不可以返回一直等待异步调用发出后不用等待返回结果该调用直接返回 线程安全与非线程安全多线程环境下对于同一份数据的读写是否能保证其正确性和一致性。 内核线程由操作系统内核管理和调度的线程运行在内核空间仅内核程序可访问用户线程由用户空间程序管理和调度的线程运行在用户空间专门app使用区别用户线程创建切换成本低但不支持多核内核线程创建切换成本高可支持多核。
## 单核CPU运行多个线程效率一定会高吗
CPU密集型线程主要进行计算和逻辑处理需要占用大量CPU资源IO密集型线程主要进行大量输入输出如读写文件、网络通信等需等待IO设备相应而不用一直占用CPU。
因此对于CPU密集型任务那么开多线程需要频繁线程切换影响效率对于IO密集型任务开多线程会提高效率当然也不能超过系统上限。
## 线程的生命周期和状态 NEW: 初始状态线程被创建出来但没有被调用 start() 。RUNNABLE: 运行状态线程被调用了 start()等待运行的状态。BLOCKED阻塞状态需要等待锁释放。WAITING等待状态表示该线程需要等待其他线程做出一些特定动作通知或中断。TIME_WAITING超时等待状态可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。TERMINATED终止状态表示该线程已经运行完毕。 正常横向过程线程创建之后处于NEW状态调用start()方法开始运行这时处于READY状态当此线程获得了CPU时间片后就处于RUNNING状态执行完run()方法后就终止。
进入等待状态线程执行wait()方法后线程进入WAITING状态依靠其他线程通知返回运行状态
进入超时等待状态在等待基础上增加超市限制sleep()、wait()执行后进入超时时间结束后线程将返回RUNNING状态。
进入阻塞状态进入synchronized方法或调用wait()但锁被其他线程占有线程就进入阻塞状态 ## 简述一下线程的上下文切换
上下文线程在执行过程中的运行条件和状态信息。比如程序计数器、栈信息等
上下文切换:(保存 - 加载) 当发生线程切换时需要保存当前线程的上下文留待下次线程占用CPU时恢复现场并加载下一个将要占用CPU的线程的上下文。
上下文切换条件
主动让出CPU如调用了sleep()、wait()等时间片用完防止长时间占用CPU导致其他线程或进程饿死调用了阻塞类型的系统中断如请求IO、线程被阻塞被终止或结束运行 ## 什么是线程死锁如何避免线程死锁 线程死锁两个及以上线程在执行过程中因争夺资源而造成互相等待的现象无外力作用下这些线程将一直相互等待无法继续运行。
线程死锁四个条件
互斥条件该资源任意时刻只由一个线程占用请求与保持条件一个线程因请求资源而阻塞时不释放已占有的资源不剥夺条件线程已获得的资源未使用完前不能被其他线程剥夺只有自己使用完后才释放循环等待条件若干线程之间形成一种头尾相接的循环等待资源关系
如何避免线程死锁 -- 至少破坏死锁发生的一个条件
互斥条件无法破坏因为使用锁的目的就是互斥请求与保持条件可以一次性请求所有资源不可剥夺设置超时。已占有资源的线程请求其他资源若长时间请求不到超时释放已占有的资源环路等待注意加锁顺序保证每个线程按同样的顺序进行加锁。
## sleep()方法与wait()方法区别
共同点都是让线程阻塞等待
wait()会释放对象的锁sleep()不会释放对象的锁wait()通常被用于线程间交互/通信sleep()用于使当前线程暂停执行wait()是Object类的本地方法sleep()是Thread类中的静态本地方法wait()方法被调用后线程不会自动苏醒需要别的线程调用同一个对象上的 notify()或者notifyAll()方法。sleep()执行完成后线程会自动苏醒或者也可以使用 wait(long timeout)超时后线程会自动苏醒。
## 为啥wait()定义Object类中sleep()定义在Thread中
wait()是让获得对象锁的线程实现等待自动释放当前线程占有的对象锁。每个Object类的对象都有对象锁既然要释放当前线程占有的对象锁并让其进入WAITING状态自然要操作对应的对象Object而不是当前的线程Thread
sleep()方法是让当前线程暂停执行不涉及对象类也不需要获得对象锁。 ## 启动线程为啥用start()不是用run()
基本知识start()方法会在新的线程中执行run() 方法对应的内容run()方法只在当前线程中执行
NEW一个线程线程进入了新建状态。调用start()方法会启动该线程进入就绪状态当分配到时间片后就可以执行。start()执行线程相应的准备工作然后自动执行run()对应的内容这是多线程工作。
但直接执行run()会把run()方法当成一个main线程下的普通方法执行并不会在新建的线程中执行
# 高阶
## 锁机制 ### 什么是锁
并发带来的数据不一致问题在并发环境下多个线程会对同一个资源进行争抢就会导致数据不一致的问题。 为了解决此问题就引入锁机制。通过一种抽象的锁来对资源进行锁定。 对于线程私有的程序计数器、虚拟机栈、本地方法栈不存在数据竞争数据能够保证正确性唯一性是线程安全的。 但对于堆、方法区是线程共享的就会存在线程安全问题因此引入锁机制。 锁三大类型
锁大致可以分为互斥锁、共享锁、读写锁在读读下是共享锁读写、写写下是互斥锁 ## JMM
### 讲讲什么是JMM
Java内存模型规定
所有变量都存储在主内存中包括实例变量、静态变量但不含局部变量和方法参数。每个线程都有自己的工作内存工作内存保存了该线程用到的变量和主内存的副本线程对变量的操作都在工作内存中进行。线程不能直接读写主内存的变量。不同线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。 ### 为什么需要JMM
在不同硬件厂商和不同操作系统下内存的访问有一定的差异会造成同一套代码运行在不同系统上会出现各种问题。所以JMM屏蔽掉各种硬件和操作系统的内存访问差异以实现Java程序在各种平台下都能达到一致的并发效果。
### 重排序-内存屏障概念
指令重排序
定义出于优化的目的对实际执行的指令的顺序进行调整。
指令重排序三种情况
编译器重排序在不改变单线程程序语义情况下对代码语句顺序进行调整处理器重排序CPU采用指令级并行技术将多条指令重叠执行若不存在数据依赖性CPU可改变语句对应的机器指令执行顺序内存重排序并不是严格意义上的重排序。CPU缓存使用缓存区进行延迟写入这个过程造成多个CPU缓存可见性问题可见性问题导致对于指令的先后执行显示不一致从结果表面看起来好像指令的顺序被改变了。
禁止重排序方式
编译器重排序 - 通过禁止特定类型编译器重排序来禁止重排序处理器重排序 - 通过插入内存屏障来禁止特定类型处理器重排序
内存屏障一种CPU指令。用来禁止处理器指令发生重排序从而保障指令执行的有序性在处理器写入、读取值之前将主内存的值写入高速缓存清空无效队列从而保障变量的可见性。
四大类内存屏障
LoadLoad 屏障对于这样的语句Load1LoadLoadLoad2。在Load2及后续读取操作要读取的数据被访问前保证Load1要读取的数据被读取完毕。StoreStore屏障对于这样的语句Store1 StoreStore Store2在Store2及后续写入操作执行前保证Store1的写入操作对其它处理器可见。LoadStore 屏障对于这样的语句Load1 LoadStoreStore2在Store2及后续写入操作被刷出前保证Load1要读取的数据被读取完毕。StoreLoad 屏障对于这样的语句Store1 StoreLoadLoad2在Load2及后续所有读取操作执行前保证Store1的写入对所有处理器可见。 ### 谈谈volatile关键字
volatile关键字主要有两个作用
一是保证多线程环境下共享变量的可见性二是通过插入内存屏障来禁止多个指令之间的重排序
### Java并发三特性
1、原子性
一个或多个操作要么全部执行且执行过程中不会被任何因素打断要么就都不执行。
经典案例银行转账问题涉及A账户减少B账户增加要么同时执行要么都不执行整个转账过程算做一个原子操作。
实现原子性方式
synchronized、Lock以及各种原子类。
synchronized和Lock可以保证任意时刻只有一个线程访问该代码块因此可保证原子性各种原子类是利用CAS操作来保证原子操作。
2、可见性
当一个线程对主内存中的共享变量进行了修改其他线程可立即看到修改后的最新值。
实现可见性方式
借助synchronized、volatile以及各种Lock。
volatile修饰的变量当一个线程改变了该变量的值其他线程是立即可见的。普通变量则需要重新读取才能获得最新值。
3、有序性
Java内存模型中允许编译器和处理器对指令进行重排序。重排序不会影响单线程程序的执行却会影响多线程并发执行的正确性。
实现有序性方式
synchronized、Lock、volatile关键字。
synchronized和Lock保证每个时刻只有一个线程执行同步代码相当于是让线程顺序执行同步代码。 ### JMM八种内存交互操作 lock(锁定)作用于主内存中的变量把变量标识为线程独占的状态。read(读取)作用于主内存的变量把变量的值从主内存传输到线程的工作内存中以便下一步的load操作使用。load(加载)作用于工作内存的变量把read操作主存的变量放入到工作内存的变量副本中。use(使用)作用于工作内存的变量把工作内存中的变量传输到执行引擎每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。assign(赋值)作用于工作内存的变量它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。store(存储)作用于工作内存的变量它把一个从工作内存中一个变量的值传送到主内存中以便后续的write使用。write(写入)作用于主内存中的变量它把store操作从工作内存中得到的变量的值放入主内存的变量中。unlock(解锁)作用于主内存的变量它把一个处于锁定状态的变量释放出来释放后的变量才可以被其他线程锁定。
read后必须loadstore后必须write ### Java内存结构和JMM区别
Java内存结构和Java虚拟机运行时区域有关定义了JVM运行时如何分区存储程序数据。
Java内存模型与Java并发编程相关抽象了线程和主内存的关系。目的是简化多线程编程增强程序可移植性。
### happens-before 原则
意义前一个操作的结果对后一个操作是可见的无论俩个操作是否在同一个线程。
定义
如果一个操作 happens-before 另一个操作那么第一个操作的执行结果将对第二个操作可见并且第一个操作的执行顺序排在第二个操作之前。两个操作之间存在 happens-before 关系并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果与按 happens-before 关系来执行的结果一致那么 JMM 也允许这样的重排序。
常见规则
程序顺序规则在一个线程中按照代码的顺序前面的操作Happens-Before于后面的任意操作。解锁规则对一个锁的解锁操作 Happens-Before于后续对这个锁的加锁操作。volatile 变量规则对一个 volatile 变量的写操作 happens-before 于后面对这个 volatile 变量的读操作。说白了就是对 volatile 变量的写操作的结果对于发生于其后的任何操作都是可见的。传递规则如果 A happens-before B且 B happens-before C那么 A happens-before C线程启动规则Thread 对象的 start()方法 happens-before 于此线程的每一个动作。
如果两个操作不满足上述任意一个 happens-before 规则那么这两个操作就没有顺序的保障JVM 可以对这两个操作进行重排序。 ## Synchronized
synchronized理解为加锁而不是锁。这样更好理解线程同步。
### synchronized是什么作用
synchronized就是同步的意思是Java中一个关键字。主要用于解决多线程之间访问资源的同步性保证被他修饰的方法或代码块在任意时刻只有一个线程执行。 Synchronized使用了内置锁monitor也称为监视器锁来实现同步。每个Java对象都有一个内置锁当该对象作为锁被获取时其他试图获取该锁的线程会被阻塞直到该锁被释放。Synchronized的锁是与对象相关联的。当一个线程进入Synchronized代码块时它必须先获取该对象的锁才能执行代码否则就会被阻塞。当该线程退出Synchronized代码块时它会自动释放该对象的锁。Synchronized具有可重入性。如果当前线程已经获得了某个对象的锁那么它可以继续访问该对象的其他Synchronized代码块而不会被自己持有的锁所阻塞。Synchronized还具有volatile变量的读写语义。在使用Synchronized关键字时内存屏障会确保本地线程中修改过的变量值被刷新回主内存从而保证了多个线程之间对变量修改的可见性。
Synchronized通过使用内置锁、与对象关联的锁、可重入性以及内存屏障等机制来实现线程的同步和锁的管理以保证对共享资源的访问具有互斥性和可见性。
synchronize会根据锁竞争情况从偏向锁--轻量级锁--重量级锁升级 ### 如何使用 synchronized 普通方法 锁对象是this所谓的方法锁本质上属于对象锁) 也就是多个线程访问方法say()会有锁的限制
public synchronized void say(){ //对方法say()加锁System.out.println(Hello,everyone...);
}
同步代码块(方法中)锁对象是synchronized(obj)的对象所谓的对象锁 public void say(boolean isYou){ //对对象obj加锁synchronized (obj){System.out.println(Hello);}}
同步静态方法锁对象是当前类的Class对象,即(XXX.class)所谓的类锁 public static synchronized void work(){ //对类work加锁System.out.println(Work hard...);} ## synchronized与Lock区别
实现方式Synchronized是Java语言内置的关键字而Lock是一个Java接口。锁的获取和释放Synchronized是隐式获取和释放锁由Java虚拟机自动完成而Lock需要显式地调用lock()方法获取锁并且必须在finally块中调用unlock()方法来释放锁。可中断性在获取锁的过程中如果线程被中断synchronized会抛出InterruptedException异常并且自动释放锁而Lock则需要手动捕获InterruptedException异常并处理同时也支持非阻塞、可轮询以及定时获取锁的方式。公平性Synchronized不保证线程获取锁的公平性而Lock可以通过构造函数指定公平或非公平锁。锁状态Synchronized无法判断锁的状态而Lock可以通过tryLock()、isLocked()来判断锁的状态(线程是否可能取到锁、锁是否被占用等)。粒度Synchronized锁的粒度较粗只能锁住整个方法或代码块而Lock可以细粒度地控制锁的范围比如锁某个对象的部分属性。场景如果在简单的并发场景下推荐使用Synchronized而在需要更高级的锁控制时可以考虑使用Lock。 乐观锁和悲观锁是两种思想用于解决并发场景下的数据竞争问题。
悲观锁的实现方式是加锁加锁既可以是对代码块加锁如Java的synchronized关键字也可以是对数据加锁如MySQL中的排它锁
## 悲观锁
悲观锁悲观锁在操作数据时比较悲观认为别人会同时修改数据。因此操作数据时直接把数据锁住直到操作完成后才会释放锁上锁期间其他人不能修改数据。
Java 中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
public void performSynchronisedTask() {synchronized (this) {// 需要同步的操作}
}
private Lock lock new ReentrantLock();
lock.lock();
try {// 需要同步的操作
} finally {lock.unlock();
}
### 对象结构
对象结构对象头 实例数据 对齐填充字节
对象头对象本身运行时信息包含俩部分Mark Word Class Pointer实例数据初始化对象设置的属性、状态等信息对齐填充字节为满足“Java对象大小是8字节的倍数”而设计
Mark Word存储当前对象运行时状态信息如HashCode、锁状态标志、指向锁记录的指针、偏向线程ID、锁标志位等Class Pointer是一个指针指向当前对象类型所在方法区中的Class信息 ## 乐观锁
乐观锁总是假设最好的情况认为共享资源每次被访问的时候不会出现问题线程可以不停地执行无需加锁也无需等待只是在提交修改的时候去验证对应的资源也就是数据是否被其它线程修改了。
### 实现乐观锁 版本号机制
读取version - 操作 - 验证当前version -新旧同则更新不同则重试
一般是在数据表中加上一个数据版本号 version 字段表示数据被修改的次数。当数据被修改时version 值会加一。
当线程 A 要更新数据值时在读取数据的同时也会读取 version 值在提交更新时若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新否则重试更新操作直到更新成功 #### CAS算法
Compare And Swap本质用一个预期值与要更新的变量值比较两值相等才会更新。
CAS是一个原子操作底层依赖于一条CPU的原子指令。
原子操作最小不可拆分操作操作一旦开始就不能被打断直到操作完成
CAS涉及到三个操作数
V: 要更新的变量值VarE: 预期值N: 拟写入的新值
当且仅当V的值等于E时CAS通过原子方式用新值N来更新变量值V。如果不等说明已经有其他线程更新了变量值V则当前线程放弃更新。
### 乐观锁存在的问题
ABA问题
如果一个变量V初次读取值为A在准备赋值时检查到仍然是A在这期间不能保证他的值是否被修改过最后又修改成了A。
解决ABA方式在变量前追加版本号或时间戳。AtomicStampedReference类的compareAndSet()方法就是首先检查当前引用是否等于预期引用并且当前标志是否等于预期标志。全部相等则以原子方式将该变量和标志的值设置为给定的更新值。
循环时间开销大
CAS使用自旋操作来重试若长时间不成功会给CPU带来很大的执行开销。
解决循环时间开销大若JVM能支持处理器提供的pause指令则效率会有一定的提升。 pause作用 延迟流水线执行指令使CPU不会消耗过多的执行资源 避免在退出循环时因内存顺序冲突引起CPU流水线被清空从而提高CPU执行效率
只能保证一个共享变量的原子操作 CAS只对单个共享变量有效当操作涉及跨多个共享变量时CAS无效。
解决方式引入AtomicReference类保证引用对象之间的原子性 使用锁或利用AtomicReference类把多个共享变量合并成一个共享变量来操作。 ## 线程池
池化技术通过复用对象、连接等资源减少创建对象连接降低垃圾回收GC的开销适当使用池化相关技术能够显著提高系统效率优化性能。
### 为什么要使用线程池
线程池管理线程好处
降低资源消耗。通过复用已存在的线程和降低线程关闭的次数来尽可能降低系统性能损耗提升系统响应速度。通过复用线程省去创建线程的过程因此整体提升了系统响应速度提高线程的可管理性。线程是稀缺资源如果无限制的创建不仅会消耗系统资源还会降低系统的稳定性因此需要使用线程池来管理线程。
### 线程池工作原理 当一个并发任务提交给线程池线程池分配线程去执行任务三阶段
1、先判断线程池中核心线程池所有线程是否都在执行任务。没有则新创建一个线程执行刚提交的任务否则核心线程池中所有线程都在执行任务进行下一步判断
2、判断当前阻塞队列是否已满未满则将提交的任务添加到阻塞队列中否则进行下一步判断
3、判断线程池中所有线程是否都在执行任务没有则创建一个新的线程执行任务否则交给饱和策略处理。
源码流程
如果当前运行的线程少于corePoolSize则会创建新的线程来执行新的任务如果运行的线程个数大于等于corePoolSize则将提交的任务存放阻塞队列workQueue中如果当前workQueue队列已满的话则会创建新的线程来执行任务如果线程个数已经超过了maximumPoolSize则会使用饱和策略RejectedExecutionHandler来进行处理。
### 线程池参数有哪些
创建线程池主要是ThreadPoolExecutor类完成ThreadPoolExecutor的构造方法为
ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueueRunnable workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)corePoolSize表示核心线程池的大小。当提交一个任务时如果当前核心线程池的线程个数没有达到corePoolSize则会创建新的线程来执行所提交的任务即使当前核心线程池有空闲的线程。如果当前核心线程池的线程个数已经达到了corePoolSize则不再重新创建线程。如果调用了prestartCoreThread()或者 prestartAllCoreThreads()线程池创建的时候所有的核心线程都会被创建并且启动。workQueue阻塞队列。用于保存任务的阻塞队列可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。maximumPoolSize表示线程池能创建线程的最大个数。如果当阻塞队列已满时并且当前线程池线程个数没有超过maximumPoolSize的话就会创建新的线程来执行任务。keepAliveTime空闲线程存活时间。如果当前线程池的线程个数已经超过了corePoolSize并且线程空闲时间超过了keepAliveTime的话就会将这些空闲线程销毁这样可以尽可能降低系统资源消耗。unit时间单位。为keepAliveTime指定时间单位。threadFactory创建线程的工程类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字如果出现并发问题也方便查找问题原因。 handler饱和策略。当线程池的阻塞队列已满和指定的线程都已经开启说明当前线程池已经处于饱和状态了那么就需要采用一种策略来处理这种情况。 ### 如何动态修改线程池参数
ThreadPollExecutors三个重要的参数为corePoolSize、workQueue、maximumSize这三个参数基本决定了线程池对于任务的处理策略。
ThredPoolExecutors提供了Set方法动态修改线程池参数 没有动态指定队列长度的方法美团的方式是自定义了一个叫做 ResizableCapacityLinkedBlockIngQueue 的队列主要就是把LinkedBlockingQueue的 capacity 字段的 final 关键字修饰给去掉了让它变为可变的。 ### 线程池常用的阻塞队列谈谈
不同的线程池会选用不同的阻塞队列主要有无界队列、同步队列、延迟阻塞队列 ### 线程池饱和策略有哪些
ThreadPoolExecutor主要有以下四种方法处理线程饱和
AbortPolicy 直接拒绝所提交的任务并抛出RejectedExecutionException异常CallerRunsPolicy只用调用者所在的线程来执行任务DiscardPolicy不处理直接丢弃掉任务DiscardOldestPolicy丢弃掉阻塞队列中存放时间最久的任务执行当前任务 ### 如何合理分配线程池参数
任务的性质CPU密集型任务IO密集型任务和混合型任务。任务的优先级高中和低。任务的执行时间长中和短。任务的依赖性是否依赖其他系统资源如数据库连接。
任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量N1。IO密集型任务则由于需要等待IO操作线程并不是一直在执行任务则配置尽可能多的线程默认2NN为CPU 核心数。混合型的任务如果可以拆分则将其拆分成一个CPU密集型任务和一个IO密集型任务只要这两个任务执行的时间相差不是太大那么分解后执行的吞吐率要高于串行执行的吞吐率如果这两个任务执行时间相差太大则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行需要注意的是如果一直有优先级高的任务提交到队列里那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理或者也可以使用优先级队列让执行时间短的任务先执行。
依赖数据库连接池的任务因为线程提交SQL后需要等待数据库返回结果如果等待的时间越长CPU空闲时间就越长那么线程数应该设置越大这样才能更好的利用CPU。
并且阻塞队列最好是使用有界队列如果采用无界队列的话一旦任务积压在阻塞队列中的话就会占用过多的内存资源甚至会使得系统崩溃。 ### 为什么不推荐使用内置线程池
使用jdk自带的Executors创建线程池存在资源耗尽的风险。
FixedThreadPool 和 SingleThreadExecutor使用的是无界的LinkedBlockingQueue任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求从而导致 OOM。CachedThreadPool使用的是同步队列 SynchronousQueue, 允许创建的线程数量为 Integer.MAX_VALUE 如果任务数量过多且执行速度较慢可能会创建大量的线程从而导致 OOM。ScheduledThreadPool 和 SingleThreadScheduledExecutor : 使用的无界的延迟阻塞队列DelayedWorkQueue任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求从而导致 OOM。 ### 创建线程池方式
主要有俩种
一、ThreadPoolExecutor构造函数最原始的创建线程池的⽅式它包含了 7 个参数可供设置 二、Execute框架的Executors工具类
Executors.newFixedThreadPool创建⼀个固定⼤⼩的线程池可控制并发的线程数超出的线程会在队列中等待Executors.newCachedThreadPool创建⼀个可缓存的线程池若线程数超过处理所需缓存⼀段时间后会回收若线程数不够则新建线程Executors.newSingleThreadExecutor创建单个线程数的线程池它可以保证先进先出的执⾏顺序Executors.newScheduledThreadPool创建⼀个可以执⾏延迟任务的线程池Executors.newSingleThreadScheduledExecutor创建⼀个单线程的可以执⾏延迟任务的线程池Executors.newWorkStealingPool创建⼀个抢占式执⾏的线程池任务执⾏顺序不确定 ### 线程池关闭
俩个方法shutdown和shutdownNow
共同点遍历线程池中所有线程依次中断线程
不同点
shutdown将线程池的状态设为SHUTDOWN中断所有空闲线程将正在执行的任务继续执行完shutdownNow将线程池状态设为STOP停止所有执行任务或空闲线程返回等待执行任务的列表调用俩方法任意一个isShutdown均返回true所有线程都关闭成功调用isTerminated才返回true 想要真正搞懂线程池-
绝对的好文章
如何设置线程池参数美团给出了一个让面试官虎躯一震的回答。
Java线程池实现原理及其在美团业务中的实践 - 美团技术团队 ## AQS
### AQS是什么
AQS 就是一个抽象类java.util.concurrent.locks 包下主要用来构建锁和同步器
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {}### 谈谈AQS工作原理 定义了一套多线程访问共享资源的同步器框架
AQS维护一个volatile int state变量 CLH虚拟双向队列
state变量
表示同步状态使用volatile关键字修饰state保证线程可见性状态信息 state 可以通过 protected 类型的getState()、setState()和compareAndSetState() 进行操作。并且这几个方法都是 final 修饰的在子类中无法被重写。 CLH虚拟双向队列
仅存在节点之间关联关系不存在队列实例AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点Node来实现锁的分配CLH 同步队列中一个节点表示一个线程它保存着线程的引用thread、 当前节点在队列中的状态waitStatus、前驱节点prev、后继节点next 举例 以 ReentrantLock 为例state 初始值为 0表示未锁定状态。A 线程 lock() 时会调用 tryAcquire() 独占该锁并将 state1 。此后其他线程再 tryAcquire() 时就会失败直到 A 线程 unlock() 直到 state0即释放锁为止其它线程才有机会获取该锁。当然释放锁之前A 线程自己是可以重复获取此锁的state 会累加这就是可重入的概念。但要注意获取多少次就要释放多少次这样才能保证 state 是能回到零态的。再以 CountDownLatch 以例任务分为 N 个子线程去执行state 也初始化为 N注意 N 要与线程个数一致。这 N 个子线程是并行执行的每个子线程执行完后countDown() 一次state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 state0 )会 unpark() 主调用线程然后主调用线程就会从 await() 函数返回继续后续动作。 ### AQS提供的俩种锁机制
排他锁ReentrantLock重入锁
共享锁CountDownLatch、Semaphore
也就是资源共享方式独占Exclusive、共享Share 面试谈谈对AQS的理解
AQS 是多线程同步器它是 J.U.C 包中多个组件的底层实现如 Lock、 CountDownLatch、Semaphore 等都用到了 AQS. 从本质上来说AQS 提供了两种锁机制分别是排它锁和共享锁。 排它锁就是存在多线程竞争同一共享资源时同一时刻只允许一个线程访问该 共享资源也就是多个线程中只能有一个线程获得锁资源比如 Lock 中的 ReentrantLock 重入锁实现就是用到了 AQS 中的排它锁功能。 共享锁也称为读锁就是在同一时刻允许多个线程同时获得锁资源比如 CountDownLatch 和 Semaphore 都是用到了 AQS 中的共享锁功能。 设计AQS整个体系需要解决的三个核心的问题①互斥变量的设计以及多线程同时更新互斥变量时的安全性②未竞争到锁资源的线程的等待以及竞争到锁资源的线程释放锁之后的唤醒③锁竞争的公平性和非公平性。 AQS采用了一个int类型的互斥变量state用来记录锁竞争的一个状态0表示当前没有任何线程竞争锁资源而大于等于1表示已经有线程正在持有锁资源。一个线程来获取锁资源的时候首先判断state是否等于0如果是(无锁状态)则把这个state更新成1表示占用到锁。此时如果多个线程进行同样的操作会造成线程安全问题。AQS采用了CAS机制来保证互斥变量state的原子性。未获取到锁资源的线程通过Unsafe类中的park方法对线程进行阻塞把阻塞的线程按照先进先出的原则加入到一个双向链表的结构中当获得锁资源的线程释放锁之后会从双向链表的头部去唤醒下一个等待的线程再去竞争锁另外关于公平性和非公平性问题AQS的处理方式是在竞争锁资源的时候公平锁需要判断双向链表中是否有阻塞的线程如果有则需要去排队等待而非公平锁的处理方式是不管双向链表中是否存在等待锁的线程都会直接尝试更改互斥变量state去竞争锁。 Synchronized、ReentrantLock均一次只允许一个线程访问某个资源。使用比较单一。主要讨论的就是这三个同步工具类/共享锁Semaphore、CountDownLatch、CyclicBarrier。
### Semaphore作用
作用控制同时访问特定资源的线程数量。
//初始共享资源的线程数量为5
final Semaphore semaphore new Semaphore(5);
//获得1个许可
semaphore.acquire();
//释放1个许可
semaphore.release();
假设有N个线程来获取Semaphore中的共享资源上面代码中只有5个线程能获取到共享资源其他线程都会阻塞只有获取到共享资源的线程才能继续执行。等到有线程释放了共享资源其他阻塞的线程才能获取到。
当初始的线程资源个数为1时Semaphore退化为排他锁。
使用场景
Semaphore 通常用于那些资源有明确访问数量限制的场景比如限流仅限于单机模式实际项目中推荐使用 Redis Lua 来做限流。
### Semaphore原理
Semaphore有俩种模式
公平模式调用acquire()方法的顺序就是获取许可证的顺序遵循FIFO非公平模式抢占式的
Semaphore是共享锁的一种实现。默认构造AQS的state值为permits可将permits值理解为许可证数量只有获得许可证的线程才能执行。
调用semaphore.acquire(),线程尝试获取许可证
如果state 0,表示获取成功。然后使用CAS操作修改state的值state state - 1如果state 0,表示许可证数量不足。然后创建一个Node节点加入阻塞队列挂起当前线程
调用semaphore.release()线程尝试释放许可证
使用CAS操作修改state state 1.释放许可证成功后同时会唤醒同步队列中的一个线程。被唤醒的线程会重新尝试修改state值state state - 1。如果state 0则获取许可证成功否则重新进入阻塞队列挂起线程。
面试
Semaphore 是共享锁的一种实现。它默认构造 AQS 的 state 为 permits。当执行任务的线程数量超出 permits那么多余的线程将会被放入等待队列 Park,并自旋判断 state 是否大于 0。只有当 state 大于 0 的时候阻塞的线程才能继续执行,此时先前执行任务的线程继续执行 release() 方法release() 方法使得 state 的变量加 1那么自旋的线程便会判断成功。 如此每次只有最多不超过 permits 数量的线程能自旋成功便限制了执行任务线程的数量。 ### CountDownLatch作用
实现一个或多个线程等待其他线程完成某个操作后再继续执行。
CountDownLatch 允许 count 个线程阻塞在一个地方直至所有线程的任务都执行完毕。
CountDownLatch 是一次性的计数器的值只能在构造方法中初始化一次之后没有任何机制再次对其设置值当 CountDownLatch 使用完毕后它不能再次被使用。 ### CountDownLatch原理
共享锁的一种实现。默认构造AQS的state值为count。
当线程使用countDown()方法时本质使用了tryReleaseShared()方法以CAS的操作来减少state直至state为0.
当调用await()方法时如果state不为0说明任务还没有执行完毕await()会一直阻塞即await方法后的语句都不会被执行。
直到count个线程调用了countDown()是state值减为0或者await()线程被中断该线程才会从阻塞中被唤醒await()之后的语句得到执行。 ### CountDownLatch俩种实例
某一线程在开始运行前等待 n 个线程执行完毕 : 将 CountDownLatch 的计数器初始化为 n new CountDownLatch(n)每当一个任务线程执行完毕就将计数器减 1 countdownlatch.countDown()当计数器的值变为 0 时在 CountDownLatch 上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时主线程需要等待多个组件加载完毕之后再继续执行。实现多个线程开始执行任务的最大并行性注意是并行性不是并发强调的是多个线程在某一时刻同时开始执行。类似于赛跑将多个线程放到起点等待发令枪响然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象将其计数器初始化为 1 new CountDownLatch(1)多个线程在开始执行任务前首先 coundownlatch.await()当主线程调用 countDown() 时计数器变为 0多个线程同时被唤醒。 ### CyclicBarrier 有什么用
当计数器的值还未到达时线程都会阻塞等待直到达到设定的数值所有的线程都会被释放
让一组线程都到达屏障同步点之后屏障才会打开所有被拦截的线程才会工作。 ### CyclicBarrier 的原理是什么
基于ReentrantLock和Condition实现
CyclicBarrier 内部通过一个 count 变量作为计数器count 的初始值为 parties 属性的初始化值每当一个线程到了栅栏这里了那么就将计数器减 1。如果 count 值为 0 了表示这是这一代最后一个线程到达栅栏就尝试执行我们构造方法中输入的任务。 ### CycliBarriar和CountdownLatch有什么区别
1、触发条件不同
CyclicBarrier在等待的线程数量达到指定值时会触发一个屏障操作所有的线程都会被释放
CountDownLatch通过计数器来触发等待操作计数器的初始值为等待的线程数量每当一个线程完成任务后计数器减1直到计数器为0时所有等到的线程将被释放
2、重用性不同
CyclicBarrier可以被重用。可以通过reset()方法重置CyclicBarrier的状态
CountDownLatch不能被重用。一旦计数器减为0就不能再使用
3、线程协作方式不同
CyclicBarrier适合在一组线程相互等待达到共同的状态然后同时开始或继续执行后续操作并且可以额外设置一个Runnable参数当一组线程达到屏障点后可以优先触发。
CountDownLatch适用于一个或多个线程等待其他线程执行完某个操作后再继续执行。 ## ThreadLocal
ThreadLocal是一种隔离机制方法
Thread是类名
ThreadLocalMap是一种数据结构定制化HashMap
### ThreadLocal原理
ThreadLocal是一种基于共享变量副本的隔离机制保证多线程环境下对共享变量修改的安全性。 在多线程访问共享变量场景中一般解决办法是对共享变量加锁从而保证在同一时刻只有一个线程能对共享变量进行更新。弊端加锁会造成性能下降。 ThreadLocal使用了空间换时间的思想也就是在每个线程内都有一个容器来存储共享变量的副本每个线程只对自己的共享变量副本做更新操作。 优点1、解决了线程安全问题2、避免了多线程竞争加锁的开销 共享变量的副本存储在Thread类里面的成员变量ThreadLocalMap可看做定制的HashMap
Thread类源码
public class Thread implements Runnable {//......//与此线程有关的ThreadLocal值。由ThreadLocal类维护ThreadLocal.ThreadLocalMap threadLocals null;//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护ThreadLocal.ThreadLocalMap inheritableThreadLocals null;//......
}
ThreadLocal数据结构 ### ThreadLocal使用场景 1、线程上下文传递在跨线程调用的场景中可使用ThreadLocal在不修改方法签名的情况下传递线程上下文信息。比如框架和中间件的请求头中的用户信息、请求ID等存储在ThreadLocal中在后续的请求链路中都可以方便的访问这些信息。 2、数据库连接管理在使用数据库连接池的情况下可以将数据库的连接信息存储在ThreadLocal中每个线程可以独立管理自己的数据库连接避免了线程之间的竞争和冲突。比如MyBatis中的sqlSession对象使用了ThreadLocal存储当前现成的数据库会话信息。 3、事务管理在需要手动管理事务的场景中可使用ThreadLocal存储事务的上下文每个线程独立的控制自己的事务保证事务的隔离性 ### ThreadLocal内存泄漏及避免策略
内存泄漏对象或变量占用的内存不会再被使用也不能被回收
强引用一个对象具有强引用不会被垃圾回收器回收。当内存不足JVM抛出OOM也不会回收强引用对象。显式的将引用赋值为nullJVM合适时间可以回收。
弱引用JVM垃圾回收时无论内存是否充足都会回收被弱引用关联的对象。
static class Entry extends WeakReference{}
GC垃圾回收机制-JVM如何找到需要回收的对象 1、引用计数法每个对象有一个引用计数属性新增引用1释放引用-1计数为0可以回收 2、可达性分析法从GC Roots开始向下搜索搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时证明此对象是不可用的JVM则判断是可回收对象。
引用计数法存在两个对象互相引用永远无法回收的问题。
ThreadLocal内存泄漏原因
ThreadLocalMap使用ThreadLocal的弱引用作为Entry数组的Key如果ThreadLocal不存在外部强引用时Key就会被GC回收这样就会导致ThreadLocalMap的Key为null而value还存在强引用。只有thread线程退出后value的强引用链条才会断掉。如果当前线程一直不结束Key为null的Entry的value就会一直存在一条强引用连无法回收造成内存泄漏。
Thread Ref - Thread - ThreaLocalMap - Entry - value 如何避免内存泄漏
1、每次使用完ThreadLocal调用remove方法清除数据
2、ThreadLocal变量设置为static final共用一个ThreadLocal保证强引用
3、ThreadLocal内部优化在我们调用set方法时进行全量清理清理出Key为null的值扩容也会继续检查。 ## Future
### Future类作用
作用获取异步任务执行后的结果。
大白话有一个任务不需要立刻获得结果因此采用异步方式让子线程去执行该任务继续做主要的任务之后直接通过future获取最后执行的结果。
Future接口包含五个方法
// V 代表了Future执行的任务返回值的类型
public interface FutureV {// 取消任务执行// 成功取消返回 true否则返回 falseboolean cancel(boolean mayInterruptIfRunning);// 判断任务是否被取消boolean isCancelled();// 判断任务是否已经执行完成boolean isDone();// 获取任务执行结果V get() throws InterruptedException, ExecutionException;// 指定时间内没有返回计算结果就抛出 TimeOutException 异常V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutExceptio}### Callable 和 Future 有什么关系
FutureTask实现Future接口封装了Callable、Runnable具有任务取消、查看、任务是否执行完成以及获取任务执行结果的方法。
FutureTask 实现类有两个构造函数可传入 Callable 或者 Runnable 对象。实际上传入 Runnable 对象也会在方法内部转换为Callable 对象。
封装Callable管理着任务执行的情况存储了 Callable 的 call 方法的任务执行结果。 ## CompletableFuture 有什么用
Future局限性如不支持异步任务的编排组合、获取结果的get()为阻塞调用
CompletableFuture实现俩接口Future、CompletionStage
public class CompletableFutureT implements FutureT, CompletionStageT {}面试满分回答 定义 五种异步回调方式
CompletableFuture是JDK1.8里面引入的一个基于事件驱动的异步回调类CompletableFuture弥补了原本Future的不足使得程序可以在非阻塞的状态下完成异步的回调机制。
简单来说就是当使用异步线程去执行一个任务时希望在任务结束以后触发一个后续的动作。
举个简单的例子比如在一个批量支付的业务逻辑里面涉及到查询订单、支付、发送邮件通知这三个逻辑。
使用Future的话这三个逻辑是按照顺序同步去实现的也就是先查询到订单以后再针对这个订单发起支付支付成功以后再发送邮件通知。 这种设计方式导致这个方法的执行性能比较慢。
可以直接使用CompletableFuture如图也就是说把查询订单的逻辑放在一个异步线程池里面去处理。然后基于CompletableFuture的事件回调机制的特性可以配置查询订单结束后自动触发支付支付结束后自动触发邮件通知。 CompletableFuture提供了5种不同的方式把多个异步任务组成一个具有先后关系的处理链然后基于事件驱动任务链的执行。
thenCombine把两个任务组合在一起当两个任务都执行结束以后触发事件回调。thenCompose把两个任务组合在一起这两个任务串行执行也就是第一个任务执行完以后自动触发执行第二个任务。thenAccept第一个任务执行结束后触发第二个任务并且第一个任务的执行结果作为第二个任务的参数这个方法是纯粹接受上一个任务的结果不返回新的计算值。thenApply和thenAccept一样但是它有返回值。thenRun就是第一个任务执行完成后触发执行一个实现了Runnable接口的任务。 ## 原子类
原子类具有原子操作特征的类。
作用和锁类似是为了保证并发情况下的线程安全。
原子操作不可拆分的操作。一旦执行不可中断要么全部执行要么不执行。
根据操作的类型JUC包中共有4种类型原子类
1、基本类型
使用原子的方式更新基本类型
AtomicInteger整型原子类AtomicLong长整型原子类AtomicBoolean布尔型原子类
2、数组类型
使用原子的方式更新数组里的某个元素
AtomicIntegerArray整型数组原子类AtomicLongArray长整型数组原子类AtomicReferenceArray引用类型数组原子类
3、引用类型
AtomicReference引用类型原子类AtomicMarkableReference原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来AtomicStampedReference原子更新带有版本号的引用类型。该类将整数值与引用关联起来可用于解决原子的更新数据和数据的版本号可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
4、对象的属性修改类型
AtomicIntegerFieldUpdater:原子更新整型字段的更新器AtomicLongFieldUpdater原子更新长整型字段的更新器AtomicReferenceFieldUpdater原子更新引用类型里的字段
### 原子类与锁对比 粒度更细 原子变量可以把竞争范围缩小到变量级别,通常情况下锁的粒度大于原子变量的粒度 效率更高 除了在高并发之外使用原子类的效率往往比使用同步互斥锁的效率更高因为原子类底层利用了CAS不会阻塞线程。