'Monitor/Java synchronized methods, how to schedule an activity in a non-blocking way?

So, basically the whole idea is that I have to simulate the entrance of a school. N students queue at four turnstiles and then they join a random classroom. When the first student joins that classroom a lecture starts and lasts for a time T, after which the lecture ends and students go home.

I'm specifically struggling at the "lasts for a time T" part. Here is what I tried:

StudentThread:

@Override
public void run() {
    try {
        /** Some uninteresting code**/
        classroom.joinClass(studentID);
       
    }catch(InterruptedException e) {
        
    }
}

Classroom:

private static final int LECTURE_DURATION = 3000;
 
public synchronized void joinClass(UUID student) throws InterruptedException {
            this.students.add(student);
            
            if( students.size() == 1 ) {
                lecture = true;
                new ScheduledThreadPoolExecutor(1).schedule(new Runnable() {
                    @Override
                    public void run() {
                        while( !students.isEmpty() )
                            students.remove(0);
                        
                        lecture = false;
                        notifyAll();
                    }
                }, LECTURE_DURATION, TimeUnit.MILLISECONDS);
                
            }
            
            while( lecture )
                wait();
        }

The apparent problem with this solution is that the first student joining a classroom will block the access to all the others, basically never leaving the joinClass method until the lecture ends. I assume it's because of that schedule call. I also can only use synchronized methods, no semaphores otherwise it would've been relatively easy. What should I do in this case?



Solution 1:[1]

You should not create a new ScheduledThreadPoolExecutor within the method. An executor is supposed to be reused. And you should call shutdown() on it once you really don’t need it anymore.

But more important is that, since the scheduled action is performed by a different thread, it must use synchronized when accessing the mutable state.

This leads to the point you’re missing about wait(). The only way for the condition you’re waiting for to correctly become fulfilled, is by having another thread performing the necessary steps within a synchronized block and calling notify() or notifyAll(), both methods insisting on being called within that synchronized block.

This can only work, when wait() releases the lock, so the other thread can acquire it and do the duty. And that’s precisely what wait does:

This method causes the current thread (referred to here as T) to place itself in the wait set for this object and then to relinquish any and all synchronization claims on this object. Note that only the locks on this object are relinquished; any other objects on which the current thread may be synchronized remain locked while the thread waits.

Thread T then becomes disabled for thread scheduling purposes and lies dormant until one of the following occurs:

The thread T is then removed from the wait set for this object and re-enabled for thread scheduling. It competes in the usual manner with other threads for the right to synchronize on the object; once it has regained control of the object, all its synchronization claims on the object are restored to the status quo ante - that is, to the situation as of the time that the wait method was invoked. Thread T then returns from the invocation of the wait method. Thus, on return from the wait method, the synchronization state of the object and of thread T is exactly as it was when the wait method was invoked.

Note that the documentation of the no-arg wait method redirects to the wait(long,int) documentation shown above

So the issue of your code is not that the initiating thread synchronizes but that the pool’s thread does not.

static final ScheduledThreadPoolExecutor EXEC = new ScheduledThreadPoolExecutor(1);

public synchronized void joinClass(UUID student) throws InterruptedException {
    this.students.add(student);

    if(students.size() == 1) {
        lecture = true;
        EXEC.schedule(new Runnable() {
            @Override
            public void run() {
                synchronized(OuterClassName.this) {
                    students.clear();
                    lecture = false;
                    OuterClassName.this.notifyAll();
                }
            }
        }, LECTURE_DURATION, TimeUnit.MILLISECONDS);
    }
    while(lecture) wait();
}

As a side note, there is no reason to remove single elements in a loop to empty a list, clear() does the job. In case of an ArrayList, repeatedly calling remove(0) is the worst way to clear it.

It’s also important to keep in mind that an inner class instance is a different object than the outer class instance. It’s simpler when using a lambda expression:

public synchronized void joinClass(UUID student) throws InterruptedException {
    this.students.add(student);

    if(students.size() == 1) {
        lecture = true;
        EXEC.schedule(() -> {
            synchronized(this) {
                students.clear();
                lecture = false;
                notifyAll();
            }
        }, LECTURE_DURATION, TimeUnit.MILLISECONDS);
    }

    while(lecture) wait();
}

Solution 2:[2]

No idea if this going to solve your problem but it might give you an idea.

public class Main
{
    public static void main(String[] args) throws Exception
    {
        Classroom classroom = new Classroom();
        Student studentA = new Student("Student A", classroom);
        Student studentB = new Student("Student B", classroom);
        Student studentC = new Student("Student C", classroom);
        Student studentD = new Student("Student D", classroom);
        
        studentA.enterClass();
        Thread.sleep(1000L); //1000 m/s early.
        classroom.start();
        Thread.sleep(1000L); //1 second late.
        studentB.enterClass();
        Thread.sleep(500L); //Late for 1.5 seconds.
        studentC.enterClass();
        classroom.join();
        Thread.sleep(2000L); //Class has ended.
        studentD.enterClass();
        System.out.println("Main Thread");
    }
}

class Student implements Runnable
{
    public String name;
    private Classroom classroom;
    public Thread thread;
    
    Student(String name, Classroom classroom)
    {
        this.name = name;
        this.classroom = classroom;
        thread = new Thread(this);
    }
    public void enterClass()
    {
        thread.start();
    }
    public synchronized void exitClass()
    {
        this.notify();
    }
    @Override
    public void run()
    {
        try {
            System.out.println(name + " entering the class.");
            classroom.joinClass(this);
            synchronized(this) {
                while(!classroom.hasEnded) this.wait();
            }
            System.out.println(name + " existing the class.");
        } catch(Exception e) {}
    }
}

class Classroom implements Runnable
{
    private static final long LECTURE_DURATION = 3000L;
    private Thread thread;
    public volatile boolean hasEnded;
    private List<Student> students;
    
    Classroom()
    {
        students = new ArrayList<Student>();
        thread = new Thread(this);
    }
    public void start()
    {
        thread.start();
    }
    public void join() throws Exception
    {
        thread.join();
    }
    @Override
    public void run()
    {
        System.out.println("Class starting...");
        try {
            Thread.sleep(LECTURE_DURATION);
        } catch(Exception e) {}
        hasEnded = true;
        System.out.println("Class ended");
        for(Student s : students) s.exitClass();
    }
    public void joinClass(Student student) throws Exception
    {
        if(!hasEnded) {
            System.out.println(student.name + " joins the class.");
            students.add(student);
        }
    }
}

Here is the output. It may vary in your system.

Student A entering the class.
Student A joins the class.
Class starting...
Student B entering the class.
Student B joins the class.
Student C entering the class.
Student C joins the class.
Class ended
Student B existing the class.
Student A existing the class.
Student C existing the class.
Main Thread
Student D entering the class.
Student D existing the class.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Holger
Solution 2