2021年11月28日30道面试题

1.String和StringBuiler、StringBuffer区别

  String底层是不可变字符串,底层是final修饰的char数组,且String类也是被final关键字修饰的

  StringBuilder和StringBuffer的底层也是char数组,只不过没有fianl修饰,而且可变,StringBuilder线程不安全,StringBuffer线程安全,因为StringBuffer的方法上有Synchronized修饰

  String的拼接操作的底层原理实际上是先创建StringBuilder对象然后调用append方法进行追加,最后再转换成String

 

2. ArrayList和LinkedList的区别?

  ArrayList的底层是基于Object数组,对内存的要求较高,因为数组的创建需要连续的内存空间,Arraylist的默认大小为10。

  LinkedList的底层是基于双向链表,链表不需要连续的内存空间,只需要将链表的next和prev指向相应的对象即可

  ArrayList和Linkedlist的add方法默认都是添加到末尾。

  ArrayList的查询快,而插入和删除慢,为什么查询快,因为ArrayList基于数组,数组有下标,通过下标查询元素的时间复杂度为O(1),为什么插入和删除慢呢,因为在ArrayList的底层,插入操作会把插入位置上的元素和后面的元素通过System.arrayCopy方法向后进行移位,删除方法也会将进行移位。

  LinkedList删除快,查找慢,linkedList不支持索引,随机访问LinkedList查找只能从链表头开始遍历查找,时间复杂度为O(n),插入快是因为linkedlist插入只需将上一个节点的next节点和下一个节点的prev指向要插入的元素即可。删除只需要将元素删除再将next和prev节点重新指向即可。

 

3.创建线程有哪几种方法?

  1.写一个类继承Thread类,覆写run方法,调用start方法启动

  2.写一个类实现Runnable接口,覆写run方法,将该类作为参数传递给Thread有参构造创建线程

  3.写一个类实现Callable接口,覆写call方法,将该类作为参数传递给FutureTask有参构造创建对象,再把该对象作为参数传递给Thread有参构造创建线程

  优先选择方式2和方式3,方式1有单继承的局限性,方式3适合有返回值的场景

 

4.调用线程的Start方法和run方法有什么区别

  调用start方法代表开启新线程,将线程转为就绪状态,而run方法则是业务方法,直接调用run方法就是执行业务代码,是执行在main线程中的

  扩展:为什么调用start就是开启新线程?

  Thread类中有一个volatile修饰的成员变量threadStatus,默认值为0,当调用start方法时,会判断threadStatus是否为0,为0就将执行start0方法开启线程。start0方法是native修饰的本地方法,是由jvm实现的,最终调用的是jvm_startThread方法

  扩展:什么本地方法?

  本地方法就是jvm调用非java代码实现的方法

 

5.ArrayList和Vector的区别?

  Vector是线程安全的,ArrayList线程不安全,Vector的方法上有Synchronized关键字修饰。Vector默认扩容是1倍,Arraylist默认扩容0.5倍

 

6.面向对象的特征?

  1.封装:将对象的属性私有化,隐藏实现细节,对外界提供公共的访问方式,目的是提高代码安全性

  2.继承:是类的抽象,将子类和父类的公共代码抽取出来,提高代码的复用性

  3.多态:就是对象的多种形态,运行时类型和编译时类型不相同就是多态,提高了代码的扩展性,不需要指定类型,用父类型接收即可

 

7.接口和抽象类

  接口有全局常量,抽象方法(jdk8后加入了静态方法和默认方法)

  抽象类有构造方法,普通方法,抽象方法

  抽象类是用来捕捉子类的共同特征的,提高代码的复用,是对类的抽象,是一种模板设计

  接口则是对象的行为集合,提高扩展性和可维护性,是对行为的抽象,是一种行为的规范

 

