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

Tags: ,



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<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() {
        //fix 2
        /*synchronized (deque) {
            if(deque.size() == 0) {
                return;
            }
        }*/
        if (deque.size() == 0) {
            //System.out.println("Cleaner: deque's size:" + 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() > 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 %dn", 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<>();//fix 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--;
        }
        //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("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 +
                '}';
    }
}

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.

Answer

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!



Source: stackoverflow