JUC
2022-02-27 12:52:08 59 举报
AI智能生成
JUC,Java并发等方面知识总结,观看黑马JUC视频总结
作者其他创作
大纲/内容
线程的状态
操作系统层面分类
初始状态
线程初始化后等待start
可运行状态
等待时间片的分配
运行状态
获得了CPU的时间片运行中状态
终止状态
线程运行完毕
线程出现异常
阻塞状态
运行了阻塞任务,当阻塞的任务结束了,操作系统将会唤醒他为可运行状态
阻塞状态不会获得时间片分配
Java API层面分类(Thread.State)
NEW
创建了一个Thread()实例,但是没有Start
创建了一个Java中的线程对象 但是未与操作系统中的线程对象关联
RUNNABLE
(包含了操作系统三种线程状态)
(包含了操作系统三种线程状态)
运行状态
可运行状态
阻塞状态
BLOCKED
想获得锁,却没有获得,被阻塞住了
WAITING
不确定时间的等待,比如:join();
TIMED_WAITING
有时间的等待 比如:Thread.sleep(10000);
TERMINATED
对应操作系统层面的终止状态
Java API
start
启动线程
run
如果直接调用Run方法,则不会开启新的线程
sleep
让线程睡眠指定时间,但是不会释放锁
此时的线程状态属于TIMEDWAITING
join
让调用线程执行结束后等待
底层原理其实就是wait
interrupt
当线程处于sleep、wait、join状态
抛出InterruptedException,可以捕获这个线程然后做相应的处理
打断标记为false
当线程处于运行状态
打断标记会被设置为true
需要线程自己根据打断标记进行对应的退出工作,反之则继续运行
把打断的操作交给线程自己本身更加合理相比于stop
打断标记默认为 false
isInterrupted() 获得打断标记
interrupted() 获得打断标记,然后重置打断标记为默认值
也可以打断park状态是线程继续运行
wait
让进入Owner线程到 waitSet中进行等待
wait(1000) 在wait中可以置顶时间参数,单位为毫秒,如果到时间点没有被唤醒,则自动进入EntryList
notify
在waitSet中随机挑选一个线程进行唤醒
notifyAll
唤醒全部在waitSet中等待的线程
由于,没法精确唤醒,但是如果唤醒全部可能就会唤醒其他无关的线程,就会产生一个虚化唤醒的问题
虚假唤醒解决:可以使用 while()来代替if()的判断,如果被虚假唤醒则可以再次判断,再次调用wait()
park
可以堵塞当前线程,使当前线程状态改为WAITING状态
实现原理
每个线程中都有自己的一个Parker对象(C++),由3各部分组成
_counter
0,获得_mutex互斥锁
1,则线程会将_counter设置为0后继续执行
_cond
条件变量
如果_counter为1时,线程将会进入_cond中堵塞
_mutex
互斥锁
如果被interrupt打断 如果下次还需要再次park的话,需要将打断标记重置才能够停下来
unpark
可以精确的唤醒被park堵塞的线程
unpark可以在park之前执行
stop(不建议使用)
杀死某一线程
注意:如果将要被杀死线程锁住了共享资源且没有释放锁,被杀死以后,其他线程将永远无法获取锁
suspend(不建议使用)
挂起线程,暂停运行
和stop一样不会释放锁
resume(不建议使用)
可以将暂停的线程恢复运行
注意事项
由于对象的字段时可以被多个字段共享的,所以在没必要使用成员字段的时候 ,尽量写成局部变量
观察一个对象是否线程安全,主要为查看他的成员变量及对应的方法是否线程安全,查看局部变量是否发生逃逸
CAS(Compare and Swap)
它是一种乐观锁思想的体现
发生在,当从主内存种获取共享变量副本时,对其进行修改后,需要重新写回主内存中时,如何保证回写操纵的这个准确性
执行过程
当拿到共享变量,对其更改后,准备进行回写主内存时,需要进行一个while的判断
如果在写之前,主内存中的值和我原来本地副本修改前的旧值相同,则表示写入操作时安全的,可以进行回写
如果在检查时,发现主内存的值与之前的旧值不一致,则会认为此次回写的操作失败,重新进行判断内存的值,一但成功则进行回写
优缺点
优点
不需要进行一个上锁解锁的操作,提升了效率
缺点
在whlie循环判断的这个过程中,需要占用CPU的资源,如果竞争过于激烈,平凡重复判断,反而会影响效率
必须工作在多核CPU的场景下,才有效
底层实现
CAS底层依赖与一个Unsafe类来直接调用操作系统底层的CAS指令(本身自己就可以保证原子性)
在concurrent包下,基于CAS思想实现的工具类
原子整数
AtomicBoolean
AtomicInteger
AtomicLong
原子引用
AtomicReference
AtomicMarkableReference
内部维护了一个boolean值,在compareAndSet()是还需要加上 这个 boolean值
AtomicStampedReference
解决ABA问题
通过getStamp() 可以获取更新版本号 从可以最终原子变化的过程
在compareAndSet()需要传入原来的版本号 同时为版本号 + 1
原子数组
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
字段更新器
AtomicReferenceFieldUpdater
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
原子累加器
LongAdder
用于累加,它的性能在JIT编译以后会进行优化,性能优于AtomicLong等原子类
性能提升的原因:再有竞争时,设置多个累加单元,多条线程本别在不同的累加单元上进行累加,最后在进行汇总,从而减少CAS比较失败的次数
所以CPU的核数越多,性能的提升越明显
LongAdder中维护了一个Cell[] 就是存放累加单元的数组
Cell类,添加了@sun.misc.Contended注解,防止缓存行伪共享
LongAccumulatar
DoubleAdder
DoubleAccumulatar
unsafe
unsafe 提供了非常底层的、操作内存、线程的方法,unsafe不能直接调用,需要通过放射获得
伪共享
ReentrantLock
特点
可重入
如果一个线程得到了该锁,那么可以重新再次进入该锁
可打断
ReentrantLock lock = new ReentrantLock();
lock.lockInterruptibly //设置 该锁可以被其他线程用 interrupt() 方法打断
lock.lockInterruptibly //设置 该锁可以被其他线程用 interrupt() 方法打断
被打断的意思为 如果 线程在尝试获得到锁时,发现已被其他线程占有,则会进入BLOCK状态,此时就可以被Interrupt() 打断这个BLOCK状态 ,重新回RUNABLE状态,抛出被打断异常
可以设置超时
通过boolean state = lock.tryLock(),去尝试获取锁该方法可以放回一个boolean 表示是否获取到该锁
boolean state = lock.tryLock(1,TimeUnit.SECONDS),设置等待时间,如果超时,则获取锁失败
可设置为公平锁,默认为非公平锁
可以通过构造函数传递 布尔值 设置是否公平
ReentrantLock lock = new ReentrantLock(false);
条件变量
可以理解为:存放着一些需要等待一些条件的线程的集合(容器)
Condition A = lock.newCondition();
为该锁创建一个 需要A资源的线程等待集合
为该锁创建一个 需要A资源的线程等待集合
A.await();将该线程堵塞,进入等待A资源的线程集合
A.signnl();随机唤醒A线程集合中的 某一个线程
A.singnal();唤醒A线程集合中所有线程
注意事项
lock() 和 unlock() 必须成对出现
共享模型
不可变类
可变类可能会存在线程安全的问题
例如:SimpleDateFormat在多线程中运行就会报错
不可变类的设计要求
该类、类中所有属性都是final的
采用保护性拷贝来修改对象,实际上为创建新的对象,并没有修改原有的对象
基本概念
线程
进程是资源分配的最小单位
线程
线程是CPU调度的最小单位
面试题
工作线程数是不是设置的越大越好?
不一定,线程的切换是需要一定的开销的,过多的切换线程不一定又更高的效率(特别时单线程的情况下)
单核CPU设定多线程是否有意义?
有意义,如果此时一个线程需要请求一个网络资源需要很长的等待时间,此时的CPU应该进行线程切换,去做其他事情
volatile
作用
保证线程可见性,防止指令重排
指令重排的概念
同一线程中如果两条语句之间没有联系,CPU未必会按照顺序执行,CPU进行指令重排 必须保证最终一致性
内存屏障
内存屏障时特殊指令,看到这种指令,前面的必须执行完,后面的才能执行
防止指令重排就需要使用到内存屏障
为什么CPU是乱序执行指令?
如果CPU向内存取一个数据时,需要较长的时间CPU则会在这段时间内,去做其他的指令,从而提高CPU的使用效率
JVM中内存屏障
在JVM规范中,规定了必须要实现四个屏障
LoadLoadBarrier
StoreLoadBarrier
StoreStoreBarrier
LoadStoreBarrier
面试题
happens-before
synchronized
也叫做"对象锁",采用的方式让同一时刻至多只有一个线程能持有"对象锁",其他线程还想获取这个对象就会被堵塞住
既能保证可见性,也能保证原子性,但是无法保证指令重排
语法使用
synchrioized(obj){}
对于obj对象,加有synchrioized的代码,同一时间只有一个线程可以对obj对象访问
对普通方法上锁
等价于synchrioized(this){}
对静态方法上锁
等价于synchrioized(obj.class){}
八锁
就是对synchronized各种情况理解
需要锁住同一个对象,才能产生互斥
obj.class 和 obj 不是同一个对象,所以不会尝试互斥
没有加synchronized的方法不会受到锁的影响
对象内存布局(32位虚拟机中)
Java对象头
普通对象(64 bits)
Mark Word(32 bits)
Klass Word(32 bits)
数组对象(96 bits)
Mark Word(32 bits)
Klass Word(32 bits)
array length(32 bits)
Mark Word(32 bits)
在运行期间,Mark Word里存储的数据会随着锁标志的变化而变化
正常状态
hashcode(25 bits)
是懒加载机制,默认为都是0,只有当主动获取hashcode后才会存储hashcode
age(4 bits)分代年龄
是否偏量级锁(1 bits)
默认:0
1 表示开启了偏量锁
在程序启动时,偏量锁不会马上生效,需要等待几秒钟
需要取消这个 启动延迟
xx:BiasedLockingStartupDelay=0
禁用偏量锁,禁用以后默认上锁直接上的锁为轻量级锁
-XX:-UseBiasedLocking
偏向锁被撤销的情况
由于,Mark Word空间限制的原因,当获取hashcode后,默认会取消偏向锁
如果有2个以上(包括两个)的线程同时竞争的情况下,偏向锁就会被撤销,升级为轻量级锁
批量重偏向
如果对象即使被多个线程访问,但没有竞争,该Class new 出来的对象,撤销偏向锁阈值超过20次后,Jvm就会修改偏向对象给新的线程
如果超过 40次被撤销了,那么该Class new 出来的对象,都无法添加偏向锁
如果超过 40次被撤销了,那么该Class new 出来的对象,都无法添加偏向锁
偏向锁初始化流程
解决的问题
锁重入还需要CAS的问题
锁标志(2 bits)
默认:01,表示无锁状态
00 表示轻量级锁的状态
10 表示重量级锁
11 GC标志
实例数据
对其填充
Monitor
被翻译为:"监视器",操作系统也叫“管程”
原理
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步
需要竞争同一个锁的线程会查看,Mark Word中是否以进记录了Monitor的地址,如果有则代表该对象已上锁
组成
WaitSet
Owner中的线程发现条件不满足,调用wait方法,就会放弃锁 ,既可进入WaitSet,该区域中线程都属于 WAITING 状态
当Owner线程调用notify 或 notifyAll时唤醒,但唤醒后需要重新回到EntryList中,与其他线程重新竞争锁
EntryList
如果当一个线程进入,发现Owner以进记录了别的线程,则该线程会进入EntryList进入 BLOCK 状态,等待
Owner
如果是一个新的线程,则会被记录到Owner中
当Owner中的线程使用完毕后,会唤醒EntryList中的所有线程,发生再次竞争
锁升级过程
锁状态
无所状态
偏向锁状态
作用,是为了解决轻量级锁在,重入时还需要进行CAS的优化
轻量级锁状态
不马上启用monitor,先使用线程栈中的锁记录,来记录加锁的过程
重量级锁状态
使用monitor来进行记录加锁的过程,以及锁竞争的队列
锁种类
死锁
多个线程各自持着对方所需要的资源,使每个线程都无法满足所需的资源,从而都陷入的堵塞状态
活锁
多个线程互相改变对方的资源,导致你改变,接着我又改变,相互都一直重复着运行
Java 内存模型 (JMM)
定义了主存、工作内存的抽象概念
几个方面的体现
原子性
可见性
有序性
并发工具
线程池
线程池状态
RUNING
高3位:111
可接受新任务
Y
处理阻塞任务队列
Y
线程被初始化出来的默认状态
SHUTDOWN
高3位:000
可接受新任务
N
处理阻塞任务队列
Y
调用Shutdown方法,不会处理新任务,但是会处理堵塞队列里的任务
STOP
高3位:001
可接受新任务
N
处理阻塞任务队列
N
调用ShutdownNow方法。会中断正在执行的任务,并抛弃阻塞队列里的任务
TIDYING
高3位:010
任务全部执行完毕,活动线程位0即将进入终结状态
TERMINATED
高3位:011
终结状态
ThreadPoolExecutor使用 int 高3位来表示线程池状态,低29位表示线程池数量
线程数目多少合适
CPU密集型
通常采用 CPU核数 + 1这样能够实现最优的CPU利用率,
+1 是保证当线程由于页缺失障碍(操作系统)或其他原因导致暂停时,额外的线程就可以顶上去,保证CPU时钟周期不被浪费。
+1 是保证当线程由于页缺失障碍(操作系统)或其他原因导致暂停时,额外的线程就可以顶上去,保证CPU时钟周期不被浪费。
I/O密集型
线程数 = 核数 * 期望CPU利用率 * 总时间(CPU 时间 + 等待时间) / CPU计算时间
timer
是一个串行的可以延迟执行的线程
在阿里巴巴开发手册红不推荐使用
TimerTask task = new TimerTask();
timer.shedule(task,1000)
timer.shedule(task,1000)
timer 可以接收多个task,按照添加的顺序,进行顺序执行,前面的执行时间会影响到后面任务的执行
Fork/Join
体现的是一种分治的思想,先把任务拆分,给多个线程执行,然后最后再合并
拆分的工作需要自己来写,拆分的方式会决定着,并行的效率,在JDK8以后Stream流的方式封装了拆分的工作。让使用更加方便
线程创建方式
线程池所有线程的操作底层都是调用了ThreadPoolExecutor()方法
7个参数
int corePoolSize
核心线程数
不会被回收
int maximumPoolSize
最大线程数
如果任务数低于核心线程数,则除核心线程数以外都会被回收
long keepAliveTime
线程存活时间
如果一个线程空闲时间超过 keepAliveTime 且总线程数大于核心线程数,则会被回收
TimeUnit unit
线程存活时间,单位
BlockingQueue<Runnable> workQueue
工作队列
ThreadFactory threadFactory
设置创建线程的方式,例如给线程一个有意义的名字
RejectedExecutionHandler handler
拒绝策略(4种)
在工作队列已满的情况下,继续添加任务的处理方式
在工作队列已满的情况下,继续添加任务的处理方式
AbortPolicy(默认拒绝策略)
抛出异常
CallerRunsPolicy
提交任务的线程自己去执行该任务
DiscardOldestPolicy
丢弃最老的任务,也就是把最早加入工作队列的任务丢弃,然后把新任务加入到工作队列中
DiscardPolicy
直接丢弃任务,无任何异常抛出
ScheduledThreadPool
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.schedule(()-> System.out.println("任务1"),2, TimeUnit.SECONDS);
pool.schedule(()-> System.out.println("任务1"),2, TimeUnit.SECONDS);
延迟执行,但是会创建多个线程,后面的线程不会因为前面的任务时间而受到影响
pool.scheduleAtFixedRate() 可以定时完成任务
该方式,间隔时间是包含运行的时间的,如果执行花费两秒,循环周期为1秒,则会在两秒后执行第一个任务后,马上执行下一个任务
pool.scheduleAtFixedRate() 设置相同的间隔时间完成任务
该方式,间隔时间时不包含运行的时间的,如果执行花费2秒,间隔时间为1秒,那下个任务将会在3秒以后执行
由于线程池创建方法ThreadPoolExecutor()参数过于多,过于复杂。所以jdk 提供了 Executors的默认快速创建方式。
不过在目前大厂的编码规范中基本上都不建议使用Executors提供的方法。
不过在目前大厂的编码规范中基本上都不建议使用Executors提供的方法。
原因:Executors提供的很多方法都是使用无界的LinkedBlockingQueue
而在高并发的情况下,无界队列很容易导致OOM,导致所有请求无法处理
而在高并发的情况下,无界队列很容易导致OOM,导致所有请求无法处理
执行线程
使用submit() 方法提交任务
主线程时无法接收到,子线程抛出的异常的
解决方式
在线程内部就进行捕获(try cath)处理
或则使用Futrue 接收放回内容,如果可以返回成功则为正确执行,反之则出现异常
有返回值(Future<T>)
参数类型
Callable
Runnable
使用Execute() 提交任务
子线程报错后,会停止线程运行
无返回值
参数类型
Runnable
aqs
全称 (AbstractQueuedSynchronizer),是阻塞式锁,和相关的同步工具框架
AbstractQueuedSynchronizer
重要的属性
内部类 Node
static final Node SHARED = new Node();
共享式中等待节点
static final Node EXCLUSIVE = null;
独占模式等待节点
线程等待状态 waitStatus
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
private transient volatile Node head;
头节点
private transient volatile Node tail;
尾节点
private volatile int state;
0:没有上锁
1:该锁已经被线程占有
>1:重入锁,进入锁的次数
重要的方法
protected final int getState() { return state; }
获得State
protected final void setState(int newState) { state = newState; }
设置State的值
protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
CAS 的方式设置State值
执行步骤
先判断是否该 Lock 是否有被占有,如果没有则,进行CAS进行 State 然后将 该锁的线程持有者 设置为当前线程
如果,该锁已经有所有者,判断是否是自己,如果是自己则且是重入锁,则增加 state 数值,进入锁
如果不是自己的锁,则进入等待队列
实现同步器的两种方式
独占的
tryAcquite
尝试获取锁
如果获取失败-可以设置进入同步队列
tryRelease
尝试释放锁
共享的
tryAcquiteShared
tryReleaseShared
实现了aqs的锁
ReentrantLock
Semaphore
CountDownLatch
用来进行线程同步协作,等待所有线程完成倒计时
构造方法传递初始值
await() 用来等待计数归零
countDown() 用来让计数减一
Cyclicbarrier
又叫做循环栅栏,作用和CountDownLatch类似,但是 CountDownLatch 只能使用一次,也就是一旦初始化的值倒计时完,便无法再次使用,所以 如果需要多词使用,就应该使用 Cyclicbarrier
当 count 值递减到0时,会自动回复为初始值
注意:有时候Cyclicbarrier的初始值应该与线程池的线程数对应
ReentrantReadWriteLock
实现ReadWriteLock
基于 Lock 实现
所以也支持 Lock 中的一些操作
基本原则
允许多个线程同时读共享变量
只允许一个线程写共享变量
如果一个写线程正在执行写操作,此时禁止其他读线程读取共享变量
与传统互斥锁的区别
传统互斥锁的读和读之间也是互斥的,如果在读多写少的情况下性能相对来所就比较低了
ReadWriteLock可以允许多个读操作同时进行
可重入
支持公平与非公平模式
不支持锁升级,但是支持锁降级
无法在读锁中获得写锁
但是可以在写锁中获取读锁
是否只是条件变量
写锁支持
读锁不支持
会抛出 UnsupportedOperationException 异常
StampedLock
JDK 8 加入的,是为了进一步优化读性能
增加读锁(悲观读锁)
StampedLock lock = new StampedLock();
long stamp = lock.readLock();
try {
//省略业相关代码
}finally {
lock.unlockRead(stamp);
}
long stamp = lock.readLock();
try {
//省略业相关代码
}finally {
lock.unlockRead(stamp);
}
添加写锁
StampedLock lock = new StampedLock();
long stamp = lock.writeLock();
try {
//省略业相关代码
}finally {
ock.unlockWrite(stamp);
}
long stamp = lock.writeLock();
try {
//省略业相关代码
}finally {
ock.unlockWrite(stamp);
}
乐观读的机制(无锁)
tryOptimisticRead()方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,则需要重新获取读锁,保证数据安全。
// 获取乐观读
long stamp = lock.tryOptimisticRead();
// 验戳
if (lock.validate(stamp)) {
//锁升级,获取一个悲观读锁
}
long stamp = lock.tryOptimisticRead();
// 验戳
if (lock.validate(stamp)) {
//锁升级,获取一个悲观读锁
}
支持锁升级降级
tryConvertToReadLock() 将锁降级悲观读锁
tryConvertToWriteLock() 将锁升级为写锁
tryConvertToOptimisticRead() 将锁降级为乐观读
注意:StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用中断悲观读锁
readLockInterruptibly()
writeLockInterruptibly()
缺点
无论写锁,读锁都不支持条件变量
不支持可重入
并发容器
传统容器
List
Map
Set
缺点
线程不安全
同步容器
是指使用synchronized实现的容器
常见同步容器
Vector
Stack
Hashtable
缺点
所有方法都使用了synchronized实现,串行度太高,性能太差
并发容器
为了弥补同步容器的缺点,JDK1.5 以后提供了性能更好的容器,并发容器
常见并发容器
List
CopyOnWriteArrayList
CopyOnWrite,顾名思义就是写的时候会将共享变量新复制一份出来,这样做的好处是读写操作完全无锁
原理
CopyOnWriteArrayList 内部维护了一个数组,成员变量 array,所有读操作都是基于 array 进行的
当在读的遍历的同时,还有写的操作。那么 CopyOnWriteArrayList 会将 array 复制一份,然后再新数组上进行进行增加操作,操作结束后将这个新数组,指向 array
缺点
会存在短暂读写不一致的问题,再写入新元素时,无法马上被遍历到
CopyOnWriteArrayList 的迭代器是只读的,不支持增删改的,因为迭代器遍历仅仅是一个快照,对快照进行增删改是没有意义的
Map
ConcurrentHashMap
底层实现
数据结构同理与hasmap
数据少
数组+链表
数据多(扩容到一定程度)
数组+红黑树
ConcurrentSkipListMap
底层实现
跳表
Set
ConcurrentSkipListSet
底层实现
跳表
CopyOnWriteArraySet
底层实现
同理于CopyOnWriteArrayList
Queue
阻塞队列
阻塞:是指队列已满,入队操作阻塞。队列已空,出队操作堵塞
BlockingDeque(双端阻塞队列)
LinkedBlockingDeque
BlockingQueue(单端阻塞队列)
LinkedBlockingQueue
链表实现
SynchronousQueue
内部没有实现队列,生产者线程需要等待消费者线程的出队
LinkedTransferQueue
融合了 LinkedBlockingQueue 以及 SynchronousQueue,且性能上更优于 LinkedBlockingQueue
PriortyBlockingQueue
优先阻塞队列
按照优先级出队
DelayQueue
可以延迟出队
ArrayBlockingQueue
数组实现
非阻塞队列
ConcurrentLinkedQueue(单端非阻塞队列)
链表实现
ConcurrentLinkedDeque(双端非阻塞队列)
链表实现
0 条评论
下一页