8.==和equals()的区别

  ==是运算符,equals()是Object类中的方法,Object类中的equals方法默认是使用==比较,一般会重写equals方法

  当比较的类型是基本数据类型时,== 比较的是值,当比较的类型是引用数据类型时,比较的是对象的地址

  equals比较的是对象的内容是否相同,基本数据类型的包装类的equals方法则要看equals方法是怎样重写的。

 

9.Int和Interger的区别

  int是基本数据类型,Integer是引入数据类型

  Integer是int的包装类

  Interger和int的比较:

  int和Integer进行比较时,如果值相等,则一定相等,因为integer会自动拆箱转化为int

  通过非new方式创建的Integer对象,如果传入的值在-128-127之间,就会被缓存,下次再通过非new创建Interger对象,就会直接从缓存中拿,即享元模式,类似通过非new方式创建String字符串

  扩展:什么是享元模式?

  共享已经存在的对象,减少对象创建的数量以及类似的系统开销,提高利用率,如通过非new方式创建String、Integer对象等...

 

10.说一下java中的集合体系

  Collection接口下有List、set、queue

  List下有:

  Arraylist:基于Object数组,随机访问快,插入和删除慢

  vector: 相当于线程安全的ArrayList,方法上有Synchronized修饰

  LinkedList:基于双向链表,查询慢,删除插入快

  List可以保证元素的添加顺序

  Set下有:

  HashSet:底层基于HashMap,不能保证元素的添加顺序,也不能保证自然排序,通过构造方法创建HashSet时,其底层其实是创建了一个HashMap。HashSet的元素是保存在Hash Map的key中,value则是统一的private static final Object PRESENT = new Object();对象,调用add方法其实是调用了hashmap的put方法

  TreeSet :底层基于TreeMap,不能保证元素的添加顺序,可以进行自然排序,也可以指定排序,通过构造方法创建TreeSet时,其实是创建了一个TreeMap,TreeSet的元素是保存在TreeMap的key中,value则是统一的private static final Object PRESENT = new Object();对象,调用add方法其实是调用了treemap的put方法

  LinkedHashSet: 继承于LinkedHashSet,不能把保存重复元素,可以保证元素的添加顺序。在通过构造方法创建linkedhashset时实际上是创建了一个linkedhashMap,所以说他是基于linkedHashmap的,在底层维护了一个双向的Entry链表,链表有head头节点和tail尾节点,在插入时默认是添加到链表的尾部,所以可以保证元素的添加顺序

  Map下有:

  HashMap:底层基于数据+双向链表+红黑树,不能保存重复元素,不能保证元素的添加顺序,key和value都可以为null

  HashTable:key和value都不能为null,线程安全

  TreeMap:可以进行自然排序,线程不安全,基于红黑树

  properties:key和value都是String类型的,线程安全 

 

11. HashMap和hashtable的区别

  HashMap的线程不安全,hashtable的线程安全,因为hashtable的方法上有Synchronized修饰。

  HashMap的key和value都可以为null,hashtable的key和value都不可以为null,因为在调用hashtable的put方法时,首先会判断value的值是否为null,如果为null,抛出空指针异常,

在调用key.hashcode()方法时,也会抛出空指针异常。而hashmap在计算哈希值时,如果key为null,哈希值就为0

  hashmap在put方法时,才会初始化数组的,而hashtable在调用构造方法时就会初始化数组。

  初始化容量和扩容容量不同:hashmap的数组初始化容量为10,扩容后数组容量是原数组容量的2倍,hashtabble的数组初始化容量为11,扩容后容量是2倍+1,负载因子都是0.75

  在不考虑线程安全的情况下优先使用hashmap,在线程安全的情况下推荐用concurrenthashmap

 

12. 为什么在高并发情况下hashtable的性能比concurrenthashmap的性能低?

  在高并发情况下,hashtable是全局加锁,整个hashtable使用的是用一把锁,没有竞争到线程就会被阻塞,从而性能低下

  而concurrenthashmap则是对每一个数组元素进行加锁,采用cas和Synchronized来保证并发的安全

 

