面试知识点-Java多线程
2021-10-11 18:35:53 839 举报
AI智能生成
java多线程(持续更新)
作者其他创作
大纲/内容
<font color="#000000">Java内存模型</font>
主内存
工作内存
线程开始运行时会将所需的变量从主内存中拷贝一份到工作内存中,在线程运行结束后再写入主内存
主内存和工作内存的交互
read,write,lock,unlock,assgin,use,load,save
三大特征
可见性
一条线程修改完一个共享变量后,另一个线程若访问这个变量将会访问到修改后的值
有序性
Happens-Before规则
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在一个happen-before的关系
重排序
<b><font color="#c41230">为了减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则对代码的执行顺序进行调整,从而提高执行效率。</font></b>
程序代码执行的结果不受JVM指令重排序的影响
原子性
一组操作必须一起完成,中途不能被中断。
<font color="#000000">synchronized</font>
一个重量级的可重入锁
监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因
作用方式
作用于代码块
作用于实例方法
持有的是当前对象实例的锁
作用于静态方法
持有的是静态对象的锁
底层实现
作用于方法(显式同步)
JVM可以从方法区中的方法表结构中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。
作用于代码块(隐式同步)
编译时,在代码块的前后加上monitorenter和monitorexit
为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令
ObjectMontior
waitSet
EntryList
owner
count
线程中断与synchronized
线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用
等待唤醒机制与synchronized
在使用notify/notifyAll和wait这3个方法时,必须处于synchronized代码块或者synchronized方法中
notify/notifyAll和wait方法都依赖于monitor
java对象头
Mark Word
对象的hashCode
CG年代
锁信息(偏向锁,轻量级锁,重量级锁)
GC标志
指向monitor的指针
Class Metadata Address
指向对象实例的指针
<font color="#000000">线程</font>
线程中断
public static boolean interrupted()
测试当前线程是否已经中断,并将线程状态设置为false
public boolean isInterrupted()
测试线程是否已经中断。线程的中断状态不受该方法的影响
public void interrupt()
中断线程,设置中断标识为为true
InterruptedException
对线程调用interrupt()时,如果该线程处于阻塞或者等待状态,那么就会抛出 InterruptedException
线程状态
新建
继承Thread类创建线程<br>
实现Runnable接口创建线程<br>
使用Callable和Future创建线程 <br>
使用线程池例如用Executor框架
运行
阻塞
同步阻塞
竞争锁失败
等待阻塞
调用wait,lockSupport.park()等方法
其他阻塞
就绪
调用thread.start()
结束
线程运行完毕
线程间通信
wait/notify/notifyAll
synchronized/lock
thread方法
thread.join()
thread.yiled()
管道通信
管道流pipeStream
是一种特殊的流,用于在不同线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输入管道中读数据。
同步工具类
CountDownLatch
CountDownLatch允许一个或多个线程等待其他线程完成操作
countDown()
count数减1
await()
线程阻塞直到count等于0
应用场景
开启多个线程分块下载一个大文件,每个线程只下载固定的一截,最后由另外一个线程来拼接所有的分段。
应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。
确保一个计算不会执行,直到所需要的资源被初始化。
CyclicBarrier
CyclicBarrier(int ),参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
当cyclicBarrier的count数等于0的时候,阻塞的线程都继续执行
应用场景
多线程计算
Semphore
可以控制同时访问的线程个数,它维护了一组"许可证"。
acquire()
消费一个许可证。如果没有许可证了,会阻塞起来
release()
添加一个许可证
应用场景
可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接
CountDownLatch和CyclicBarrier的区别
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置<br>
CountDownLatch.await一般阻塞主线程,而CyclicBarrierton一般阻塞工作线程
CountDownLatch主要用于描述一个或者多个线程等待其他线程执行完毕
CyclicBarrier主要用于描述多个线程之间相互等待<br>
Exchanger
Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
线程安全
所谓的线程安全问题,本质在于线程对共享变量的操作的原子性,可见性,有序性不能同时满足,因此解决线程安全问题的关键在于使其同时满足以上三个特征
<font color="#000000">线程池(Executor)</font>
线程池基本概念
线程池可以看做是线程的集合,请求到来时线程池给这个请求分配一个空闲的线程,任务完成后回到线程池中等待下次任务(而不是销毁)。这样就实现了线程的重用。
ThreadPoolExecutor
构造函数
阻塞队列策略
同步移交
SynchronousQueue
不存储元素的阻塞队列,因此超出核心线程数的任务会创建新的线程来指执行。
无界队列
常用的为无界的LinkedBlockingQueue
当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM
DelayedWorkQueue
一个按超时时间升序排序的队列
使用了优先级队列的无界阻塞队列,支持延时获取,所谓延时队列就是消费线程将会延时一段时间来消费元素。队列里的元素要实现Delay接口。
有界队列
常见队列
ArrayBlockingQueue
遵循FIFO的队列
LinkedBlockingQueue
有节的LinkedBlockingQueue
PriorityBlockingQueue
优先级队列
可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量(因为任务数量少了)
拒绝策略
丢弃当前任务
丢弃最老的任务
直接抛出异常
抛回调用者的线程处理
线程池状态
running
自然是运行状态,指可以接受任务执行队列里的任务
shutdonw
SHUTDOWN 指调用了 shutdown() 方法,不再接受新任务了,但是阻塞队列里的任务得执行完毕。
stop
STOP 指调用了 shutdownNow() 方法,不再接受新任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务。
tidying
所有任务都执行完毕,在调用 shutdown()/shutdownNow() 中都会尝试更新为这个状态。
terminated
当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态
线程池的配置
IO密集型
尽量使用较小的线程池,一般为CPU核心数+1
Cpu密集型
可以使用稍大的线程池,一般为2*CPU核心数。
混合型
常见的线程池
newFixedThreadPool
固定大小的线程池,有新任务时,根据当前线程池的线程数量确定后续步骤
创建参数
LinkedBlockingQueue(无界)
线程存活时间:永久存活
核心线程数:n(用户指定)
最大线程数:n(用户指定)
newCachedThreadPool
当有新任务时,直接新建线程
创建参数
SynchronousQueue
线程存活时间:60s
核心线程数:0
最大线程数:Interget.MAX_VALUE
newSingleThreadExecutor
创建单个线程的线程池。
创建参数
LinkedBlockingQueue(无界)
线程存活时间:永久存活
核心线程数:1
最大线程数:1
NewScheduledThreadPool:
创建一个定长线程池,支持定时及周期性任务执行。
创建参数
DelayedWorkQueue
一个按超时时间升序排序的队列
使用了优先级队列的无界阻塞队列,支持延时获取,所谓延时队列就是消费线程将会延时一段时间来消费元素。队列里的元素要实现Delay接口。
线程存活时间:永久存活
核心线程数:用户指定
最大线程数:Integer.MAX_VALUE
线程池的关闭
Shutdown
线程池状态变为shutdown
ShutdownNow
线程状态变为stop
中断线程池中的某一个线程的方法
可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。
常见接口和类
Executor接口
只有一个 void excute(Runnable task)方法,用户执行任务
ExecutorService
Executor的子接口,定义了sumbit,invokeAll,invokeAny等方法
AbstractExecutorService
实现了sumbit,invokeAll()等方法
源码
Worker
Worker是线程池中的线程
继承AQS,实现runnable
既是一个可执行的任务,又可以达到锁的效果
初始state=-1
这样构造的原因主要是为了实现对中断的控制
1.worker未运行时
1.shutdown()线程池时,会对每个worker tryLock()上锁,tryAcquire是尝试通过CAS将state由0设置为1,因此会失败
2.shutdownNow()线程池时,不用tryLock()上锁,但调用worker.interruptIfStarted()终止worker时也要求state=0,因此也会失败
2.worker运行时
runWorker中,会对正在运行中的worker加锁,所以如果调用了shutdown()方法,中断也会失败,但是如果调用shutdownNow()方法,该方法会通过worker.interruptIfStarted来中断任务
addWorker
1.判断线程池状态,如果状态正常,进入下一步
2.比较当前线程池的线程数和最大线程数/核心线程数的大小(由参数core确定比较对象),如果没有超过的话,进入下一步,否则返回false
3.在线程池自带的成员变量ReentrantLock的加锁的情况下,向Workers的HashSet中添加新创建的worker实例,添加完成后解锁,并start该worker实例,worker.start()方法底层其实调用的就是runWorker()方法
基本概念:创建新线程并执行
runWorker
1.将state设置为0
2.在mainLock.lock的情况下,进行task.run
3.在finally块中释放锁mainLock.unlock
4.在使用getTask方法去阻塞队列中获取锁
<font color="#000000">线程死锁</font>
死锁产生的四个必要条件
互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。<br>
不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。<br>
请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。<br>
循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所申请的资源。
解决方法
解决任一条件
<font color="#000000">volatile</font>
可见性
当被volatile修饰的变量进行写操作时,这个变量将会被直接写入共享内存,而非线程的专属存储空间。
根本原因是在写入后执行了一个空操作,使得cpu的cache写入内存
当读取一个被volatile修饰的变量时,会直接从共享内存中读,而非线程专属的存储空间中读。
有序性
<b><font color="#c41230">volatile在指令间加上了内存屏障,内存屏障指的是重排序的时候不能把后面的指令重排序到内存屏障之前的位置。</font></b>
原子性(不能保证)
原子性指的是一组操作必须一起完成,中途不能被中断。
volatile能确保long、double读写的原子性
java内存模型保证声明为volatile的long和double变量的get和set操作是原子的。
应用场景
多个变量之间或者某个变量的当前值与修改后值之间没有约束。
状态标志
全局变量
一读多写
<font color="#000000">JUC</font>
Lock
Reentrantlock
特点
可实现公平锁
在构造函数中设置
通过fair这个boolean型变量设置
是通过hasQueuedPredecessors()函数实现的
这个函数会判断当前获取锁的线程是否是请求队列的首部线程
绑定多个条件(Condition)
等待可中断
可重入锁
通过setExclusiveOwnerThread()实现
内部类
Reentrantlock默认实现在sync中(sync继承了AQS)
非公平锁NonfairSync(继承sync)
公平锁FairSync(继承sync)
ReentrantReadWriteLock
特点
state的变量高16位是读锁,低16位是写锁。
读锁不能升级为写锁
写锁可以降级为读锁
当访问方式是读取操作时,使用读锁即可,当访问方式是修改操作时,则使用写锁
内部类
ReentrantReadWriteLock默认实现在sync中(sync继承了AQS)
NonfairSync
FairSync
WriteLock(实现Lock接口)<br>
ReadLock(实现Lock接口)
synchronized和lock的区别
synchronized是关键字,lock是接口
发生异常时,synchronized会自动释放锁,lock需要手动释放锁
lock能够知道是否成功获取到锁,synchronized则不行
通过tryAcquire()去尝试获取锁,获取成功则设置锁状态并返回true,否则返回false。
在竞争激烈的情况下,lock效率更高
JDK1.5之前Lock效率高很多
JDK1.6之后效率差不多
lock可以实现等待可中断,synchronized不可以
<b><font color="#c41230">通过tryLock(long time, TimeUnit unit)设置等待时间,如果规定时间内没获取到锁,才返回false</font></b>
lock可以实现公平锁
lock可以实现读写锁
Atomic
对数据进行操作时(使用CAS来保证)可以保证原子性
存在CAS的ABA问题
使用AtomicStampedReference的版本号机制来管理
通过unsafe实现的包装类
Unsafe底层实际上是调用C代码,C代码调用汇编,最后生成出一条CPU指令cmpxchg,完成操作
CompareAndSwap...()
JDK增加了LongAdder等四个类,高并发时效率更高,但是消耗空间更多
AQS
一个抽象的队列式的同步器,定义了一套多线程访问共享资源的同步器框架(实现锁的框架),许多同步类实现都依赖于它,本质上其实就是对于state的获取和释放
ReentrantLock/ReentrantReadWriteLock /Semaphore/ CountDownLatch的实现都依赖于它
数据结构
共享资源 volatile int state
默认实现了FIFO线程等待队列,底层是双向链表
线程模式
独享模式
tryAcquire(int)/tryAcquireShared(int )
共享模式
tryRelease/tryReleaseShared(int)
既可独占,也可共享(ReentrantReadWriteLock)
线程池相关
callable
一般和ExecutorService配合来使用
有返回值
future
判断任务是否完成
future.isDone()
返回执行结果
future.get()
中断线程的执行
future.cancel()
futureTask
future接口的唯一实现类
既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
CAS算法
CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。,CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
使用AtomicStampedReference的版本号机制来管理
ABA问题
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,但可能期间它已经被修改过了
解决方法:使用AtomicStampedReference的版本号机制来管理
通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。
CAS操作是一条CPU指令,不会被打断,所以是原子操作
<font color="#000000">锁分类</font>
乐观锁,悲观锁
乐观锁
CAS算法
悲观锁
synchronized
独享锁,共享锁
公平锁,非公平锁
可重入锁
线程得到一个对象锁后再次请求该对象锁,是允许的
实现方法
为每一个锁关联一个获取计数器和一个所有者线程,在获取锁的时候判断owner,如果是相同的owner,则count加1
分段锁
通过分段锁的形式来细化锁的粒度,从而实现高效的并发操作,例如concurrentHashMap
锁优化
偏向锁
CAS操作:将线程ID保存在对象的Mark Word中
如果成功,则说明该线程已经获取了对象的偏向锁
有其他线程获取对象锁时失效
对象锁定时膨胀为轻量级锁
对象未锁定时恢复到未锁定状态
经验依据:在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得
轻量级锁
CAS操作:将对象的Mark Word更新为指向Lock Record的指针
如果成功,则该线程已经获取了对象的轻量级锁
如果失败,检查对象的Mark Word是否指向当前线程
如果指向当前线程,则进入同步代码块
如果没有,则膨胀为重量级锁
如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
经验依据:对绝大部分的锁,在整个同步周期内都不存在竞争
重量级锁
自旋锁
尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
锁粗化
如果一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
锁消除
对于一些代码上要求同步,但实际上并不需要同步的锁进行消除
<font color="#000000">ThreadLocal</font>
ThreadLocal提供了线程的局部变量,且不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。
基本用法
set(value)
首先获取当前thread的ThreadLocalMap
如果map已经初始化,则将kv存入map中
否则初始化map(此时构造函数已经将kv存入)
get()
remove()
initialValue()
底层实现
ThreadLocalMap是ThreadLocal的内部类
set():向ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
get(): 从ThreadLocalMap获取值,key是ThreadLocal对象
ThreadLocalMap
用Entry进行存储,Entry的key的类型为ThreadLocal
<b><font color="#c41230">每个Thread维护了一个ThreadLocalMap的成员变量,这是实现ThreadLocal的核心</font></b>
hash冲突解决方法:线性探测法
内存泄漏
ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,这时候ThreadLocalMap里面就会存在key为null但是value不为null的entry项。如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
解决方法
调用threadLocal.remove()方法来清理key为null的元素
根本原因
由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用
为什么key要用弱引用
引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
应用场景
单个线程
线程上下文信息存储
数据库连接
session管理
<font color="#000000">并发容器</font>
concurrentHashMap
数据结构
JDK1.7
segment
继承了ReentrantLock
一个concurrentHashMap包含了一个segment数组
一个segment包含了一个hashEntry链表
hashEntry
链表结构的元素
hashEntry的成员变量除了value都定义为final
为了维护链表结构,防止并发问题
JDK1.8
Node数组
链表
红黑树
常用方法底层实现(JDK1.7)
put()
流程
1.首先Hash定位到Segment
2.对当前Segment加锁,如果Segment中元素的数量超过了阈值,则需要进行扩容并且进行rehash
3.再定位到链表头部
get()
不用加锁,是非阻塞的
因为共享变量都定义为了volatile
根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景
volatile能够保证内存可见性
需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部
定位到链表头部之后根据key取出对应的value值
如果取出的value是null,则对取出value这一过程进行加锁(lock())
取出的value是null的原因是可能现在正在进行put操作
如果不是value,则直接返回value值
remove()
因为HashEntry中的next是final的,一经赋值以后就不可修改,所以在定位到待删除元素e的位置以后,程序就将待删除元素前面的那一些元素全部复制一遍,然后再一个一个重新接到链表上去。尾结点指向e的下一个结点。e后面的结点不需要复制,它们可以重用。e之前的元素在remove()之后为remove之前的逆置
size()
size()操作涉及到多个segment
size操作就是遍历了两次Segment,每次记录Segment的modCount值,然后将两次的modCount进行比较,如果相同,则表示期间没有发生过写入操作,就将原先遍历的结果返回,如果不相同,则把这个过程再重复做一次,如果再不相同,则就需要将所有的Segment都锁住,然后一个一个遍历了
特点
key和value都不可以为null
get()方法不加锁,是非阻塞的
是线程安全的
JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。
常用方法底层实现(JDK1.7)
ConcurrentSkipListMap 和ConcurrentSkipListSet
ConcurrentLinkedQueue
CopyOnWriteArrayList和CopyOnWriteArraySet
<font color="#000000">线程分类</font>
用户线程
一般是程序中创建的线程
守护线程
为用户服务的线程,当所有用户线程停止时才会被终止,如JVM的垃圾回收
通过Thread.setDaemon(true)方法设置守护线程
0 条评论
下一页