2022年java知识点汇总,面试大全!超级全面,逐步完善!

文章目录

一、java基础知识

1.1 String类相关

  1. String、StringBuilder和StringBuffer

​ String是不可变的,后两者可变,StringBuffer线程安全,StringBuilder线程不安全,但是StringBuilder效率高;

​ 另外,StringBuffer和Stringbuilder都没有重写equals方法,在进行equals比较时,还是比较的是地址,而不是值

public static void main(String[] args) {
        String a="abc";
        String b="abc";
        System.out.println(a==b);
        System.out.println(a.equals(b));
}

​ 结果都是:true true ,因为都在常量池中,比较的是值,地址也相同;

 public static void test1(){
        String a="abc";
        String b=new String("abc");
        System.out.println(a==b);
        System.out.println(a.equals(b));
    }

​ 此时结果是:false true,==比较的是地址,equals比较的是值;

  String t0 = new String("hello") + new String("world");
        t0.intern();
        String t1 = "helloworld";
        System.out.println(t0 == t1);
        
这个运行结果,在JDK1.8中是true,1.7中是false,JDK1.7之前的版本中,intern方法会优先在方法区的运行时常量池中查找是否已经存在相同的字符串,倘若已经存在,则返回已存在的字符串,否则则在常量池中添加一个字符串常量,并返回字符串。从JDK1.7开始,HotSpot虚拟机将字符串常量移至Java Heap,intern方法的实现也发生了变化,首先还是会先去查询常量池中是否已经存在,如果存在,则返回常量池中的字符串,否则不再将字符串拷贝到常量池,而只是在常量池中保存字符串对象的引用。

  1. String几个经典方法作用解析
equals(Object obj): 比较字符串的内容是否相同,区分大小写;
equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写
contains(String str):判断大字符串中是否包含小字符串
startsWith(String str):判断字符串是否以某个指定的字符串开头
endsWidth(String str):判断字符串是否以某个指定的字符串结尾
isEmpty():判断字符串是否为空
  1. 为什么String用final 修饰
为了安全,多个线程对String进行读取时,不会发生线程安全问题,因为final是不可变性;
还能保证Arrays数组的安全;
实现字符串常量池,可提高效率,实现对内存的优化;
标记为final。不可被继承,保证String类型的对象只能是String类型,不会是其他子类型;
String 对象是缓存在String池中,由于缓存的字符串是在多个客户端之间共享,因此存在风险,因此通过String类不可变来规避这种风险

  1. String类常用方法
char charAt(int index);//返回指定索引处的char值

int compareTo(Object o);//把这个字符串跟另一个字符串比较

String concat(String str);//连接两个字符串

boolean contentEquals(StringBuffer sb);//当且仅当字符串与指定的StringBuffer有相同顺序的字符时候返回真

String substring(int beginIndex);
String substring(int beginIndex,int endIndex); //截取字符串

int indexOf(String str, int fromIndex): 返回从 fromIndex 位置开始查找指定字符在字符串中第一次出现处的索引,如果此字符串中没有这样的字符,则返回 -1。
例:获取指定字符串中某个子串出现的次数
public static int strNum(String obj,String str){
        int fromIndex = 0;
        int count =0;
        while (true){
            int index = str.indexOf(obj,fromIndex);
            if(-1 != index){
                fromIndex = index +1;
                count ++;
            }else {
                break;
            }
        }
        return count;
}

  1. String类可以有多长
   临时变量一般是存在java堆中的,String类的长度理论上取决于传入的byte数组长度,byte[]数据数组最大长度理论上应该是Integer.MAX_VALUE,但是实际从ArrayList源码可以看出,应该是Integer.MAX_VALUE-8,但是还是会受到java堆可分配内存大小的限制
   如果String变量是一个全局变量,其变量是存在java方法区的,此时他的长度取决于.class描述全局String类型变量的数据结构,如果是u2(描述是2个字节的数据类型),这意味着最大是65535。
   如果是字符数,一个UTF-8编码的字符对应3个字节,所以此时可容纳的字符数应该是65535/3;
   

  1. HashCode、equals、==区别?

    equals() 定义在JDK的Object中,通过判断两个对象的地址是否相等(即,是否是同一个对象)来区分它们是否相等,默认的equals方法等价于“==”,所以一般要重新equals方法,若两个对象的内容相等,则返回true,否则返回false

    在String中,“==”用来比较两个变量的内存地址是否相同,而equals用来比较内容是否相同(因为String重写了equals方法)

        String a="abc";
        String b="abc";
        String c =new String("abc");
        System.out.println(a==b); // true
        System.out.println(a.equals(b));//true
        System.out.println(a==c);//false
        System.out.println(a.equals(c)); //true

HashCodes()是获取哈希码,确定该对象在哈希表中的位置,仅当创建了某个类的散列表,该类的hashCode()才有用,

在散列表中,若两个元素相等,那么他们的散列码一定相同,但是反过来,若两个对象相等,他们的hashCode()并不一定相等;

1. 2 多线程

1.2.1 实现多线程的方式

​ 主要有5种方式,继承Thread类,实现Runnable接口,实现Callable接口通过FutureTask包装器来创建Thread线程,使用线程池 接口ExecutorService结合Callable、future实现有返回结果的多线程,前两种没有返回值,后两种有返回值,还可以使用Spring的@Async注解非常方便的实现多线程。

  1. 继承Thread类重写run方法创建线程

    Thread 类本质上是实现了Runnable接口的一个实例,启动线程是通过Thread类的start()方法,start方法是一个native方法,将 会启动一个新线程,并执行run()方法;

public class Mythread extends Thread {

    @Override
    public void run() {
        System.out.println(Mythread.currentThread().getName());
    }

    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        mythread.start();

        Mythread mythread1 = new Mythread();
        mythread1.start();
    }
}
结果:
Thread-0
Thread-1
Process finished with exit code 0
  1. 实现Runnable接口
    如果自己的类已经继承了另外一个类,则无法再继承,此时需要实现接口
public class RunnableTest  extends ParentDemo implements Runnable{

    @Override
    public void testDemo() {
        System.out.println("123");
    }

    @Override
    public void run() {
        System.out.println("这是Runnable执行结果");
    }

    public static void main(String[] args) {
        RunnableTest runnableTest=new RunnableTest();

        Thread thread=new Thread(runnableTest);
        thread.start();
    }
}

​ 当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run();

​ 继承Thread类存在局限性,因为java是单继承多实现,所以如果继承Thread类就不能在继承其它类, 其次,Thread类更适合开启多个线程完成多个任务的场景,而实现Runnable接口更适合一个任务多个线程去完成的场景。

  1. 实现Callable接口通过FutureTask包装器来创建Thread线程
public class CallAbleTest implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 10000; i++) {
            sum += i;
        }
        return sum;
    }

    public static void main(String[] args) {
        CallAbleTest callAbleTest = new CallAbleTest();
        FutureTask<Integer> result = new FutureTask<>(callAbleTest);

        new Thread(result).start();
        new Thread(result).start();

        try {
            Integer sum = result.get();
            System.out.println(sum);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

​ Callable 对象实际属于Executor框架的功能类;
​ Callable 可以在任务结束的时候提供一个返回值,Runnable没有;
​ Callable 中的call方法可以抛出异常,Runnable不能;
​ 运行Callable可以拿到一个future对象,future对象表示异步计算的结果,可提供检查运算是否结束的方法,由于线程属于异步计算模型,所以无法从其他线程中得到方法的返回值,在这种情况之下,就可以使用future来监视目标线程调用call方法的情况,当调用future的get方法以获取结果时,当前线程就会被阻塞,直到call方法结束返回结果

  1. 使用线程池接口ExecutorService结合Callable、Future实现有返回结果的线程
public class ExecutorTest {

    static class MyCallable implements Callable<Object> {
        private String taskNum;

        MyCallable(String taskNum) {
            this.taskNum = taskNum;
        }

        @Override
        public Object call() throws Exception {
            System.out.println(">>>>" + taskNum + "开启任务");
            Thread.sleep(1000);
            System.out.println(">>>>" + taskNum + "任务结束");
            return taskNum;
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("开始执行-------");
        //创建一个线程池
        ExecutorService pool = Executors.newFixedThreadPool(5);
        //创建多个有返回值的任务
        List<Future> list = new ArrayList<Future>();
        for (int i = 0; i < 5; i++) {
            Callable c = new MyCallable(i + " ");

            //执行任务并获取Future对象
            Future f = pool.submit(c);
            list.add(f);
        }
        //关闭线程池
        pool.shutdown();

        for (Future f : list) {
            System.out.println(">>>" + f.get().toString());
        }
        System.out.println("程序运行结束--------");
    }
}

​ Executors类提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口,ExecutorService中提供submit()方法,传递一个Callable,或者Runnable,返回Future,如果Executor没有完成Callable的计算,调用返回Future对象的get()方法将阻塞,直到计算完成。

  1. Spring的@Async注解实现多线程

​ 首选需要配置线程参数

#核心线程数,当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程,设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时退出
spring.task.pool.corePoolSize=20
#最大线程数,当线程数大于等于corePoolSize,且任务队列已满时,线程池会创建新线程来处理任务
spring.task.pool.maxPoolSize=60
#线程空闲时间,当线程空闲时间达到keepAliveSeconds(秒)时,线程会退出,直到线程数量等于corePoolSize,如果allowCoreThreadTimeout=true,则会直到线程数量等于0
spring.task.pool.keepAliveSeconds=1
#任务队列容量,当核心线程数达到最大时,新任务会放在队列中排队等待执行
spring.task.pool.queueCapacity=400

​ 创建线程池配置类

@Configuration
@EnableAsync
public class AsyncTaskConfig {

    @Autowired
    private ThreadPoolCOnfig threadPoolCOnfig;

    public ThreadPoolTaskExecutor myThreadPool(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        //设置参数
        executor.setCorePoolSize(threadPoolCOnfig.getCorePoolSize());
        executor.setMaxPoolSize(threadPoolCOnfig.getMaxPoolSize());
        executor.setQueueCapacity(threadPoolCOnfig.getQueueCapacity());
        executor.setKeepAliveSeconds(threadPoolCOnfig.getKeepAliveSeconds());
        executor.setAllowCoreThreadTimeOut(true);
        executor.setThreadNamePrefix("MyThreadPool");

        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

​ 目前String已经实现5种线程池

1. SimpleAsyncTaskExecutor :不是真的线程池,这个类不重用线程,默 认每次调用都会创建一个新的线程。
2. SyncTaskExecutor: 这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
3. ConcurrentTaskExecutor: Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个    类。
4. SimpleThreadPoolTaskExecutor: 是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此    类。
5. ThreadPoolTaskExecutor : 最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor的包装。

@Async正常使用默认线程池,为SimpleAsyncTaskExecutor,一般有两种调用,一种是无返回值的调用,一种是有返回值的Future调用。

基于@Async无返回值调用,直接作用在类,使用方法上,加上@Async注解;
有返回值的Future调用,返回类型设置为Future<Object>,需要我们在方法中捕获异常并处理,或者在调用方调用Future.get时捕获异常进行处理

有返回值CompletableFuture调用,在jdk1.8中提供了强大的Future拓展功能,可以简化异步编程的复杂性,并且提供函数式编程能力

private static final ThreadPoolExecutor SELECT_POOL_EXECUTOR = new ThreadPoolExecutor(10, 20, 5000,
            TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), new ThreadFactoryBuilder().setNameFormat("selectThreadPoolExecutor-%d").build());
         // tradeMapper.countTradeLog(tradeSearchBean)方法表示,获取数量,返回值为int
         // 获取总条数
         CompletableFuture<Integer> countFuture = CompletableFuture.supplyAsync(() -> tradeMapper.countTradeLog(tradeSearchBean), SELECT_POOL_EXECUTOR);
         // 同步阻塞
         CompletableFuture.allOf(countFuture).join();

         // 获取结果
         int count = countFuture.get();

​ 默认线程池存在比较多的弊端,一般不用Executors去创建,推荐使用ThreadPoolExecutor方式,Executors方法各个弊端如下

newFixedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
newCachedThreadPool和newScheduledThreadPool:要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

@Async 默认是使用SimpleAsyncTaskExecutor,该线程池默认是来一个任务创建一个线程,如果系统不断创建线程,最终会导致占用内存过高,引发OutOfMemoryError错误,但是它本身也存在一个限流机制,ConcurrencyLimit属性,其大于等于0,开启限流机制,等于-1,关闭限流

@Async 存在自定义线程池,方法如下
实现接口AsyncConfigurer
继承AsyncConfigurerSupport
配置自定义的TaskExecutor配置参数代替内置的任务执行器
不管是继承还是重新实现接口,都需要指定一个线程池,并且重新实现getAsyncExecutor()方法

1.2.2 线程状态结构图

2020126155605111.jpg

初始状态(new):new一个线程实例,线程进入初始状态;

就绪状态(ready):

  • 调用线程的start()方法,线程进入就绪状态,当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态;
  • 当前线程时间片段用完了,调用当前线程的yield()方法,当前线程进入就绪状态;
  • 锁池里的线程拿到对象锁后,进入就绪状态;

运行中状态(Running):线程调用run方法,进入运行态;

阻塞状态(Blocked): 线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态;

等待(waiting): 此状态的线程不会被CPU分配执行时间,要等待被显示的唤醒,否则将无限等待;

超时等待(TIMED_WAITING): 和等待状态类似,但是此状态不需要无限期等待,当达到一定时间,会被自动唤醒;

终止状态(TERMINATED): 当线程run()方法完成,或者主线程main()方法完成,我们就可以认为是终止的,一旦终止,不能复生,此时调用start()方法会抛出异常;

1.2.3 线程常用方法

start(): 用于开启多线程,使线程处于就绪态,当CPU分配时间片后可 执行run()方法,并且start()方法是一个synchronized修饰的方法,而run只是一个普通方法,start()可实现并发

run(): 是一个普通方法,当线程调用了start()方法,一旦获取CPU调度,处于运行态,线程就会调用这个run();

wait(): 线程等待,让线程进入阻塞状态,2种方式,一种无限等待,需要使用notify()方法唤醒,另外一种,设置等待时间,时间一到自动唤醒wait(long time), wait()方法属于Object方法,释放CPU执行权,同时释放锁,同时wait()方法只能在同步代码方法中或者同步代码块中使用,使用时需要捕获InterruptedException异常

sleep(): 强迫一个线程睡眠N毫秒,释放CPU资源,不释放锁,使用地点也不需要限制,但是需要捕获异常,而且sleep()是Thread类的方法

isAlive(): 判断一个线程是否存活

currentThread(): 获取当前线程

yield(): 线程礼让,暂停当前正在执行的线程对象,虽然让出CPU时间,但是不会让线程阻塞,线程任然处于可执行状态,随时可再分的CPU执行时间

getPriority()与setPriority(): 设置获取线程优先级

getId():

join(): 线程自闭,等待其他线程终止,在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪。

package com.example.day1.one;

public class Mythread extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(Mythread.currentThread().getName());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Mythread mythread = new Mythread();
        Mythread mythread1 = new Mythread();
        Mythread mythread2 = new Mythread();

        mythread.start();
        //mythread.join()
        
        mythread1.start();
        //mythread1.join()
        
        mythread2.start();
        //mythread2.join()

    }
}

结果:如果不加join(),在第一个线程还没结束的时候,第二个或者第三个就可能开始执行,如果加上join(),一定是第一个执行完,第二个执行,然后第三个执行

interrupted(): 判断是否被中断,并清除当前中断状态;

interrupt(): 中断线程,设置中断标志位;

isInterrupted(): 判断是否被中断;

isDaemon()与setDaemon(boolean on): 设置一个线程为守护线程(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)

stop():

notify(): 唤醒一个线程继续运行

notifyAll(): 唤醒所有线程进入争抢锁状态

1.2.4 线程调度

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有:协同式线程调度和抢占式线程调度;

协同式线程调度:线程的执行时间由线程本身控制,线程把自己工作执行完成之后,要主动通知系统切换到另外一个线程上

抢占式线程调度: 每个线程都将由系统来分配时间,线程的切换不由线程本身决定

1.2.5 线程池原理

线程池作用:
降低系统资源消耗,通过重用已存在的线程,降低线程创建与销毁造成的消耗;
提高系统响应速度,有任务到达,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
方便线程并发数管理;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hHAh7hpE-1618197750701)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210301204725267.png)]

​ submit(task): 可用来提交Callable或Runnable任务,并返回代表此任务的Future对象

​ shutdown(): 在完成已提交的任务后封闭办事,不在接管新任务

​ shutdownNow(): 停止所有正在履行的任务并封闭办事;

​ isTerminated(): 测试是否所有任务都履行完毕了;

​ ThreadPoolExecutor参数解析

Ctl: 对线程池的运行状态和线程池中有效的数量进行控制的一个字段,包含2部分信息:线程池的运行状态(runState)和线程池内有效线程的数量(workerCount)

