钉钉一面面经整理

以下为牛客网精选面试题,网上资料整理解答。

钉钉一面面经

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有非常大的风险。

扫描二维码关注公众号,回复: 9259412 查看本文章

参考链接:https://www.cnblogs.com/chengdabelief/p/7503987.html

发布了18 篇原创文章 · 获赞 4 · 访问量 922

猜你喜欢

转载自blog.csdn.net/weixin_43698561/article/details/104228824