Java Deque在多线程环境下的奇怪行为

huangapple go评论85阅读模式
英文:

The strange behavior of the Java Deque in muti-thread environment

问题

我写了一个简单的演示代码来测试“守护线程的工作原理”。但是演示显示了另一种奇怪的行为:
我创建了一个“Deque”来保存名为Event的元素,并在两个工作线程之间共享它,一个线程将元素添加到Deque中,另一个线程检查Deque的大小并删除3秒前创建的元素。奇怪的地方发生在这里,对Deque的size()的调用总是返回0。我知道ArrayDeque和LinkedDeque不是线程安全的,但我可以像这样修复奇怪的问题:
1、将Deque的实现更改为ConcurrentLinkedDeque。
2、同步deque实例。
3、在初始化Cleaner之前,将一个元素放入共享的deque中。
4、检查大小并打印它。
所有这些都运行得很好,这非常奇怪,我不知道为什么。

以下是演示代码和我的运行环境:

java版本:“1.8.0_141”
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)

MacBook Pro (13-inch, 2016, Two Thunderbolt 3 ports)
MacOS Catalina 10.15.6

public class CleanerTask extends Thread {

    private transient Deque<Event> deque;

    private AtomicInteger doRemoveTimes = new AtomicInteger(0);

    public AtomicInteger getDoRemoveTimes() {
        return doRemoveTimes;
    }

    public CleanerTask(Deque<Event> deque) {
        this.deque = deque;
        setDaemon(true);
    }

    @Override
    public void run() {
        //System.out.println("Cleaner: watch deque " + deque);
        while (true) {
            clean();
        }
    }

    private void clean() {
        //修复2
        /*synchronized (deque) {
            if(deque.size() == 0) {
                return;
            }
        }*/
        if (deque.size() == 0) {
            //System.out.println("Cleaner: deque's size:" + deque.size());//修复3
            return;
        }
        int removes = 0;
        int beforeNext;
        do {
            beforeNext = removes;
            Event event = deque.getLast();
            if (Duration.between(event.getTime(), LocalTime.now()).getSeconds() > 3) {
                deque.removeLast();
                System.out.println(event + " is removed");
                removes++;
            }
        } while (removes > beforeNext && deque.size() > 0);
        if (removes > 0) {
            doRemoveTimes.addAndGet(removes);
            System.out.printf("Cleaner: cleaned %d, remained %d\n", removes, deque.size());
        }
    }
}
public class WriterTask implements Runnable {

    private Deque<Event> deque;

    public WriterTask(Deque<Event> deque) {
        this.deque = deque;
    }

    @Override
    public void run() {
        System.out.println(LocalTime.now() + "-" + Thread.currentThread().getId() + ": start write event to the deque: " + deque);
        for(int i = 0; i < 10; i++) {
            Event event = new Event("event generated by " + Thread.currentThread().getId());
            deque.addFirst(event);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(LocalTime.now() + "-" + Thread.currentThread().getId() + ": finished");
    }
}
public class DaemonCleanMain {

