我正在研究 java 线程和死锁,我了解死锁的例子,但我想知道是否有一般规则可以遵循来防止它。
我的问题是是否有规则或提示可以应用于java中的源代码以防止死锁?如果是,您能解释一下如何实施吗?
我正在研究 java 线程和死锁,我了解死锁的例子,但我想知道是否有一般规则可以遵循来防止它。
我的问题是是否有规则或提示可以应用于java中的源代码以防止死锁?如果是,您能解释一下如何实施吗?
我脑海中的一些快速提示
封装,封装,封装!使用锁可能犯的最危险的错误是将锁暴露给世界(公开)。不知道如果你这样做会发生什么,因为任何人都可以在对象不知道的情况下获得锁(这也是你不应该 lock 的原因this
)。如果您将锁保密,那么您将拥有完全的控制权,这使得它更易于管理。
ConcurrentLinkedQueue
而不是 synchronized ArrayList
)阅读并理解Java:并发与实践。 这不是关于避免死锁的“技巧”。我永远不会聘请知道一些避免死锁的技巧并且经常避免死锁的开发人员。这是关于理解并发性。幸运的是,有一本关于该主题的全面的中级书籍,所以去阅读吧。
给定一个设计选择,在队列推送/弹出中只有锁的情况下使用消息传递。这并不总是可能的,但如果是这样,您将很少遇到死锁。你仍然可以得到它们,但你必须非常努力:)
在防止死锁方面,几乎只有一条大规则:
如果您需要在代码中使用多个锁,请确保每个人始终以相同的顺序获取它们。
不过,让您的代码不受锁定应该始终是您的目标。您可以尝试通过使用不可变或线程本地对象和无锁数据结构来摆脱它们。
我喜欢这个例子。它启动两个共享布尔标志的线程:
public class UntilYouUpdateIt
{
public static boolean flag = true;
public static void main(String[] args) throws InterruptedException
{
Thread t1 = new Thread(()->
{
while(flag){}
System.out.println("end");
});
t1.start();
Thread.sleep(100);
Thread t2 = new Thread(()->
{
flag = false;
System.out.println("changed");
});
t2.start();
}
}
第一个线程将循环直到flag
为假,这发生在第二个线程的第一行。该程序永远不会完成,它的输出将是:
changed
第二个线程死了,同时第一个线程将永远循环。
为什么会发生?Compiler opmitizations
. Thread1 将永远不会再次检查标志的值,如:
换句话说,Thread1 将始终从设置为flag
的 中读取值。cache
true
解决/测试此问题的两种方法:
Thread t1 = new Thread(()->
{
while(flag)
{
System.out.print("I'm loopinnnng");
}
System.out.println("end");
});
如果包含一些“繁重”操作(int i=1
或者类似的操作都不起作用),例如System
调用,优化器会更加小心,检查flag
布尔值以确定他是否没有浪费资源。输出将是:
I'm loopinnnng
I'm loopinnnng
I'm loopinnnng
I'm loopinnnng
I'm loopinnnng
(....)
changed
end
或者
I'm loopinnnng
I'm loopinnnng
I'm loopinnnng
I'm loopinnnng
I'm loopinnnng
(....)
end
changed
取决于哪个线程最后分配了 cpu 时间。
在使用布尔变量时,避免此类死锁的正确解决方案应该包括volatile
关键字。
volatile
告诉编译器:当涉及到这个变量时不要尝试优化。
因此,仅添加了该关键字的相同代码:
public class UntilYouUpdateIt
{
public static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException
{
Thread t1 = new Thread(()->
{
while(flag){}
System.out.println("end");
});
t1.start();
Thread.sleep(100);
Thread t2 = new Thread(()->
{
flag = false;
System.out.println("changed");
});
t2.start();
}
}
将输出:
changed
end
或者
end
changed
结果是两个线程都正确完成,避免了任何死锁。
这是一个基本的:
public void methodA()
{
//...
synchronized(lockA)
{
//...
synchronized(lockB)
{
//...
}
}
}
public void methodB()
{
//...
synchronized(lockB)
{
//...
synchronized(lockA)
{
//...
}
}
}
如果被许多线程调用,这种方法可能会造成很大的死锁。这是因为对象以不同的顺序锁定。这是死锁最常见的原因之一,因此如果要避免死锁,请确保按顺序获取锁。
Java中的死锁是一种编程情况,其中两个或多个线程被永远阻塞。至少有两个线程和两个或更多资源会出现 Java 死锁情况。
要检测 Java 中的死锁,我们需要查看应用程序的java 线程转储,我们可以使用VisualVM分析器或使用jstack实用程序生成线程转储。
为了分析死锁,我们需要注意状态为BLOCKED的线程,然后是它等待锁定的资源。每个资源都有一个唯一的 ID,使用它我们可以找到哪个线程已经持有对象的锁。
这些是我们可以避免大多数死锁情况的一些指导方针。
synchronized
块或Lock。synchronized
代码块,请确保以特定顺序获取/释放锁。Lock
API相关的 SE 问题:
避免嵌套锁。这是死锁最常见的原因。如果您已经持有另一个资源,请避免锁定另一个资源。如果您只使用一个对象锁,几乎不可能出现死锁。
仅锁定需要的内容。如果它符合您的目的,就像锁定对象的特定字段而不是锁定整个对象。
不要无限期地等待。
不幸的是,存在跨多个线程的循环数据流(A->B->C->A)的数据模型。上面说的都不管用。从可能有帮助的角度来看,线程之间的数据传输方法可以使用无锁缓冲输入,如 java 并发集合。数据块本身被实现为无状态的不可变对象。GUI 系统很可能使用这种方法。这种方式似乎不需要“lockfull”等待通知方案,但是线程并发控制仍然存在,但它隐藏在集合中,并且这种方式是本地的。这个问题真的很难避免。根源源于数学模型假设的零延迟。但在实际系统中,每个动作都需要时间,因此会产生延迟并最终导致死锁。