DeepCode为Java、JavaScript和TypeScript以及Python提供了基于AI的静态程序分析。如您所知,DeepCode使用数千个开源repos来训练我们的引擎。我们要求引擎团队提供一些调查结果的统计数据。在我们的引擎的顶部建议中,我们想在本系列的博客文章中介绍和给出一些背景知识。
语言: Java
缺陷:不要使用字符串进行同步。
诊断:不要使用字符串进行同步,这可能导致不必要的死锁和竞争条件。具有相同值的字符串在JVM中可能表示为同一对象。
我在JDK 14代码中找到了这个。我将部分内容复制到了此仓库中,网址为https://github.com/CU-0xff/jdk14_parts,以使事情易于管理。因此,如果您想继续,请在DeepCode.ai中打开该存储库。这是一个非常令人讨厌的问题,因为它迫使您了解JVM的一些实现细节,以理解这个问题。以下是感兴趣的代码:
class WindowsPath implements Path {
...
// normalized path
private final String path;
...
// cache the resolved path (except drive relative paths as the working
// directory on removal media devices can change during the lifetime
// of the VM)
if (type != WindowsPathType.DRIVE_RELATIVE) {
synchronized (path) {
pathForWin32Calls = new WeakReference<String>(resolved);
}
}
...
背景:
已同步
让我们首先介绍synchronized()的概念。在多线程环境中,我们需要确保正确地管理对变量的访问。Java提供同步的函数或块来管理并发访问。下面是一个简单的例子:
class ListOfNumbers {
int number;
void printListOfNumbers(final int n) {
this.number = 0;
for (int i = 1; i <= 10; i++) {
try {
// Simulate some substantial work
Thread.sleep(400);
} catch (final Exception e) {
System.out.println(e);
}
this.number += n;
System.out.print(String.format("%d ",this.number));
}
}
}
class MyThread extends Thread {
ListOfNumbers l;
int flag;
MyThread(final ListOfNumbers l, int flag) {
this.l = l;
this.flag = flag;
}
public void run() {
l.printListOfNumbers(flag);
}
}
class TestSynchronization1 {
public static void main(final String args[]) {
final ListOfNumbers obj = new ListOfNumbers();// only one object
final MyThread t1 = new MyThread(obj, 1);
final MyThread t2 = new MyThread(obj, 7);
t1.start();
t2.start();
}
}
在这里,我们有一个class ListOfNumbers
,它存储一个整数并通过循环和添加来打印一系列整数。现在,我们有两个线程正在处理的一个对象实例,ListOfNumbers
并且它按您期望的那样发生。我们想在控制台上看到1 2 3 4 5 6 7 8 9 10 7 14 21 28 35 42 49 56 63 70
,但是我们得到8 8 9 16 24 24 25 32 39 39 46 47 55 55 62 62 69 69 76 77
,然后1 1 9 9 17 10 24 25 32 32 40 40 48 48 55 56 57 57 65 65
,然后...
这告诉我们两件事:(1)显然,两个线程指向同一对象时彼此会覆盖。(2)结果是不可预测的,因为它取决于线程调用方式的顺序。
通过简单的更改,我们可以对问题进行排序:
class MyThread extends Thread {
...
public void run() {
synchronized(l) {
l.printListOfNumbers(flag);
}
}
...
这样一来,线程会在通过调用方法更改实例状态之前要求对实例进行控制。 到现在为止还挺好。
这样一来,线程在通过调用方法更改其状态之前要求对实例进行控制。到目前为止,一切都很好。
为什么不使用synchronized()
字符串?
在Java中Strings
是不可变的;它们无法更改。如果覆盖现有实例,则会生成一个新实例。语言设计者决定这样做的原因有很多,其中有一个Strings
是线程安全的(因为它们不会更改,因此多个线程可以同时使用它们)。但是有些东西叫做String Pool。在后台,JVM优化了数据使用。如果程序要求创建一个字符串,并且池中已经存在一个具有相同内容的字符串,则JVM将返回现有实例,而不是创建一个新实例。
现在,更大的问题是如何在synchronized中使用字符串。您可能会锁定应用程序,因为两个字符串具有相同的内容(也就是指向相同的对象),但是在代码中,它们是不同的变量。祝您调试成功。顺便说一下,请参阅deepcode在这方面提供的解释。我会说,完全正确。在上面的例子中,path变量是一个字符串,它不能保证是唯一的。
如果使用synchronized(),请确保使用的对象不是字符串。我们的示例的一种方法是使用synchronized(this),因为它将使用一个复杂类的实例。
此示例还显示了动态和静态分析之间的区别。在动态情况下,我们将不得不拥有两个内容与测试用例具有相同内容的字符串,或者通过运行系统来取代它。在静态案例中,我们测试案例是否在所有可能的案例之内,然后按铃。
从文本中可以看到,DeepCode在57个项目中发现了此问题,因此并不少见。在deepcode.ai上查看您自己的代码。