以下为牛客网精选面试题,网上资料整理解答。
钉钉一面面经
1.项目细节。
2.字符串连接的几种方式,区别。
(1)String的连接方法
可以看出连接方式是新建了一个包含两个长度的字符数组,然后进行连接。
(2)StringBuilder中存储字符串其实用的是一个char数组,capacity其实就是指定这个char数组的大小,StringBuilder的连接方法是继承AbstractStringBuilder的方法的,线程不安全的。
在append(str)函数调用的时候,首先会判断原来用于存储字符串的values的字符串数组有没有足够的大小来存储将要新添加入StringBuilder的字符串。如果不够用,那么就调用expandCapacity(int minimumCapacity)让容量翻两倍(一般是扩大两倍)
(3)StringBuffer的连接方法,利用了同步(synchronized关键字),线程安全的,但这样会相对的降低速度。
(4)“+”号拼接
利用+进行拼接的时候会将+号的拼接方式变换成StringBuilder的append方式,也就是说每次利用+会建立一个StringBuilder。因此,在循环内部意味着每执行一次循环,就会创建一个StringBuilder对象。因此循环里面不要用+号,在循环外面建立一个StringBuilder或StirngBuffer。
3.自己实现线程池,如何实现。
(1)设置一个生产者消费者队列,作为临界资源
(2)初始化n个线程,并让其运行起来,加锁去队列取任务运行
(3)当任务队列为空的时候,所有线程阻塞
(4)当生产者队列来了一个任务后,先对队列加锁,把任务挂在到队列上,然后使用条件变量去通知阻塞中的一个线程
任务类
public class Task {
private int id;
private Runnable job;
public Task(Runnable job) {
this.job = job;
}
public Task(int id, Runnable job) {
this.id = id;
this.job = job;
}
public void job() {
job.run();
}
public int getid() {
return id;
}
}
线程池类
public class ThreadPoolExecutor {
//线程池的容量
private int poolSize = 0;
//核心线程的数量
private int coreSize = 0;
//阻塞队列
private BlockingQueue<Task> blockingQueue;
//是否关闭线程池,用 volatile 保证可见性,确保线程可以及时关闭
private volatile boolean shutdown = false;
/**
* @title: ThreadPoolExecutor
* @description: 构造方法
* @param: @param size 线程池容量
*/
public ThreadPoolExecutor(int poolsize) {
this.poolSize = poolsize;
blockingQueue = new LinkedBlockingQueue<Task>();
}
/**
* @title: execute
* @description: 添加任务
* @author: JerryG
* @param task 要添加的任务
* @throws InterruptedException
*/
public void execute(Task task) throws InterruptedException {
//判断线程池是否关闭
if(shutdown == true) {
return;
}
//判空
if(task == null) {
throw new NullPointerException("ERROR:传入的task为空!");
}
if(coreSize < poolSize) {
//如果核心线程数小于线程池容量,将任务加入队列并新建核心线程
blockingQueue.put(task);
addWorker(task);
}else {
//否则,只将任务加入队列
blockingQueue.put(task);
}
}
/**
*
* @title: addWorker
* @description: 添加真正用于执行任务的线程
* @author: JerryG
* @date: 2019年8月22日 下午2:28:31
* @param task
* @throws:
*/
public void addWorker(Task task) {
Thread thread = new Thread(new Worker());
thread.start();
coreSize ++;
}
/**
*
* @title: showdown
* @description: 停止线程池
* @author: JerryG
*/
public void showdown() {
shutdown = true;
}
/**
* @description:具体进行工作的线程
* @author:JerryG
*/
class Worker implements Runnable{
@Override
public void run() {
while(!shutdown) {
try {
//循环从队列中取出任务并执行
Task task = blockingQueue.take();
task.job();
System.out.println("taskid = " + task.getid() + " 执行完毕" );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
关于队列的选择
之所以选择 LinkedBlockingQueue 原因如下:
LinkedBlockingQueue 底层是基于链表的,如果不指定容量,其最大存储容量将是Integer.MAX_VALUE,几乎可以认为是一个“无界”的队列,由于其节点的创建都是动态创建,并且在节点出队列后可以被GC所回收,因此其具有灵活的伸缩性。任务多的情况下,如果使用一个有界的阻塞队列(例如ArrayBlockingQueue)来进行处理,那么就非常有可能很快导致队列满的情况发生。
LinkedBlockingQueue的读取和插入操作所使用的锁是两个不同的lock,它们之间的操作互相不受干扰,因此两种操作可以并行完成,因此其吞吐量较高;
实现代码链接:https://www.jianshu.com/p/f177522debe7
4.如何防止重复下单。
流程:
①当进入商品详情页时,去生成一个全局唯一ID(可用雪花算法);
②将这个全局唯一ID和订单信息传给服务器;
③判断这个ID对应的订单号存在,则直接返回;
④生成订单号,保存订单信息;
5.String为什么要设计为final的?
(1)将方法或类声明为final主要目的是:确保它们不会在子类中改变语义。String类是final类,这意味着不允许任何人定义String的子类。
String基本约定中最重要的一条是immutable。
但是假如String没有声明为final, 那么你的StringChilld(声明为final就不可被继承)就有可能是被复写为mutable的,这样就打破了成为共识的基本约定。
(2)String源码前几行
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
String类用final关键字修饰,说明String不可继承。
字段value 是char【】数组,用final修饰,说明value这个引用地址不可变,但是Array数组是可变的。
栈指针不可变,但是堆上的数据本体可以变。
(3)不可变有什么好处
安全:多线程下对资源做写操作有危险。不可变对象不能被写,所以线程安全。
可以共用一个实例(在多线程中共享一个不可变对象而不用担心线程安全问题):当代码中出现字面量形式创建字符串对象时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用。String one = “someString”;String two = “someString”,都用字面量“someString赋值”,他们其实都指向同一个内存地址。当代码中出现字面量形式创建字符串对象时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用。(PS:运行时常量池是方法区的一部分,用于存放各种字面量和符号引用)
String是几乎每个类都会使用的类,特别是作为Hashmap之类的集合的key值时候,mutable的String有非常大的风险。
参考链接:https://www.cnblogs.com/chengdabelief/p/7503987.html