    public static void main(String[] args) {
        //Deque<Event> deque = new ConcurrentLinkedDeque<>();//修复1
        Deque<Event> deque = new ArrayDeque<>();
        WriterTask writer = new WriterTask(deque);
        int i = args.length > 0 ? Integer.parseInt(args[0]) : 1;
        while (i > 0) {
            Thread thread = new Thread(writer);
            thread.start();
            i--;
        }
        //修复4
       /* try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
        CleanerTask cleaner = new CleanerTask(deque);
        cleaner.start();
        while (true) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("size of deque: " + deque.size());
            System.out.println("Cleaner is work? " + cleaner.getDoRemoveTimes());
        }
    }
}
public class Event {

    public Event(String name) {
        this.name = name;
        this.time = LocalTime.now();
    }

    public Event(String name, LocalTime time) {
        this.name = name;
        this.time = time;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public LocalTime getTime() {
        return time;
    }

    public void setTime(LocalTime time) {
        this.time = time;
    }

    private String name;

    private LocalTime time;

    @Override
    public String toString() {
        return "Event{" +
                "name='" + name + '\'' +
                ", time=" + time +
                '}';
    }
}

问题是:修复3和4,让我非常惊讶,因为我认为这是一种非常奇怪的修复方法。尽管ArrayDeque不是线程安全的,但当Writer停止时,Cleaner仍然会得到size()返回0,实际上只有一个线程正在运行(除了主线程),它的工作方式就像deque对于Cleaner是不可变的。

英文:

I wrote a simple demo code to test "how the Daemon Thread works". But the demo shows another strange behavior:
I make a Deque to hold the element called Event, and share it for two work threads, one add the element to the Deque. another check the Deque's size and remove the element which is created 3 seconds ago. The strange happened here, the call to the Deque's size() always returns 0. I know the ArrayDeque and LinkedDeque is not thread-safe, but I can fix the strange thing like this:
1、 change the Deque's implements to ConcurrentLinkedDeque.
2、 synchronized the deque instance.
3、 before init the Cleaner, put an element to the share deque.
4、 check the size, and print it.
All of this works well, that's very strange, and I don't know why.

Here's the demo code and my runtime environment:

java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)

MacBook Pro (13-inch, 2016, Two Thunderbolt 3 ports)
MacOS Catalina 10.15.6

public class CleanerTask extends Thread {
private transient Deque&lt;Event&gt; deque;
private AtomicInteger doRemoveTimes = new AtomicInteger(0);
public AtomicInteger getDoRemoveTimes() {
return doRemoveTimes;
}
public CleanerTask(Deque&lt;Event&gt; deque) {
this.deque = deque;
setDaemon(true);
}
@Override
public void run() {
//System.out.println(&quot;Cleaner: watch deque &quot; + deque);
while (true) {
clean();
}
}
private void clean() {
//fix 2
/*synchronized (deque) {
if(deque.size() == 0) {
return;
}
}*/
if (deque.size() == 0) {
//System.out.println(&quot;Cleaner: deque&#39;s size:&quot; + deque.size());//fix 3
return;
}
int removes = 0;
int beforeNext;
do {
beforeNext = removes;
Event event = deque.getLast();
if (Duration.between(event.getTime(), LocalTime.now()).getSeconds() &gt; 3) {
deque.removeLast();
System.out.println(event + &quot; is removed&quot;);
removes++;
}
} while (removes &gt; beforeNext &amp;&amp; deque.size() &gt; 0);
if (removes &gt; 0) {
doRemoveTimes.addAndGet(removes);
System.out.printf(&quot;Cleaner: cleaned %d, remained %d\n&quot;, removes, deque.size());
}
}
}
public class WriterTask implements Runnable {
private Deque&lt;Event&gt; deque;
public WriterTask(Deque&lt;Event&gt; deque) {
this.deque = deque;
}
@Override
public void run() {
System.out.println(LocalTime.now() + &quot;-&quot; + Thread.currentThread().getId() + &quot;: start write event to the deque: &quot; + deque);
for(int i = 0; i &lt; 10; i++) {
Event event = new Event(&quot;event generated by &quot; + Thread.currentThread().getId());
deque.addFirst(event);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(LocalTime.now() + &quot;-&quot; + Thread.currentThread().getId() + &quot;: finished&quot;);
}
}
public class DaemonCleanMain {
public static void main(String[] args) {
//Deque&lt;Event&gt; deque = new ConcurrentLinkedDeque&lt;&gt;();//fix 1
Deque&lt;Event&gt; deque = new ArrayDeque&lt;&gt;();
WriterTask writer = new WriterTask(deque);
int i = args.length &gt; 0 ? Integer.parseInt(args[0]) : 1;
while (i &gt; 0) {
Thread thread = new Thread(writer);
thread.start();
i--;
}
//fix 4
/* try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
CleanerTask cleaner = new CleanerTask(deque);
cleaner.start();
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(&quot;size of deque: &quot; + deque.size());
System.out.println(&quot;Cleaner is work? &quot; + cleaner.getDoRemoveTimes());
}
}
}
public class Event {
public Event(String name) {
this.name = name;
this.time = LocalTime.now();
}
public Event(String name, LocalTime time) {
this.name = name;
this.time = time;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public LocalTime getTime() {
return time;
}
public void setTime(LocalTime time) {
this.time = time;
}
private String name;
private LocalTime time;
@Override
public String toString() {
return &quot;Event{&quot; +
&quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +
&quot;, time=&quot; + time +
&#39;}&#39;;
}
}

The question is: The 3 and 4 fix, makes me very surprise, because I think it's very strange fix method. And, Although , the ArrayDeque is not thread-safe, but when the Writer is stop, the Cleaner still get the size() return 0, when actual only one thread is runing now(except the main one), it works like the deque to the Cleaner is final and immutable.

答案1

得分: 1

有关并发的一个奇怪之处是内存模型。如果不同步,两个线程可能对数据有不同的“副本”或“视图”。所以,这就是为什么你看到大小为0的原因。这是违反直觉的,因为你可能会认为它们指向相同的东西,但事实并非如此。

这里有更详细的信息:
http://tutorials.jenkov.com/java-concurrency/java-memory-model.html

好消息是你知道如何解决这个问题!

英文:

One curious thing about concurrency is the memory model. If you dont synchronize, two threads may have a different "copies" or "views" of the data. So, that's why you see the size as 0. It's contra-intuitive, as you can think that they point to the same thing, and that's not it.

Here you have more detailed info:
http://tutorials.jenkov.com/java-concurrency/java-memory-model.html

The good news is that you know about how to solve it!

huangapple
  • 本文由 发表于 2020年8月2日 21:38:05
  • 转载请务必保留本文链接:https://go.coder-hub.com/63216663.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定