Log service for producers and consumers of practical reading notes in java Concurrent Programming

In daily development, we often deal with the log system, which is a typical producer consumer model system. (multiple different) producer threads are responsible for producing log messages. These systems play the role of producer. The log system is responsible for uploading the logs in the message queue to the server, which belongs to the role of consumer. It is a design method of multi producer and single consumer mode. If the producer's production speed is greater than the LoggerThread's consumption speed, the BlockingDeque thread will block the producer until the LoggerThread has the ability to process new messages.

Closed producer consumer log service (bug version) is not supported

public class LogService {
    private final BlockingQueue<String> queue;
    private final LoggerThread loggerThread;

    public LogService() {
        //Note that the capacity of the queue is 10
        queue = new LinkedBlockingQueue<>(10);
        loggerThread = new LoggerThread();
    }
    public void start() {
        loggerThread.start();
    }

    /**
     * The producer produces messages. The caller of each log is OK, which is equivalent to one producer
     *
     * @param msg
     * @throws InterruptedException
     */
    public void log(String msg) throws InterruptedException {
        queue.put(msg);
    }

    /**
     * The consumer thread consumes messages, and the thread plays the role of consumer.
     */
    private class LoggerThread extends Thread {

       public  LoggerThread(){
        	
        }
        public void run() {
            try {
                while (true) {//Infinite loop
                   System.out.println(queue.take());
                }//end while
            } catch (InterruptedException e) {//Response interrupt out of loop
            } finally {
            	System.out.println("log service is close");
            }
        }
      }
    }

The above code looks normal, but it has the following disadvantages:
1. LoggerThread does not provide a shutdown method. When there is no producer production message, LoggerThread will always be blocked when calling queue.take(), so that the JVM cannot be shut down normally. For example, the following calling code will cause the LoggerThread to fail to close.

 public static void main(String[] args) {
    	LogService logService = new LogService();
    
    	for(int i=0;i<100;i++) {
    		final int index =i;
    		new Thread(new Runnable() {

				@Override
				public void run() {
				
					try {
						logService.log("Producer production message===="+index);
					} catch (InterruptedException e) {
						System.out.println("response interception");
					}
					
				}
    			
    		}).start();
    	}
    	
    	logService.start();
    }

It's easy to close LoggerThread. Just change the LoggerThread method to the following, right?

 public void run() {
            try {
                while (!queue.isEmpty()) {//Judge whether the queue is empty. If it is empty, it will be closed
                   System.out.println(queue.take());
                }
            } catch (InterruptedException e) {
            	System.out.println("response interception");
            } finally {
            	System.out.println("log service is close="+queue.size());
            }
        }

In the above changes, we changed while(true) to while(!queue.isEmpty()). Is that ok? The answer is no! In our demo, the size of the queue is limited. If the consumer's consumption speed is greater than the producer's speed, the queue becomes empty at a certain moment, that is, queue.isEmpty() is true. At this time, the consumer is closed, but the producer can continue to produce. When the queue is full, the producer will block. The most reliable way to cancel a producer consumer model is to cancel both producers and consumers, because when consumers are closed separately, producers will be blocked; When producers are shut down individually, consumers will be blocked. The closing principle should be: we should provide a stop method. When we call the stop method, it means that the log service has been closed. At this time, the producer can not reproduce messages. The log service will not be officially closed until the consumer consumes the messages in the queue to avoid log loss,

Let's look at the following code. We provide a stop method for LogService and modify the log method. When isShutDown is true, messages cannot be generated in production. However, this seemingly perfect operation has a huge loophole. The stop method is not atomic, and the if(!isShutdown) is not atomic, making the shutdown method unreliable

  private boolean isShutdown=false;
    public void stop(){
        isShutdown=true;
    }
    public void log(String msg) throws InterruptedException {
        if(!isShutdown){
            queue.put(msg);
        }
    }

So we need to transform. Just make it atomic. Our transformation is as follows