五种状态:Runing:可接受新任务,并且处理已经添加的任务,线程池的初始化状态就是running
shutdown: 不接受新任务,但是可以处理已经添加的新任务,调用shutdown()方法,由running变成shutdown
stop: 不接受新任务,不处理已添加的任务,并且还会中断正在处理的任务;
tidying: 当所有任务终止,ctl记录的任务数为0,线程池就会变成tidying状态,此时会执行钩子函数terminated();
terminated: 当线程池处于tidying状态并且执行了terminated()方法,就会变成此状态;

参数解析
corePoolSize:线程池核心线程数,如果当线程池中的线程数为corePoolSize,继续提交的任务将被保存到"阻塞队列中",等待被执行,如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程
maximumPoolSize:线程池中允许的最大线程数,如果当前阻塞队列满了,且继续提交任务,则创建新的线程任务,前提是当前线程数少于maximumPoolSize;
keepAliveTime: 线程池维护线程所允许的空闲时间,当线程池中的线程数大于corePoolSize的时候,如果此时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;
unit: keepAliveTime的单位
workQueue: 用来保存等待被执行的任务的阻塞队列,任务必须实现Runnable接口,有如下几种阻塞队列
   1. ArrayBlockingQueue:基于数组结构的有界阻塞队列,按   FIFO排序任务;
   2. LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务
   3. SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态;
   4. priorityBlockingQuene:具有优先级的无界阻塞队列;
   5. DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列
   6. LinkedTransferQueue:由链表构成的无界阻塞队列;
   7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列,队列头部和尾部都可以添加和移除元素,多线程并发时,可将锁的竞争降到一半;

threadFactory :ThreadFactory,用来创建新线程。默认使用 Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。
Handler:线程池的拒绝策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
  1、AbortPolicy:直接抛出异常,默认策略;
  2、CallerRunsPolicy:用调用者所在的线程来执行任务;
  3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
  4、DiscardPolicy:直接丢弃任务;
上面的4种策略都是ThreadPoolExecutor的内部类。 当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义策略。

​ **注意:**通过execute或者submit向线程池提交任务。任务提交的时候先提交给核心线程(corePoolSize);

​ 如果核心线程满了,就将任务放到workQueue里面去排队等待;如果队列也满了(取决用的什么队列,以及

​ 设置的大小),就会将新进来的任务提交给非核心线程,非核心线程数量等于maximumPoolSize -

​ corePoolSize,非核心线程使用之后会被回收。如果非核心线程也满了,那么就执行相应的拒绝策略

​ RejectedExecutionHandler。

​ 原理解析

1. execute方法解析:先获取当前工作线程数,-->添加任务到核心线程上去执行-->核心线程数满了,线程池状态是running,则添加到任务队列中去-->添加任务之后,保证线程池状态还是running,否则移除刚才添加的任务,并知心拒绝策略;

2. 关键在于AddWork方法,先判断状态是否是running,shutdown时队列是否有数据,判断工作线程是否大于容量;

3. worker内部类,继承AbstractQueuedSynchronizer并且实现了Runnable接口


1.2.6 几种常见线程池

​ newFixedThreadPool (固定数目线程的线程池)

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

​ 核心线程数和最大线程数大小一样;

​ 没有所谓的非空闲时间,keepAliveTime为0;

​ 阻塞队列为无界阻塞队列LinkedBlockingQueue;

特点

​ 如果线程数少于核心线程数,创建核心线程执行任务;

​ 等于核心线程数,把任务添加到阻塞队列;

​ 任务执行完毕去阻塞队列获取任务继续执行;

ExecutorService executor = Executors.newFixedThreadPool(10);
                    for (int i = 0; i < Integer.MAX_VALUE; i++) {
                        executor.execute(()->{
                            try {
                                Thread.sleep(10000);
                            } catch (InterruptedException e) {
                                //do nothing
                            }
            });

​ 因为是无界阻塞队列,最大可容纳INTEGER.MAX个,可能会导致OOM,适用于处理CPU密集型的任务,适用

​ 于执行长期的任务

​ newCachedThreadPool(可缓存线程的线程池)

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }

特点

​ 核心线程数为0;

​ 最大线程数为Integer.MAX_VAULE,阻塞队列是SynchronousQuene;

​ 非核心线程空闲存活时间为60秒;

ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName()+"正在执行");
            });
        }

​ 用于执行大量短期的小任务;

​ newSingleThreadExecutor(单线程的线程池)

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

特点:

​ 核心线程数和最大线程数都是1;

​ 阻塞队列是LinkedBlockingQueue;

​ keepAliveTime 为0

​ 适用于串行执行任务的场景,一个任务一个任务的执行

​ newScheduledThreadPool(定时及周期执行的线程池)

 public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

特点:

​ 最大线程数为Integer.MAX_VALUE

​ 阻塞队列是DelayedWorkQueue

​ keepAliveTime为0

​ scheduleAtFixedRate() :按某种速率周期执行

​ scheduleWithFixedDelay():在某个延迟后执行

技能提升

为什么不用Executors创建线程池?

​ 因为LinkedBlockingQueue.offer方法,如果使用LinkedBlockingQueue队列而不限制大小的话,就是一个无

​ 边界的队列,会导致内存溢出问题,一般采用构造函数ThreadPoolExecutor来创建,并指定容量,另外还可

​ 以使用开源库,apache的guava,可以提供ThreadFactoryBuilder来创建线程池

public class ExecutorsDemo {

    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();

    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {

        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            pool.execute(new SubThread());
        }
    }
}

1.2.7 线程池参数设计规则

​ 首先需要理解一些概念:

​ CPU密集型,也叫计算密集型,指的是系统的硬盘,内存性能相对CPU要好很多,CPU读/写I/O(硬盘/内存),

​ IO在很短的时间就可以完成,CPU占用率很高。

​ IO密集型,指系统的CPU性能相对硬盘,内存要好很多,此时,系统大部分状况是CPU在等IO(硬盘/内存)读写

​ 操作,CPU利用并不高

​ 一般情况下,一个服务器只部署一个应用并且只有一个线程池,

​ 针对CPU密集型:线程数=n+1,n为CPU核数

​ IO密集型:线程数=2n+1

​ 通常应该考虑线程的执行时间,可以这么设置

​ 最佳线程数=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU核数;

​ 另外,也可根据情况动态调整;

技术文章:美团线程池实践:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html

1.2.8 ThreadLocal

​ 实现线程同步,除了用synchronized,另外也可以用ThreadLocal,前者是用时间换空间,保证每次只有

​ 一个线程去访问,后者采用空间换时间,ThreadLocal在每个线程中都是独立的,一个变量在每一个线程中都

​ 存在其实例的引用,各个线程之间没有关系。

​ **原理:**其内部有一个ThreadLocalMap内部类,ThreadLocalMap内部有个Entry内部类。

​ 在每个线程Thread内部有一个ThreadLocalMap 类型的成员变量ThreadLocals,其就是用来存储实际的变

​ 量副本,键值为当前ThreadLocal变量,value 为变量副本;

​ 最开始,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会

​ 对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副

​ 本变量为value,存到threadLocals

使用场景

​ 在同一个线程的不同开发层次中共享数据

  1. 建立一个类,并在其中封装一个静态的ThreadLocal变量,使其成为一个共享数据环境;
  2. 在类中实现访问静态ThreadLocal变量的静态方法(设值和取值)

​ 最常见的场景是用来解决数据库连接、Session管理等

private` `static` `ThreadLocal<Connection> connectionHolder
= ` `new` `ThreadLocal<Connection>() {
public` `Connection initialValue() {
  ` `return` `DriverManager.getConnection(DB_URL);
}
};
public` `static` `Connection getConnection() {
return` `connectionHolder.get();
}



private  static  final  ThreadLocal threadSession =  new  ThreadLocal();
 
public  static  Session getSession()  throws  InfrastructureException {
     Session s = (Session) threadSession.get();
     try  {
         if  (s ==  null ) {
             s = getSessionFactory().openSession();
             threadSession.set(s);
         }
     }  catch  (HibernateException ex) {
         throw  new  InfrastructureException(ex);
     }
     return  s;
}

ThreadLocal内存泄露问题

内存泄露:程序在申请内存后,无法释放已申请的内存空间,特别是内存泄露堆积,危害很大,不会被使用的对象或者变量占用的内存不能被回收,就是内存泄露

强引用:一个对象具有强引用,不会被垃圾回收器回收,当内存空间不足,java虚拟机宁愿抛出异常,使程序异常终止,也不回收这种对象,
如果想取消强引用和某个对象的关联,可以显示的将引用赋值为null;

弱引用:JVM进行垃圾回收的时候,无论内存是否充足,都会回收被弱引用关联的对象,在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用

每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本,hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链,永远无法回收,造成内存泄漏

由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用;

所以,每次使用完ThreadLocal都要调用他的remove()方法清除数据,将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉;

如何在子线程中获取主线程threadLocal中set()方法设置的值

​ 可以使用InheritableThreadLocal,ThreadLocal threadLocal = new InheritableThreadLocal(),这样在子线程

​ 中就可以通过get方法获取到主线程set方法设置的值了,

​ InheritableThreadLocal继承了ThreadLocal,并且重写了childValue、getMap和createMap方法,当在主线程

​ 中创建InheritableThreadLocal实例对象后,当前线程Thread对象中维护了一个inheritableThreadLocals变

​ 量,它也是ThreadLocalMap类型,在创建子线程的过程中,将主线程维护的inheritableThreadLocals变量的

​ 值复制到子线程维护的inheritableThreadLocals变量中,这样子线程就可以获取到主线程设置的值了

1.2.9 volatile

​ volatile是java提供的一种轻量级的同步机制,java中包含两种在内的同步机制,同步块(或方法)和volatitle

