并发编程锁机制!
并发编程锁机制!
月伴飞鱼锁分类
偏向锁/轻量级锁/重量级锁:
偏向锁:
- 一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的。
- 当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来。
- 以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。
轻量级锁:
- 当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁。
- 线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。
重量级锁:
重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。
当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。
重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。
性能对比:
偏向锁性能最好,可以避免执行
CAS
操作。轻量级锁利用自旋和
CAS
避免了重量级锁带来的线程阻塞和唤醒,性能中等。重量级锁则会把获取不到锁的线程阻塞,性能最差。
可重入锁/非可重入锁:
可重入锁:
- 线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。
不可重入锁:
- 虽然线程当前持有了这把锁,但是如果想再次获取这把锁,也必须要先释放锁后才能再次尝试获取。
共享锁/独占锁:
共享锁:
- 同一把锁可以被多个线程同时获得。
独占锁:
- 这把锁只能同时被一个线程获得。
公平锁/非公平锁:
公平锁:
- 如果线程现在拿不到这把锁,那么线程就都会进入等待,开始排队。
- 在等待队列里等待时间长的线程会优先拿到这把锁。
非公平锁:
- 它会在一定情况下,忽略掉已经在排队的线程,发生插队现象。
- 整体执行速度更快,吞吐量更大,但同时也可能产生线程饥饿问题。
悲观锁/乐观锁:
悲观锁:
在获取资源之前,必须先拿到锁,以便达到 独占 的状态。
- 当前线程在操作资源的时候,其他线程由于不能拿到锁。
适合用于并发写入多、临界区代码复杂、竞争激烈等场景。
- 这种场景下悲观锁可以避免大量的无用的反复尝试等消耗。
乐观锁:
它并不要求在获取资源前拿到锁,也不会锁住资源。
乐观锁利用
CAS
理念,在不独占资源的情况下,完成了对资源的修改。适用于大部分是读取,少部分是修改的场景,也适合虽然读写都很多,但是并发并不激烈的场景。
自旋锁/非自旋锁:
自旋锁:
- 如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁。
- 优点是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
非自旋锁:
- 如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。
可中断锁/不可中断锁:
不可中断锁:
- 一旦线程申请了锁,只能等到拿到锁以后才能进行其他的逻辑处理。
可中断锁:
- 在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情,不需要一直等到获取到锁才离开。
锁升级
锁升级细化流程:
锁优化
自适应的自旋锁:
自旋的时间不再固定,而是会根据最近自旋尝试的成功率、失败率
以及当前锁的拥有者的状态等多种因素来共同决定。
如果最近尝试自旋获取某一把锁成功了,那么下一次可能还会继续使用自旋,并且允许自旋更长的时间。
但是如果最近自旋获取某一把锁失败了,那么可能会省略掉自旋的过程,以便减少无用的自旋,提高效率。
锁消除:
1 |
|
如果编译器能确定这个 StringBuffer 对象只会在一个线程内被使用,就代表肯定是线程安全的
那么我们的编译器便会做出优化,把对应的 Synchronized 给消除
- 省去加锁和解锁的操作,以便增加整体的效率。
锁粗化:
1 | public void lockCoarsening() { |
把同步区域扩大,也就是只在最开始加一次锁,并且在最后直接解锁
那么就可以把中间这些无意义的解锁和加锁的过程消除。
锁粗化不适用于循环的场景,仅适用于非循环的场景。
死锁
死锁就是两个或多个线程(或进程)被无限期地阻塞,相互等待对方手中资源的一种状态。
1 | /** |
排查死锁:
通过jdk工具jps、jstack排查死锁问题。
jps:jdk提供的一个工具,可以查看到正在运行的java进程。
jstack:jdk提供的一个工具,可以查看java进程中线程堆栈信息。
堆栈信息中我们可以发现这个内容:Found one Java-level deadlock,表示程序中发现了一个死锁。
通过jdk提供的工具jconsole排查死锁问题。
jconsole:jdk提供的一个可视化的工具,方便排查程序的一些问题,如:程序内存溢出、死锁问题等等。
通过jdk提供的工具VisualVM排查死锁问题。
VisualVM:jdk提供的一个排查java程序问题的一个工具,可以监控程序的性能、查看jvm配置信息、堆快照、线程堆栈信息。
如何避免死锁
正确的顺序获得锁。
- 死锁的根本原因就是获取锁的顺序是乱序的。
超时放弃。
- 当线程获取锁超时了则放弃。