Java正则表达式是否线程安全?

121

我有一个函数,它使用Pattern#compile和一个Matcher来搜索字符串列表中的模式。

这个函数被多个线程使用。每个线程在创建时都会传递一个唯一的模式给Pattern#compile。线程数和模式数是动态的,也就是说我可以在配置期间添加更多的Pattern和线程。

如果这个函数使用正则表达式,我需要在这个函数上放置synchronize吗?Java中的正则表达式是线程安全的吗?

5个回答

155

是的,来自于Java API文档中的Pattern类

此类(Pattern)的实例是不可变的,且可以安全地供多个并发线程使用。Matcher类的实例则不适合这样的用法。

如果您正在查看性能为中心的代码,请尝试使用reset()方法重置Matcher实例,而不是创建新实例。这将重置Matcher实例的状态,使其可以用于下一个正则表达式操作。实际上,是Matcher实例中维护的状态导致了它不适合进行并发访问。


21
Pattern 对象是线程安全的,但 compile() 方法可能不是。多年来已经发现了两到三个导致在多线程环境下编译失败的 bug。我建议在同步块中执行编译操作。 - Alan Moore
5
是的,Pattern类中出现了并发性错误,并且你提供的同步访问建议受到了赞赏。然而,Pattern类的原始开发人员旨在使Pattern类成为线程安全的,这是任何Java程序员都应该能够依赖的契约。坦白地说,我宁愿使用线程本地变量并接受最小的性能损失,也不愿仅仅依赖于契约上的线程安全行为(除非我看过代码)。正如人们所说:“多线程很容易,正确的同步却很难。” - Vineet Reynolds
1
请注意,“Pattern”的源代码在Oracle JDK发行版中(根据http://www.oracle.com/technetwork/java/faq-141681.html#A14:“Java 2 SDK,标准版本身包含一个名为src.zip的文件,其中包含java包中公共类的源代码”),因此可以自己快速查看。 - David Tonhofer
@DavidTonhofer 我认为我们最新的JDK可能具有正确的无缺陷代码,但由于Java的中间.class文件可以在任何兼容的VM上的任何平台上进行解释,因此您不能确定这些修复是否存在于该运行时中。当然,大多数情况下,您知道服务器正在运行哪个版本,但检查每个单独的版本是很繁琐的。 - TWiStErRob

13

Java中正则表达式的线程安全问题

摘要:

Java正则表达式API被设计成允许一个编译好的模式在多个匹配操作中共享使用。

您可以在不同的线程上安全地调用 Pattern.matcher() 并同时使用matcher对象。 Pattern.matcher() 可以安全地构造无需同步的matcher对象。虽然该方法本身并没有同步,但在Pattern类内部,构造完一个pattern后总是会设置维护一个名为compiled的volatile变量,而在每次调用 matcher() 前该变量都会被读取。这样就可以确保任何引用Pattern对象的线程都可以正确“看到”该对象的内容。

另一方面,您不应该在不同的线程之间共享Matcher对象。或者至少,如果确实需要这样做,那么就应该使用显式的同步机制。


2
@akf,顺便说一下,你应该注意那是一个讨论网站(就像这个)。我认为你在那里找到的任何信息都不比你在这里找到的信息更好或更差(即它不是来自James Gosling的唯一真实之言)。 - Bob Cross

2
快速查看Matcher.java的代码,可以看到一堆成员变量,包括正在匹配的文本、用于分组的数组、一些维护位置的索引以及一些用于其他状态的boolean。这都指向了一个有状态的Matcher,如果被多个Threads访问,它将无法正常工作。因此,JavaDoc中也提到:

该类的实例不适合在多个并发线程中使用。

只有在您特意允许在单独的Threads中使用Matcher时,才会出现这个问题,就像@Bob Cross指出的那样。如果您需要这样做,并且认为同步对您的代码是一个问题,您可以使用ThreadLocal存储对象来维护每个工作线程的Matcher


2

虽然您需要记住线程安全需要考虑周围的代码,但您似乎很幸运。Matchers是使用Pattern的matcher工厂方法创建且缺少公共构造函数的事实是一个积极的迹象。同样,您可以使用compile静态方法来创建包含的Pattern

因此,简而言之,如果您像示例一样执行某些操作:

Pattern p = Pattern.compile("a*b");
Matcher m = p.matcher("aaaaab");
boolean b = m.matches();

你应该做得很不错。

为了澄清代码示例,需要注意的是:这个示例强烈暗示了与 Pattern 和 test 一起创建的 Matcher 是线程本地的。也就是说,不应该将这个 Matcher 暴露给其他线程。

坦白说,这就是任何线程安全问题的风险所在。现实情况是,如果你足够努力,任何代码都可能变得线程不安全。幸运的是,有wonderful books能够教我们许多我们可能毁掉自己代码的方法。只要远离这些错误,我们就大大减少了自己的线程问题概率。


@Jason S:线程本地性是实现线程安全的一种非常直接的方法,即使内部代码不是线程安全的。如果只有一个方法可以同时访问特定的方法,那么你已经在外部强制实施了线程安全性。 - Bob Cross
1
好的,所以你是在说在使用时从字符串重新创建一个模式比存储它更有效,尽管存在处理并发问题的风险?我同意你的看法。关于工厂方法和公共构造函数那个句子让我感到困惑,似乎与这个主题无关。 - Jason S
@Jason S,不,工厂方法和缺乏构造函数是减少与其他线程耦合的方式之一。如果您获取与我的模式匹配器相对应的Matcher的唯一方法是通过p.matcher(),则没有其他人可以影响我的Matcher。但是,我仍然可能给自己带来麻烦:如果我有一个公共方法返回该Matcher,则另一个线程可以访问它并影响它。简而言之,并发编程在任何语言中都很困难。 - Bob Cross

1
总之,您可以重复使用(保留静态变量)编译的Pattern,并告诉它们在需要验证这些正则表达式模式与某个字符串匹配时给您新的Matcher。
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Validation helpers
 */
public final class Validators {

private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$";

private static Pattern email_pattern;

  static {
    email_pattern = Pattern.compile(EMAIL_PATTERN);
  }

  /**
   * Check if e-mail is valid
   */
  public static boolean isValidEmail(String email) { 
    Matcher matcher = email_pattern.matcher(email);
    return matcher.matches();
  }

}

请参见http://zoomicon.wordpress.com/2012/06/01/validating-e-mails-using-regular-expressions-in-java/(接近结尾处),了解上面用于验证电子邮件的正则表达式模式(如果它不适合您在此处发布的电子邮件验证需求)。

3
感谢您发布您的答案!请务必仔细阅读有关“自我推广”的FAQ。某些人可能会看到此答案和链接到的博客文章并认为您只是为了在这里添加链接而发布了博客文章。 - Andrew Barber
2
为什么要使用 static {}?您可以将变量初始化内联并将 Pattern 设置为 final - TWiStErRob
2
我赞同 TWiStErRob 的观点:private static final Pattern emailPattern = Pattern.compile(EMAIL_PATTERN); 更好。 - Christophe Roussy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接