Java面试题整理

java面试题整理


前言

面试期间自己整理的一些关于java面试题的小知识,后续持续添加~


一、集合

  1. Collection(单列集合)
     List(有序,可重复)
       ArrayList
        底层数据结构是数组,查询快,增删慢
        线程不安全,效率高
        扩容:初始容量为0,自动扩容,第一次添加元素时容量为10,,之后1.5倍增加。
     Vector
        底层数据结构是数组,查询快,增删慢
        线程安全,效率低
        扩容:初始容量为10,自动扩容,当增量为0时,扩容为原来的2倍。
     LinkedList
        底层数据结构是链表,查询慢,增删快
        线程不安全,效率高
     Set(无序,唯一)
      HashSet
        底层数据结构是哈希表。
        哈希表依赖两个方法:hashCode()和equals()
        执行顺序:
         首先判断hashCode()值是否相同
         是:继续执行equals(),看其返回值
          是true:说明元素重复,不添加
          是false:就直接添加到集合
         否:就直接添加到集合
         最终:
         自动生成hashCode()和equals()即可

   LinkedHashSet
      底层数据结构由链表和哈希表组成。
      由链表保证元素有序。
      由哈希表保证元素唯一。
   TreeSet
      底层数据结构是红黑树。(是一种自平衡的二叉树)
      如何保证元素唯一性呢?
       根据比较的返回值是否是0来决定
      如何保证元素的排序呢?
        两种方式:
       自然排序(元素具备比较性)
        让元素所属的类实现Comparable接口
       比较器排序(集合具备比较性)
        让集合接收一个Comparator的实现类对象
2. Map
  A:Map集合的数据结构仅仅针对键有效,与值无关。
  B:存储的是键值对形式的元素,键唯一,值可重复。

 HashMap
   底层数据结构是哈希表。线程不安全,效率高
   哈希表依赖两个方法:hashCode()和equals()
   执行顺序:
   首先判断hashCode()值是否相同
    是:继续执行equals(),看其返回值
     是true:说明元素重复,不添加
     是false:就直接添加到集合
    否:就直接添加到集合
   最终:
    自动生成hashCode()和equals()即可
 LinkedHashMap
   底层数据结构由链表和哈希表组成。
    由链表保证元素有序。
    由哈希表保证元素唯一。
 Hashtable
   底层数据结构是哈希表。线程安全,效率低
   哈希表依赖两个方法:hashCode()和equals()
   执行顺序:
   首先判断hashCode()值是否相同
     是:继续执行equals(),看其返回值
      是true:说明元素重复,不添加
      是false:就直接添加到集合
    否:就直接添加到集合
   最终:
   自动生成hashCode()和equals()即可
 TreeMap
   底层数据结构是红黑树。(是一种自平衡的二叉树)
     如何保证元素唯一性呢?
     根据比较的返回值是否是0来决定
    如何保证元素的排序呢?
     两种方式
       自然排序(元素具备比较性)
       让元素所属的类实现Comparable接口
      比较器排序(集合具备比较性)
       让集合接收一个Comparator的实现类对象

  HashMap和Hashtable的区别

  相同点:
hashmap和Hashtable都实现了map、Cloneable(可克隆)、Serializable(可序列化)这三个接口

  不同点:

  1. 底层数据结构不同:jdk1.7底层都是数组+链表,但jdk1.8 HashMap加入了红黑树
  2. Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null。
  3. 添加key-value的hash值算法不同:HashMap添加元素时,是使用自定义的哈希算法,而HashTable是直接采用key的hashCode()
  4. 实现方式不同:Hashtable 继承的是 Dictionary类,而 HashMap 继承的是 AbstractMap 类。
  5. 初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。
  6. 扩容机制不同:当已用容量>总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 +1。
  7. 支持的遍历种类不同:HashMap只支持Iterator遍历,而HashTable支持Iterator和Enumeration两种方式遍历
  8. 迭代器不同:HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。而Hashtable 则不会。
  9. 部分API不同:HashMap不支持contains(Object value)方法,没有重写toString()方法,而HashTable支持contains(Object value)方法,而且重写了toString()方法
  10. 同步性不同: Hashtable是同步(synchronized)的,适用于多线程环境,
    而hashmap不是同步的,适用于单线程环境。多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。

二、IO流