13. java代码的执行流程

  Java程序通过编译器将java代码编译成字节码文件,再通过jvm将字节码解释成机器码再执行,这就是解释执行

  但是为了提高效率,部分热点代码会直接通过JIT(动态编译器)直接编译成机器码执行,hotspot jvm就提供了JIT(Just-in-time)编译器

 

14. Synchronized和lock的区别

  1.Synchronized是关键字,是基于jvm实现的,基于悲观锁,在执行时会被编译成moniterenter和monitorexit,而lock是一个类,基于volatile和CAS实现的

  2.Synchronized在发生异常时会自动释放锁,不会造成死锁,而lock不会自动释放,可能造成死锁

  3.Synchronized是非公平锁,而lock可以在创建时指定是公平锁还是非公平锁

  4.Synchronized是不可中断的,a获取锁后,如果a线程阻塞,b线程会一直等待,而lock是可中断的,b线程在等待的过程中可中断

  5.lock中有一个trylock方法,可以尝试获取锁判断锁的状态,被占用就返回false,否则返回true

  

15.悲观锁、乐观锁、自旋锁、公平锁、非公平锁、可重入锁,读写锁、互斥锁 // TODO

 

16. 线程的几种状态

  1.新建状态:新创建出来的线程,还未调用start方法

  2.就绪状态:新建状态的线程调用start方法后,等待cpu的调度

  3.运行状态:就绪状态的线程获得了cpu执行权,开始执行run或call方法

  4.阻塞状态:无法获取到锁的状态,也不会获得cpu执行权

  5.等待状态:让出cpu执行权,让其他线程执行,等待其他线程执行唤醒方法唤醒

  6.死亡状态:线程执行结束:如run或call方法代码执行完成,抛出了未捕获的Exception,调用了线程的stop方法

  

17.sleep方法和wait方法区别

  1.sleep是Thread类的静态方法,wait是Object类的方法

  2.sleep方法不会释放对象锁,wait方法会释放对象锁

  3.sleep方法使用时要捕获InterruptedException,wait方法不用捕获异常

 

18.Synchronized加在实例方法和静态方法上锁的是什么对象?Synchronized(this)和(User.class)锁的是什么

  1.加在实例方法上锁的是这个对象实例,不会对所有对象实例共享

  2.加在静态方法上锁的是这个类的字节码对象,会对所有实例对象共享

  3.Synchronized(this)锁的是当前实例对象,不会对所有实例共享

  4.Synchronized(User.class)表示是对这个类加锁,会对这个类的所有实例共享

 

19.Synchronized和volatile的区别

  1.他们两者都是关键字,Synchronized可以保证线程安全,volatile不能保证线程安全,因为volatile只能保证共享数据的可见性和禁止指令重排序,不能保证原子性

  2.Synchronized可以作用于方法和代码块,而volatile只能作用在变量上

  3.Synchronized也可以保证共享变量的可见性,当线程获取到锁后,首先会清空工作内存,去主存中获取共享变量最新的值,在释放锁之前会将共享变量同步到主存中。

    而volatile则是每次对共享变量进行操作时都会从主存中获取,操作完后立即同步到主存。共享变量被volatile修饰后,某一线程修改了共享变量,就会使其他线程中工作内存中的副本失效,其他线程通过嗅探机制判断工作内存中的变量是否失效,如果失效就从主存中重新获取

  4.在高并发情况下,Synchronized会造成线程阻塞,volatile不会
  

20. 双重校验锁单例模式指令重排序

  双重校验锁单例模式可能会产生指令重排问题,因为new对象的操作不是一个原子操作,分为3步:1.分配内存空间 2.调用构造器,实例化对象  3.返回地址给引用。在这个过程中,可能发生指令重排,先执行第一步再执行第三步,此时若线程A刚好执行到第三步,线程B正好进入,因为对象已经不为null,线程二访问该对象就会出现问题

 

