锁的活跃性(死锁、活锁、饥饿)
死锁
锁是个非常有用的工具,运用场景非常多,它使用起来非常简单,而且易于理解。但
同时它也会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可
用。
定义
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。
死锁的产生的一些特定条件:
1、互斥条件:即一个资源只能被一个线程占用,直到被该线程释放 。
2、请求和保持条件:一个线程因请求资源而发生阻塞时,对已获得的资源保持不放。
3、不剥夺条件:一个资源被一个线程占用时,其他线程都无法对这个资源剥夺占用。
4、循环等待条件:当发生死锁时,所等待的线程会形成一个环路(类似于死循环),造成永久阻塞。
造成死锁必须达到4个条件,避免死锁,只需不满足其中一个条件即可,前三个都是作为锁要符合的条件,所以要避免死锁就要打破第四个条件
产生死锁的主要原因
系统资源不足
进程运行推进的顺序不合适
资源分配不当
package com.dongguo.sync;
import jdk.internal.dynalink.beans.StaticClass;
import java.util.concurrent.TimeUnit;
/** * @author Dongguo * @date 2021/8/24 0024-14:25 * @description: 模拟死锁 */
public class DeadLock {
static Object obj1 = new Object();
static Object obj2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj1) {
System.out.println(Thread.currentThread().getName() + "持有锁1,试图获取锁2");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
System.out.println(Thread.currentThread().getName() + "持有锁1,获取锁2成功");
}
}
}, "ThreadA").start();
new Thread(() -> {
synchronized (obj2) {
System.out.println(Thread.currentThread().getName() + "持有锁2,试图获取锁1");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1) {
System.out.println(Thread.currentThread().getName() + "持有锁2,获取锁1成功");
}
}
}, "ThreadB").start();
}
}
运行结果:
这段代码会引起死锁,使线程ThreadA和线程ThreadAB互相等待对方释放锁。
这段代码只是演示死锁的场景,在现实中你可能不会写出这样的代码。但是,在一些更为
复杂的场景中,你可能会遇到这样的问题,比如ThreadA拿到锁之后,因为一些异常情况没有释放锁
(死循环)。又或者是ThreadA拿到一个数据库锁,释放锁的时候抛出了异常,没释放掉 。那么ThreadAB
获取锁时会等待ThreadA去释放锁,这样就造成了死锁。
死锁验证
一旦出现死锁,业务是可感知的,因为不能继续提供服务了,那么只能通过dump线程查看
到底是哪个线程出现了问题
使用java命令行jstack方式
C:\Users\Administrator>jps
4288 RemoteMavenServer36
10968 Jps
17352 Launcher
13212 DeadLock
13436
C:\Users\Administrator>jstack 13212
2021-08-24 14:32:51
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.301-b09 mixed mode):
"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x000002b418b40000 nid=0x3fa0 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"ThreadB" #12 prio=5 os_prio=0 tid=0x000002b42dc62800 nid=0x2ea4 waiting for monitor entry [0x000000c4c43ff000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.dongguo.sync.DeadLock.lambda$main$1(DeadLock.java:39)
- waiting to lock <0x00000000eb4b5288> (a java.lang.Object)
- locked <0x00000000eb4b5298> (a java.lang.Object)
at com.dongguo.sync.DeadLock$$Lambda$2/1480010240.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"ThreadA" #11 prio=5 os_prio=0 tid=0x000002b42dc1a800 nid=0x2a00 waiting for monitor entry [0x000000c4c42ff000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.dongguo.sync.DeadLock.lambda$main$0(DeadLock.java:26)
- waiting to lock <0x00000000eb4b5298> (a java.lang.Object)
- locked <0x00000000eb4b5288> (a java.lang.Object)
at com.dongguo.sync.DeadLock$$Lambda$1/2074407503.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"Service Thread" #10 daemon prio=9 os_prio=0 tid=0x000002b42d921800 nid=0x43c0 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000002b42ce5f800 nid=0xc0 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000002b42ce5e000 nid=0xc8c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000002b42ce5c800 nid=0x2830 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000002b42ce59000 nid=0x41e4 runnable [0x000000c4c3cfe000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x00000000eb492430> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x00000000eb492430> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:48)
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000002b42cd7e800 nid=0x3e44 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000002b42cdd5000 nid=0xde0 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000002b42cd52800 nid=0x3004 in Object.wait() [0x000000c4c39fe000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000eb308ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x00000000eb308ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000002b42cd4b800 nid=0x884 in Object.wait() [0x000000c4c38ff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000eb306c00> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x00000000eb306c00> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os_prio=2 tid=0x000002b42cd21800 nid=0x324 runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x000002b418b55000 nid=0xd9c runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x000002b418b56800 nid=0x4034 runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x000002b418b58000 nid=0x431c runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x000002b418b59000 nid=0x3b54 runnable
"VM Periodic Task Thread" os_prio=2 tid=0x000002b42d925000 nid=0x4108 waiting on condition
JNI global references: 317
Found one Java-level deadlock:
=============================
"ThreadB":
waiting to lock monitor 0x000002b42cd4fa18 (object 0x00000000eb4b5288, a java.lang.Object),
which is held by "ThreadA"
"ThreadA":
waiting to lock monitor 0x000002b42cd522a8 (object 0x00000000eb4b5298, a java.lang.Object),
which is held by "ThreadB"
Java stack information for the threads listed above:
===================================================
"ThreadB":
at com.dongguo.sync.DeadLock.lambda$main$1(DeadLock.java:39)
- waiting to lock <0x00000000eb4b5288> (a java.lang.Object)
- locked <0x00000000eb4b5298> (a java.lang.Object)
at com.dongguo.sync.DeadLock$$Lambda$2/1480010240.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"ThreadA":
at com.dongguo.sync.DeadLock.lambda$main$0(DeadLock.java:26)
- waiting to lock <0x00000000eb4b5298> (a java.lang.Object)
- locked <0x00000000eb4b5288> (a java.lang.Object)
at com.dongguo.sync.DeadLock$$Lambda$1/2074407503.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock. C:\Users\Administrator>
着重看最后Java stack information for the threads listed above:
ThreadB waiting to lock <0x00000000eb4b5288> locked <0x00000000eb4b5298>
ThreadA waiting to lock <0x00000000eb4b5298> locked <0x00000000eb4b5288>
Found 1 deadlock.
ThreadB 等待锁5288 ,持有锁5298
ThreadA 等待锁5298 ,持有锁5288
Found 1 deadlock.
如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到
CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查
当然也可以使用可视化工具,比如jvisualvm
显示的内容是一样的
Jconsole
如何避免死锁:
1、加锁顺序: 注意加锁顺序,保证每个线程按照同样的顺序进行加锁
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。当然这种方式需要你事先知道所有可能会用到的锁,然而总有些时候是无法预知的。
2、加锁时限: 针对设置一个超时时间
加上一个超时时间,若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。但是如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。
3、死锁检测:预防机制,确保第一时间发现死锁进行解决
死锁检测即每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。
避免死锁的几个常见方法。
·避免一个线程同时获取多个锁。
·避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
·尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
·对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,
例如
package com.dongguo.lock;
import java.util.concurrent.TimeUnit;
/** * @author Dongguo * @date 2021/9/12 0012-14:15 * @description: */
public class LiveLockDemo {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+" count="+count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
System.out.println(Thread.currentThread().getName()+" count="+count);
}
}, "t2").start();
}
}
count初始为10
t1线程执行count–操作,期望count值为0时继续往下执行
t2线程 执行count++操作,期望count值为20时继续往下执行
t1不断执行count–操作,t2不断执行coun++操作,导致两个线程一直无法满足条件。
饥饿
多个线程访问同一个同步资源,有些线程总是没有机会得到互斥锁,这种就叫做饥饿。
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束
出现饥饿的三种情况
a,高优先级的线程吞噬了低优先级的线程的CPU时间片
理论上来说,线程优先级高的线程会比线程优先级低的线程获得更多的执行机会,
经过测试,优先级高的出现频率会比优先级低的高很多
不同的操作系统对线程的优先级支持是不同的,规定是在1-10之间,java通过3个常量来屏蔽这种操作系统的底层差异化。
b,线程被永久阻塞在等待进入同步代码块的状态
c,等待的线程永远不被唤醒
举例
比如synchronized是非公平锁、ReentrantLock默认是非公平锁,就可能导致一个或多个线程将资源全部抢占,导致某些线程无法获得运行的机会。
ReentrantLock非公平锁实现3个售票员卖出100张票的案例
package com.dongguo.concurrent.synchronize;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/** * @author Dongguo * @date 2021/9/3 0003-10:14 * @description: 实现3个售票员卖出100张票的案例 * 使用Lock */
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
//循环100次保证能够卖光票
for (int i = 0; i < 100; i++) {
ticket.saleTicket();
}
}, "T1").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
ticket.saleTicket();
}
}, "T2").start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
ticket.saleTicket();
}
}, "T3").start();
}
}
/** * @author Dongguo * @description: 资源类 */
class Ticket {
private int count = 100;
//创建可重入锁ReentrantLock
private Lock lock = new ReentrantLock();//默认为false
public void saleTicket() {
//上锁
lock.lock();
try {
if (count > 0) {
count--;
System.out.println(Thread.currentThread().getName() + "卖票成功,还剩" + count + "张票!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//解锁
lock.unlock();
}
}
}
运行结果:
T1卖票成功,还剩99张票!
T1卖票成功,还剩98张票!
T1卖票成功,还剩97张票!
T1卖票成功,还剩96张票!
T1卖票成功,还剩95张票!
T1卖票成功,还剩94张票!
T1卖票成功,还剩93张票!
T1卖票成功,还剩92张票!
T1卖票成功,还剩91张票!
T1卖票成功,还剩90张票!
T1卖票成功,还剩89张票!
T1卖票成功,还剩88张票!
T1卖票成功,还剩87张票!
T1卖票成功,还剩86张票!
T1卖票成功,还剩85张票!
T1卖票成功,还剩84张票!
T1卖票成功,还剩83张票!
T1卖票成功,还剩82张票!
T1卖票成功,还剩81张票!
T1卖票成功,还剩80张票!
T1卖票成功,还剩79张票!
T1卖票成功,还剩78张票!
T1卖票成功,还剩77张票!
T1卖票成功,还剩76张票!
T1卖票成功,还剩75张票!
T1卖票成功,还剩74张票!
T1卖票成功,还剩73张票!
T1卖票成功,还剩72张票!
T1卖票成功,还剩71张票!
T1卖票成功,还剩70张票!
T1卖票成功,还剩69张票!
T1卖票成功,还剩68张票!
T1卖票成功,还剩67张票!
T1卖票成功,还剩66张票!
T1卖票成功,还剩65张票!
T1卖票成功,还剩64张票!
T1卖票成功,还剩63张票!
T1卖票成功,还剩62张票!
T1卖票成功,还剩61张票!
T1卖票成功,还剩60张票!
T1卖票成功,还剩59张票!
T1卖票成功,还剩58张票!
T1卖票成功,还剩57张票!
T1卖票成功,还剩56张票!
T1卖票成功,还剩55张票!
T1卖票成功,还剩54张票!
T1卖票成功,还剩53张票!
T1卖票成功,还剩52张票!
T1卖票成功,还剩51张票!
T1卖票成功,还剩50张票!
T1卖票成功,还剩49张票!
T1卖票成功,还剩48张票!
T1卖票成功,还剩47张票!
T1卖票成功,还剩46张票!
T1卖票成功,还剩45张票!
T1卖票成功,还剩44张票!
T1卖票成功,还剩43张票!
T1卖票成功,还剩42张票!
T1卖票成功,还剩41张票!
T1卖票成功,还剩40张票!
T1卖票成功,还剩39张票!
T1卖票成功,还剩38张票!
T1卖票成功,还剩37张票!
T1卖票成功,还剩36张票!
T1卖票成功,还剩35张票!
T1卖票成功,还剩34张票!
T1卖票成功,还剩33张票!
T1卖票成功,还剩32张票!
T1卖票成功,还剩31张票!
T1卖票成功,还剩30张票!
T1卖票成功,还剩29张票!
T1卖票成功,还剩28张票!
T2卖票成功,还剩27张票!
T2卖票成功,还剩26张票!
T2卖票成功,还剩25张票!
T2卖票成功,还剩24张票!
T2卖票成功,还剩23张票!
T2卖票成功,还剩22张票!
T2卖票成功,还剩21张票!
T2卖票成功,还剩20张票!
T2卖票成功,还剩19张票!
T2卖票成功,还剩18张票!
T2卖票成功,还剩17张票!
T2卖票成功,还剩16张票!
T2卖票成功,还剩15张票!
T2卖票成功,还剩14张票!
T2卖票成功,还剩13张票!
T2卖票成功,还剩12张票!
T2卖票成功,还剩11张票!
T2卖票成功,还剩10张票!
T2卖票成功,还剩9张票!
T2卖票成功,还剩8张票!
T2卖票成功,还剩7张票!
T2卖票成功,还剩6张票!
T2卖票成功,还剩5张票!
T2卖票成功,还剩4张票!
T2卖票成功,还剩3张票!
T2卖票成功,还剩2张票!
T2卖票成功,还剩1张票!
T2卖票成功,还剩0张票!
所有的票都被线程t1、t2卖掉了,t3线程没有运行。
比如ReentrantReadWriteLock中
一旦读操作比较多的时候,想要获取写锁就变得比较困难了,
假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了
因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写
如何避免饥饿问题
a,设置合理的优先级
b,使用公平锁来代替synchronized这种互斥锁(可以使用邮戳锁StampedLock替代ReentrantReadWriteLock)
还没有评论,来说两句吧...