1. 介绍
IO,即in和out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。
流(Stream),是一个抽象的概念,是指一连串的数据(字符或字节),是以先进先出的方式发送信息的通道。
一般来说关于流的特性有下面几点:
1. 先进先出:最先写入输出流的数据最先被输入流读取到。
2. 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。(RandomAccessFile除外)
3. 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。
4. 在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。
而在UTF-8编码中,一个中文字符是3个字节。
例如下面图中,“云深不知处”5个中文对应的是15个字节:-28-70-111-26-73-79-28-72-115-25-97-91-27-92-124

在这里插入图片描述

  那么问题来了,如果使用字节流处理中文,如果一次读写一个字符对应的字节数就不会有问题,一旦将一个字符对应的字节分裂开来,就会出现乱码了。为了更方便地处理中文这些字符,Java就推出了字符流。
  字节流和字符流的用法几乎完成全一样,区别在于字节流和字符流所操作的数据单元不同,字节流操作的单元是数据单元是8位的字节,字符流操作的是数据单元为16位的字符。
  字节流和字符流的其他区别:
  字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。
  字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。
  用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。

2. NIO 与BIO 模型对比
  Java NIO 中的 N 是指 New ,即新的意思, Java NIO 即新的 IO ,是相对于 OIO (Old IO) 或者 BIO(Blocking IO) 来说的。

  BIO 是同步阻塞IO,服务器的模式是一个线程处理一个请求,当无响应时,会阻塞线程

  NIO 同步非阻塞IO,会有一个Selector管理多个线程,当有事件发生后,进行处理、不会发生阻塞

3. NIO 与BIO的差异

  1、BIO 以流的方式处理数据,而NIO以块的方式处理数据,块I/O 的效率比流I/O高很多
  2、BIO 是阻塞的,NIO则是非阻塞的
  3、BIO基 于字节流和字符流进行操作,而NIO 基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

4. 什么是Buffer(缓冲区)

  缓冲区(Buffer) : 缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。
   在buffer类中都有4个属性

属性说明
Capacity容量,即可以容纳的最大数据量
Limit表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
Position位置,下一个要被读或写的元素的索引
Mark标记

5. 什么是Channel(通道)

  NIO的通道类似与流但是又有区别:通道可以进行读写,而流只能读或者写;通道可以支持异步读写。

6. 什么是Selector(选择器)

  Selector能够检测多个注册的通道上是否有事件发生(多个Channel以事件的方式可以注册到同一个Selector)

  如果有事件发生,便获取事件(通过selectKey)然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

  只有在连接真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程, 不用去维护多个线程避免了多线程之间的上下文切换导致的开销

