导图社区 java知识
java知识思维导图,包括集合、注解、异常处理、反射、虚拟机、设计模式、String相关,java并发的概念、种类和原理等内容。
编辑于2021-12-16 07:43:16java知识
集合
List
扩容机制
ModifiedException
arraylist多线程问题
Set
Map
HashMap
何时扩容
扩容的算法
如何解决散列碰撞
为什么非线程安全
jdk1.7 resize 导致的null问题
jdk1.7 循环引用导致的cpu 100%
jdk1.8 红黑树节点循环引用 cpu 100%
ConcurrentHashmap
jdk1.7 put
尝试获取锁,获取到直接插入
未获取,则自旋等待,知道最大阀值转为阻塞锁
jdk1.8 put
桶为空,则cas写入,失败则自旋直到成功
hashcode == MOVED == -1 则扩容
如果都不满足,则使用synchronize写入
jdk1.7 实现原理
采用分段锁实现, segment, 继承自ReenterLock
jdk1.8 实现原理
采用 cas和 synchronize
SparseArray
稀疏矩阵压缩
注解
元注解
关键字
@Target
@Rentention
注解的作用
检查格式/减少配置/减少重复工作
怎么工作的
异常处理
Exception
运行时异常都是RuntimeException等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般都是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免此类异常的发生。
RuntimeException
NullPointerException
IndexOutOfBoundsException
error
error发生时, java虚拟机一般会选择线程终止。
OutOfMemoryError
StackOverflowError
异常处理的俩个基本原则
尽量不捕获exception通用异常,而是应捕获特定异常
不要生吞异常
NoClassDefError/ClassNotFoundException
ClassNotFoundException产生的原因主要是:java支持反射运行时加载类,例如Class.forName方法来加载类时,如果没有在类路径找到,则抛出此异常。 NoClassDefFoundError产生的原因:jvm或者classloader实例尝试加载(正常的方法调用,或new)类时找不到类的定义。编译的时候存在,但是运行时找不到。
反射
Class.forName
类名.class
this.getClass
Method
Field
Constructor
虚拟机
设计模式
String相关
string为什么要设计成不可变的
java中编码方式
String/StringBuffer/StringBuilder
java并发
线程池
概念
线程池就是事先将多个线程对象放到一个容器中,使用的时候不用new线程,而是直接在池子中拿,节省了开辟线程的时间,提高了代码执行效率。
种类
new CachedThreadPool 不固定线程数量 有空闲线程则复用空闲线程,若无空闲线程则新建线程 一定程度减少频繁创建/销毁线程,减少系统开销 new FixedThreadPool 可控制线程最大并发数 超出的线程会在队列中等待 new SingleThreadExecutor 单线程 new ScheduledThreadPool
原理
从数据结构的角度来看,线程池主要使用了blockingqueue和hashset集合来实现的。 从任务提交的流程来看: 1 如果正在运行的线程数 < coresize,马上创建核心线程执行task,不排队等待。 2 如果正在运行的线程数 > coresize, 则把该任务加入阻塞队列 3 如果队列已满 && 正在运行的线程数 < maximumPoolSize,创建新的非核心线程执行该任务 4 如果队已满 && 正在运行的线程数 >= maximumPoolSize,线程池调用handler的reject方法拒绝本次提交。
复用
1.execute方法提交一个runnable热恩物 2. 调用addworker方法创建一个Worker对象,worker对象里以自己为参数创建一个thread,并会启动这个thread,也就启动了worker对象的run方法。 3.worker中的run方法会委托给线程池的runWorker方法。而runworker中的源码如下: 循环不断调用getTask方法获取任务。getTask方法会从blockingqueue workqueue中获取任务。 Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // allow interrupts boolean completedAbruptly = true; try { while (task != null || (task = getTask()) != null) { w.lock(); // If pool is stopping, ensure thread is interrupted; // if not, ensure thread is not interrupted. This // requires a recheck in second case to deal with // shutdownNow race while clearing interrupt if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { beforeExecute(wt, task); Throwable thrown = null; try { task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { afterExecute(task, thrown); } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); }
几种队列
ArrayBlockingQueue LinkedBlockingQueue SynchronousQueue PriorityBlockingQueue
怎么理解有界队列和无界队列
有界队列: 初始poolSize < corelSize, 提交任务时,直接new一个worker对象,并start(worker#thread#start) 当提交的任务数 > coreSize,会把任务添加到workqueue中(blockingqueue) 如果workqueue添加失败,会检查poolSize < maximumPoolSize是否成立,如果成立就会创建新的线程执行任务,直到线程总数等于maximunPoolSize 不成立,则执行拒绝策略。 无界队列: 与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增加,若后续仍有新的任务加入,而没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。当线程池的任务缓存队列已满并且线程池中的线程数达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略。
Sychronized/volatile/lock
java的对象头 synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表: 虚拟机位数 | 头对象结构 | 说明 32/64bit | Mark Word | 存储对象的hashCode、锁信息或分代年龄或GC标志等信息 32/64bit | Class Metadata Address| 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。 其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构 锁状态 |25bit |4bit |1bit是否是偏向锁 |2bit 锁标志位 无锁状态 |对象HashCode|对象分代年龄 |0 |01 synchronized原理 - 从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令。其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。 - JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。 ynchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理 java虚拟机对sychronized的优化 早起java中的synchronize属于重量级锁,效率低下,监视器(monitor)依赖与操作系统的mutex lock来实现的,需要从用户态切换到核心态。 jdk6后 jvm引入了偏向锁和轻量级锁。 - 偏向锁 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。 - 偏向锁的获取:当一个线程访问同步块并获取锁的时候,会在对象头和栈帧中的锁记录里存储锁偏向的线程id。以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程 - 偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。 - 轻量级锁加锁 :线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁 - 解锁:轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。 - 自旋锁 轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段 - 锁消除 编译时检查是否会存在竞态,如果不会 就会擦除锁  锁会不会降级? 当jvm进入安全点 saepoint的额时候,会检查是否有闲置的monitor,然后试图进行降级 sychronized和lock的区别 java关键字,jvm层面上 lock底层实现主要是volatile和cas,在类层面上。 锁的释放:sychronized执行完同步代码,或者线程执行发生异常,jvm就会让线程释放锁。 lock是必须在finally手动释放。 锁的获取: 锁的状态: 锁的类型: 可重入 不可中断 非公平 lock--可重入 可中断 可公平 性能: 少量同步 大量同步 lock 底层实现主要是volatile和cas,而sychronized是一种悲观锁,性能较差,但是jdk6后加入了偏向锁/轻量级锁/自旋锁/重量级锁,在并发量不大的情况下,性能可能优于lock。 Volatile原理 可见行/有序性/原子性 防止指令重排序
wait/sleep/notify
wait/sleep的区别 wait会释放锁,而sleep会一直持有锁,wait通常被用于线程交互,sleep通常被用于暂停执行。 notify运行过程 当a调用notify,a让出锁,进入等待状态,同时加入锁对象的等待队列。b获取锁后,调用notify方法通知锁对象的等待队列,使得线程a从等待队列进入阻塞队列。a进入阻塞队列后,直至线程b释放锁,线程a竞争得到锁继续从wait方法后执行。
基础知识
oop概念
访问修饰符
solid原则
单一职责(single responsibility principle)
开闭原则
里氏替换
依赖倒置
接口隔离
重载和重写区别
泛型
类型擦除
public static void main(String[] args) { List<Integer> intList = new ArrayList<>(); List<String> strList = new ArrayList<>(); System.out.println(intList.getClass() == strList.getClass()); }
原始类型
原始类型就是泛型类型擦出泛型信息后,在字节码中真正的类型。 例如: //泛型类型 class Pair<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } } //原始类型 class Pair { private Object value; public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } } 因为在Pair<T>中,T是一个无限定的类型变量,所以用Object替换。如果是Pair<T extends Number>,擦除后,类型变量用Number类型替换。
extends和super
反射突破泛型类型
public static void main(String[] args) { List<Integer> intList = new ArrayList<>(); intList.add(1); Class<?> listClass = intList.getClass(); try { Method addMethod = listClass.getMethod("add", Object.class); addMethod.invoke(intList, "hello"); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } int len = intList.size(); for (int i = 0; i < len; i++) { System.out.println(intList.get(i)); } } //说明java里面的泛型是编译器层面实现的伪泛型
java不支持泛型引用传递
ArrayList<String> arrayList1=new ArrayList<Object>();//编译错误 ArrayList<Object> arrayList1=new ArrayList<String>();//编译错误
静态类和静态方法中的泛型
public class Test2<T> { public static T one; //编译错误 public static T show(T one){ //编译错误 return null; } } 因为泛型类中的泛型参数的实例化是在定义泛型类型对象(例如ArrayList<Integer>)的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。 public class Test2<T> { public static <T >T show(T one){//这是正确的 return null; } } 因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的T,而不是泛型类中的T。
泛型常见面试题
1. Java中的泛型是什么 ? 使用泛型的好处是什么? 泛型是一种参数化类型的机制。它可以使得代码适用于各种类型,从而编写更加通用的代码,例如集合框架。 泛型是一种编译时类型确认机制。它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现ClassCastException。 2、Java的泛型是如何工作的 ? 什么是类型擦除 ? 泛型的正常工作是依赖编译器在编译源码的时候,先进行类型检查,然后进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。 编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List<String>在运行时仅用一个List类型来表示。为什么要进行擦除呢?这是为了避免类型膨胀。 3. 什么是泛型中的限定通配符和非限定通配符 ? 限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界,另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面<?>表示了非限定通配符,因为<?>可以用任意类型来替代。 4. List<? extends T>和List <? super T>之间有什么区别 ? 这和上一个面试题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是限定通配符的例子,List<? extends T>可以接受任何继承自T的类型的List,而List<? super T>可以接受任何T的父类构成的List。例如List<? extends Number>可以接受List<Integer>或List<Float>。 5. 如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型? 编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的情况下,一个泛型方法可能会像这样: public V put(K key, V value) { return cache.put(key, value); } 6. Java中如何使用泛型编写带有参数的类? 这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符。 7. 编写一段泛型程序来实现LRU缓存? 对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。 8. 你可以把List<String>传递给一个接受List<Object>参数的方法吗? 对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List<String>应当可以用在需要List<Object>的地方,但是事实并非如此。真这样做的话会导致编译错误。如果你再深一步考虑,你会发现Java这样做是有意义的,因为List<Object>可以存储任何类型的对象包括String, Integer等等,而List<String>却只能用来存储Strings。
数组支持泛型吗
多态和继承
匿名内部类
没有名字/只能继承一个父类或接口/Outerclass$N 匿名内部类内存泄漏的问题? 匿名内部类会默认持有外部类的引用,可能会导致内存泄漏。 由编译器生成。 参数列表包括: 定义在非静态域内的外部对象 父类的外部对象 父类的构造方法参数 外部捕获的变量(方法体内有引用外部的final变量) 匿名内部类引用外部变量,为什么要用final修饰? 匿名内部类最终会编译成一个单独的class文件,而被该类使用的变量会被内部类捕获 以构造函数参数的形式传递给该类,例如:Integer paramInteger, 如果变量不定义成final的, paramInteger在匿名内部类中可以被修改,进而造成和外部的paramInteger不一致的问题,为了避免这种不一致的情况,因此java规定匿名内部类只能访问final修饰的外部变量。 什么是内部类?内部类的作用 内部类的对象创建不依赖于外围类对象的创建。 内部类并没有令人迷惑的is-a关系,他就是一个独立的实体 内部类提供更好的封装,除了该外围类,其它类都不能访问
抽象类的意义
为其子类提供一个公共的类型,封装子类中的重复内容,定义抽象方法,子类虽然有不同的实现, 但是定义是一致的。
静态内部类/非静态内部类的理解
静态内部类:只是为了降低包的深度,方便类的使用,静态内部类适用于包含在类中,但️不依赖与外在的类,不用使用外在类的非静态属性和方法,只是为了方便管理类结构而定义。在创建静态内部类的时候,不需要外部类对象的引用。 非静态内部类:持有外部类的引用,可以自由使用外部类的所有变量和方法。
equals和hashcode
为什么 重写 equals 同时需要重写 hashcode, equals为true时,hashcode是否相同, 反之hashcode相同时,equals是否相等? equals为true我们认为就是同一个对象,当equals为true时,hashcode如果不同,在hashmap等集合中,就会因为hashcode不同而存放在不同等bucket中,被认为是俩个不同的对象。所以equals被重写,hashcode也要被重写。 equals为true时,hashcode肯定要相同,反之因为hash碰撞不成立。 equals方法要遵循约定如下 自反性/对称性/传递性/一致性/
浮点数的表示/负数的表示
final/finally/finalize的区别
final用来修饰类/方法/变量,分别有不同的意义,final修饰的class不可以被继承,final的变量是不可以修改的,而final的方法也是不可以ovveride finally 是java保证重点代码一定要被执行的一种机制。 finalize是基础类Object的一个方法,它的设计目的是保证对象在被垃圾回收前完成特定资源的回收。现在逐步使用java.lang.ref.Cleaner来替换。 cleaner实现利用了PhantomReference。
java中的四种引用
强引用 softreference 内存不足时回收 weakReference 无论内存是否充足,只要垃圾回收线程启动,它就会被回收。 PhantomReference 虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。 要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
java中类加载过程
Person person = new Person() 1. 首先找到Person.class文件,并加载到内存中 2.执行该类中到static代码块,如果有的话给Person.class类进行初始化 3.在堆内存中开辟空间分配内存地址 4.在堆内存中建立对象的特有属性,并进行默认初始化 5.堆属性进行显示初始化 6.对 对象进行构造代码块初始化 7.对 对象进行与之对应的构造函数进行初始化 8.将内存地址给栈内存中的person句柄。