【问题标题】:Why this servlet code sample is thread unsafe?为什么这个 servlet 代码示例是线程不安全的?
【发布时间】:2014-06-22 18:44:08
【问题描述】:

我在网站上看到了一个servlet示例代码,据说这个代码是线程不安全的,但是我不知道为什么它是线程不安全的,当我使用这个代码时会发生什么。这个代码是一个servlet代码,用于保存每位访客的姓名。

public class UnsafeGuestbookServlet extends HttpServlet {

    private Set visitorSet = new HashSet();

    protected void doGet(HttpServletRequest httpServletRequest,

             HttpServletResponse httpServletResponse) throws ServletException, IOException {

        String visitorName = httpServletRequest.getParameter("NAME");

        if (visitorName != null)

            visitorSet.add(visitorName);

    }

}

人们说如果我像这样更改代码:

private Set visitorSet = Collections.synchronizedSet(new HashSet());

线程不安全问题将得到解决。

我知道如果这段代码是线程不安全的,肯定是visitorSet造成的,它是一个共享数据结构。由于String visitorName是一个局部变量,所以每个线程都会在自己的调用栈中进行复制,对吧?

【问题讨论】:

    标签: java servlets thread-safety


    【解决方案1】:

    为了使这个 servlet 线程安全,我会做两件事。首先引用集合final

    public class UnsafeGuestbookServlet extends HttpServlet {
    
        private final Set visitorSet = new HashSet();    
    

    在对象构造时初始化的最终引用,并安全发布。没有它,doGet 方法可能看不到构造集,尽管它正确初始化。

    其次,要么将集合包装在Collections.synchronizedSet()中,这实际上不是最好的解决方案,因为它的迭代器容易出现ConcurrentModificationException,并且所有方法都需要对集合进行排他锁定,或者使用为并发使用而设计的集合实现:

    private final Set visitorSet = Collections.newSetFromMap(new ConcurrentHashMap());
    

    该集合将使用 ConcurrentHashMap keySet 使用的条带模式,它提供线程安全的迭代器和更好的“并发性”。

    【讨论】:

      【解决方案2】:

      很简单,UnsafeGuestbookServlet 初始化一次,doGetdoPost 方法由多个线程并行调用。

      注意HashSet 的实现是不同步的。如果多个线程同时访问一个哈希集,并且至少有一个线程修改了该集,则必须对外同步。

      因此要同步我们使用的HashSet Collections.synchronizedSet()。现在 HashSet 是同步的,并且对这个特定 HashSet 的写入访问将被控制一次。

      如果你想了解更多关于线程、线程安全方法为什么和如何等的信息,最好阅读这个http://www.tutorialspoint.com/java/java_thread_synchronization.htm。顺便说一句,简单来说就是这样,当允许两个或更多人同时获得一个相同的东西时,就会出现竞争条件或不稳定条件。如果您的代码允许这种情况发生,它的线程不安全:-)

      还有一点,这完全取决于您在代码中如何处理它。如果您认为,没有 2 个线程可以从数据结构中覆盖或删除相同的值,那么您不想介意线程安全。请注意为什么线程安全很重要,以及在哪些情况下最重要。

      【讨论】:

      • 为什么对不安全的 HashSet 进行简单的添加操作可能会导致一些问题?您能给我举个例子吗?
      • 现在你问的是线程和同步的问题。我正在编辑我的答案以包含更多关于此的信息
      【解决方案3】:

      由于 String visitorName 是一个局部变量,每个线程都会使 在自己的调用堆栈中复制一份,对吧?

      指针的副本是的,但是 String 类可以重用对象(因为它们不可修改)以节省内存,因此理论上您可以有两个线程尝试将相同的对象存储在共享集中。即使对象不同,Set 也会使用equals 来知道对象是否已经被添加,所以你的代码仍然暴露在不安全的检查和添加操作中,迭代器在添加新元素时会遍历 Set。 ..

      【讨论】:

        【解决方案4】:

        我知道如果这段代码是线程不安全的,那一定是visitorSet引起的,它是一个共享数据结构。

        是的,它是您的 servlet 的成员。 servlet 容器创建您的 servlet 的一个实例,但它可能会在不同的线程中同时处理多个 http 请求,这些线程都在同一个 UnsafeGuestbookServlet 对象上调用您的 doGet() 方法 - 这意味着可以有多个线程操作 visitorSet HashSet . HashSet 不是线程安全的。

        由于 String visitorName 是一个局部变量,每个线程都会在自己的调用栈中创建一个副本,>对吧?

        是的。

        【讨论】:

        • 让我困惑的是,会发生什么问题?我正在做的只是一个插入操作,注意其他......
        • @Vico_Wu 什么都不会发生。竞争条件导致数据不一致,例如你得到重复的条目,条目消失了。或者对于 HashMap,它可能会进入 infinte loop
        【解决方案5】:

        第一个示例中的HashSet 是Servlet 的一个字段,它只会被初始化一次(因为Servlet 也会被初始化一次)。每个请求都将由您的 Servlet 的这个实例处理,因此您的示例中提到的线程不安全存在问题。

        HashSet 并非旨在用作共享资源,因此可能会出现问题,例如在另一个线程仍在迭代 Set 时尝试添加值。这就是为什么您需要在您自己的解决方案中提到的某种同步 Set。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2018-04-12
          • 2018-11-07
          • 2015-07-24
          • 2012-03-22
          • 1970-01-01
          • 2012-05-02
          相关资源
          最近更新 更多