三、多线程

  1.现多线程有三实现方法:一种是继承Thread类;另一种是实现Runnable接口;另一种是实现Callable接口。

  2. Runnable接口和Callable接口的区别
  Runnable接口中的run()方法的返回值是void,它只是纯粹地去执行run()方法中的代码而已;
  Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。(并且必须使用ExecutorService.submit()方法调用,submit()方法会返回产生的Future对象)

  3. CyclicBarrier和CountDownLatch的区别
    两个类都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:
  (1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,该线程会继续运行。
  (2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务。
  (3)CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了

  4. 线程安全的级别
  代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么代码就是线程安全的。线程安全也是有级别之分的:
  (1)不可变
  像String、Integer、Long这些,都是final类型的类,要改变除非新创建一个。
  (2)绝对线程安全
  不管运行时环境如何都不需要额外的同步措施。Java中有绝对线程安全的类,比如CopyOnWriteArrayList、CopyOnWriteArraySet。
  (3)相对线程安全
  像Vector这种,add、remove方法都是原子操作,不会被打断。如果有个线程在遍历某个Vector,同时另一个线程对其结构进行修改,会出现ConcurrentModificationException(fail-fast机制)。
  (4)线程非安全
    这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类。

  5. 如何在两个线程之间共享数据
  通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的。

  6.为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用
  这是JDK强制的,wait()方法和notify()/notifyAll()方法(都是Object的方法)在调用前都必须先获得对象的锁。

  7. wait()方法和notify()/notifyAll()方法在放弃对象锁时有什么区别
  wait()方法立即释放锁,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃锁。

  8. 怎么检测一个线程是否持有对象监视器
  Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的锁被当前线程持有的时候才会返回true。

  9.ConcurrentHashMap的并发度是什么
  ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多可以同时有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势。

  11. 如果你提交任务时,线程池队列已满,这时会发生什么
  如果使用的无界队列(如LinkedBlockingQueue)的话,继续添加任务到阻塞队列中等待执行。
  如果你使用的是有界队列(如ArrayBlockingQueue)的话,则会使用拒绝策略。

  12. Java中用到的线程调度算法是什么
  抢占式:一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

  13. 多线程中的忙循环是什么
  忙循环就是程序员用空循环让一个线程等待,不像传统方法wait(),sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU。这么做的目的是为了保留并避免重建CPU缓存(在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存)。

  14. 什么是自旋
  很多时候synchronized代码块中逻辑简单执行速度快,此时等待的线程都加锁可能是一种不太值得的操作。因此不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞。

  15. 什么是乐观锁和悲观锁
  (1)乐观锁:认为竞争不总是会发生,因此它拿数据时不上锁,但是在更新的时候会去判断在此期间有没有人去更新这个数据,适用于多读的场景。
  (2)悲观锁:认为竞争总是会发生,因此每次拿数据的时候都会上锁。

  16. 什么是死锁
  当两个或多个线程在等待彼此释放所需的资源(锁定)并陷入无限等待即是死锁。它仅在多任务或多线程的情况下发生。
  死锁形成需要四个条件:
1、互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
2、请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
4、环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

  避免死锁:
1、以确定的顺序获得锁
2、超时放弃

  预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的


  1. volatile修饰的变量具有可见性
  volatile是变量修饰符,其修饰的变量具有可见性。
可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。
在Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主存。

  2. volatile禁止指令重排
  volatile可以禁止进行指令重排。指令重排是指处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。程序执行到volatile修饰变量的读操作或者写操作时,在其前面的操作肯定已经完成,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。

  3. synchronized
  synchronized可作用于一段代码或方法,既可以保证可见性,又能够保证原子性。
  可见性体现在:通过synchronized或者Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中。
  原子性表现在:要么不执行,要么执行到底。

  4.volatile和synchronized总结
  (1)从而我们可以看出volatile虽然具有可见性但是并不能保证原子性。
  (2)性能方面,synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized。但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。

  sleep():在指定时间内让当前正在执行的线程暂停执行,wait()方法进入等待状态时会释放同步锁,而sleep() 方法不会释放同步锁。
  wait():通常被用于线程间交互,sleep()通常被用于暂停执行。wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException。

  (1)synchronized是JVM层面的实现的,JVM会确保释放锁,而且synchronized使用简单;而Lock是个普通类,需要在代码中finally中显式释放锁lock.unlock(),但是使用灵活。
  (2)synchronized采用的是悲观锁机制,线程获得独占锁,而其他线程只能阻塞来等待释放锁。当竞争激烈时CPU频繁的上下文切换会降低效率。而Lock是乐观锁机制,每次假设不存在竞争而不上锁,若存在竞争就重试。当竞争激烈时JVM可以花更少的时间来调度线程,把更多时间用在执行线程上,因此性能最佳。
  (3)ReentrantLock可以实现定时锁、轮询锁,可以选择放弃等待或者轮询请求。有效防止了死锁。
  (4)synchronized是非公平锁。而ReentrantLock可以通过构造函数传入true实现公平锁,即按照申请锁顺序获得锁。
  (5)ReentrantLock类有一个重要的函数newCondition(),用于获取Lock上的一个条件,Condition可用于线程间通信。

   5. 可重入锁
   可重入锁的意思是,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是线程A已经持有了该对象的锁,这样线程A会一直等待永远不会获取到的锁。
  synchronized以及Lock类锁,两者都是可重入锁。

class MyClass {
    public synchronized void method1() {
        method2();
    }
public synchronized void method2() {    
    }
}

四、Spring

  1. Spring是什么?
      Spring是一个轻量级的IoC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发

  2. Spring的IoC理解(重点)
      (1)IOC就是控制反转,指创建对象的控制权转移给Spring框架进行管理,并由Spring根据配置文件去创建实例和管理各个实例之间的依赖关系,对象与对象之间松散耦合,也利于功能的复用。DI依赖注入,和控制反转是同一个概念的不同角度的描述,即 应用程序在运行时依赖IoC容器来动态注入对象需要的外部依赖。
      (2)最直观的表达就是,以前创建对象的主动权和时机都是由自己把控的,IOC让对象的创建不用去new了,可以由spring自动生产,使用java的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的。
      (3)Spring的IOC有三种注入方式 :构造器注入、setter方法注入、根据注解注入。

  3. Spring的AOP理解(重点)
      OOP面向对象,允许开发者定义纵向的关系,但并不适用于定义横向的关系,会导致大量代码的重复,而不利于各个模块的重用。
      AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。
      AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。

  4. aop的动态代理和静态代理
      (1)AspectJ是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
      (2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

  Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理:
​   ① JDK动态代理只提供接口的代理,不支持类的代理,要求被代理类实现接口。JDK动态代理的核心是InvocationHandler接口和Proxy类,在获取代理对象时,使用Proxy类来动态创建目标类的代理类(即最终真正的代理类,这个类继承自Proxy并实现了我们定义的接口),当代理对象调用真实对象的方法时, InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;是最终生成的代理对象; method 是被代理目标实例的某个具体方法; args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。

​   ② 如果被代理类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
  总结
  静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。

5.spring bean 容器的生命周期是什么样的?

  1. Spring 对 Bean进行实例化,默认是单例的
  2. Spring 对 Bean进行依赖注入
  3. 如果 Bean已经实现了 BeanNameAware 接口,则调用它实现的setBeanName(String)方法,传递的参数就是 Spring 配置文件中 Bean的id值
  4. 如果Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(BeanFactory),传递的是Spring工厂自身
  5. 如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文
  6. 如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法
  7. 如果bean中有方法添加了@PostConstruct注解,那么该方法将被调用
  8. 如果bean实现了InitializingBean接口,spring将调用它的afterPropertiesSet()接口方法,类似的如果在xml文件中通过标签的init-method元素指定了初始化方法,那么该方法将被调用
  9. 如果bean实现了BeanPostProcessor接口,它的postProcessAfterInitialization()接口方法将被调用
  10. 此时bean已经准备就绪,可以被应用程序使用了,他们将一直驻留在应用上下文中,直到该应用上下文被销毁
  11. 如果bean中有方法添加了@PreDestroy注解,那么该方法将被调用
  12. 若bean实现了DisposableBean接口,spring将调用它的destroy()接口方法。同样的,如果bean使用了destroy-method属性声明了销毁方法,则该方法被调用

6、Spring bean 支持 5 种 scope
  Singleton - 每个 Spring IoC 容器仅有一个单实例。
  Prototype - 每次请求都会产生一个新的实例。
  Request - 每一次 HTTP 请求都会产生一个新的实例,并且该 bean 仅在当前 HTTP 请求内有效。
  Session - 每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前 HTTP session 内有效。
  Global-session - 类似于标准的 HTTP Session 作用域,不过它仅仅在基于portlet 的 web 应用中才有意义。

  1. Spring框架中的Bean是线程安全的么?如果线程不安全,那么如何处理?
      Spring容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体情况还是要结合Bean的作用域来讨论。
      对于prototype作用域的Bean,每次都创建一个新对象,也就是线程之间不存在Bean共享,因此不会有线程安全问题。
      对于singleton作用域的Bean,所有的线程都共享一个单例实例的Bean,因此是存在线程安全问题的。但是如果单例Bean是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Controller类、Service类和Dao等,这些Bean大多是无状态的,只关注于方法本身。
      有状态Bean(Stateful Bean) :就是有实例变量的对象,可以保存数据,是非线程安全的。
      无状态Bean(Stateless Bean):就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。
      有状态的bean(比如Model和View),就需要自行保证线程安全,最浅显的解决办法就是将有状态的bean的作用域由“singleton”改为“prototype”。
      也可以采用ThreadLocal解决线程安全问题,为每个线程提供一个独立的变量副本,不同线程只操作自己线程的副本变量。
      Dao 会操作数据库Connection,Connection 是带有状态的,比如说数据库事务,Spring事务管理器使用ThreadLocal为不同线程维护了一套独立的connection副本,保证线程之间不会互相影响(这就是spring保证事务获取同一个connection的原因)

@Autowired和@Resource之间的区别:
  (1) @Autowired默认是按照类型装 配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。
  (2) @Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。

五、JVM

1、JVM内存结构是与JVM的内部存储结构相关,而Java内存模型是与多线程编程相关
jvm内存模型
1.1、程序计数器

内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成

如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

1.2、Java 虚拟机栈
线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)

StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

1.3、本地方法栈

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。

1.4、Java 堆

对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。

OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

1.5、方法区

属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

在这里插入图片描述
2. 垃圾回收器与内存分配策略
2.1 概述
程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。

2.2 对象已死吗?
在进行内存回收之前要做的事情就是判断那些对象是‘死’的,哪些是‘活’的。

2.2.1 引用计数法

给对象添加一个引用计数器。但是难以解决循环引用问题。
在这里插入图片描述

从图中可以看出,如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。则在 Java 堆当中的两块内存依然保持着互相引用无法回收。

2.2.2 可达性分析法

通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。
在这里插入图片描述
可作为 GC Roots 的对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象

总结

  愿各位早日找到心仪工作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值