21. Synchronized的原理

  当Synchronized修饰方法时,编译器会将将Synchronized编译成关键字来标识要加锁的方法,当线程要执行该方法,需要获得锁才能执行

  当Synchronized修饰代码块时,编译器是将其编译成monitorenter和monitorexit指令来进行加锁的,当执行到monitorenter指令时,需要获得monitor对象进行加锁,当执行到monitorexit

就会释放锁

  无论是修饰方法还是代码块,都是在获取Monitor对象,这个对象在jvm中是通过C++实现的,叫做ObjectMonitor。在执行到monitorenter时就加锁,执行到moniterexit就释放锁,当某个线程获取到monitor对象时,就会将monitor对象中的owner变量设置为当前线程

  monitor对象的底层是操作系统的锁 mutex lock, 性能较低

 

22.monitor对象

  _owner:记录当前获取到锁的线程

  _entrylist: 记录阻塞队列的线程

  _waitSet:记录调用了wait()未被唤醒的线程

 

23.Java对象头中的markword

  java对象头中的markword储存的是对象的hashcode,分代年龄,锁标记位。偏向锁,轻量级锁,重量级锁对应的锁标志位分别位01,00,10

 

24.Synchronized锁升级

  在jdk1.6后,Synchronized进行了升级。分为无锁,偏向锁,轻量级锁,重量级锁。偏向锁指的是一段同步代码一直被同一线程访问,不存在锁竞争,当线程再次访问这段同步代码时会自动进行加锁,提升性能。

  无锁的锁标志位是01,当一个线程访问同步方法或同步代码时并获取锁时,会在markword中的threadId记录当前线程的id,并将偏向锁位改为1。当下一次有线程来获取锁时,首先先比较当前线程id和markword中的线程id,如果相同,说明已经获取锁了,直接往下执行

  当发生锁竞争时,偏向锁就会升为轻量级锁,会将偏向锁进行撤销,轻量级锁分为自旋锁和自适应自旋锁,自旋锁会导致空耗cpu,自适应自旋锁是根据上一次线程是否成功或成功获取锁的自旋次数来设置自旋次数,如果上一次是失败,则很可能直接升级为重量级锁

  当竞争线程增多时,就会升级为重量级锁,即通过获取monitor对象来加锁

 

25.乐观锁的使用场景

  1.es中对version的控制

    es中创建一个document,version就是1,以后每修改一次,version就加1

  2.mysql中对版本号的控制

  3.原子类中的CompareAndSwap操作,如AtomicInteger

zhi

26.什么是CAS

  CompareAndSwap,先比较再交换,是乐观锁的一种的实现

  比如:线程A将共享变量读取到工作内存中,对共享变量进行修改,最后将读取到的值和主存中的值进行对比,如果相等就将修改后的值同步到主存,如果不等则说明共享变量已经被其他线程修改了,就会进行自旋

 

27.AtomicInteger如何保证并发安全

  AtomicInteger是基于CAS和Volatile并发安全的

  AtomicInteger的value变量是由volatile修饰的,从而保证了共享变量的可见性

  当调用AtomicInteger的方法时,实际上是在调用unsafe类中的CompareAndSwapInt,而这个方法的底层是调用Atomic的compxchg函数,x86平台就是使用cpu的lock指令和compxchg指令

 

28.乐观锁、悲观锁、自旋锁、重入锁、互斥锁、阻塞

  乐观锁:在操作共享变量时认为别的线程不会对共享变量进行修改,不会使用加锁的方式来操作数据,而是在提交更新数据时判断操作期间是否有别的线程修改过共享变量

  阻塞:某个线程获取锁失败,就会阻塞,线程阻塞会发生内核态和用户态的切换,线程切换也会发生用户态和内核态的切换

 

29.什么是用户态和内核态

  内核态:cpu可以计算机中所有的软硬件资源

  用户态:cpu权限受限,只能访问自己内存中的资源

 

30.你用过JUC中的类吗

  lock concurrenthashmap AtomicInteger

posted @ 2021-11-29 00:56  切勿动怒  阅读(75)  评论(0)    收藏  举报