Java source code analysis and interview questions-Marvel at the interviewer: from shallow to deep handwriting queue

This series of related blog, Mu class reference column Java source code and system manufacturers interviewer succinctly Zhenti
below this column is GitHub address:
Source resolved: https://github.com/luanqiu/java8
article Demo: HTTPS: // GitHub. com / luanqiu / java8_demo
classmates can look at it if necessary)

Java source code analysis and interview questions-Marvel at the interviewer: from shallow to deep handwriting queue

Introductory language
Many large factories now require handwritten codes during interviews. I have seen a large factory interview that requires writing code online. The topic is: Without using the existing Java queue API, write a queue. Implemented, the data structure of the queue, the enqueue and dequeue methods are defined by yourself.

This question actually examines several points:

  1. Investigate whether you are familiar with the internal structure of the queue;
  2. Investigate your skills in defining APIs;
  3. Examine the basic skills of writing code, code style.

In this chapter, we will work with you to combine the above points and hand write a queue to familiarize yourself with the ideas and process. For the complete queue code, see: demo.four.DIYQueue and demo.four.DIYQueueDemo

1 Interface definition

Before implementing the queue, we first need to define the interface of the queue, which is the API we often say. The API is the facade of our queue. The main principle when defining is simple and easy to use.

The queue we implemented this time only defines two functions of putting data and taking data. The interface is defined as follows:

/**
* 定义队列的接口,定义泛型,可以让使用者放任意类型到队列中去
* author  wenhe
* date 2019/9/1
*/
public interface Queue<T> {
 
  /**
   * 放数据
   * @param item 入参
   * @return true 成功、false 失败
   */
  boolean put(T item);
 
  /**
   * 拿数据,返回一个泛型值
   * @return
   */
  T take();
 
  // 队列中元素的基本结构
  class Node<T> {
    // 数据本身
    T item;
    // 下一个元素
    Node<T> next;
 
    // 构造器
    public Node(T item) {
      this.item = item;
    }
  }
}

There are a few points we explain:

  1. When defining an interface, be sure to write comments, interface comments, method comments, etc., so that others will be a lot easier when looking at our interface ';
  2. When defining an interface, the naming needs to be concise and clear. It is best to let others know what the interface does as soon as it is named. For example, if we name it Queue, others will know that this interface is related to the queue at a glance;
  3. Use generics well, because we do n’t know exactly what values ​​are put in the queue, so we used generic T, which means we can put any value in the queue;
  4. There is no need to write a public method for the method in the interface, because the methods in the interface are all public by default, and you will be grayed out when you write the compiler, as shown below:
    Insert picture description here
  5. We define the basic element Node in the interface, so that if the queue subclass wants to use it, it can be used directly, increasing the possibility of reuse.

2 Queue subclass implementation

Then we will start to write subclass implementation, we are going to write a queue of the most commonly used linked list data structure.

2.1 Data structure

We use a linked list for the underlying data structure. When it comes to linked lists, you should immediately think of three key elements: linked list header, linked list tail, and linked list elements.

/**
 * 队列头
 */
private volatile Node<T> head;
 
/**
 * 队列尾
 */
private volatile Node<T> tail;
 
/**
 * 自定义队列元素
 */
class DIYNode extends Node<T>{
  public DIYNode(T item) {
    super(item);
  }
}

In addition to these elements, we also have the capacity of the queue container, the current size of the queue, the data lock, the data lock, etc. The code is as follows:

/**
 * 队列的大小,使用 AtomicInteger 来保证其线程安全
 */
private AtomicInteger size = new AtomicInteger(0);
 
/**
 * 容量
 */
private final Integer capacity;
 
/**
 * 放数据锁
 */
private ReentrantLock putLock = new ReentrantLock();
 
/**
 * 拿数据锁
 */
private ReentrantLock takeLock = new ReentrantLock();

2.2 Initialization

We provide two ways to use the default capacity (the maximum value of Integer) and the specified capacity, the code is as follows:

/**
 * 无参数构造器,默认最大容量是 Integer.MAX_VALUE
 */
public DIYQueue() {
  capacity = Integer.MAX_VALUE;
  head = tail = new DIYNode(null);
}
 
/**
 * 有参数构造器,可以设定容量的大小
 * @param capacity
 */
public DIYQueue(Integer capacity) {
  // 进行边界的校验
  if(null == capacity || capacity < 0){
    throw new IllegalArgumentException();
  }
  this.capacity = capacity;
  head = tail = new DIYNode(null);
}

2.3 Implementation of put method

public boolean put(T item) {
  // 禁止空数据
  if(null == item){
    return false;
  }
  try{
    // 尝试加锁,500 毫秒未获得锁直接被打断
    boolean lockSuccess = putLock.tryLock(300, TimeUnit.MILLISECONDS);
	if(!lockSuccess){
	  return false;
	}
    // 校验队列大小
    if(size.get() >= capacity){
      log.info("queue is full");
      return false;
    }
    // 追加到队尾
    tail = tail.next = new DIYNode(item);
    // 计数
    size.incrementAndGet();
    return true;
  } catch (InterruptedException e){
    log.info("tryLock 500 timeOut", e);
    return false;
  } catch(Exception e){
    log.error("put error", e);
    return false;
  } finally {
    putLock.unlock();
  }
}