Support for closed producer consumer log service (unsafe version)

Use the synchronized keyword to transform stop into atomic. It should be noted that because the put method itself can be blocked, we don't need to hold a lock when the message is added to the queue, so we put the put method outside the synchronized statement block.

    private boolean isShutDown = false;
    public void stop(){
    	synchronized(this) { isShutDown=true;}
    }

    /**
     * The producer produces messages. The caller of each log is OK, which is equivalent to one producer
     *
     * @param msg
     * @throws InterruptedException
     */
    public void log(String msg) throws InterruptedException {
    	synchronized(this) {
    		if(isShutDown) {
    			return;
    		}
    	}
    	queue.put(msg);   	
    }

With this improvement, when we call stop, the producer thread will not continue to produce messages. We can transform LoggerThread as follows:

 public void run() {
     try {
          while (true) {
          	if(isShutDown) {//Exit if closed
          		break;
          	}
             System.out.println(queue.take());
          }
      } catch (InterruptedException e) {//Note that we really respond to interrupts outside the while loop.
      	System.out.println("response interception");
      } finally {
      	System.out.println("log service is close="+queue.size());
      }
  }

So is that all right? The answer is that the problem is big. For example, when we call the stop method, the LoggerThread consumer is blocked in queue.take(), then if(isShutDown) {break} will never be executed, and LoggerThread cannot exit. The correct approach is that we need to transform the stop method again, as follows:

    public void stop(){
    	synchronized(this) { isShutDown=true;}
    	loggerThread.interrupt();
    }

We added loggerThread.interrupt();, At this time, when the take method is blocked, it will respond to the interrupt, because if we respond to the interrupt outside the while loop, we will jump out of the while loop and print as follows:

  public static void main(String[] args) {
    	LogService logService = new LogService();
    
    	for(int i=0;i<90;i++) {
    		final int index =i;
    		Thread thread=new Thread(new Runnable() {
				@Override
				public void run() {				
					try {				
						logService.log("Producer production message===="+index);	
					} catch (InterruptedException e) {
						System.out.println("response interception");
					}				
				}		
    		});
    		thread.start();
    	}
    	
    	logService.start();
    	try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {		
		}
		//Shut down the logging system after two seconds
    	logService.stop();
    }

Print as follows:

response interception
log service is close=0

So is there no problem at this point? Of course, there are still problems. If we respond to an interrupt outside the while loop and there are still messages in the message queue, we can't continue to process the remaining logs. Then the shutdown of the log service is bound to cause the loss of logs. If the Key log information is lost, we have no place to cry.

Therefore, our log service system still has room for optimization.

Support for closed producer consumer log service (safe version)

Our goal is to process as many messages as the producer produces. When the logging service is shut down, the producer cannot produce messages. After the consumer has processed all the messages, it closes itself.
Therefore, we continue to transform and use a variable logCount. For each call of log, logCount+1; Note that the logCount increment operation needs to be atomic.
Therefore, we have modified the log method as follows:

    private int logCount=0;
    public void log(String msg) throws InterruptedException {
    	synchronized(this) {
    		if(isShutDown) {
    			return;
    		}
    		//Message increment
    		logCount++;
    	}
    	queue.put(msg);
    	
    }

After processing the producer, we need to transform the consumer. Note that the consumer before LoggerThread captures the InterruptedException outside the while loop, and we need to put it inside the loop now. In this way, we can correctly handle interrupts and ensure that the messages in the queue are processed. The modified LoggerThread is as follows:

So far, this blog post is over. When reading the chapter of Java Concurrent Programming practice, I still have some questions about why it is written like this. Until I finish writing this blog and knock on the program to verify it, I can fully understand its significance. Sure enough, blogging is very helpful. At least this blog is very helpful for bloggers to deeply understand java concurrent programming.

Tags: Java

Posted on Sat, 06 Nov 2021 12:43:47 -0400 by rochakchauhan