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