Analysis of super detailed Java producer consumer pattern

Keywords: Java JDK

Producer consumer pattern is the most common pattern in multithreading: producer thread (one or more) generates bread and puts it into basket (set or array), while consumer thread (one or more) takes bread consumption out of basket (set or array). Although their tasks are different, the resources they process are the same, which embodies a way of communication between threads.

This paper will first explain the situation of single producer and single consumer, and then explain the situation of multi producer and multi consumer model. We will also use wait() / nofit() / nofityall() mechanism and lock()/unlock() mechanism to implement these two modes.

Before you begin to introduce patterns, explain the usage details of wait(), notify(), and notifyAll() methods, as well as the improved usage of lock()/unlock(), await()/signal()/signalAll().

1. Principle of waiting and waking mechanism

wait(), notify(), and notifyAll() represent the threads that let the thread sleep, wake up the sleep thread, and wake up all sleep, respectively. But which thread is the object? In addition, all three methods described in the API documentation must be used under the premise of a valid monitor (which can be understood as holding a lock). What do these three methods have to do with locks?

Take synchronized code block (obj) {} or synchronization function for example. wait(), notify(), and notifyAll() can be used in their code structure because they all hold locks.

For the following two synchronization code blocks, lock obp1 and lock obp2 are used respectively. Thread 1 and thread 2 execute the synchronization code corresponding to obp1, and thread 3 and thread 4 execute the synchronization code corresponding to obp2.

class MyLock implements Runnable {
    public int flag = 0;
    Object obj1 = new Object();
    Object obj2 = new Object();
    public void run(){
        while(true){
            if(flag%2=0){
                synchronized(obj1){     //Threads t1 and t2 perform this synchronization task
                    //try{obj1.wait();}catch(InterruptedException i){}
                    //obj1.notify()
                    //obj1.notifyAll()
                }
            } else {
                synchronized(obj2){   //Threads t3 and t4 perform this synchronization task
                    //try{obj2.wait();}catch(InterruptedException i){}
                    //obj2.notify()
                    //obj2.notifyAll()
                }
            }
        }
    }
}
class Demo {
    public static void main(String[] args){
        MyLock ml = new MyLock();
        Thread t1 = new Thread(ml);
        Thread t2 = new Thread(ml);
        Thread t3 = new Thread(ml);
        Thread t4 = new Thread(ml);
        t1.start();
        t2.start();
        try{Thread.sleep(1)}catch(InterruptedException i){};
        ml.flag++;
        t3.start();
        t4.start();
    }
}

When t1 starts to execute wait(), it will enter sleep state, but it is not general sleep, but sleep in a thread pool identified by obg1 (in fact, the monitor corresponds to the thread pool, but the monitor and lock are bound together at this time). When t2 starts to execute, it finds that the lock obg1 is held by other threads, and it will enter sleep state. This sleep is due to the sleep of lock resource waiting instead of wait(). Because t2 has determined that it wants to apply for the obj 1 lock, it will also enter the obj 1 thread pool for sleep instead of ordinary sleep. In the same way, t3 and t4 will enter the obj 2 thread pool for sleep.

When a thread executes to notify(), the notify() will randomly wake up any thread in the corresponding thread pool of the lock it belongs to. For example, obj 1. Notify() will wake up any sleeping thread in the obj 1 thread pool (of course, if there is no sleeping thread, nothing will be done). Similarly, notifyAll() wakes all the sleeping threads in the thread pool corresponding to the lock.

It must be made clear that "corresponding lock" is required, because the lock must be specified explicitly when calling wait(), notify(), and notifyAll(). For example, obg1. Wait(). If the owner lock is omitted, it means this object, that is to say, the prefixes of these three methods can only be omitted in the non-static synchronization function.

In short, when synchronization is used, the lock is used, and the thread has its own ownership. All its bases are determined by the lock. For example, when a thread is synchronized, it determines whether the lock is idle to decide whether to execute the following code, and whether to sleep in a specific thread pool. When the lock is awakened, only the threads in the corresponding thread pool will be awakened.

In the application of these methods, generally in a task, wait() and notify()/notifyAll() appear in pairs and execute one by one. In other words, during this round of atomic synchronization, either wait() is executed to enter sleep, or notify() is executed to wake up the sleep thread in the thread pool. In order to realize alternative execution, we can consider the way of using tags as the basis of judgment. Refer to the following examples.