The implementation of the put method has several points that we need to pay attention to:

  1. Pay attention to the rhythm of try catch finally, catch can catch many types of exceptions, we have caught timeout exceptions and unknown exceptions here, we must remember to release the lock in finally, otherwise the lock will not be automatically released, this must not be used wrong, Reflects the accuracy of our code;
  2. The necessary logical checks are still needed, such as the null pointer check whether the input parameter is empty, and the critical check whether the queue is full. These check codes can reflect the rigor of our logic;
  3. It is also very important to add logs and comments in key places of the code. We do n’t want to have key logic code comments and logs, which is not conducive to reading the code and troubleshooting problems;
  4. Pay attention to thread safety. In addition to locking, for the size of the capacity, we choose a thread-safe counting class: AtomicInteger to ensure thread safety;
  5. When locking, we better not use the permanent blocking method. We must use the blocking method with a timeout period. The timeout period we set here is 300 milliseconds, which means that if the lock has not been acquired within 300 milliseconds, The put method directly returns false, of course, you can set the time size according to the situation;
  6. Set different return values ​​according to different situations. The put method returns false. When an exception occurs, we can choose to return false or directly throw an exception;
  7. When putting data, it is appended to the end of the queue, so we only need to convert the new data into DIYNode and put it at the end of the queue.

2.4 Implementation of take method

The implementation of the take method is very similar to the put method, except that take takes data from the head. The code implementation is as follows:

public T take() {
  // 队列是空的,返回 null
  if(size.get() == 0){
    return null;
  }
  try {
    // 拿数据我们设置的超时时间更短
    boolean lockSuccess = takeLock.tryLock(200,TimeUnit.MILLISECONDS);
	if(!lockSuccess){
	    throw new RuntimeException("加锁失败");
	}
    // 把头结点的下一个元素拿出来
    Node expectHead = head.next;
    // 把头结点的值拿出来
    T result = head.item;
    // 把头结点的值置为 null,帮助 gc
    head.item = null;
    // 重新设置头结点的值
    head = (DIYNode) expectHead;
    size.decrementAndGet();
    // 返回头结点的值
    return result;
  } catch (InterruptedException e) {
    log.info(" tryLock 200 timeOut",e);
  } catch (Exception e) {
    log.info(" take error ",e);
  }finally {
      takeLock.unlock();
 }
  return null;
}

Through the above steps, our queue has been written, the complete code see: demo.four.DIYQueue.

3 Test

The API is written. Next, we will write some scene tests and unit tests for the API. Let's write a scene test to see if the API can run through. The code is as follows:

public class DIYQueueDemo {
	// 我们需要测试的队列
  	private final static Queue<String> queue = new DIYQueue<>();
	
	// 生产者
  	class Product implements Runnable{
    	private final String message;
 
    	public Product(String message) {
      		this.message = message;
    	}
 
    	@Override
    	public void run() {
      		try {
        		boolean success = queue.put(message);
        		if (success) {
          			log.info("put {} success", message);
          			return;
        		}
        		log.info("put {} fail", message);
      		} catch (Exception e) {
        		log.info("put {} fail", message);
      		}
    	}
  	}
	
	// 消费者
  	class Consumer implements Runnable{
    	@Override
    	public void run() {
      		try {
        		String message = (String) queue.take();
        		log.info("consumer message :{}",message);
      		} catch (Exception e) {
        		log.info("consumer message fail",e);
      		}
    	}
 	}
	
	// 场景测试
  	@Test
  	public void testDIYQueue() throws InterruptedException {
    	ThreadPoolExecutor executor =
        	new ThreadPoolExecutor(10,10,0,TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
    	for (int i = 0; i < 1000; i++) {
        	// 是偶数的话,就提交一个生产者,奇数的话提交一个消费者
        	if(i % 2 == 0){
          		executor.submit(new Product(i+""));
          		continue;
        	}
        	executor.submit(new Consumer());
    	}
    	Thread.sleep(10000);
  	}

The code test scenario is relatively simple, looping from 0 to 1000. If it is even, let the producer produce the data and put it in the queue. If it is odd, let the consumer take the data out of the queue to consume and run The following results are as follows:
Insert picture description here
From the results shown, the DIYQueue we wrote does not have much problems. Of course, if you want to use it on a large scale, you need detailed unit tests and performance tests.

4 Summary

Through the study of this chapter, I don't know if you have a feeling that the queue is very simple. In fact, the queue itself is very simple, not as complicated as imagined.
As long as we understand the basic principles of queues and understand several commonly used data structures, the problem of handwriting queues is actually not big. You should try it quickly.

Published 40 original articles · won praise 1 · views 4979

Guess you like

Origin blog.csdn.net/aha_jasper/article/details/105525921