​ 变量,相比于synchronized(重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是

​ volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

并发编程3个基本概念

  1. 原子性

  2. 可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值

    在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的,java提供了volatitle来保证可见性,当一个变量被volatitle修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

  3. 有序性:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象,Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

锁的互斥和可见性

  1. 互斥:一次只允许一个线程持有某个特定的锁,一次就只有一个线程能够使用该共享数据
  2. 可见性:它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。也即当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的**。**如果没有同步机制提供的这种可见性保证,线程看到的共享变 量可能是修改前的值或不一致的值,这将引发许多严重问题

volatile 变量提供理想的线程安全,必须同时满足下面两个条件

  1. 对变量的写操作不依赖于当前值
  2. 该变量没有包含在具有其他变量的不变式中;

JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile;

volatitle变量特性

  1. 保证可见性,不保证原子性,当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去,这个写会操作会导致其他线程中的volatile变量缓存无效;
  2. 禁止指令重排:重排序操作不会对存在数据依赖关系的操作进行重排序,重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变

使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:

  1. 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
  2. 在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行;

volatitle不适用的场景

  1. 不适合复合操作;可用synchronized代替,或者Lock,或者CAS操作,java并发包里AtomicInteger()类

volatitle原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将对缓存的修改操作立即写入主存;
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

单列模式的双重锁为啥要加volatitle

如果发生指令重排,会导致程序初始化错误

第一重锁,判断是否初始化,第二重锁,判断是否被多次实例化

class Singleton{
    // 确保产生的对象完整性
    private volatile static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if(instance==null) { // 第一次检查
            synchronized (Singleton.class) {
                if(instance==null) // 第二次检查
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

Memory Barrier(内存屏障)

  • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

volatile语义中的内存屏障

  • 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
  • 在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;

final语义中的内存屏障

  • 新建对象过程中,构造体中对final域的初始化写入(StoreStore屏障)和这个对象赋值给其他引用变量,这两个操作不能重排序;
  • 初次读包含final域的对象引用和读取这个final域(LoadLoad屏障),这两个操作不能重排序;

优秀博文:https://blog.csdn.net/cy973071263/article/details/104383636

1.2.10 synchronized

资源共享解决方案

  1. 使用static关键字修饰要共享的变量,将其变为全局变量,也就是放到了JVM主内存中,实现资源共享;
  2. 实现Runnable接口,因为实现Runnable接口的线程所操作的资源对象本质是同一个对象;

synchronized原理

synchronized用来保证原子性和可见性,是利用锁的机制来实现线程同步,synchronized能保证原子性,可见性,有序性,和可重入性(避免死锁)

一个变量在同一个时刻只允许一个线程对其进行lock,持有一个锁的两个同步块只能串行执行,这就保证了原子性和有序性;

在对一个变量进行unlock操作前,必须也先把此变量同步回主内存中,这就实现了可见性

synchronized用法

synchronized可以修饰静态方法,成员函数(非静态方法),还可以直接修饰代码块,但是上锁的资源只有两类,对象和类

  1. 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
  2. 修饰一个静态的方法:其作用的范围是整个方法,作用的对象是这个类的所有对象
  3. 修饰一个代码块,指定加锁对象:被修饰的代码块称为同步语句块,其作用范围是大括号{}括起来的代码块,如果synchronized后面括号括起来的是****一个类*,那么*作用的对象是这个类的所有实例对象*;如果synchronized后面括号括起来的是*一个对象实例*,那么*作用的对象是这个对象实例****;

同步方法:是通过头部标志位用ACC_synchronized标志,告诉JVM这是一个同步方法;

同步代码块:通过Moninorenter和Monitorexit指令,会让线程在执行时,使其持有的锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个线程在尝试获得与对象相关联的Monitor锁的所有权的时候;

java对象头

在java中,每个对象都会有一个monitor对象(监视器),JVM中,对象分三部分组成(对象头、实例数据、填充数据)

img

monitor对象是实现锁机制的基础,线程获取锁本质是线程获取Java对象对应的monitor对象。*重量级锁就是通过**ObjectMonitor**实现的,也就是说重量级锁是基于对象的**monitor**来实现的*

ObjectMonitor中有2个队列,—WaitSet和—EntryList

JVM对synchronized的优化

在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock(互斥锁) 来实现的。Java 的线程是映射到操作系统的原生线程之上的(详见Java线程和操作系统线程的关系)。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态

jdk1.6作出了大量的优化,主要有自旋锁、适应性自旋锁、锁消除、锁粗化等优化方法,又增加了两个锁的状态:偏向锁,轻量级锁,所以现在synchronized一共有四种锁状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态

synchronized锁优化方法

锁膨胀:根据实际情况进行膨胀升级,依次是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。也就是说对象对应的锁是会根据当前线程申请,抢占锁的情况自行改变锁的类型。

**偏向锁:**减少同一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,使用偏向锁也就去掉了这一部分的负担,也取消掉了加锁和解锁的过程消耗。

*轻量级锁在无竞争的情况下使用* *CAS 操作**去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉*

轻量级锁:偏向锁只允许一个线程获得锁,轻量级锁是允许多个线程获得锁,但是只允许他们顺序拿,不允许存在竞争,轻量级锁的加锁和解锁都是通过CAS操作实现

自旋锁:轮询申请锁,在轻量级锁转重量级锁之间,可能会存在自旋;

重量级锁:当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。它通过操作系统的互斥量和线程的阻塞和唤醒来实现锁机制;

各自优缺点

img

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在****JIT****编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。锁消除可以节省毫无意义的请求锁的时间。比如下面代码的method1和method2的执行效率是一样的,因为object锁是私有局部变量,不存在所得竞争关系。

主要通过"逃逸分析"技术来实现,判断一个同步块所使用的锁对象是否只能被一个线程访问而没有被发布到其他线程;

锁粗化

锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁

博文连接:https://blog.csdn.net/cy973071263/article/details/104388899

1.2.11 ReentrantLock

加锁可以使用 synchronized 关键字,也可以使用 ReentrantLock 对象。synchronized 加锁后,会在同步代码块内添加 monitorenter他monitorexit这两个字节码指令。而 ReentrantLock 加锁,则是api层面实现的,它主要依赖于 Unsafe类的线程挂起和恢复功能。

ReentrantLock 重入锁,实现lock接口的一个类,支持可重入性,公平锁和非公平锁,

可重入获取锁,加锁核心方法是nonfairTryAcquire;

释放锁:tryRelease方法;

ReentrantLock 与synchronized区别

  • 实现方式:synchronized由JVM实现,所有JDK版本都支持,ReentrantLock 由JDK实现,1.5以后的版本才支持;
  • 性能差异:JDK1.6在Synchronized中引入了自旋锁、偏向锁、轻量级锁的优化后,性能和ReentrantLock差不多,之前的版本Synchronized效率比ReentrantLock低很多;
  • Synchronized加锁和解锁都是由JVM自动完成的,ReentrantLock需要手动加锁和解锁;
  • ReentrantLock独有的三大功能:
    • 可以指定是公平锁还是非公平锁,Synchronized只能是非公平锁
    • 可以结合Condition类实现有条件的分组唤醒,Synchronized结合wait/notify/notifyAll只能随机唤醒一个线程或者全部唤醒;
    • 提供了能够中断等待锁的线程的机制
  • Synchronized比较好定位异常,因为其生成线程dump文件的时候,能包括锁定信息,能标识死锁或者其他异常问题来源,而ReentrantLock只是JDK中的一个普通类,JVM并不知道哪些线程拥有Lock对象;
  • Synchronized以监视器模式实现锁,而ReentrantLock是采用AQS模式实现锁,支持响应中断、超时、尝试获取锁,同时可关联多个条件队列,Synchronized只能关联一个

ReentrantLock指定公平锁和非公平锁

​ 公平锁和非公平锁描述的是多个线程竞争锁的时候要不要排队,直接排队的就是公平锁;先尝试插队,插队失败再排队的就是非公平锁

//ReentrantLock两个构造方法,ReentrantLock默认是非公平锁,第二个传入参数能够声明是公平还是非公平
public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}


ReentrantLock结合Condition类实现有条件的分组唤醒

如果有三个线程Thread1,Thread2,Thread3按顺序打印A、B、C,可以使用ReentrantLock结合Condition

public class ReentrantLockTest {

    private static final Lock lock = new ReentrantLock();

    private static Condition conditionA = lock.newCondition();

    private static Condition conditionB = lock.newCondition();

    private static Condition conditionC = lock.newCondition();

    public static volatile int permit = 0;

    public static void main(String[] args) {
        Thread thread1 =new Thread(() -> {
            try {
                Thread.sleep(200);
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    while ((permit % 3) !=0) {
                        conditionA.await();
                    }
                    System.out.println("A");
                    permit++;
                    conditionB.signal();
                }
                conditionB.signal();
            } catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        });

        Thread thread2 = new Thread(() ->{
            try {
                Thread.sleep(200);
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    while ((permit % 3) !=1) {
                        conditionB.await();
                    }
                    System.out.println("B");
                    permit++;
                    conditionC.signal();
                }
                conditionC.signal();
            } catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        });

        Thread thread3 = new Thread(() ->{
            try {
                Thread.sleep(200);
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    while ((permit % 3) !=2) {
                        conditionC.await();
                    }
                    System.out.println("C");
                    permit++;
                    conditionA.signal();
                }
                conditionA.signal();
            } catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

上面的方法还可以优化一下,去掉取余运算

public class AbcTest implements Runnable {

    //打印次数
    private static final int PRINT_COUNT = 10;
    //锁
    private final ReentrantLock reentrantLock;

    //本次要打印所需要的condition
    private final Condition thisCondition;

    //下次要打印所需要的condition
    private final Condition nextCondition;

    //要打印的字符
    private final char printChar;

    public AbcTest(ReentrantLock reentrantLock, Condition thisCondition,
                   Condition nextCondition, char printChar) {
        this.reentrantLock = reentrantLock;
        this.thisCondition = thisCondition;
        this.nextCondition = nextCondition;
        this.printChar = printChar;
    }

    @Override
    public void run() {
        //进入临界区
        reentrantLock.lock();
        try {
            for (int i = 0; i < PRINT_COUNT; i++) {
                System.out.println(printChar);
                //使用nextCondition唤醒下一个线程
                //因为只有一个线程,所以signal或者signalAll都可以
                nextCondition.signal();
                //不是最后一次,则通过thisCondition等待被唤醒
                //此处要加判断,不然10次后会死锁
                if (i < PRINT_COUNT - 1) {
                    try {
                        thisCondition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        } finally {
            reentrantLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Condition conditionA = lock.newCondition();
        Condition conditionB = lock.newCondition();
        Condition conditionC = lock.newCondition();

        Thread thread = new Thread(new AbcTest(lock, conditionA, conditionB, 'A'));
        Thread thread1 = new Thread(new AbcTest(lock, conditionB, conditionC, 'B'));
        Thread thread2 = new Thread(new AbcTest(lock, conditionC, conditionA, 'C'));

        thread.start();
        Thread.sleep(100);
        thread1.start();
        Thread.sleep(100);
        thread2.start();
        Thread.sleep(100);
    }
}

方法二:通过一个锁和一个状态变量来实现(推荐)
public class AbcTest1 {

    //状态变量
    private volatile int state = 0;

    //打印线程
    private class Printer implements Runnable {

        private static final int PRINT_COUNT = 10;
        //打印锁
        private final Object printLock;

        //打印标志位和state变量相关
        private final int printFlag;

        //后续线程的线程打印标志位
        private final int nextPrintFlag;

        //打印字符
        private final char printChar;

        public Printer(Object printLock, int printFlag, int nextPrintFlag, char printChar) {
            this.printLock = printLock;
            this.printFlag = printFlag;
            this.nextPrintFlag = nextPrintFlag;
            this.printChar = printChar;
        }

        @Override
        public void run() {
            //获取打印锁,进入临界区
            synchronized (printLock) {
                //联系打印printCount次
                for (int i = 0; i < PRINT_COUNT; i++) {
                    //循环检验标志位,每次都阻塞然后等待唤醒
                    while (state != printFlag) {
                        try {
                            printLock.wait();
                        } catch (InterruptedException e) {
                            return;
                        }
                    }
                    System.out.print(printChar);
                    //设置状态变量为下一个线程标志位
                    state = nextPrintFlag;
                    //一个线程在操作,另外两个在等待,所以需要notifyAll();
                    printLock.notifyAll();
                }
            }
        }
    }

    public void test() throws InterruptedException {
        Object lock = new Object();
        Thread threadA = new Thread(new Printer(lock, 0, 1, 'A'));
        Thread threadB = new Thread(new Printer(lock, 1, 2, 'B'));
        Thread threadC = new Thread(new Printer(lock, 2, 0, 'C'));

        threadA.start();
        Thread.sleep(100);
        threadB.start();
        Thread.sleep(100);
        threadC.start();
        Thread.sleep(100);
    }

    public static void main(String[] args) throws InterruptedException {
        AbcTest1 abcTest1 = new AbcTest1();
        abcTest1.test();
    }
}

三个Java多线程循环打印递增的数字,每个线程打印5个数值,打印周期1-75

public class AddNumberTest {
    //计数器
    private final AtomicInteger atomicInteger = new AtomicInteger(0);

    private class Printer implements Runnable {
        //总打印次数
        private static final int TOTAL_PRINT_COUNT = 5;

        //每次打印5次
        private static final int PER_PRINT_COUNT = 5;

        private final ReentrantLock reentrantLock;

        //本线程的Condition
        private final Condition thisCondition;
        //前一个线程的Condition
        private final Condition afterCondition;

        public Printer(ReentrantLock reentrantLock, Condition thisCondition, Condition afterCondition) {
            this.reentrantLock = reentrantLock;
            this.thisCondition = thisCondition;
            this.afterCondition = afterCondition;
        }

        @Override
        public void run() {
            //进入临界区
            reentrantLock.lock();
            try {
                //循环打印5次
                for (int i = 0; i < TOTAL_PRINT_COUNT; i++) {
                    //打印操作,每次打印5个数
                    for (int j = 0; j < PER_PRINT_COUNT; j++) {
                        //以原子方式将当前值加1
                        //incrementAndGet返回的是新值(加1后的值)
                        System.out.print(atomicInteger.incrementAndGet());
                    }
                    //操作完成通知后面的线程
                    afterCondition.signalAll();
                    if (i < TOTAL_PRINT_COUNT - 1) {
                        try {
                            //本线程释放锁并等待唤醒
                            thisCondition.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            } finally {
                reentrantLock.unlock();
            }
        }
    }

    public void test() throws InterruptedException {
        ReentrantLock reentrantLock = new ReentrantLock();
        Condition conditionA = reentrantLock.newCondition();
        Condition conditionB = reentrantLock.newCondition();
        Condition conditionC = reentrantLock.newCondition();

        Thread threadA = new Thread(new Printer(reentrantLock, conditionA, conditionB));
        Thread threadB = new Thread(new Printer(reentrantLock, conditionB, conditionC));
        Thread threadC = new Thread(new Printer(reentrantLock, conditionC, conditionA));

        threadA.start();
        Thread.sleep(100);
        threadB.start();
        Thread.sleep(100);
        threadC.start();
    }

    public static void main(String[] args) throws InterruptedException {
        AddNumberTest addNumberTest=new AddNumberTest();
        addNumberTest.test();
    }
}

ReentrantLock提供了能够中断等待锁的线程的机制

ReentrantLock主要是靠ReentrantLock#lockInterruptibly()方法来提供一种可中断的机制

public class LockDemo {

    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            try {
                lock.lockInterruptibly();
                System.out.println(System.currentTimeMillis() + " -> thread-1 获取了锁");
            } catch (InterruptedException e) {
                System.out.println(System.currentTimeMillis() + "-> thread-1 被中断");
            } finally {
                //lock.unlock();
            }
        });

        Thread threadB = new Thread(() -> {
            try {
                lock.lockInterruptibly();
                System.out.println(System.currentTimeMillis() + "-> thread-2 获取到了锁");
                Thread.sleep(3000);
                System.out.println(System.currentTimeMillis() + "-> thread-2 中断thread-1");
                threadA.interrupt();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        threadB.start();
        Thread.sleep(200);
        threadA.start();
    }
}
输出
1614852259509-> thread-2 获取到了锁
1614852262510-> thread-2 中断thread-1
1614852262510-> thread-1 被中断

ReentrantLock内部有一个成员变量,sync,它继承自AbstractQueuedSynchronizer类,同时 sync 有两个实现类,NonfairSync 和 FairSync类,分别代表着非公平锁模式和公平锁模式。ReentrantLock内部的 sync 成员变量默认是非公平锁模式。

其实 ReentrantLock 的逻辑有一大半依靠 AbstractQueuedSynchronizer 类实现,它就是大名顶顶的 AQS,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch

先简单介绍AQS

AbstractQueuedSynchronizer(简称AQS),提供原子式管理状态、阻塞和唤醒线程功能以及队列模型的简单框架

static final class NonfairSync extends Sync {
	...
	final void lock() {
		if (compareAndSetState(0, 1))
			setExclusiveOwnerThread(Thread.currentThread());
		else
			acquire(1);
		}
  ...
}
这段代码含义为:
a. 若通过CAS设置变量State(同步状态)成功,也就是获取锁成功,则将当前    线程设置为独占线程。
b. 若通过CAS设置变量State(同步状态)失败,也就是获取锁失败,则进入      Acquire方法进行后续处理。

在看公平锁
tatic final class FairSync extends Sync {
  ...  
	final void lock() {
		acquire(1);
	}
  ...
}
Lock函数通过Acquire方法进行加锁,但是具体是如何加锁的呢?

结合公平锁和非公平锁的加锁流程,虽然流程上有一定的不同,但是都调用了Acquire方法,而Acquire方法是FairSync和UnfairSync的父类AQS中的核心方法

AQS整体架构

img

AQS框架共分为5层:API层、锁获取方法层、对列方法层、排队方法层、数据提供层;

**AQS核心思想:**如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态,如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配,这个机制主要是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中;

CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配

img

AQS使用一个volatitle的int成员变量来表示同步状态(state),通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对state值修改,下图(AQS数据结构)

img

waitStatus: 当前节点在队列中的状态

thread: 表示处于该节点的线程

prev: 前驱指针

predecessor:返回前驱节点,没有抛出npe

nextWaiter: 指向下一个处于CONDITION状态节点

next: 后续指针

线程锁的2种模式

shared: 表示线程以共享的模式等待锁;

exclusive: 表示线程正在以独占的方式等待锁;

JUC中AQS应用场景

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-svZVQcnv-1618197750707)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210304190836375.png)]

AQS主要是加锁acquire()方法,解锁release()方法以及state标识

AQS解析:https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html

美团技术团队:https://tech.meituan.com/

1.2.12 Unsafe

功能:

普通读写:读写一个Object对象的field,直接从内存中的一个地址读写

volatitle读写:可以保证可见性和有序性;

有序写:保证有序性不保证可见性;

直接内存操作:申请内存,重新申请内存,内存初始化,释放内存,内存复制

CAS相关:提供int,long,和Object的CAS操作;

偏移量相关:staticFieldOffset方法用于获取静态属性Field在对象中的偏移量,读写静态属性时必须获取其偏移量。objectFieldOffset方法用于获取非静态属性Field在对象实例中的偏移量,读写对象的非静态属性时会用到这个偏移量。staticFieldBase方法用于返回Field所在的对象。arrayBaseOffset方法用于返回数组中第一个元素实际地址相对整个数组对象的地址的偏移量。arrayIndexScale方法用于计算数组中第一个元素所占用的内存空间。

线程调度:LockSupport中的park和unpark方法正是通过Unsafe来实现的,park挂起线程,unpark唤醒线程;

类加载:defineClass方法定义一个类,用于动态地创建类。
defineAnonymousClass用于动态的创建一个匿名内部类。
allocateInstance方法用于创建一个类的实例,但是不会调用这个实例的构造方法,如果这个类还未被初始化,则初始化这个类。
shouldBeInitialized方法用于判断是否需要初始化一个类。
ensureClassInitialized方法用于保证已经初始化过一个类。

内存屏障:loadFence:保证在这个屏障之前的所有读操作都已经完成。
storeFence:保证在这个屏障之前的所有写操作都已经完成。
fullFence:保证在这个屏障之前的所有读写操作都已经完成。

unsafe应用

  • 内存操作:内存分配、释放、拷贝等,java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法;

    为什么使用堆外内存:对垃圾回收停顿的改善,堆外内存直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响;

    提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存;

    DirectByteBuffer是java用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在Netty、MINA等NIO框架中应用广泛。DirectByteBuffer对于堆外内存的创建、使用、销毁等逻辑均由Unsafe提供的堆外内存API来实现;

    创建DirectByteBuffer的时候,通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放

unsafe应用解析:https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html

1.2.13 并发工具三巨头CountDownLatch、CyclicBarrier、Semaphore使用

1. 2.13 多线程相关面试题

  1. 执行execute()方法和submit()方法的区别是什么呢?

    execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

    2)submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完;

  2. JUC包中的原子类是哪4类?

    基本类型:

    • AtomicInter:整型原子类
    • AtomicLong:长整型原子类
    • AtomicBoolean:布尔型原子类

    数组类型:

    • AtomicIntegerArray:整形数组原子类
    • AtomicLongArray:长整形数组原子类
    • AtomicReferenceArray :引用类型数组原子类

    引用类型:

    • AtomicReference:引用类型原子类
    • AtomicStampedRerence:原子更新引用类型里的字段原子类
    • AtomicMarkableReference :原子更新带有标记位的引用类型

    对象的属性修改类型:

    • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
    • AtomicLongFieldUpdater:原子更新长整形字段的更新器
    • AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题
  3. AtomicInteger的使用

    public final int get() //获取当前的值
    public final int getAndSet(int newValue)//获取当前的值,并设置新的值
    public final int getAndIncrement()//获取当前的值,并自增
    public final int getAndDecrement() //获取当前的值,并自减
    public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
    boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
    public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
    
    inCrementAndGet();//实现++操作,原子性,不需要加锁也是线程安全的;
    
    

    AtomicInteger主要使用CAS+volatitle和native方法保证原子性,从而避免synchronized的高开销,设计到Unsafe方法;

  4. AQS组件总结

    • Semaphore(信号量)-允许多个线程同时访问:synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源;
    • CountDownLatch(倒计时器):CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行;
    • CyclicBarrier(循环栅栏):CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞;
  5. CountDownLatch 和CycliBarrier有什么不同

    CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能,一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行

    CyclicBarrier通过它可以实现让一组线程等待至某个状态之后再全部同时执行,CyclicBarrier可以被重用

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kv71Wceh-1618197750707)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210305102855558.png)]

  1. volatitle关键字场景

    • 状态标记量

    • 双重检查

      public class Singleton {
          private volatile static Singleton instance = null;
       
          private Singleton() {}
       
          public static Singleton getInstance() {
              if (instance == null) {
                  synchronized (Singleton.class) {// 1
                      if (instance == null) {// 2
                          instance = new Singleton();// 3
                      }
                  }
              }
              return instance;
          }
      }   
      
    • 独立观察

    • 开销较低的读-写锁策略

  2. 如何终止一个线程

    一般是当使用run()或者call()方法时,执行完毕线程会自动结束,如果要手动结束一个线程,可以使用volatitle布尔变量来退出run()方法的循环或者是取消任务来中断线程;

  3. 生产者消费者模型

    public class ProductAndConsumer {
    
        final Lock lock = new ReentrantLock();
        final Condition notFull = lock.newCondition();
        final Condition notEmpty = lock.newCondition();
    
        final Object[] items = new Object[100];
    
        int putptr, takeptr, count;
    
        public void put(Object x) throws InterruptedException {
            lock.lock();
            try {
                while (count == items.length) {
                    notFull.await();
                }
                items[putptr] = x;
                if (++putptr == items.length) {
                    putptr = 0;
                }
                ++count;
                notEmpty.signal();
            } finally {
                lock.unlock();
            }
        }
    
        public Object take() throws InterruptedException {
            lock.lock();
            try {
                while (count == 0)
                    notEmpty.await();
                Object x = items[takeptr];
                if (++takeptr == items.length)
                    takeptr = 0;
                --count;
                notFull.signal();
                return x;
            } finally {
                lock.unlock();
            }
        }
    }
    
  4. 什么是FutureTask

    表示一个可以取消的异步运算,它有启动和取消运算、查询运算是否完成和取回运算结果等方法,此类提供了对 Future 的基本实现。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。可使用 FutureTask 包装 Callable 或 Runnable 对象。因为 FutureTask 实现了 Runnable,所以可将 FutureTask 提交给 Executor 执行

  5. Java中interrupt 、interrupted 和 isInterruptedd方法的区别?

    interrupt 中断线程,如果没有中断,则该线程的checkAccess方法会被调 用,可能抛出SecurityException异常;

    如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException;

    如果该线程在可中断的通道上的 I/O 操作中受阻,则该通道将被关闭,该线程的中断状态将被设置并且该线程将收到一个 ClosedByInterruptException。

    如果该线程在一个 Selector 中受阻,则该线程的中断状态将被设置,它将立即从选择操作返回,并可能带有一个非零值,就好像调用了选择器的 wakeup 方法一样。
    interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。

    interrupted 是作用于当前线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程

  6. java中的同步集合与并发集合有什么区别?

    都支持线程安全,主要区别体现在性能和可扩展性,还有如何实现线程安全的;

    同步:HashMap,Hashtable,HashSet,Vector,ArrayList相对比他们并发实现(如ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteHashSet)会慢很多,造成此种原因是因为锁,同步集合会加锁,而并发集合不会加锁,并发集合实现线程安全是通过先进和成熟的技术像锁剥离,比如ConcurrentHashMap分段;

  7. fail-fast和fail-safe机制

    fail-fast即快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出ConcurrentModificationException异常

    fail-safe 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历

优秀博文:https://blog.csdn.net/hzau_itdog/article/details/91359145

优秀博文:https://blog.csdn.net/qq_36860032/article/details/89928062

java多线程实现消费者生产者模型:https://www.jb51.net/article/183079.htm

1.3 集合

1.3.1 理论知识

img

img

Collection接口是List、set、Queue的父级接口;

Set接口有三个常用实现类,HashSet、TreeSet;EnumSet;

List下有ArrayList和Vector;

1.3.2 原理及使用场景

1.3.1 集合相关面试题

  1. Collection框架中实现比较要怎么做?

    • 实体类实现Compareable接口,并实现compareTo(T t)方法,此法为内部比较器;

    • 创建一个外部比较器,外部比较器要实现Comparator接口的compare(T t1,T t2)方法

      1. 定义外部类Student,无需实现任何接口
      2. 定义外部比较器实现Comparator接口
      public class StudentComparator implements Comparator<Student>{
          @Override
          public int compare(Student o1, Student o2) {
              if (o1.getAge() > o2.getAge()) {
                  return 1;
              }else if (o1.getAge() == o2.getAge()) {
                  return 0;
              }else{
                  return -1;
              }
          }
      
  2. ArrayList和Vector的区别(是否有序,是否重复、数据结构、底层实现)

    • 两者都实现了List接口,都是有序集合,都允许重复,底层都是数组,查询快,增删改慢(查询是对引用地址的访问,不需要遍历,增删慢是因为每次都需要向前或者向后移动元素,插入元素还需要判断是否扩容,扩容是需要创建一个新的数组,增加length再将元素放入较为繁琐);
    • ArrayList线程不安全,Vector线程安全,前者效率高
    • ArrayList每次增长为原来的0.5倍,Vector增长为原来的一倍,两者都可以设置初始空间大小,Vector还可以设置增长空间大小;
  3. ArrayList和LinkedList区别?

    • ArrayList是基于数组实现,LinkedList基于双向链表实现;
    • 随机访问List(get和set),ArrayList效率高,因为LinkedList是线性的数据存储方式,需要移动指针从前往后依次查找;
    • 增加删除时,LinkedList效率要高,只需要改变指针的地址,不影响其他;
    • 自由性不同,ArrayList需要手动设置固定大小的容量;
    • ArrayList会预留空间,而LinkedList需要存储节点信息和节点指针信息;
  4. ArrayList和Set的区别?

    • set元素无序、不重复,有两个实现类HashSet和TreeSet,HashSet是根据对象的Hash值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能,TreeSet则是二叉树的方式存储元素,可以对集合进行排序;
  5. ArrayList、Vector、LinkedList、Set遍历方式

    • For循环遍历ArrayList

    • 增强for循环foreach 遍历ArrayList

    • 迭代器Iterator遍历

      Iterator iterator = list.iterator();
        while(iterator.hasNext()) {
      	System.out.print(iterator.next() + "  ");
      		}
      
    • 双向迭代器listIterator

      ListIterator listIterator = list.listIterator();
      	while (listIterator.hasNext()) {
      		System.out.print(listIterator.next() + "  ");
      		}
      
  6. HashMap与HashTable区别?

    • HashMap线程不安全、HashTable 线程安全,HashMap允许null键值,hashTable 不允许;

    • HashMap线程不安全是因为主要考虑到了多线程环境下进行扩容可能出现HashMap死循环,hashtable 单个方法操作是线程安全的,但是符合操作时不一定能保证,容易导致越界异常

    • 继承的父类不同,hashMap继承自AbstractMap类,而hashtable继承自Dictionary类,不过都同时实现了map,Cloneable(可复制)、Serializable(可序列化)三个接口

    • 当需要线程安全的时候也可以实现ConcurrentHashMap,效率也比hashTable高,因为其使用了分段锁;

    • 扩容不同,HashTable默认初始化大小是11,之后每次扩容,容量是原来2n+1倍,HashMap默认是16,每次扩容为原来的2倍,

      hashtable会尽量使用素数,基数,hashMap总是使用2的幂作为哈希表大小;

    • 计算hash值方法不同,HashTable直接使用对象的hashCode,**hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数法来获得最终的位置。**所以比较耗时,hashMap是将hash表的大小固定在2的幂,这样在取模运算时,不需要做除法运算,只需要做位运算,效率比除法高,但是相应的hash冲突也多了,hashMap也采取了分散法来打散运算结果;

    • 或者可以使用Collections的synchronizedMap方法使hashMap具有同步的能力,LinkedHashMap记录了插入顺序(有序)

    • synchronizedMap使用互斥锁+synchronized关键字实现线程安全

  7. HashMap遍历?

    1.通过Map.keySet来遍历key和value
     for (String key : map.keySet()) {
          System.out.println("key= "+key+" and value= "+map.get(key));
       }
       
    2. 通过Map.entrySet使用iterator遍历key和value
    Iterator<Map.Entry<String, String>> it = 
                    map.entrySet().iterator();
            while(it.hasNext()){
                Map.Entry<String, String> entry = it.next();
                System.out.println("key= "+entry.getKey()+" and value= "+entry.getValue());
            }
            
    3. 通过Map.values()遍历所有的value,但是不能遍历key;
    
  8. Map集合java8新特性?

    • 结构上,1.7中底层是数组+链表形式,1.8中是数组+链表+红黑树(解决了链表太长导致的查询变慢的问题);
    • 初始化方式:1.7 infateTable(),1.8直接集成了扩容函数resize()
    • hash值计算方式:1.7中9次扰动=4次位运算+5次异或运算,1.8中 2次扰动=1次位运算+1次异或运算
    • 存放规则:1.7 无冲突存放数组,有冲突,数组+链表,1.8无冲突数组,有冲突,链表长度小于8,数组+链表,链表>8,加红黑树
    • 插入数据方式:1.7 头插法,1.8尾插发
  9. HashMap扩容机制?

    • 初始容量是16,扩容因子是0.75,扩容完成,容量为2n,当容量达到16*0.75=12,就可能需要扩容;
    • 为什么长度是2的幂:减少冲突(碰撞)的次数,提高效率,如果是2的幂,length-1转化为二进制是1111…的形式,如果不是2的幂,则length-1转化为二进制是1110…,最后一位都是0,会导致空间浪费,会增加碰撞几率;
  10. hashSet和TreeSet的区别

    • **HashSet底层使用了Hash表实现。**保证元素唯一性的原理:判断元素的hashCode值是否相同。如果相同,还会继续判断元素的equals方法,是否为true
    • **TreeSet底层使用了红黑树来实现。**保证元素唯一性是通过Comparable或者Comparator接口实现
  11. 解决hash冲突方法?

    • 拉链法
    • 线性探测再散列法
    • 二次探测再散列法
    • 伪随机探测再散列法
    • 使用2次扰动函数(hash函数)来降低哈希冲突的概率;
    • 使用红黑树进一步降低遍历的时间复杂度;
  12. 数组和集合List之间的转换?
    主要通过Arrays.asList以及List.toArray方法

  13. hashMap的put方法的具体流程?

    1. 如果table为空或者长度为0,即没有元素,使用resize方法扩容;
    2. 计算插入存储的数组索引i,如果数组为空,即不存在hash冲突,则直接插入数组;
    3. 插入时如果发生hash冲突,则进行判断:
       a. 判断table[i]的元素key是否与需要插入的key相同,若相同则直接使用新的vlue覆盖掉旧的value,使用equals方法;
       b. 继续判断,需要插入的数据结构是红黑树还是链表,如果是红黑树,则直接在树中插入or更新键值对,如果是链表,则在链表中插入或者更新键值对;链表长度>8,则转为红黑树;
    4. 插入成功,检查存在的键值对数量size>最大容量,大于则进行扩容;
       
    
  14. jdk1.8,Map为什么要引入红黑树

    主要是为了解决二叉树的缺陷,二叉树在特殊情况下会变成一条线性结构(会跟链表一样,造成很深的问题),遍历查询会非常慢,而红黑树在插入新数据时可能通过左旋、右旋、变色操作保持平衡,可以解决查找数据慢,链表深度太深的问题,当然链表很短就不需要引入红黑树;

  15. 为什么hashMap在链表长度大于8的时候转为红黑树?
    主要是考虑到树需要存储节点占用空间是普通节点的2倍,节点足够多的时候,可以一空间换时间,保证效率,通常情况下链表长度很难达到8,在链表长度为8时性能已经很差;

1.4 文件流以及IO

1.4.1 理论知识以及结构图

这里写图片描述

这里写图片描述

1.4.2 实际操作

1.4.3 NIO、BIO、AIO

  1. 介绍

    BIO:同步阻塞IO,在读写操作完成之前,线程一直阻塞;

    NIO:多路复用同步非阻塞IO,提供了Chanel、selector、buffer等新的概念

    AIO: 异步非阻塞IO

    NIO主要有三大核心:channel(通道)、Buffer(缓冲区)、Selector(选择区)

    channel(通道):双向的,可进行读操作,也可进行写操作,主要实现有FileChannel,DatagramChannel,SocketChannel,ServerSocketChannel;

    Buffer:缓冲区

    Selector(选择区):能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理;

  2. 多路复用三种实现?

    • **select:**它仅仅知道,有I/O事件发生,并不知道是那个流,只能无差别轮询所有流,找出能读或者写入的流,操作流程

      使用copy_from_user 从用户空间拷贝fd_set到内核空间;

      注册回调函数_pollwait

      遍历所有fd,调用其对应的poll方法;

      以tcp_poll为例,其核心实现就是_pollwait

      __pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列;

      poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值;

      如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠;

      把fd_set从内核空间拷贝到用户空间;

      **缺点:**单个进程所打印的fd是有限制的,通过FD_SETSIZE设置,默认1024;

      都会把fd从用户态拷贝到内核态,这个开销在fd很多时很大;

    • poll:将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,没有最大连接数的限制,基于链表存储;

    • 链接:https://blog.csdn.net/weixin_39662142/article/details/110396979

1.4.4 文件流相关面试题

  1. 字节流和字符流那个好,怎么选择?

    大多数情况使用字节流,大多数IO都是直接操作磁盘文件,所以一般都是以字节的形式进行;

    如果操作IO需要通过内存中频繁处理字符串的情况,就用字符流,因为字符流具备缓冲区,提高了性能;

  2. 什么是java序列化,如何实现java序列化?

    处理对象流的一种机制,将对象进行流花,可以对流化后的对象进行读写操作,一般实现Serialize接口;

  3. BufferedReader属于那种流,主要用来做什么?

    用于处理流中的缓冲流,可以将读取的数据存在内存里面,有readLine方法,用来读取一行;

1.5 锁

1.5.1 锁的定义、原理、作用

1.5.2 锁的分类以及实现方法

img

1.5.3 锁的实际应用

1.5.4 锁优化、以及使用过程中的问题

1.5.5 锁相关面试题

1.6 网络协议

1.6.1 网络协议原理

1.6.2 网络协议分类

1.6.3 网络协议运用

1.6.4 http请求相关问题

1.6.5 相关面试题

  1. Get请求和Post请求的区别?

    • get请求可被缓存,将保留在浏览器历史记录中,可被收藏为书签,post都不可以
    • get请求不要再处理铭感数据时使用;
    • get请求有长度限制,post没有;
  2. DNS使用的协议

    TCP和UDP都有使用,UDP报文最大长度是512字节,而TCP则超过512字节,DNS查询超过512字节,尽量使用TCP;

    区域传送使用TCP:可以保证数据准确性,还有传输数据量大的问题;

    域名解析使用UDP协议:不会经过TCP三次握手,字节数少,此时DNS负载低,响应快;

  3. 幂等

    一个幂等操作的特点是其任意多次执行所产生的影响均与第一次执行的影响相同,幂等函数或者幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数,这些函数不会影响系统状态;

    http get请求可以认为是幂等;

    http post请求不是幂等,put幂等,patch非幂等,delete是幂等;

  4. http、UDP、TCP区别?

    • 网络七层模型,应用层、表示层、会话层、传输层、网络层、数据链路层、物理层;
    • http:超文本传输协议,是一种无状态协议,客户端发送请求,服务器接收请求,经过处理返回给客户端;
    • TCP: 传输控制协议,是面向连接的协议,在发数据前必须先和对方建立可靠的链接,一个TCP要经过3次对话才能建立起来,其属于传输层;
    • UDP:用户数据报协议,面向非连接,传输不可靠,用于传输少量数据
  5. TCP三次握手?四次挥手?

    • TCP建立过程,A-B,B收到信息会用一个带有确定应答的(ACK)和同步序列号(SYN)标志位的数据段响应主机A,A收到数据在发送确认应答,没有应用层的数据;
    • TCP断开(4次握手):主机A完成数据传输后,将控制位FIN置为1,提出停止TCP连接,主机B收到FIN作出响应,确认这一方向上的TCP连接关闭,将ACK置1,B再反方向提出关闭请求,将FIN置1,主机A对主机B的请求进行确认,将ACKz置1,
  6. cookies和session区别?

    • Cookiess是一种能够让网站服务器把少量数据存储到客户端的硬盘或者内存;
    • cookie数据存在客户浏览器上,session数据存储在服务器上;
    • cookie不是很安全,但是可以加密,cookie可以过期,session可以被销毁;
  7. TCP粘包和拆包区别?解决策略?

  8. 正向代理、反向代理区别联系?应用?

    • 正向代理:是一个位于客户端和原始服务器之间的服务器,代理客户端和原始服务器的,服务器端并不知道真正的客户端到底是谁,隐匿的是客户端,配置的也是客户端
    • 反向代理:服务器的代理,客户端不知道真正的服务器是谁,隐匿的是服务端,配置的也是服务端
  9. 一次完整http请求过程?
    域名解析–>发起TCP的3次握手–>建立TCP连接后发起httpq请求–>服务器响应http请求,浏览器得到html代码–>浏览器解析html代码,并请求html代码中的资源–>浏览器对页面进行渲染呈现给用户;

  10. TCP如何保证可靠传输?

    • 三次握手
    • 将数据截断为合理的长度,应用数据被分割成TCP认为最适合发送的数据块
    • 超时重发,当TCP发出一个段后,它启动一个定时器,如果不能及时收到一个确认就重发
    • 对于收到的请求,给出确认响应
    • 校验出包有错,丢弃报文段,不给出响应
    • 对失效数据进行重新排序,然后才交给应用层

1.7 tomcat、webSocket等服务器知识

1.7.1 tomcat介绍、原理、作用

1.7.2 tomcat使用

1.7.3 tomcat 调优

1.7.4 webSocket原理、作用

1.7.5 相关面试题

  1. tomcat使用哪种IO,为什么?
    tomcat默认使用的是BIO,客户系统使用BIO的时候往往为每一个web请求引入多线程,每个web请求一个单独的线程,如果并发量上去,线程数就上去了,CPU忙着线程切换,所以BIO不适合高吞吐量,高可伸缩性的web服务器;

    NIO是使用单线程(单个CPU)或者少量的多线程来接受socket,可以大大提高web服务器的可伸缩性;

  2. 有哪些服务器?区别和用途?

    • 有tomcat服务器、Jetty服务器、Jboss服务器、Glassfish服务器、Websphere服务器、WebLogic服务器;
    • Tomcat和jetty都是一种Servlet引擎,都支持标准的servlet规范和javaEE规范;
    • jetty架构是基于Handler 来实现的,tomcat是基于容器设计的;
    • jetty可以同时处理大量连接而且可以长时间保持连接,适合做web聊天应用等等,可以减少服务器内存开销,从而提高服务器性能,默认采用NIO,在处理I/O请求更占优势,适合处理静态资源,tomcat适合处理生命周期短的,不适合处理静态资源;
    • Jboss不仅是Servlet容器,还是EJB容器
  3. Tomcat组成?

    • 主要有Container和Connector以及相关组件构成;
    • Server: 整个tomcat服务器,包含多组服务,负责管理和启动各个Service,同时监听8005端口发过来的shutdown命令,用于关闭整个容器;
    • Service:tomcat封装的、对外提供完整的、基于组件的Web服务,包含Connectors、Container两个核心组件,各个Service之间独立,但是共享同一个JVM资源;
    • Connector: tomcat与外部世界的链接器,监听固定端口接收外部请求,传递给Container、并将Container处理结果返回给外部;
    • Container :用于管理Servlet生命周期,调用servlet相关方法;
    • Loader:封装了java ClassLoader,用于Container加载类文件

1.8 反射原理

1.8.1 反射原理、底层

定义:指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用他的任意一个方法,动态获取信息,以及动态调用对象的方法叫做java的反射机制;

应用: 生成动态代理,面向切面编程

常见实现方法

  • 对象.getClass()
  • Class.forName(className);
  • 类名.class;

三种实现方式之间的区别:

  • 类加载方式不同,Class.forName()属于动态加载类,在代码运行时加载指定类,Class.class属于静态加载类在代码编译时加载指定;
  • Object.getClass()取决于对象的产生方式,即可以是静态加载类(通过new创建的对象)也可以是动态加载类;
  • **Class对象创建方式不同:**Class.forName()运行阶段,JVM使用类装载器,将类装入内存中,并对类进行初始化(静态代码块、非静态代码块、构造函数调用以及静态变量)最后返回Class对象;
  • class,编译阶段,JVM使用类装载器,将类装入内存中,并对类进行初始化操作,最后返回Class的对象;对于GetClass(),没有其他操作

1.8.2 反射应用、常见方法

  1. 获取所有公共构造函数:getConstructors()

  2. 获取所有的构造函数:getDeclaredConstructors()

  3. 操作方法:

    • 使用Class获取对应方法:getMethods(),获取所有的公共方法,包括父类的公共方法;

    • getDeclaredMethods(),获取所有本类的方法,包括本类的私有方法;

    • getDeclareMethod(String name,Class<?>…parameterTypes)

      获取指定方法名称的方法,和访问权限无关;

  4. 创建对象的方法:

    • 构造函数创建,newInstance(Object initargs)
    • Class类中创建:newInstance();
    • 注意,如果是私有的构造方法,反射默认是无法直接执行的,可以找到父类的Accessible(boolean flag)的方法,设置为true,即可忽略访问权限;
  5. Method执行方法

    • invoke(Object obj,Object…args):执行方法;
  6. Class中获取字段:

    • getFields(),获取当前class所表示类中所有的public字段,包括继承的字段;
    • getDeclaredFields(),获取当前Class所表示类中所有的字段,不包括继承的字段
    • getField(String name):
    • getDeclaredField(String name):获取指定名字的字段,但是不包括继承的字段;
  7. JDK动态代理反射机制?

    • 动态代理:使用Proxy类的newProxyInstance方法创建代理对象,使用InvocationHandler来实现增强的逻辑(通常是创建一个InvocationHandler接口的实现类,在其invock方法中实现增强的逻辑),利用拦截器加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理;

    • CGLib代理:使用MethodInterceptor接口实现增强逻辑,使用Enhancer生成代理对象,利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理;

    • 如果目标对象实现了接口,默认情况下会采用jdk的动态代理实现AOP;

    • 如果目标对象实现了接口,可以强制使用CGLIB实现AOP

    • 如果目标对象没有实现接口,必须使用CGLIB库;并在Spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class = “true”/>

1.8.3 反射相关面试题

  1. java序列化的作用和方法?

    将对象状态转化为字节流,以后可以通过这些值再生出相同状态的对象,用于存储和传输,可以实现数据的持久化,可以把数据永久保存到硬盘上,序列化可以实现远程通信,转成在网络上传输的字节序列;

    **方法:**java.io.ObjectOutputStream对象输出流,他的writeObject(Object obj)可以对参数指定的obj对象进行序列化;

    ObjectInputStream对象输入流,他的readObject()方法可以从输入流读取字节序列,再把它们反序列化成为一个对象;

    实现Serializable或者Externalizable接口,可以实现序列化;

    serialVersionUID :它决定着是否能够反序列化成功,相当于一个版本号,在反序列化的时候会进行序列号对比;

    谷歌的一些工具也可以实现序列化;

    继承中方法执行顺序

    父类静态方法>子类静态方法>父类普通方法>父类构造方法>子类普通方法>子类构造方法

1.9 对象相关知识

  1. 静态内部内和非静态内部类?匿名内部类、静态内部类、非静态内部类初始化顺序?
    • 静态内部类方法和变量可以是静态的也可以是非静态的,静态方法可以在外层通过静态调用,而非静态方法必须创建类的对象才能调用;
    • 非静态内部类不能有静态成员(方法、属性);
    • 静态内部类只能访问外部类的静态成员,无法访问非静态成员,非静态可以访问所有;
    • 一般静态内部类先执行
  2. 继承中静态方法,构造方法,普通方法执行顺序问题?
  3. 抽象类与接口?
  4. 方法重载与重写?

1.20 基本数据类型

  1. 各种数据类型占用字节数?
  2. 各种数据类型范围?

1.21 进制转换

1.22 计算机原理

二、JVM

2.1 JVM内存模型

  1. 内存模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-07gnCuuc-1618197750710)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210308094500874.png)]

img

  • **程序计数器:**一块较小的内存空间,当前线程所执行的字节码的行号指示器,分支、循环、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成,程序计数器是线程私有的,唯一一个没有OOM的区域
  • **java虚拟机栈:**线程私有,java方法执行的内存模型,每个方法在执行时都会创建一个栈帧(用于存储局部变量表、操作数栈、动态链接、方法出口等信息),在此区域规定了2种异常情况,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出stackOverflowError异常,虚拟机栈也支持动态扩展长度,但是如果不能申请到足够内存,也会抛出outOfMeeoryError异常;
  • **本地方法栈:**为虚拟机使用到的Native方法服务,也会抛出stackOverflowError异常和outOfMeeoryError异常
  • java堆:java内存中最大的一块,所有线程共享的一块内存,存放对象实例,也是垃圾收集器管理的主要区域,可分为新生代、老年代,再细致一点就是Eden空间、From Survivor空间、To Survivor空间;
  • 方法区:与java堆一样,也是线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,也会出现outOfMeeoryError异常;
  • **运行时常量池:**是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放,当常量池无法再申请到内存会抛出OutOfMemoryError异常;
  • 直接内存:不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;
  1. 堆内存分配
    • 新生代,存放新生的对象,一般占据堆1/3空间,新生代又分为Eden区,ServivorFrom, ServivorTo三个区
    • Eden区:java新生对象出生地;
    • ServivorFrom:上一次GC幸存者,作为这一次GC的被扫描者;
    • ServivorTo:保留了一次 MinorGC 过程中的幸存者;

2.2 类加载机制

2.3 垃圾回收机制、工具、算法

  1. 判断对象是活着还是死亡算法?

    • 引用计数算法:有引用计数加1,引用失效计数减1,任何时刻计数器为0,就表示不会再被使用,但是存在一个问题就是,很难解决对象循环互相引用的问题
    • 在主流实现中,大部分都是通过可达性分析来判断对象是否存活,核心思想:通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,表示对象不可用,可作为GC Roots的对象包括:虚拟机栈中引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象,本地方法栈中JNI引用的对象
  2. 垃圾收集算法

    • 标记-清除算法:效率不高,容易产生大量不连续的内存碎片;
    • **复制算法:**将内存按容量化为大小相等的2块,每次只使用一块,一块用完,将这块还活着的对象复制到另外一块,再把使用过的一次性清除,以空间换时间;分为一块较大的Eden空间和2块较小的Survivor空间;
    • 标记-整理:针对老年代的垃圾回收算法,先标记,再整理-清除;
  3. 垃圾收集器?

    • Serial收集器:单线程版收集器,可能会造成停顿,新生代,复制算法;
    • ParNew收集器:Serial收集器的多线程版,采用复制算法,用在新生代,可以通过-XX:parallelGCThreads参数来控制收集的线程数;
    • Parallel Scavenge收集器:使用复制算法的手机器,又是并行的收集器
    • Serial Old收集器:使用标记-整理算法,作用与老年代;
    • parallel Old收集器:多线程的标记-整理算法;
    • CMS收集器:以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现,包括初始标记、并发标记、重新标记、并发清除,会产生空间碎片,作用于老年代;
    • G1收集器:支持并行与并发,特点:分代收集、空间整合、可预测的停顿,实现步骤:初始标记、并发标记、最终标记、筛选回收,不会产生空间碎片,可精准控制停顿,作用于新生代和老年代;
    • ZGC:JDK11中推送出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收
  4. 内存分配与回收策略?

    • 对象优先在Eden分配;
    • 大对象直接进入老年代;
    • 长期存活的对象将进入老年代;
    • 动态对象年龄判定
    • 空间分配担保
  5. 虚拟机类加载机制?

    • 加载:在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据入口;
    • **验证:**确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求
    • **准备:**在方法区中分配这些变量所使用的内存空间;
    • **解析:**虚拟机将常量池中的符号引用替换为直接引用的过程;
    • 初始化:
  6. 虚拟机类加载器?

    • **启动类加载器(Bootstrap ClassLoader):**负责将JAVA_HOME/lib目录中,或者被-Xbootclasspath参数所指引的路径中,并且是虚拟机识别的类库加载到虚拟机内存中,不能直接引用,需要委派给引导类加载器;
    • **拓展类加载器(Extension ClassLoader)😗*可直接使用扩展类加载器;
    • **应用程序类加载器(Application ClassLoader)😗*又称系统类加载器,也是程序中默认的类加载器
  7. 常用工具:

    • 命令行终端:标准终端类:jps、jinfo、jstat、jstack、jmap,功能整合类:jcmd、vjtools、arthas、greys
    • 可视化界面:Jconsole、Jvisualvm、HA、GCHisto、GCViewer、MAT、JProfiler
  8. 双亲委派?

    防止内存中出现多份同样的字节码,打破双亲委派需要继承ClassLoader类,还要重写loadClass和findClass方法

  9. 各命令详解

    • JPS:虚拟机进程状态工具

      -L:输出Java应用程序的main class完整包;

      -q 仅显示pid ,不显示其他任何信息;

      -m 输出传递给main方法的参数;

      -v 输出传递给JVM的参数;

    • jstat: 虚拟机统计信息监视工具,可用来查看堆信息、垃圾回收信息等;

      img

    • jinfo:java 配置信息工具

      jinfo -flag 打印指定JVM的参数值

      jinfo -flag [+/-] 设置指定JVM参数布尔值

      jinfo -flag =设置指定JVM参数的值

    • jmap:java内存映像工具;

      jmap -head pid查看进程堆内存使用情况;

      jmap -histo[:live] pid 查看堆内存中的对象数目,大小统计直方图;

      jmap -dump:将当前程序堆快照导出到文件中

    • jstack:用来查看java应用程序堆栈信息

      jstack -l 查看进程中线程状态、检查死锁等;

  10. JVM配置参数详解

  • 堆大小设置(-Xms -Xmx -Xmn -Xss):堆大小有三方面限制,操作系统的数据模型(32-bt还是64-bit)限制,系统可用虚拟内存限制,系统可用物理内幕才能限制,32位一般可设置为1.5G~2G,64位无限制;

    -Xmx:jvm最大可用内存;

    -Xms:JVM初始内存,可以设置为与-Xmx一样,避免每次垃圾回收完后JVM重新分配内存;

    -Xmn:设置年轻代内存大小,占整个堆大小的3/8;

    -Xss: 设置每个线程的堆栈大小:经验值3000k-5000k左右;

  • 垃圾回收器设置(-XX):

    -Xmx300m

    -Xms300m

    -Xmn100m

    -XX:SurvivorRatio=8

    -XX:+UseG1GC

    -XX:MaxTenuringThreshold=14

    -XX:ParallelGCThreads=8

    -XX:ConcGCThreads=8

    -XX:+DisableExplicitGC

    -XX:+HeapDumpOnOutOfMemoryError

    -XX:HeapDumpPath=d:/a.dump

    -XX:+PrintGCDetails

    -XX:+PrintGCTimeStamps

    -XX:+PrintHeapAtGC

    -XX:+TraceClassLoading

    -XX:+PrintClassHistogram

  1. 一次完整GC流程?

    答:一般先描述堆的构成。

    • java堆 = 老年代 +新生代(1:2)
    • 新生代 = Eden +survivor space S0 +survivor space S1(8:1:1);
    • 当Eden区填满了,java虚拟机会触发一次Minor GC,以收集新生代的垃圾,存活的对象进入survivor区;
    • 大对象直接进入老年代
    • 如果对象在Eden出生,并经过一次Minor GC后任然存活,并且被S0容纳,年龄设为1,每熬过一次Minor GC,年龄加1,若年龄超过一定限制(默认15,通过-XX:+MaxTenuringThreshold设置),则被晋升到老年代;
    • 老年代满了无法容纳更多对象,Minor GC之后通常就会进行FUll GC(包括年轻代喝老年代);
    • Major GC发生在老年代,通常会伴随着至少一次Minor GC,比Minor GC慢10倍

2.4 JVM调优方法

2.5 JVM 常见指令以及在服务器中的配置

2.6 JVM常见问题排查(内存溢出、内存不足、死锁等)

  • JDK1.7及以前,java类信息、常量池、静态变量都存在永久代里,类的元数据和静态变量在类加载的时候分配给永久代,当类被卸载的时候垃圾收集器从永久代处理掉;
  • JDK1.8对JVM架构进行了改造,去掉了方法区,换成了元空间,常量池,静态变量放到java堆里面,最明显的变化是元空间从虚拟机转移到本地内存

2.7 学习相关连接

  • https://blog.csdn.net/qq_41701956/article/details/100074023
  • https://blog.csdn.net/cy973071263/article/details/104318355
  • GC问题:https://tech.meituan.com/2020/11/12/java-9-cms-gc.html
  • Springboot堆外内存泄露问题:https://tech.meituan.com/2019/01/03/spring-boot-native-memory-leak.html;
  • 新一代垃圾回收器ZGC:https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html;
  • JVM性能调优监控工具JPS、jstack、jmap、jhat、jstat、hprof使用;

2.8 JVM堆外内存

定义:把内存对象分配在java虚拟机堆以外的内存,直接受操作系统管理(而不是虚拟机),能够一定程度上减少垃圾回收对应用程序造成的影响;

java中分配堆外内存有2种方式:

a. 通过ByteBuffer.java#allocateDirect得到一个DirectByteBuffer对象;

b. 直接调用unsafe.java#allocateMemory分配内存

JVM的方法区属于非堆内存

2.9 JVM相关面试题

  1. 什么情况下会发生栈溢出?

    答:栈是线程私有的,生命周期与线程相同,用来存储局部变量表,操作数栈,动态链接,方法出口等信息,如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError异常,方法递归调用可能产生这种异常,新建线程没有足够内存的时候,可以抛出outOfMemory异常,通过-Xss去调整JVM栈大小;

  2. 几种垃圾回收器?

    答:Serial收集器: 单线程的收集器,收集垃圾时,必须stop the world,使用复制算法
    ParNew收集器: Serial收集器的多线程版本,也需要stop the world,复制算法。
    Parallel Scavenge收集器新生代收集器复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%。
    Serial Old收集器: 是Serial收集器的老年代版本,单线程收集器,使用标记整理算法
    Parallel Old收集器: 是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法
    CMS(Concurrent Mark Sweep) 收集器: 是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片。
    G1收集器标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确地控制停顿。
    CMS是老年代收集器,可以配合新生代的Serial和ParNew 收集器一起使用;

    G1收集器是老年代和新生代;

    CMS以最小停顿为目标的收集器;

    G1收集器可预测垃圾回收的停顿时间;

    CMS标记-清除,容易产生内存碎片;但是CMS可以配置参数进行内存整理,将碎片化的空间整理成一片连续的空间,也可配置碎片整理的Full GC次数;

    G1 标记-整理,进行空间整合,降低内存空间碎片化

  3. new对象时发生了什么?

    • 先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用并检查这个符号引用所代表的类是否已经被加载、解析和初始化过,如果没有就执行类的加载过程;
    • 类加载检查通过,可以为新生对象分配内存(两种方式"指针碰撞",一种"空闲列表"),再将分配到的内存空间都初始化为零值,
    • 对对象头进行设置,包括这个对象属于哪个类的实例,对象哈希码,对象的GC分代年龄等信息;
    • 接着执行init方法,进行初始化
    • String str = new String(“hello”):str放在栈中,用new创建出来的字符串对象放在堆上,而"hello"这个字面量则放在方法区;
  4. JVM内存模型、重排序、内存屏障、happen-before?

    • JVM内存分为主内存,工作内存;

    • 指令重排序:指令的执行不一定需要按顺序,有时候可以打乱顺序;

    • 内存屏障:用于控制特定条件下的重排序和内存可见性问题,分为以下几种:

      LoadLoad屏障;读屏障;

      StoreStore屏障:写屏障;

      LoadStore屏障:先读后写;

      StoreLoad屏障:先写后读;

  5. JVM类加载器?

    定义:根据指定全限定名称将class文件加载到JVM内存,转为Class对象;

    分为:启动类加载器,扩展类加载器,应用程序类加载器,自定义类加载器;

    使用双亲委派的好处:防止内存中出现多份同样的字节码;

    打破双亲委派:要继承ClassLoader类,还要重写LoadClass和findClass

  6. JVM线上调优?

    • 通过top指令发现一个java线程cpu和内存使用率比较高;
    • jinfo pid 查看当前进程详情(配置信息);
    • 查看GC情况:jstat -gccause pid,可以查看新生代YGC(gc次数),YGCT(gc时间),老年代gc(FGC,FGCT),如果是新生代频繁GC,可能是新生代设置太小;
    • 此时可以查看当前内存分配:jmap -heap pid;
    • 查看gc后的JVM详情信息:jstat -gc pid 1000,1000表示查看1000行日志
    • 如果手动Full gc发现内存并没有被释放,说明当前系统可能存在内存泄露,导致对象不能被清理释放,可以查看当前系统线程详细情况:jstack pid | head -50
    • 查看jvm开启的线程数:jstack pid | wc -l;
    • 再查看当前服务器对不同IP建立的链接数,并通过分析链接最多的ip,找到问题所在
  7. JVM参数配置?

    • 配置堆栈大小:-Xmx -Xms -Xmn -Xss;
    • 配置持久代大小:-XX:MaxPermSize,年轻代与年老代比值:-XX:NewRatio=4设置年轻代中Eden和Survivor区大小比值:-XX:SurvivorRatio=4
    • 配置垃圾最大年龄:XX:MaxTenuringThreshold
    • 配置垃圾收集器:-XX:+UseParallelGC: 选择垃圾收集器为并行收集器,-XX:ParallelGCThreads=20: 配置并行收集器的线程数,-XX:+UseConcMarkSweepGC: 设置年老代为并发收集,-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理
    • 辅助类信息相关:-XX:+PrintGC 输出形式-XX:+PrintGCDetails 输出形式
  8. java 对象头结构?

    • java对象有三部分组成,对象头、实例数据、对其填充;
    • 对象头有两部分组成:第一部分是存储对象自身运行时数据,哈希码,GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID,第二部分是指针类型,指向对象的类元数据类型
  9. JVM内存分配?

    • 寄存器:jvm内部虚拟寄存器,存取速度非常快,程序不可控制
    • 栈:保存局部变量,操作数栈,动态链接,方法出口,指针,基本数据类型的值;
    • 堆:动态产生的数据,比如new 的对象
    • 常量池:直接常量,符号引用,常量池存于堆中;
    • 代码段:从硬盘上读取的源程序代码;
    • 数据段:用来存放static定义的静态成员;
  10. java堆内存结构?

    新生代、老年代、永久代;

三、网络编程

3.1 http原理以及三次握手

3.2 常用IP/TCP协议

3.3 网页请求方式,请求头,请求体

3.4 常见的java方法

3.5 常见面试题

四、数据库技术

4.1 基本操作

4.2 索引

  1. 索引类型有哪些?

  2. 索引底层原理

  3. B树,B+树区别

    https://blog.csdn.net/a519640026/article/details/106940115/

4.3 引擎

  1. Mysql 引擎mylsam 和innodb的区别?

    Mylsam存储引擎,基于传统的ISAMl类型,支持全文检索,但不是事务安全的,不支持外键,每张Mylsam表存放在三个文件中:frim文件存放表格定义,数据文件是MYD(MyData),索引文件是MYI(MyIndex)

    InnoDB是事务型引擎,支持回滚,崩溃恢复能力,多版本并发控制、ACID事务,支持行级锁定,也可以支持锁全表,表和索引在同一个表空间

    两者核心区别:

    Mylsam是非事务安全的,而InnoDB是事务安全的;

    Mylsam锁的粒度是表级,而InnoDB支持行级锁定;

    Mylsam支持全文类型索引,而InnoDB不支持全文索引;

    Mylsam 小型应用效率要优于InnoDB;

    应用上的区别:

    Mylsam:以读操作和写操作为主,很少的更新和删除

    InnoDB:用于事务处理,支持外键,支持数据一致性;

    Memory:能提供极快的访问,但是对表的大小有限制,太大的表无法缓存到内存中

    merge:高写性能和高压缩率;

    TokuDB,高效的插入性能,优秀的压缩特性

4.4 事务

4.5 锁

4.6 常用函数

  1. 数字函数
  • ABS(x):返回x的绝对值;
  • MOD(N,M):返回N被M除的余数;
  • Floor(x):返回不大于x的最大整数值;
  • ceiling(x):返回不小于x的最小整数值;
  • round(x):返回参数x的四舍五入的一个整数;
  1. 字符串函数

    • Ascll(str):返回字符串最左边ASCLL值;

    • Concat(str1,str2):返回两个字符串拼接结果,可以有多个参数,有一个参数为null则返回null;

    • instr(str,substr):返回子串substr在字符串str中的第一个出现的位置;

  2. 日期函数

    • DATE_ADD(date,INTERVAL expr type) ,进行日期增加的操作,可以精确到秒;
    • DATE_SUB(date,INTERVAL expr type) ,进行日期减少的操作,可以精确到秒;
    • CURRENT_DATE:以‘YYYY-MM-DD’或YYYYMMDD格式返回今天日期值;
    • CURRENT_TIME:以‘HH:MM:SS’或HHMMSS格式返回当前时间值;

4.7 复杂查询

4.8 分库分表以及分库分表带来的数据一致性问题解决

​ 水平分库分表:每个表、库结构一样,每个表、库数据不一样,所有表、库并集是全量数据;

​ 垂直分库分表:每个库、表结构不一样,数据也不一样,所有表、库并集是全量数据;

​ **工具:**TDDL、Mycat,tddl是jar包,Mycat是中间件;

​ 优秀文档:分库分表:

​ https://blog.csdn.net/qq_17231297/article/details/108526004

4.9 优化以及执行计划

  1. 字段层面上

    尽量使用Tinyint、smallint、Medium_int代替int;

    varchar长度只分配真正需要的空间;

    使用枚举或者整数代替字符串类型;

    尽量使用timestamp而非DateTime;

    单表字段不要太多;

    避免使用null;

    用整型来存IP;

  2. 开启查询缓存;

  3. sql书写层面

    • 在mysql 5.0之前版本尽量避免使用or,可以用union或者子查询代替,早期版本or会导致索引时失效,在之后的版本引入了索引合并,就不会导致索引失效的问题;
    • 避免在where中使用!=或者<>操作符,会导致索引失效;
    • 适当使用前缀索引,定义字符串的一部分作为索引,减少空间占用;
    • 避免Select*
    • 尽量使用连接查询代替子查询,子查询会新创建临时表,join不会建立临时表;
    • 先查小表后大表
    • 不要再列字段上进行算术运算或者其他表达式运算;
    • 适当增加冗余字段;
    • where条件中尽量避免字段类型转换;
    • 尽量使用union all 代替union,union会涉及排序,增大CPU运算;
    • 尽量少排序order by;
    • 需要查询一条数据使用limit 1;
    • 避免where中对null进行判断;
    • 不建议使用%前缀模糊查询,如果使用建议使用全文索引;
    • 遵守最左前缀
  4. 查询计划参数

    id:选择标识符,id越大优先级越高,就越先被执行;

    select_type:查询类型

    table:输出结果集的表

    partitions:匹配的分区

    type:表的连接类型,取值有:

    • all :扫描全表数据;
    • index:遍历索引;
    • range: 索引范围查找
    • index_subquery:在子查询中使用ref
    • unique_subquery:在子查询中使用eq_ref;
    • ref_or_null:对null进行索引的优化的ref;
    • fulltext:使用全文索引;
    • ref:使用非唯一索引查找数据;
    • eq_ref:在join查询中使用主键或者唯一索引关联;

    possible_keys:在查询时,可能使用的索引;

    key:实际使用的索引;

    key_len:索引字段的长度;

    ref:列与索引的比较;

    rows:大概估算的行数;

    filtered:按表条件过滤的行百分比

    Extra:执行情况的描述和说明

  5. 数据库结构优化

    最小数据长度,比如身份证,可以设置为char(18),不要设置成varchar(18);

    使用最简单数据类型;

    尽量少定义text类型

  6. 选择合适的存储引擎;

  7. 分库分表,读写分离;

  8. 系统硬件优化

    磁盘优化,固态硬盘,多个小磁盘代替一个大磁盘;

    网络方面优化;

    内存优化,内存越大,存储和缓存的信息也就越多;

  9. 优化慢查询;

4.10 相关面试题

  1. 锁的类型有哪些?

    共享锁、排它锁,共享锁也叫读锁,可以通过lock in share mode实现,只能读不能写,写锁是排它锁,他会阻塞其他的写锁和读锁,从颗粒度上来分,又可以分为表锁和行锁两种;

    行锁又可以分为乐观锁和悲观锁,乐观锁通过版本号实现,悲观锁通过for update实现

  2. 事务基本特性和隔离级别?

    基本特性:

    原子性:要么全部成功,要么全部失败,可以由undo log日志保证;

    **一致性:**总是从一个一致性状态转换为另外一个一致性状态,代码层面来 保证;

    **隔离性:**一个事务的修改在提交前对其他事物是不可见的,由MVCC保证;

    持久性:事务一旦提交,所有的修改就会永久保存,由内存和redo log保证;

    隔离级别:

    **读未提交:**可能读到其他事物未提交的数据,也叫脏读;

    读已提交: 两次读取结果不一致,也叫不可重复读;

    **可重复读:**mysql默认级别,每次读取结果一样,但是有可能产生幻读;

    **串行:**给每一行读取的数据加锁,会导致大量超时和锁竞争问题;

    MVVC:多版本并发控制,保存数据在某个时间节点的快照;查找创建版本小于或者等于当前事务版本,删除版本为空或者大于当前事务版本;

  3. 分表后如何保证ID唯一性?

    设定步长;

    分部式ID,自己实现或者基于雪花算法;

    分表后不使用主键作为查询依据;

  4. mysql主从同步?

    原理:

    master提交事务后,写入binlog;

    slave连接到master,或者binlog;

    master创建dump线程,推送binglog到slave

    slave启动一个IO线程读取同步过来的master的binglog日志,记录到relay log中;

    slave 再开启一个sql线程读取relay log事件并在slave执行,完成同步;

    slave 记录自己的binglog;

    引申出“全同步复制”,半同步复制(ACK确认机制)

  5. MySQL B Tree索引和Hash索引的区别?

    B+树是一个平衡二叉树,从根节点到每个叶子节点高度差值不超过1,而且同层级的节点有指针相互链接,基于索引的顺序扫描时可以利用双向指针快速左右移动,效率很高;因此被广泛应用于数据库,文件系统;

    哈希索引:采用哈希算法,把键值换算成新的哈希值,不需要逐级查找,一次哈希就可以定位,速度很快;

    等值查询时,哈希索引有绝对优势;

    如果是范围查询检索,最好使用B+索引,所以哈希不适合用于排序,以及 like ’xxx%‘这样的部分模糊查询;

    哈希索引不支持多列联合索引的最左匹配原则;

    如果有大量重复值,也不适合用哈希,因为存在哈希碰撞;

  6. 数据库范式?

    第一范式:确保每列保持原子性;

    第二范式:确保表中的每列都和主键相关;

    第三范式:确保每列都和主键列直接相关,而不能间接相关;

  7. 数据库崩溃时事务恢复机制?

    Undo log:实现事务的原子性,也可以实现多版本并发控制,数据提交前先写入磁盘,会导致大量磁盘IO,性能低;

    Redo Log: 记录的是新数据备份

  8. 分库分表主键问题

    UUID:业务无关性

    雪花算法:由64位bit二进制数组组成,分为4大部分:

    • 1位默认不使用
    • 41位时间戳
    • 10位机器ID
    • 12位序列号

4.11 拓展工具

  1. mysql 数据库压测工具

    sysbench:压测数据库和压测系统资源,

    fio:测试linux磁盘IO性能

    tpcc-mysql:比较全面会统计出系统的资源情况

    **性能匹配指标:**TPS(读),QPS(每秒可处理事务数),RT,稳定性,CPU ,IO,内存,网络

  2. mysql 分库分表工具

    Shareding-sphere:是一个jar包

    TDDL: jar包

    Mycat:中间件

  3. mysql 数据同步工具

    DBSync

    • 非侵入式,独立运行
    • 支持异构数据库同步
    • 支持增量同步
    • 支持二进制字段
    • 先比较再同步
    • 无人值守同步
    • 支持各种数据库
    • 支持跨平台以及异地同步
    • 支持双向同步
    • 支持同步后处理
    • 秒级实时同步

    SyncNavigator:

    • 试用于mysql,SQL Server
    • 自动/定时同步数据
    • 无人值守
    • 故障自动恢复
    • 同构/异构数据库同步
    • 断点续传和增量同步

    sql比较

    sql增量

  4. mysql 性能监控工具

    Prometheus:

    Grafana:

五、Spring框架

5.1 Spring原理解析

5.2 认识bean

5.3 IOC、AOP、DI原理

5.4 设计模式

5.5 相关源码解析

5.6 注解

5.7 相关面试题

  1. 什么是Spring、原理、结构、作用?

    开源应用框架,轻量级,松耦合的容器框架;

    能够简化应用程序开发,提供IOC容器,根据需要生成对象并处理对象之间的依赖关系,管理对象的生命周期;

    提供面向切面编程(AOP)思想,降低代码耦合,给应用程序动态添加一些功能,完善业务逻辑;

    提供事务管理(主要是声明式事务),提高开发效率和质量;

    也是一种粘合剂,除了自己提供的data(数据处理),web应用程序,AOP,Beans,Core,Context,test等模块,还能粘合springMVC,mybatis等框架进行联合开发;

  2. Spring应用上下文?

    框架本身提供了很多容器实现,大概分为2种,一种是不常见的BeanFactory,最简单的容器,提供基本的DI功能,另外一种就是集成了BeanFactory后派生而来的引应用上下文,抽象接口就是ApplicationContext

    • AnnotationConfigApplicationContext:从一个或多个基于java的配置类 中加载上下文定义,适用于java注解的方式;
    • ClassPathXmlApplicationContext:从类路径下的一个或多个xml配置文件中加载上下文定义,适用于xml配置的方式;
    • FileSystemXmlApplicationContext:从文件系统下的一个或多个xml配置文件中加载上下文定义,也就是说系统盘符中加载;
    • AnnotationConfigWebApplicationContext:专门为web应用准备的,适用于注解方式;
    • XmlWebApplicationContext:从web应用下的一个或多个xml配置文件加载上下文定义,适用于xml配置方式;

    系统默认的是XmlWebApplicationContext;

    在java项目中,会通过ClassPathXmlApplicationContext来实例化ApplicationContext容器,而在Web项目中是通过Web服务器来完成;

    ​ WebApplicationContext扩展了ApplicationContext,它允许从相对于Web根目录的路径中装载配置文件完成初始化工作。从WebApplicationContext中可以获得ServletContext的引用,整个Web应用上下文对象将作为属性放置到ServletContext`中,以便Web应用环境可以访问Spring应用上下文;

    在非Web应用中,bean只有singleton和prototype两种作用域,WebApplicationContext为bean添加了三个新的作用域:request、session、global session;

  3. BeanFactory 和ApplicationContext 区别;

    • bean集合的工厂类,包含各种bean定义,以便在接收到客户端请求时将对应的bean实例化;
    • beanFactory包含了bean生命周期的控制,调用客户端的初始化和销毁方法;
    • ApplicationContext 在此基础上还提供了以下功能:
    1. 支持国际化消息;
    2. 统一资源文件读取方式;
    3. 监听接口可以监听容器事件,并对事件进行响应处理;
    4. **LifeCycle:**该接口是Spring 2.0 加入的,该接口提供了start()和stop()两个方法,主要用于控制异步处理过程;
    5. ConfigurableApplicationContext 扩展于ApplicationContext,它新增加了两个主要的方法:refresh()和close(),让ApplicationContext 具有启动、刷新和关闭应用上下文的能力;
  4. 什么是bean?

    • bean的本质是java中的类,spring中的bean实际就是对实体类的引 用,来产生java类对象,从而实现生产和管理bean;
    • bean工厂是spring最核心的接口,提供高级IOC配置机制;
    • bean提供三种实例化方式:构造器实例化,静态工厂方式实例化、实例工厂方式实力化;
  5. IOC实现方式?实现机制?

    (1)基于XML文件的显示装配:

    • 构造方法注入(需要含有有参构造器);
    • set方法注入(需要含有无参构造器);

    ​ 此方法装配可能导致XML文件过于臃肿,不好管理;

    (2)基于注解(Annotation)的隐式装配:此时需要使用context:annotation-config/在配置文件中开启相应的注解器;

    (3)接口注入:具备侵入性,实际体验并不好;

    实现机制:工厂模式加反射机制

  6. bean 的作用域?

    singleton: 单例作用域,在IOC容器中仅存在一个Bean实例;

    prototype:每次从容器中调用Bean时,都返回一个新的实例;适用于有状态的bean

    request: 每次Http请求都会创建一个新的Bean,该作用域仅适用于web的 Spring WebApplicationContext环境;

    session: 同一个http Session共享一个Bean,不同的Session使用不同的Bean,该作用域仅适用于web的 Spring WebApplicationContext环境;

    application: 限定一个Bean的作用域为ServletContext的生命周期。该作用域仅适用于web的Spring WebApplicationContext环境

  7. Spring几种配置方式?

    基于Xml配置;

    基于注解配置;

    基于java API实现:通过@Bean 和@Configuration来实现

  8. bean的生命周期?

    实例化—>属性赋值–>初始化—>销毁(容器关闭)

    createBeanInstance()->实例化

    populateBean()->属性赋值

    initializBean()->初始化

  9. spring bean自动装配?

    • no:默认设置,表示没有自动装配,应使用显式 bean 引用进行装配;
    • byName - 它根据 bean 的名称注入对象依赖项。它匹配并装配其属性与 XML 文件中由相同名称定义的 bean;
    • byType - 它根据类型注入对象依赖项。如果属性的类型与 XML 文件中的一个 bean 名称匹配,则匹配并装配属性;
    • 构造函数 - 它通过调用类的构造函数来注入依赖项。它有大量的参数。
    • autodetect - 首先容器尝试通过构造函数使用 autowire 装配,如果不能,则尝试通过 byType 自动装配;
  10. spring 设计模式?

  11. spring事务传播?

    事务类别:

    有事务就用已有的,没有就重新开启一个;

    有就用已有的,没有也不重新开启;

    必须有事务,没有抛异常;

    开启新事物,若当前已有事务,挂起当前事务;

    不需要事务,当前已有则挂起;

    不需要事务,当前已有则抛异常;

    嵌套事务;

    事务实现方式:

    基于TransactionProxyFactoryBean声明式事务管理;

    基于@Transactional 的声明式事务管理;

    基于Aspectj AOP配置事务;

    编程式事务:

    • 使用TransactionTemplate 事务模板对象,
    • 直接使用PlatformTransactionManager配置事务管理器;

    声明式事务:

    • 定义事务管理器;
    • 配置事务属性;
  12. spring核心类有哪些?

    • DefaultListableBeanFactory:整个bean加载的核心部分,是spring注册以及加载bean的默认实现,继承自AbstractAutowireCapableBeanFactory ;
    • XmlBeanFactory继承自DefaultListableBeanFactory,对其进行了扩展,增加了XmlBeanDefinitionReader类型的reader属性,在XmlBeanFactory中主要使用reader属性对资源文件进行读取和注册;

    • XmlBeanDefinitionReader:资源文件读取,解析以及注册
  13. AutoWired注解和Resource注解区别?

    • Resource作用相当于AutoWired,前者按byName注入,后者按byType注入;
    • Resource可以指定byName和byType;
    • AutoWired默认情况下要求依赖对象必须存在,如果允许null,可以设置required属性为false,指定名称可以和@Qualifier注解组合使用;
    • Resource属于J2EE提供,需要JDK1.6以上,而AutoWired属于spring提供;

六、SpringMVC框架

6.1 原理

6.2 请求流程

img

  1. 客户端发起http请求被DispatcherServlet 捕获;

  2. DispatcherServlet 根据 -servlet.xml 中的配置对请求的 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用 HandlerMapping 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以HandlerExecutionChain 对象的形式返回;

  3. DispatcherServlet 根据获得的Handler,选择一个合适的适配处理器 HandlerAdapter;

  4. 提取Request中的模型数据,填充Handler入参,执行controller,经过数据转换,数据根式化,数据验证;

  5. Handler(Controller)执行完成后,向 DispatcherServlet 返回一个 ModelAndView 对象;

  6. 根据返回的ModelAndView,选择一个适合的 ViewResolver(必须是已经注册到 Spring 容器中的ViewResolver)返回给DispatcherServlet;

  7. ViewResolver 结合Model和View,来渲染视图,视图负责将渲染结果返回给客户端。

6.3 常见配置

6.4 相关面试题

七、Mybatis框架

7.1 理论基础

  1. 每一个Mybatis应用程序入口都是:SqlSessionFactoryBuilder,作用是通过XML配置文件创建Configuration对象,然后通过build方法创建SqlSessionFactory对象;

  2. SqlSessionFactory:实例中心,创建SqlSession会话;

  3. SqlSession:Mybatis工作顶层API,表示和数据库交互的对话,完成必要的数据库增删改查功能,SqlSession通过调用api的Statement ID找到对应的MappedStatement对象;

    SqlSession是一个接口,默认使用DefaultSqlSession,他有2个必须配置的属性:Configuration和Executor、SQLSession通过内部存放的执行器对数据进行CRUD;

  4. Executor:Mybatis执行器,是Mybatis调度核心,在创建COnfiguration对象的时候创建,并且缓存在Configuration 对象里,负责sql语句的生成,调用StatementHandler访问数据库,查询缓存的维护,解析MappedStatement对象,sql参数转化,动态sql拼接,生成jdbc Statement对象,执行器有2个实现类,BaseExecutor(有三个继承类,分被是BatchExecutor:重用语句并执行批量更新,ReuseExecutor:重用预处理语句prepared statements,SimpleExecutor:普通执行器);

  5. StatementHandler:封装了JDBC Statement操作,负责对JDBCstatement的操作,如设置参数、将Statement结果集转换成List集合,是真正访问数据库的地方,并调用ResultSetHandler处理查询结果;

  6. ResultSetHandler :负责将用户传递的参数转换成JDBC Statement 所需要的参数;

  7. MappedStatement :用来存放我们SQL映射文件中的信息包括sql语句,输入参数,输出参数等等。一个SQL节点对应一个MappedStatement对象;

  8. SqlSource:负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回;

7.2 执行流程

7.3 常见面试题

  1. Mybatis 一级缓存、二级缓存?
    • 一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存;
    • 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。
      默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;

7.4 Mybatis Plus

7.5 常见优化工具

美团技术沙龙:https://tech.meituan.com/2020/06/18/inf-bom-mybatis.html

八、SpringBoot

8.1 原理解析

  1. 启动流程?

    模块划分:

    • 第一部分,运行SpringApplication的初始化模块,配置基本的环境变量、资源、构造器、监听器;
    • 第二部分,实现应用具体的气动方案,包括启动流程的监听模块,加载配置模块,以及核心的创建上下文环境模块;
    • 第三部分,自动化配置模块;

    启动流程:

    • main方法是入口,通过SpringApplication.run()启动;

    • SpringBootApplication注解包含三个注解,分别是:

      @EnableAutoConfiguration:SpringBoot根据应用所声明的依赖来对Spring框架进行自动配置;

      @SpringBootConfiguration(内部为@Configuration):被标注的类等于在spring的XML配置文件中(applicationContext.xml),装配所有bean事务,提供了一个spring的上下文环境;

      @ComponentScan:组件扫描,可自动发现和装配Bean,默认扫描SpringApplication的run方法里的Booter.class所在的包路径下文件:组件扫描,可自动发现和装配Bean,默认扫描SpringApplication的run方法里的Booter.class所在的包路径下文件

    • run()方法内部先调用一个initialize(sources)方法;

    • 创建应用的监听器SpringApplicationRunListeners并开始监听,

    • 加载SpringBoot配置环境(ConfigurableEnvironment),如果通过Web容器发布,会加载StandardEnvironment;

    • 将配置环境加入到监听器对象中;

    • 创建run方法的返回对象ConfigurableApplicationContext(应用配置上下文),会先先获取显示设置的应用上下文,如果不存在,在加载默认环境配置,默认使用AnnotationConfigApplicationContext注解上下文,最后通过BeanUtis实例化上下文对象;

      调用初始化refreshContext(context)方法,加载Spring.factories,主要用到SpringFactoriesLoader;

      @EnableAutoConfiguration自动配置就变成了:从classpath中搜寻所有的META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项通过反射(Java Refletion)实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,然后汇总为一个并加载到IoC容器;

      优秀连接:https://blog.csdn.net/zlc3323/article/details/100137222?utm_medium=distribute.pc_relevant.none-task-blog-BlogCfData-1.baidujs&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCfData-1.baidujs

8.2 与Spring相比优势

8.3 微服务核心思想

8.4 拓展框架

8.5 springboot相关面试题

九、SpringClould

9.1 springCloud原理

9.2 springcloud各类微服务模块原理以及作用

  1. 服务注册与发现(Eureka、consul、Nacos、zookeeper)

    **概念:**服务治理模块,实现服务治理与发现,分为服务端与客户端,服务端用作注册中心,支持集群部署,客户端用来处理服务注册与发现;

    原理:

    Eureka Server注册表直接基于纯内存,在内存里维护了一个数据结构,基于CocurrentHashMap;

    服务注册,服务下线,服务故障,全部在内存里维护和更新注册表;

    Eureka Server端采用多级缓存机制,避免同时读写内存数据结构造成并发冲突问题,进一步提升服务请求的响应速度;

    在拉取注册表的时候:先从ReadOnlyCacheMap里查缓存的注册表

    若没有,就找ReadWriteCacheMap里的缓存注册表,如果还没有就在内存中获取实际的注册表数据;

    **注册表发生变更时:**在内存中更新变更的注册表,同时过期掉ReadWriteCacheMap,不会影响ReadOnlyCacheMap,当发现ReadWriteCacheMap被清空,也会清空ReadOnlyCacheMap;

    eureka自我保护:如果在15分钟内超过15%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现网络故障,Eureka Server自动进入自我保护机制,会有三种情况:

    • Eureka Server不再从注册列表中移除因为长时间没收到心跳而应该过期的服务。
    • Eureka Server仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用。
    • 当网络稳定时,当前Eureka Server新的注册信息会被同步到其它节点中;
    • 可通过配置 eureka.server.enable-self-preservationtrue打开/false禁用自我保护机制,默认打开状态,建议生产环境打开此配置;
    • 也可以修改减短服务心跳的时间:lease-expiration-duration-in-seconds: 默认90秒,lease-renewal-interval-in-seconds: 默认30秒;

    eureka集群:各节点之间相互注册,保证AP;

    eureka配置详解

    • eureka客户端:配置@EnableEurekaServer注解
    • eureka服务端:@EnableEurekaClientz注解能实现但是只支持eureka作为注册中心,@EnableDiscoveryClient可以是其他注册中心;

    eureka负载均衡策略:

    在RestTemplate配置方法上添加@LoadBalanced注解

  2. 服务调用(rest、RPC、GRPC,SOAP)

    RPC:远程过程调用,用于解决协议约定、网络传输、服务发现三个问题

    RPCRuntime负责最底层的网络传输;Stub处理客户端和服务端约定好的语法语义的封装和解封装;用户服务器这层负责处理业务逻辑;

    **SOAP:**基于文本xml的一种应用协议

    协议约定使用WSDL(web服务描述语言),面向对象;

    基于HTTP进行传输;

    服务发现使用UDDI(统一描述发现集成)

    **gRPC:**二进制RPC框架,使用netty Channel进行数据传输,压缩性高,速度快,基于HTTP2.0流传输,支持多语言;

    REST:一种架构风格,通过http协议实现;

  3. 服务熔断(Hystrix)

    Hystrix是一个通过添加超时容错和失败容错逻辑来帮助程序控制这些分布式系统的交互,通过隔离服务之间的访问,阻止他们之间的级联故障以及提供后背选项来实现这些;

    作用:

    • 通过第三方的调用,给与保护和控制延迟和失败;
    • 在复杂的分布式系统中复制级联失败;
    • 快速失败和修复;
    • 可能情况下,回滚优雅失败;
    • 实时监控,报警和操作控制;

    隔离方式:

    • 线程隔离
    • 信号量隔离
    • 请求合并
    • 请求缓存
    • 仪表盘

    使用:

    • 增加服务熔断机制@HystrixCommand;
    • 主启动类添加注解@EnableCircuitBreaker
  4. 负载均衡(Ribbon Nginx)

    基于http和TCP的客户端的负载均衡工具

    将微服务之间的rest请求转为客户端的负载均衡的RPC调用;

    默认策略是轮询,但是不止一种;

    工作原理:

    • 微服务之间通过Feign调用,最后通过loadBalancerFeignClient发送请求;
    • loadBalancerFeignClient端从client端服务上的上下文环境中找到负载均衡器,并把提取到的服务名称交给负载均衡器;
    • 负载均衡器选择server实例,将client端的请求包装成调用请求loadBalancerCommand;
    • 根据封装的信息,发送远程调用到具体的服务实例;
    • 核心是LoadBalancer

    负载均衡策略:

    • 轮询的方式;
    • 随机方式;
    • **最大可用策略:**会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量小的服务;
    • **加权轮询策略:**根据权重,响应越快服务权重越大,被选中的概率也越大;
    • **可用过滤策略:**先过滤掉由于多次访问故障而处于断路器状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问;
    • 先按照轮询策略获取服务,如果获取失败则在指定时间内进行重试,获取可用的服务;
    • **区域感知策略:**复合判断Server所在区域的性能和Server的可用性选择服务器;
  5. 服务接口调用(fegin)

    简化服务接口调用,通过@EnableFeignClients注解开启fegin功能,在需要使用的接口上@FeignClient

  6. 消息队列(kafka、Mq)

  7. 配置中心

  8. 服务网关Gateway

    作用:

    • 统一入口,为服务提供一个唯一的入口,网关起到外部和内部隔离的作用,保障了后台服务的安全性;
    • 鉴权校验:识别每个请求的权限,拒绝不符合要求的请求
    • 动态路由:动态的将请求路由到不同的后端集群中;
    • 减少客户端与服务端的耦合:服务可以独立发展,通过网关层来做映射;

    网关启动器:@EnableZuulProxy

    网关配置:默认、url、服务名称、排除|忽略、前缀

    ZUUL网关过滤器:用来过滤代理请求,提供额外功能逻辑,Zuul提供的过滤器是一个父类,父类是ZuulFilter,通过父类中的定义的抽象方法filterType,来决定当前的Filter种类是什么,有前置过滤、路由后过滤、后置过滤、异常过滤;

    优秀博客:https://blog.csdn.net/rain_web/article/details/102469745

  9. 消息总线

    轻量级的消息代理来构建一个共用的消息主题来连接各个微服务实例,它广播的消息会被所有在注册中心的微服务实例监听和消费,也称消息总线;

  10. Nacos

    功能:

    • 服务地址管理
    • 服务注册
    • 服务动态感知

    原理:

    • 服务提供者:Provider APP
    • 服务消费者:Consumer APP
    • Name Server:通过Virtual IP或者DNS的方式实现Nacos高可用集群服务路由
    • Nacos Server :Nacos服务提供者

    集群节点数据同步,采用Raft算法实现

9.3 springCloud服务治理、监控

9.4 各大版本差异性

9.5 springcloud相关面试题

  1. 什么是SpringCloud?
    • 一系列框架的有序集合,利用Spring boot的开发遍历巧妙的简化了分布式系统基础设施的开发,如服务注册,配置中心,智能路由、消息总线、负载均衡、断路器、数据监控等
  2. SpringCloudApplication注解?

十、redis

10.1 原理解析

10.2 常见数据类型以及作用

10.3 redis高级特性

10.4 redis常用高级组件

10.5 redis 应用

10.6 redis 相关面试题

  1. redis和Memcache的区别?

    • Redis和Memcache都是将数据存放在内存中,都是内存数据库,不过后者还可以缓存图片,视频等;
    • redis 不仅支持简单的k/v结构,还提供list,set,hash,string,SortedSet,HyperLogLog、Geo、pub/sub等数据存储结构;
    • 虚拟内存–Redis当物理内存用完时,可以将一些很久没用到的value 交换到磁盘;
    • 过期策略–memcache在set时就指定,例如set key1 0 0 8,即永不过期。Redis可以通过例如expire 设定,例如expire name 10;
    • 分布式–设定memcache集群,利用magent做一主多从;redis可以做一主多从。都可以一主一从;
    • 存储数据安全–memcache挂掉后,数据没了;redis可以定期保存到磁盘(持久化);
    • 灾难恢复–memcache挂掉后,数据不可恢复; redis数据丢失后可以通过aof恢复;
    • Redis支持数据的备份,即master-slave模式的数据备份;
    • 应用场景不一样:Redis出来作为NoSQL数据库使用外,还能用做消息队列、数据堆栈和数据缓存等;Memcached适合于缓存SQL语句、数据集、用户临时性数据、延迟查询数据和session等。
  2. redis 高级应用?

    • redis Module

    • BloomFilter:实际上是一个很长的二进制向量和一系列随机映射函数,可用于检索一个元素是否在一个集合中,优点是空间利用率和查询时间远超一般算法,缺点是存在一定误识和删除困难

      **原理:**当一个元素加入集合,通过k个散列函数将这个元素映射成一个位数组中的k个点,把他们置为1,检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。

    • RedisSearch

    • Redis-ML

十一、Dubbo

11.1 dubbo 底层原理解析

11.2 dubbo 常见配置

11.3 dubbo底层协议

11.4 dubbo高级特性

11.5 dubbo应用场景

11.6 dubbo重复消费,消息丢失等问题

11.7 关于Zookeeper相关

11.8 dubbo集群

11.9 dubbo相关面试题

十二、消息中间件以及Zookeeper

12.1 zookeeper底层原理

12.2 zookeeper 常见问题分析

  1. zk数据存储类型?

12.3 zookeeper 分布式锁实现

12.4 zookeeper 高级特性

12.5 zookeeper 集群配置

12.6 zookeeper 相关面试题

12.7 消息中间件AQ、MQ、Rb相关原理解析

  1. RocketMq原理解析

    • NameServer:mq注册中心,管理2部分数据,集群的Topic-queue的路由配置,Broker的实时配置信息,其它模块通过Nameserver提供的接口获取最新的topic配置和路由信息;

    • producer/Consumer ,通过查询接口获取Topic对应的Broker的地址信息;consumer可以以两种模式启动,广播和集群,广播模式,一条信息会发给所有consumer,集群模式下信息只会发给一个consumer;

    • Broker:注册配置信息到nameserver,实时更新topic信息到nameServer,负责接收并存储信息,同事提供push/pull接口来将信息发送给consumer

    • Broker:接收Producer发送过来的信息、处理Consumer的消息请求、消息的持久化存储、以及服务端过滤功能等;

    • 消息存储是由ConsumeQueue CommitLog配合完成的,真正消息存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,CommitLog是以物理文件的方式存储,CommitLog顺序写,随机读ConsumeQueue被写到磁盘文件里做永久存储,虽然是随机读,但是利用操作系统的pagecache机制,可以批量地从磁盘读取,作为cache存到内存中,

    • RocketMq集群中,Master支持读写,slave只支持读,并且维持了自动切换机制,master出现故障,也不会影响业务正常进行读,保证消费端高可用,服务端高可用,创建topic的时候,可以吧topic的多个Message queue创建在多个Broker组上

    • 同步刷盘和异步刷盘,通过broker中的flushDiskType参数设置,值可以取SYNC_FLUSH、ASYNC_FLUSH中一个;

    • 同步复制,异步复制:异步复制有较低延迟较高吞吐量,但是容易造成数据丢失,同步复制,slave有备份,数据容易恢复,但是会增大数据写入延迟,降低系统吞吐量,可以通过broker中的borkerRole参数设定,取值为ASYNC_MASTER、SYNC_MASTER、SLAVE;

  2. 中间件常用协议

    • AMQP:统一消息服务的应用层标准高级消息队列协议
    • MQTT协议:即时通讯协议;
    • STOMP:流文本定向消息协议;
    • XMPP:可拓展消息处理现场协议;
  3. 各消息队列区别

    RabbitMq:基于主从做高可用的,有三种模式,单机模式,普通集群模式,镜像集群模式,每台机器上存储着全量的数据

12.8 消息丢失、重复消费、事务相关

十三、高并发集群问题

13.1 高并发解决办法

13.2 高并发常用工具框架

13.3 集群问题分析解决

13.4 高并发集群相关面试题

十四、算法以及数据结构

14.1 java常见的算法

在这里插入图片描述

  1. 冒泡排序

    属于比较排序,平均时间复杂度O(n^2),空间复杂度1,核心思想,两个两个比较,大的往下沉

    伪代码:

    public static void maoPao() {
            int[] array = new int[10];
            Scanner sc = new Scanner(System.in);
            for (int i = 0; i < array.length; i++) {
                array[i] = sc.nextInt();
            }
            int len = array.length;
            for (int i = 0; i < len; i++) {
                for (int j = 0; j < len - 1 - i; j++) {
                    if (array[j] > array[j + 1]) {
                        int temp = array[j];
                        array[j] = array[j + 1];
                        array[j + 1] = temp;
                    }
                }
            }
            for (int i = 0; i < len - 1; i++) {
                System.out.print(array[i] + ",");
            }
            System.out.print(array[len - 1]);
        }
    
  2. 选择排序

    最稳定的排序算法之一,不管什么数据,时间复杂度都是O(n^2),所以用选择排序,数据规模越小越好;

    **核心思想:**首选在未排序列中找到最小(大)元素,存放到排序列的起始位置,然后再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序的末尾,以此类推,直到所有元素排序完毕;

    for (int i = 0; i < len - 1; i++) {
           int minIndex = i;
           for (int j = i + 1; j < len; j++) {
                 if (array[j] < array[minIndex]) {
                      minIndex = j;
                  }
           }
           //交换位置
           if (i != minIndex) {
                 int temp = array[i];
                 array[i] = array[minIndex];
                 array[minIndex] = temp;
           }
    }
    
  3. 插入排序

    核心思想:通过构建有序序列,对于未排序数据,在已经排序序列中从后向前扫描,找到相应位置并插入;空间复杂度O(1),时间复杂度O(N)

    步骤:

    • 从第一个元素开始,该元素可以认为已经被排序;
    • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
    • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
    • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
    • 将新元素插入该位置后;
    • 重复步骤2~5;
    int inserNum; //要插入排序的数
    for (int i=1;i<length;i++){
      insertNum = array[i];
      //已经排好序的元素个数
      int j = i-1;
      while(j >= 0 && array[j] >insertNum) {
         array[j+1] = array[j];
         j--;
      }
      array[j+1] = insertNum;
    }
    
  4. 希尔排序

    核心思想:按一定增量分组,对每组使用直接插入排序算法,随着增量逐渐减少,每组包含的关键词越来越少,当增量减至1时,整个文件刚好被分成一组

    第一部分,选择增量length/2,将其分成length/2组,缩小增量,再以(length/2)/2,依次进行

    img

    代码:

    int length = array.length;
    int temp,gap = length/2;
    while (gap >0) {
        for (int i = gap;i<length; i++){
             temp = array[i];
             int preIndex = i-gap;
             while (preIndex >= 0 && array[preIndex]                                     >temp){
                   array[preIndex+gap] = array[preIndex];
                   preIndex -= gap;
             }
             array[preIndex+gap] = temp;
        }
        gap /= 2;
    }
    

    最佳情况:T(n) = O(nlog2 n) 最坏情况:T(n) = O(nlog2 n) 平均情况:T(n) =O(nlog2n)

  5. 归并排序

    • 采用分治法,把长度为n的序列分成2个长度为n/2的子序列;
    • 对这2个子序列分别采用归并排序;
    • 将两个排序好的子序列合并成一个最终的排序序列
    sort(array,array.length -1);
    
    sort(int[] array,int left,int right) {
         if(left == right){
             return;
          }
         int mid = left +((right-left) >>1);
         //对左侧子序列进行递归排序
         sort(array,left,mid);
         //对右侧子序列进行递归排序
         sort(array,mid +1,right);
         //合并
         merge(array,left,mid,right);
    }
    
    merge(int[] array, int left, int mid, int right) {
    	int[] temp = new int[right - left + 1];
    	int i = 0;
    	int p1 = left;
    	int p2 = mid + 1;
    	// 比较左右两部分的元素,哪个小,把那个元素填入temp中
    	while (p1 <= mid && p2 <= right) {
    		temp[i++] = array[p1] < array[p2] ? array[p1++] : array[p2++];
    	}
    	// 上面的循环退出后,把剩余的元素依次填入到temp中
    	// 以下两个while只有一个会执行
    	while (p1 <= mid) {
    		temp[i++] = array[p1++];
    	}
    	while (p2 <= right) {
    		temp[i++] = array[p2++];
    	}
    	// 把最终的排序的结果复制给原数组
    	for (i = 0; i < temp.length; i++) {
    		array[left + i] = temp[i];
    	}
    
    
    
    
    
  6. 快速排序

    • 从数列中挑选一个元素,称为基准;
    • 重新排序数列,所有比基准值晓得元素放在基准前面,所有比基准大的元素放在基准后面(相同可以放在任意一边)
    • 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序;
    public static void quickSort(int[] array) {
    	quickSort(array, 0, array.length - 1);
    }
    
    
    private static void quickSort(int[] array, int left, int right) {
    	if (array == null || left >= right || array.length <= 1) {
    		return;
    	}
    	int mid = partition(array, left, right);
    	quickSort(array, left, mid);
    	quickSort(array, mid + 1, right);
    }
    
    
    private static int partition(int[] array, int left, int right) {
    	int temp = array[left];
    	while (right > left) {
    		// 先判断基准数和后面的数依次比较
    		while (temp <= array[right] && left < right) {
    			--right;
    		}
    		// 当基准数大于了 arr[left],则填坑
    		if (left < right) {
    			array[left] = array[right];
    			++left;
    		}
    		// 现在是 arr[right] 需要填坑了
    		while (temp >= array[left] && left < right) {
    			++left;
    		}
    		if (left < right) {
    			array[right] = array[left];
    			--right;
    		}
    	}
    	array[left] = temp;
    	return left;
    }
    
    

    最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(nlogn)

  7. 堆排序

    堆是一个近似完成二叉树的结构,满足堆的性质,子节点的键值或者索引总是小于(或者大于)他的父节点大于等于其左右孩子节点的值,称为大顶堆,小于或等于其左右孩子节点的值,称为小顶堆

    • 将初始待排序关键字序列构建成大顶堆;
    • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
    • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
    public static void duiPx(){
            int[] array = {1,34,2,67,3,89,56,23,11,5,8} ;
        }
        private static void heapSort(int[] array){
            if (array == null || array.length <=1){
                return;
            }
            int length = array.length;
            //构建大顶堆
            for (int i = length/2; i >=0; i--) {
                addHeap(array,i,length);
            }
            for (int j = length-1; j >0 ; j--) {
                //将堆顶元素与末尾元素尽心交换
                swap(array,0,j);
                //重新对堆进行调整
                addHeap(array,0,j);
            }
        }
        private  static void addHeap(int[] array,int                                         i,int length){
            int temp = array[i];
            for (int k = i*2+1; k <length ; k =k*2+1) {
                if(k+1 <length && array[k] <array[k+1]){
                    k++;
                }
                if(array[k] > temp){
                    array[i] = array[k];
                    i=k;
                }else {
                    break;
                }
            }
            array[i] = temp;
        }
    
  8. 计数排序

  9. 桶排序

  10. 基数排序

​ 参考链接:https://javajgs.com/archives/1599

14.2 java常见的数据结构以及应用

  1. 链表
  2. 队列

14.3 算法、数据结构相关面试题

  1. 如何在一个1到100的整数数组中找到丢失的数字?

    提示:如果是只有一个数,可以用求和的方式,如果多个,可以用位图BitSet操作

  2. 如何在给定的整数数组中找到重复的数字?

    答:使用set集合,使用map的key

  3. https://www.jianshu.com/p/cec86f055b02

十五、Netty通信

15.1 Netty原理

15.2 Netty 应用

15.3 Netty相关面试题

十六、工具类

16.1 POI导入导出(以及各种性能优化问题)

16.2 文件传输、存储相关工具类(前端、后端)

16.3 ES大数据相关

16.4 单点登录以及JWT

16.5 权限管理以及技术拓展

16.6 定时任务XXJOB

16.7 美团万亿级KV存储架构与实践:美团万亿级 KV 存储架构与实践

十七、Maven、Git工具集

17.1 Maven基础知识

17.2 Maven高级特性

17.3 Maven关键字解析

17.4 git 原理(以及与SVN区别)

17.5 git常用命令

17.6 Maven与git常见面试题

十八、高级前端面试

18.1 vue高级面试

  1. 父子组件传值?

    子组件通过props方法接受父组件传来的值,子组件通过$emit方法向父组件传达数据;

  2. vue生命周期函数?

    beforeCreate

    created

    beforeMount

    mounted

    beforeUpdate

    updated

    beforeDestroy

    destroyed

  3. 创建指令?

    directives结合inserted

  4. 动态路由?

    组件被重复利用,通过参数传递不同的数据,进行渲染;

    router-link组件:导航器,利用to属性导航到目标组件;

  5. axios和ajax区别?

    axios是通过promise实现对ajax的一种封装,就像JQuery实现ajax封装一样,ajax实现了网页的局部数据刷新,axios实现了对ajax的封装,axios是ajax;ajax不止axios;

    axios用于浏览器和node.js的基于promise的http客户端防止csRF

    从浏览器制作XML HttpRequests

    拦截请求和响应

    转换请求和响应数据

  6. vue路由钩子函数?

    主要有6个

    全局路由钩子函数:beforeEach每次路由改变的时候都得执行一遍,有三个参数,to,from,next,可进行一些页面跳转前处理,afterEach:页面加载之后

    单个的路由钩子函数:beforeEnter路由内钩子

    组件内的路由钩子函数:beforeRouteEnter、beforeRouteLeave、beforeRouteUpdate

  7. vue解决跨域问题?

    方法一:

    header(‘Access-Control-Allow-Origin:*’);//允许所有来源访问

    header(‘Access-Control-Allow-Method:POST,GET’);//允许访问的方式

    方法二:

    使用jQuery提供的JSonp,将ajax请求的dataType设置为JSONP

    方法三:

    使用http-proxy-middleware代理解决,在config/index.js中的proxyTable

    配置pathRewrite;

  8. 代替v-model?

    v-bind:value和v-on:input动态监听

  9. 如何给自定义组件添加点击事件?

    在@Click后面加上.native

  10. vue中深拷贝与浅拷贝?

    浅拷贝:新对象赋值,只是取的旧的对象栈中的值,也就是引用对象的值

    深拷贝:会在堆里面开辟一个空间,存放自己的对象值;经常使用JSON.stringfy,递归,localStorage实现对象数组储存,jQ的extend方法(三个参数:deep是否为深拷贝,target Object类型,目标对象,object1,objectN可选***$*.extend( [deep ], target, object1 [, objectN ] )**

    浅拷贝针对基本数据类型,包括number、string、boolean、null、undefined五类,主要存在栈内存中;

    深拷贝针对引用数据类型,包括无序对象,数组,以及函数,名字存在栈内存,值存在于堆内存中,栈内存会提供一个引用的地址指向堆内存中的值,

  11. vue中获取dom元素?

    1.0版本中,通过v-el绑定,通过this.els.XXX获取;

    2.0版本中,通过给元素绑定ref = “XXX”,然后通过this.$refs.XXX或者this.refs[‘XXX’]获取;

  12. 虚拟dom?

    深度遍历diff算法

18.2 react高级面试

  1. 区分Real dom和Virtual DOM?

    real DOM:更新缓慢,可直接更新html,元素更新,则创建新DOM,DOM操作代价很高,消耗内存多

    Virtual DOM:更新更快,无法直接更新html,元素更新,则更新JSX,DOM操作非常简单,很少内存消耗;

    Virtual DOM是一个节点树,他将元素。他们的属性和内容作为对象及其属性,属于real DOM的一个副本

  2. React特点?

    使用虚拟dom

    服务器端渲染

    遵循单向数据流或者数据绑定

    基于JSX,代码可读性好

    只是一个库,而不是一个完整的框架

  3. 浏览器无法读取JSX,需要使用Babel转换器;

    优秀文档:https://blog.csdn.net/eyeofangel/article/details/88797314

18.3 js+H5高级面试

18.4 小程序开发

18.5 app打包常用命令

18.6 优秀表单、报表、统计图开发工具

十九、高级架构思想

19.1 架构流程图

19.2 架构需要考虑的因素

19.3 架构案列

图片

19.4 架构常见面试题

19.5 性能监测、跟踪相关

二十、高并发编程、事务、锁机制

20.1 高并发编程

20.2 高并发如何解决事务

20.3 高并发中的锁

20.4 高并发需要解决的问题

20.5 高并发场景

20.5 相关面试题

二十一、Linux常用命令

21.1 操作系统常用命令

21.2 优化调优命令

21.3 网络环境搭建

21.4 项目部署

21.5 日志,问题查找

21.6 Docker技术

21.7 K8s自动化部署

二十二、源码解析

22.1 spring源码

22.2 springboot源码

22.3 springMVC源码

22.4 springcloud相关

22.5 源码相关面试

  1. UML

​ https://blog.csdn.net/qq_35495763/article/details/80764914

  1. Linux相关命令

    https://blog.csdn.net/qq_23329167/article/details/8385643

        3. 权限框架有哪些?各自区别用途?
           4. 
    
  2. 服务器搭建配置

    https://www.kancloud.cn/javamall/scbssmsc/710067

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一事无成只会写代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值