2.Lock and Condition

The three methods in the wait() series are very limited, because both sleep and wake-up actions are completely coupled with locks. For example, the thread associated with lock obj 1 can only wake up the thread in the obj 1 thread pool, but not the thread associated with lock obj 2. For example, when the original synchronized synchronization was started, the lock was automatically acquired implicitly at the beginning of synchronization, and was automatically released implicitly after the completion of a whole task, that is to say, the action of acquiring and releasing the lock was not controlled by legal person.

Starting from JDK 1.5, Java provides the java.util.concurrent.locks package, which provides the Lock interface, Condition interface and ReadWriteLock interface. The first two interfaces decouple the Lock and monitor methods (sleep and wake-up operations). The Lock interface only provides locks. Through the Lock method newcondition(), one or more monitors associated with the Lock can be generated. Each monitor has its own sleep and wake-up methods. That is to say, Lock replaces the use of synchronized method and synchronized code block, and Condition replaces the use of Object monitor method.
The following picture:

When a thread executes condition1.await(), the thread will enter the thread pool sleep corresponding to condition1 monitor. When executing condition1.signal(), any thread in condition1 thread pool will be awakened randomly. When executing condition1.signalAll(), all threads in condition1 thread pool will be awakened. Similarly, the same is true for the condition2 monitor.

Even if there are multiple monitors, as long as they are associated with the same lock object, you can operate the opposite thread across monitors. For example, a thread in condition1 can execute condition2.signal() to wake up a thread in the condition2 thread pool.
To use this lock monitor association method, refer to the following steps:

import java.util.concurrent.locks.*;
Lock l = new ReentrantLock();
Condition con1 = l.newCondition();
condition con2 = l.newCondition();

