1、类的加载顺序
- (1) 父类静态代码块(包括静态初始化块,静态属性,但不包括静态方法)
- (2) 子类静态代码块(包括静态初始化块,静态属性,但不包括静态方法 )
- (3) 父类非静态代码块( 包括非静态初始化块,非静态属性 )
- (4) 父类构造函数
- (5) 子类非静态代码块 ( 包括非静态初始化块,非静态属性 )
- (6) 子类构造函数
2、List、Map、Set各有什么特点?
- List:存储的顺序是有序的、可重复的,按照添加顺序进行排列
- Set:存储的顺序是无序的、不可重复的
- Map:使用键值对存储,Key和Value都是无序的,其中Key不可重复,而Value可重复
3、HashMap和HashSet的区别
- HashMap存储键值对,通过put()添加元素,而HashSet仅存储对象,通过add()添加元素
- HashMap通过Key计算hashcode值,而HashSet通过成员对象计算hashcode值,但它们的最终顺序都是根据计算出来的HashCode值进行排列
- HashSet底层就是基于HashMap实现的
- HashMap和HashSet的默认初始长度都是16,加载因子为0.75,后续每次扩充都为原来的2倍
- 注意:hashSet底层是hashMap(数组 +链表)的结构,new HashSet()在不指定容量的情况下,包含的数组默认大小是16个,那么当随机数出现超过16的数值时,可能会产生hash冲突,所以输出就不是排序的,为什么在第13次会排序?是因为hashMap的默认扩容因子是0.75,也就是说超过16*0.75 = 12个时,会发生扩容,然后重新hash,这个时候就不会出现hash冲突的情况了,也就是会"自动排序"输出了
4、HashMap和ConcurrentHashMap的区别
- HashMap是线程不安全的,当出现多线程操作时,会出现安全隐患;而ConcurrentHashMap是线程安全的。
- HashMap不支持并发操作,没有同步方法,ConcurrentHashMap支持并发操作,通进行加锁(分段锁),每次需要加锁的操作锁住的是一个segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全
5、ArrayList和Vector的区别
- 同步性:Vector是线程安全的,而ArrayList是线程不安全的;Vector中的方法都是同步方法(synchronized),所以ArrayList的执行效率要高于Vector
- 数据增长:Vector默认增长为原来的两倍,而ArrayList增加为原来的1.5倍
6、线程的生命周期
- 初始状态:线程被创建出来,但没有被调用
- 运行状态:线程被调用了start()等待运行的状态(这里包括了就绪和运行中两种状态)
- 阻塞状态:需要等待锁被释放
- 等待状态:表示该线程需要等待其他线程做出一些特定动作(通知或中断)
- 超时等待状态:可以在指定的时间后自行返回
- 终止状态:表示该线程已经运行完毕
7、volatile关键字
- volatile关键字可以保证变量的可见性,指示JVM这个变量共享且不稳定,每次使用它都得到主存中进行读取;这意味着读取该变量时不会读错,但它不能保证多线程同时操作每次只有一个线程在操作
- volatile关键字不能保证对变量的操作是原子性的,但synchronized可以,即加上锁后,能保证只有一个线程在操作当前变量
8、乐观锁和悲观锁
1、乐观锁:总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)
- 通常用于读比较多的情况
- 版本号机制:每次修改数据都会更新版本号,每次提交更新数据时会比较版本号,只有版本号一致时才更新
- CAS算法:全称Compare And Swap(比较与交换),思想就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新;CAS是一条原子操作,因此当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作
2、悲观锁:总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放(例如synchronized的独占锁);通常用于写比较多的情况
9、synchronized和volatile有什么区别
- synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!
- volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好。但是 volatile关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块
- volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性
10、Future类
- 1、Future类: 是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低
- 2、简单理解就是: 我有一个任务,提交给了 Future来处理。任务执行期间我自己可以去做任何想做的事情。并且,在这期间我还可以取消任务以及获取任务的执行状态。一段时间之后,我就可以从Future 那里直接取出任务执行结果
11、CountDownLatch和CyclicBarrier关键字
1、CountDownLatch 的作用就是允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
- 子线程并不会因为调用latch.countDown而阻塞,会继续进行该做的工作,只是通知计数器-1,只需要在所有进程都进行到某一节点后才会执行被阻塞的进程(被阻塞的进程是另一个进程,不是调用countDown的进程)
- 举例来说就是如果有门卫必须站岗等待十个员工上班后才能休息,那么员工上班时只需要跟门卫报个到,后面想干嘛就干嘛,只是当员工都上班后,门卫就可以去休息
2、CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活
- 所有线程在其他线程没有准备好之前都在被阻塞中,等到所有线程都准备好了才继续执行
- 举例来说就是十个运动员进场,等运动员都准备好了,裁判就发枪让运动员一起开跑。运动员进场之后,它什么都干不了,必须在赛道上等待裁判发枪
12、Java线程的创建方式
- 继承Thread类,重写run方法
- 实现Runnable接口,重写run方法
- 实现Callable接口,重写call方法,配合FutrueTask(一般用于同步非阻塞)
- 基于线程池构建线程
其实都是一种,其本质都是实现Runnable接口
13、Java程序的具体运行过程
- 编译:Java程序首先需要通过Java编译器将源代码(.java文件)编译成字节码(.class文件)。
- 装载:Java虚拟机(JVM)会将字节码文件加载到内存中,并将其转化为机器码。
- 链接:在链接阶段,Java虚拟机会对字节码进行验证、准备和解析等操作。其中,验证操作主要是检查字节码文件是否符合Java语言规范和安全性要求;准备操作主要是为类的静态变量分配内存,并赋予默认值;解析操作主要是将常量池中的符号引用转化为直接引用。
- 初始化:在初始化阶段,Java虚拟机会对类的静态变量进行初始化,执行静态代码块中的代码等操作。
- 执行:在执行阶段,Java虚拟机会执行程序的主方法,并按照程序的逻辑进行操作,包括调用方法、创建对象、赋值等操作。在程序执行完毕后,Java虚拟机会自动回收内存并退出程序。
14、JVM的类加载机制
1、JVM的类加载阶段: 加载、验证、准备、解析、初始化。
- 加载阶段:将类的二进制数据加载到内存中,并在方法区中生成一个代表该类的Class对象。类的二进制数据可以来自于本地文件系统、网络等各种途径。
- 验证阶段:对类的二进制数据进行合法性验证,包括文件格式验证、元数据验证、字节码验证和符号引用验证等。验证阶段的目的是确保类的二进制数据符合Java虚拟机规范,并且不会对JVM造成安全威胁。
- 准备阶段:为类的静态变量分配内存,并初始化为默认值(0、null等)。这个阶段并不会对静态变量进行初始化,而是为它们分配内存空间。
- 解析阶段:将类的符号引用转化为直接引用。符号引用是一组符号来描述目标,可以是类、接口、方法、字段等,而直接引用则是直接指向目标的指针、相对偏移量或者句柄等。解析阶段的目的是将符号引用转化为直接引用,以便于JVM在程序运行时能够准确地找到目标。
- 初始化阶段:为类的静态变量赋值,并执行静态代码块中的代码。在这个阶段,JVM会根据程序中对类的使用情况来执行相应的初始化操作,例如创建类的实例、调用类的静态方法等。
- 一句话总结:虚拟机把Class文件加载到内存,并对数据进行校验、转换解析和初始化,形成虚拟机可以直接使用的java类型,即java.lang.Class
2、类加载器: 启动类加载器、扩展类加载器和系统类加载器和自定义类加载器
- 启动类加载器:负责加载Java_HOME/lib目录中的类库,或通过-Xbootclasspath参数指定路径中被虚拟机认可的类库
- 扩展类加载器:负责加载Java_HOME/lib/ext目录中的类库,或通过java.ext.dirs系统变量加载指定路径中的类库
- 系统类(应用程序类)加载器:负责加载用户路径(classpath)中指定jar包和Djava.class.path所指定目录下的类和jar包
- 自定义类加载器:通过Java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader
3、双亲委派机制
- JVM通过双亲委派机制对类进行加载。双亲委派机制指一个类在收到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类去完成,其父类在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中
15、int i=1,它们的存储位置分别在哪?
- 成员变量 int a = 1,a作为变量名,在JVM中是以代码的形式存在,存放在方法区,当有线程执行到该代码的时候,会加载该代码进行执行,而1作为参数 a的值在运行时存放在堆内存中,a指向该内存
- int a=1为局部变量的时候,这个时候a同样存在方法区的代码中,运行时a存在于该方法对应的栈帧的局部变量表中,而该变量表中a的值为1,所以1存在栈内存中
16、栈和堆分别存放什么?
- 栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量)
- 堆内存存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象)
- 堆和栈的联系:例如int [] arr=new int [3];首先给堆分配一个地址,然后把堆的地址赋给arr,arr就通过地址指向了数组。所以arr想操纵数组时,就通过地址,而不是直接把实体都赋给它。这种我们不称为基本数据类型,而叫引用数据类型,即arr引用了堆内存当中的实体。数组也是一种对象。
17、list遍历
- 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
- 可以使用 Collection#removeIf()方法删除满足特定条件的元素,如下
java
list.removeIf(filter -> filter % 2 == 0); /* 删除list中的所有偶数 */