l.lock();
try{
    //Code snippet containing await(), signal(), or signalAll()
} finally {
    l.unlock();    //Because the code snippet may be abnormal, but unlock() must be executed, you must use try and put unlock() in the finally snippet

See the following sample code about Lock and condition for specific usage.

3. Single producer single consumer model

A producer thread, a consumer thread. Every time a producer produces a bread and puts it on a plate, the consumer takes the bread from the plate for consumption. The basis for producers to judge whether to continue production is that there is no bread on the plate, while the basis for consumers to judge whether to consume is that there is bread on the plate. In this mode, only one bread is placed on the plate, so the plate can be omitted, and the producer and consumer can hand in the bread directly.

First of all, we need to describe these three classes. One is the resource operated by multiple threads (bread here), the other is the producer, and the third is the consumer. In the following example, I encapsulate the methods of producing bread and consuming bread into the producer and consumer classes respectively, which is easier to understand.

//Description resource: the name and number of bread. The number determines the number of bread
class Bread {
    public String name;
    public int count = 1;

    public boolean flag = false;   //This tag provides a judgment tag for wait() and notify()
}

//The bread resources that producers and consumers deal with are the same, so we should ensure this,
//The bread class can be designed according to the singleton pattern, or the same bread object can be passed to the producer and consumer through the construction method. The latter method is used here.

//Describe the producer
class Producer implements Runnable {
    private Bread b;   //Producer's member: the resource it will process

    Producer(Bread b){
        this.b = b;
    }

    //Provide methods of bread production
    public void produce(String name){
        b.name = name + b.count;
        b.count++;
    }

    public void run(){
        while(true){
            synchronized(Bread.class){   //Using Bread.class as the lock identifier enables producers and consumers to use the same lock for synchronized code blocks
                if(b.flag){              //wait() must be inside the synchronization code block, not only because the lock must be held to sleep, but also the judgment of the lock resource will be confused
                    try{Bread.class.wait();}catch(InterruptedException i){}
                }
                produce("bread");
                System.out.println(Thread.currentThread().getName()+"----Producer------"+b.name);
                try{Thread.sleep(10);}catch(InterruptedException i){}
                b.flag = true;             //The switching of tags must also be synchronized
                Bread.class.notify();      //notify() must also be synchronized, otherwise the lock has been released and the wake-up action cannot be performed
                //ps: in a synchronization task, wait() and notify() should only execute one of them, otherwise the other thread will be confused
            }
        }
    }
}

//Describe the consumer
class Consumer implements Runnable {
    private Bread b;   //Consumer's members: the resources it's dealing with

    Consumer(Bread b){
        this.b = b;
    }

    //How to provide bread for consumption
    public String consume(){
        return b.name;
    }

    public void run(){
        while(true){
            synchronized(Bread.class){
                if(!b.flag){
                    try{Bread.class.wait();}catch(InterruptedException i){}
                }
                System.out.println(Thread.currentThread().getName()+"----Consumer-------------"+consume());
                try{Thread.sleep(10);}catch(InterruptedException i){}
                b.flag = false;
                Bread.class.notify();
            }
        }
    }
}

public class ProduceConsume_1{
    public static void main(String[] args) {
        //1. Create resource object
        Bread b = new Bread();

        //2. Create producer and consumer objects and pass the same bread object to producer and consumer
        Producer pro = new Producer(b);
        Consumer con = new Consumer(b);

        //3. Create thread object
        Thread pro_t = new Thread(pro);
        Thread con_t = new Thread(con);

        pro_t.start();
        con_t.start();
    }
}

The final result of implementation should be one for production and one for consumption, so that it will continue to circulate. As follows:

Thread-0 ---- producer ---- bread 1
 Thread-1 --- consumer --- bread 1
 Thread-0 ---- producer ---- bread 2
 Thread-1 --- consumer --- bread 2
 Thread-0 ---- producer ---- bread 3
 Thread-1 --- consumer --- bread 3
 Thread-0 ---- producer ---- bread 4
 Thread-1 --- consumer --- bread 4
 Thread-0 --- producer --- bread 5
 Thread-1 --- consumer --- bread 5
 Thread-0 --- producer --- bread 6
 Thread-1 --- consumer --- bread 6

4. Use Lock and Condition to realize single production and single consumption mode

The code is as follows:

import java.util.concurrent.locks.*;

class Bread {
    public String name;
    public int count = 1;

    public boolean flag = false;

    //Provide the same lock object and the same Condition object for producers and consumers
    public static Lock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();

}

class Producer implements Runnable {
    private Bread b;

    Producer(Bread b){
        this.b = b;
    }

    public void produce(String name){
        b.name = name + b.count;
        b.count++;
    }

    public void run(){
        while(true){
            //Use Bread.lock to lock resources
            Bread.lock.lock();   
            try{
                if(b.flag){
                    try{Bread.condition.await();}catch(InterruptedException i){}
                }
                produce("bread");
                System.out.println(Thread.currentThread().getName()+"----Producer------"+b.name);
                try{Thread.sleep(10);}catch(InterruptedException i){}
                b.flag = true;  
                Bread.condition.signal(); 
            } finally {
                Bread.lock.unlock();
            }
        }
    }
}

class Consumer implements Runnable {
    private Bread b;   

    Consumer(Bread b){
        this.b = b;
    }

    public String consume(){
        return b.name;
    }

    public void run(){
        while(true){
            //Use Bread.lock to lock resources
            Bread.lock.lock();
            try{
                if(!b.flag){
                    try{Bread.condition.await();}catch(InterruptedException i){}
                }
                System.out.println(Thread.currentThread().getName()+"----Consumer-------------"+consume());
                try{Thread.sleep(10);}catch(InterruptedException i){}
                b.flag = false;
                Bread.condition.signal();
            } finally {
                Bread.lock.unlock();
            }
        }
    }
}

public class ProduceConsume_1{
    public static void main(String[] args) {
        //1. Create resource object
        Bread b = new Bread();

        //2. Create producer and consumer objects and pass the same bread object to producer and consumer
        Producer pro = new Producer(b);
        Consumer con = new Consumer(b);

        //3. Create thread object
        Thread pro_t = new Thread(pro);
        Thread con_t = new Thread(con);

        pro_t.start();
        con_t.start();
    }
}

5. Multi production and multi consumption mode (single bread)

First, I will explain the mode of multi producer and multi consumer, but there can only be one bread mode at a time. This mode may not be ideal in practice. But in order to lead to the real multi producer and multi consumer mode, I think it is necessary to explain this mode here, and analyze this mode and how it evolves from the code of single production and single consumption.
The following picture:

From single production and single consumption to multi production and multi consumption, there are two problems to be considered because of multi thread security and deadlock

For one party, how can multithreading achieve the same production or consumption capacity as single thread? That is, how to make multithreading look like a single thread. The biggest difference between multithreading and single thread is multithreading safety. Therefore, as long as the tasks executed by multithreading can be synchronized.

The first problem is to consider the multi-threaded problem of one party, and the second is to consider how the two parties can cooperate harmoniously to complete production and consumption. That is to say, how to ensure that one side of the production side and the other side sleep while the other side is active. Just wake up the other party when one party finishes the synchronization task.

In fact, from single thread to multi thread, there are two issues to consider: asynchronous and deadlock. (1) when both the producer and the consumer have multithreading, we can regard the multithreading of the producer as a whole thread and the multithreading of the consumer as a whole, which solves the problem of thread safety. (2) combine the whole producer and the whole consumer as multi threads to solve the deadlock problem. In java, the way to solve the deadlock is to wake up the other party or all.

The problem is how to ensure the synchronization between multiple threads of one party? Take multithreading to execute single consumer code as an example.

while(true){
    synchronized(Bread.class){
        if(!b.flag){
            try{Bread.class.wait();}catch(InterruptedException i){}
        }
        System.out.println(Thread.currentThread().getName()+"----Consumer-------------"+consume());
        try{Thread.sleep(10);}catch(InterruptedException i){}
        b.flag = false;
        Bread.class.notify();
    }
}

Suppose consumer thread 1 wakes up consumer thread 2 after consuming a bread, and continues to loop. Judge if(!flag), it will wait, and then the lock is released. Assuming that CPU just selects consuming thread 2, consuming thread 2 will also enter wait. After the producer has produced a bread, suppose that the consumer thread 1 is awakened, it will continue to consume the bread just finished from the wait statement. Suppose that the consumer thread 2 is awakened again. When the consumer thread 2 is selected by the CPU, the consumer thread 2 will also consume the bread just produced from the wait statement. The problem arises again, the consumer line that is awakened continuously Cheng 1 and Cheng 2 consume the same bread, that is to say, bread is consumed repeatedly. This is the problem of multi-threaded synchronization.

For a long time, it's very simple to analyze after zooming in. As long as two or more threads of one party are wait ing because of judging b.flag, then these two or more threads may be awakened continuously and continue to produce or consume downward. This causes the multi-threaded out of sync problem.

The problem with insecurity is that multiple threads on the same side continue to produce or consume after a continuous wake-up. This is caused by if statement. If the thread of wait can go back to judge whether b.flag is true after wake-up, it can make it decide whether to continue wait or to produce or consume downward.

You can replace the if statement with the while statement to meet the requirements. In this way, no matter whether multiple threads of one party are awakened continuously or not, they will go back to judge b.flag.

while(true){
    synchronized(Bread.class){
        while(!b.flag){
            try{Bread.class.wait();}catch(InterruptedException i){}
        }
        System.out.println(Thread.currentThread().getName()+"----Consumer-------------"+consume());
        try{Thread.sleep(10);}catch(InterruptedException i){}
        b.flag = false;
        Bread.class.notify();
    }
}

It solves the first problem of multithreading security, but deadlock occurs. This is easy to analyze. The producer is regarded as a whole, and the consumer is also regarded as a whole. When all the threads of the producer are waiting (when the threads of the producer are continuously awakened, all the threads of the producer will wait), the consumer is waiting, and the deadlock occurs. In fact, it is enlarged to see that the production side and the consumer side are regarded as one thread respectively. These two threads constitute multiple threads. When one side cannot wake up the other side after waiting, the other side will surely wait, so it will deadlock.

As for the deadlock problem of both parties, it can be solved as long as we can guarantee to wake up the other party instead of our party continuously. Use notifyAll() or signalAll(), or wake up the other thread through signal(), as shown in the second code below.

According to the above analysis, we can change the code of single production and single consumption mode to multi production, multi consumption and single bread mode. ,

//Code segment 1
class Bread {
    public String name;
    public int count = 1;

    public boolean flag = false;   
}

//Describe the producer
class Producer implements Runnable {
    private Bread b;   

    Producer(Bread b){
        this.b = b;
    }

    public void produce(String name){
        b.name = name + b.count;
        b.count++;
    }

    public void run(){
        while(true){
                synchronized(Bread.class){
                    while(b.flag){ 
                        try{Bread.class.wait();}catch(InterruptedException i){}
                    }
                    produce("bread");
                    System.out.println(Thread.currentThread().getName()+"----Producer------"+b.name);
                    try{Thread.sleep(10);}catch(InterruptedException i){}
                    b.flag = true; 
                    Bread.class.notifyAll();
            }
        }
    }
}

//Describe the consumer
class Consumer implements Runnable {
    private Bread b;

    Consumer(Bread b){
        this.b = b;
    }

    public String consume(){
        return b.name;
    }

    public void run(){
        while(true){
                synchronized(Bread.class){
                    while(!b.flag){
                        try{Bread.class.wait();}catch(InterruptedException i){}
                    }
                    System.out.println(Thread.currentThread().getName()+"----Consumer-------------"+consume());
                    try{Thread.sleep(10);}catch(InterruptedException i){}
                    b.flag = false;
                    Bread.class.notifyAll();
                }
        }
    }
}

public class ProduceConsume_5 {
    public static void main(String[] args) {
        //1. Create resource object
        Bread b = new Bread();

        //2. Create producer and consumer objects
        Producer pro = new Producer(b);
        Consumer con = new Consumer(b);

        //3. Create thread object
        Thread pro_t1 = new Thread(pro);  //Production thread 1
        Thread pro_t2 = new Thread(pro);  //Production thread 2
        Thread con_t1 = new Thread(con);  //Consumption thread 1
        Thread con_t2 = new Thread(con);  //Consumption thread 2

        pro_t1.start();
        pro_t2.start();
        con_t1.start();
        con_t2.start();
    }
}

The following is the code after refactoring with Lock and condition. It uses the method of signal() to wake up the other thread.

//Code segment 2
import java.util.concurrent.locks.*;

class Bread {
    public String name;
    public int count = 1;

    public boolean flag = false;

    public static Lock lock = new ReentrantLock();
    public static Condition pro_con = lock.newCondition();
    public static Condition con_con = lock.newCondition();
}

//Describe the producer
class Producer implements Runnable {
    private Bread b;

    Producer(Bread b){
        this.b = b;
    }

    public void produce(String name){
        b.name = name + b.count;
        b.count++;
    }

    public void run(){
        while(true){
            Bread.lock.lock();
            try{
                while(b.flag){
                    try{Bread.pro_con.await();}catch(InterruptedException i){}
                }
                produce("bread");
                System.out.println(Thread.currentThread().getName()+"----Producer------"+b.name);
                try{Thread.sleep(10);}catch(InterruptedException i){}
                b.flag = true;
                Bread.con_con.signal();   //The consumer thread wakes up
            } finally {
                Bread.lock.unlock();
            }
        }
    }
}

//Describe the consumer
class Consumer implements Runnable {
    private Bread b;

    Consumer(Bread b){
        this.b = b;
    }

    public String consume(){
        return b.name;
    }

    public void run(){
        while(true){
            Bread.lock.lock();
            try{
                while(!b.flag){
                    try{Bread.con_con.await();}catch(InterruptedException i){}
                }
                System.out.println(Thread.currentThread().getName()+"----Consumer-------------"+consume());
                try{Thread.sleep(10);}catch(InterruptedException i){}
                b.flag = false;
                Bread.pro_con.signal();     //Wakes up the producer thread
            } finally {
                Bread.lock.unlock();
            }
        }
    }
}

public class ProduceConsume_6 {
    public static void main(String[] args) {
        //1. Create resource object
        Bread b = new Bread();

        //2. Create producer and consumer objects
        Producer pro = new Producer(b);
        Consumer con = new Consumer(b);

        //3. Create thread object
        Thread pro_t1 = new Thread(pro);
        Thread pro_t2 = new Thread(pro);
        Thread con_t1 = new Thread(con);
        Thread con_t2 = new Thread(con);

        pro_t1.start();
        pro_t2.start();
        con_t1.start();
        con_t2.start();
    }
}

Make a summary on the issue of more production and more consumption:
(1) the solution to the multi-threaded asynchronous problem of one party is to use while(flag) to judge whether to wait or not;
(2) the solution to the deadlock problem is to wake up the other party. You can use notifyAll(), signalAll() or the signal() method of the other party's monitor.

6. Multi production and multi consumption mode

There are multiple producer threads and multiple consumer threads. The producer puts the produced bread into the basket (set or array), and the consumer takes the bread out of the basket. The basis for the producer to judge the continuous production is that the basket is full, and the basis for the consumer to judge the continuous consumption is whether the basket is empty. In addition, when the consumer takes out the bread, the corresponding position is empty again, and the producer can go back to the starting position of the basket to continue production, which can be achieved by resetting the pointer of the basket.

In this model, in addition to describing producers, consumers and bread, we need to describe the container of basket. Suppose we use array as container. For every producer producing one, the production pointer shifts backward. For every consumer consuming one, the consumption pointer shifts backward.

The code is as follows: refer to the example code given in the API -- > condition class

import java.util.concurrent.locks.*;

class Basket {
    private Bread[] arr;

    //the size of basket
    Basket(int size){
        arr = new Bread[size];
    }
    //the pointer of in and out
    private int in_ptr,out_ptr;

    //how many breads left in basket
    private int left;

    private Lock lock = new ReentrantLock();
    private Condition full = lock.newCondition();
    private Condition empty = lock.newCondition();

    //bread into basket
    public void in(){
        lock.lock();
        try{
            while(left == arr.length){
                try{full.await();} catch (InterruptedException i) {i.printStackTrace();}
            }
            arr[in_ptr] = new Bread("MianBao",Producer.num++);
            System.out.println("Put the bread: "+arr[in_ptr].getName()+"------into basket["+in_ptr+"]");
            left++;
            if(++in_ptr == arr.length){in_ptr = 0;}
            empty.signal();
        } finally {
            lock.unlock();
        }
    }

    //bread out from basket
    public Bread out(){
        lock.lock();
        try{
            while(left == 0){
                try{empty.await();} catch (InterruptedException i) {i.printStackTrace();}
            }
            Bread out_bread = arr[out_ptr];
            System.out.println("Get the bread: "+out_bread.getName()+"-----------from basket["+out_ptr+"]");
            left--;
            if(++out_ptr == arr.length){out_ptr = 0;}
            full.signal();
            return out_bread;
        } finally {
            lock.unlock();
        }
    }
}

class Bread {
    private String name;

    Bread(String name,int num){
        this.name = name + num;
    }
    public String getName(){
        return this.name;
    }
}

class Producer implements Runnable {
    private Basket basket;
    public static int num = 1;  //the first number for Bread's name

    Producer(Basket b){
        this.basket = b;
    }

    public void run(){
        while(true) {
            basket.in();
            try{Thread.sleep(10);}catch(InterruptedException i){}
        }
    }
}

class Consumer implements Runnable {
    private Basket basket;
    private Bread i_get;

    Consumer(Basket b){
        this.basket = b;
    }

    public void run(){
        while(true){
            i_get = basket.out();
            try{Thread.sleep(10);}catch(InterruptedException i){}
        }
    }
}

public class ProduceConsume_7 {
    public static void main(String[] args) {
        Basket b = new Basket(20);  // the basket size = 20

        Producer pro = new Producer(b);
        Consumer con = new Consumer(b);

        Thread pro_t1 = new Thread(pro);
        Thread pro_t2 = new Thread(pro);
        Thread con_t1 = new Thread(con);
        Thread con_t2 = new Thread(con);
        Thread con_t3 = new Thread(con);

        pro_t1.start();
        pro_t2.start();
        con_t1.start();
        con_t2.start();
        con_t3.start();
    }
}

This involves consumers, producers, bread and baskets, in which bread and baskets are resources operated jointly by multiple threads. The producer thread produces bread and puts it into the basket, and the consumer thread takes bread out of the basket. The ideal code is to encapsulate the production task and consumption task in the resource class. Because bread is the element of the basket container, it is not suitable to encapsulate in the bread class, and to encapsulate in the basket, so it is more convenient to operate the container.

Note that it is necessary to put all the code related to resource operation into the lock, otherwise the multi-threaded asynchronous problem will occur. For example, in the Producer class, the bread production method, produce(), is defined as a parameter of the basket.in() method, which is put into the basket, that is, basket.in(producer()), which is the wrong behavior, because produce() is passed to the in() method after it is executed outside the lock.

Posted by ThYGrEaTCoDeR201 on Sun, 03 Nov 2019 06:15:07 -0800