JAVA 开发部分面试题-基础篇

220 阅读21分钟

本文主要介绍部分Java基础,多线程知识

1:Java基础

1.1:基础

1.1.1:char 和varChar的区别

char是一种固定长度的类型,varchar则是一种可变长度的类型 char(M)类型的数据列里,每个值都占用M个字节,如果某个长度小于M,MySQL就会在它的右边用空格字符补足.(在检索操作中那些填补出来的空格字符将被去掉)在varchar(M)类型的数据列里,每个值只占用刚好够用的字节再加上一个用来记录其长度的字节(即总长度为L+1字节). 对于InnoDB表,因为它的数据行内部存储格式对固定长度的数据行和可变长度的数据行不加区分(所有数据行共用一个表头部分,这个标头部分存放着指向各有关数据列的指针),所以使用char类型不见得会比使用varchar类型好。事实上,因为char类型通常要比varchar类型占用更多的空间,所以从减少空间占用量和减少磁盘i/o的角度,使用varchar类型反而更有利.

1.1.2:static 关键字有哪些作用?

1: static修饰变量

2: static修饰方法

3: 静态块

4: 静态内部类

5: 静态导包

1.1.3:final关键字的作用(变量、方法、类。)

1:final修饰变量,如果该变量是基本数据类型,则其数值一旦在初始化之后便不能更改;如果是引用类型,则在对其初始化之后便不能再让其指向另一个对象。

2: final修饰类,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。 把方法锁定,以防任何继承类修改它的含义;类中所有的private方法都隐式地指定为final。

1.1.4:transient关键字的作用

1)transient修饰的变量不能被序列化;

2)transient只作用于实现 Serializable 接口;

3)transient只能用来修饰普通成员变量字段;

4)不管有没有 transient 修饰,静态变量都不能被序列化,因为读取的值是在JVM内存中读取的值,与序列化设置的值不一致;


1.1.5:volatile关键字的底层实现原理

volatile的底层就是通过内存屏障来实现的 Java source code->Java class->JVM->汇编指令->cpu执行 java中使用的并发机制依赖于JVM实现和cpu指令。 编译器和执行器 可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。 内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪个CPU执行的。

1.1.6:this,super

this关键字用于引用类的当前实例。 super关键字用于从子类访问父类的变量和方法。

1.1.7:. Object 类有哪些方法?

object 是所有类的父类,

public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。 
public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。 
protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。 
public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。 public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。 public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。 
public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。 public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。 
public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念 protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作

1.1.8: &和&&共同点和区别

共同点:两者都可做逻辑运算符。它们都表示运算符的两边都是 true 时,结果为 true;

不同点: &也是位运算符。& 表示在运算时两边都会计算,然后再判断;&&表示先运算符号左边的东西,然后判断是否为 true,是 true 就继续运算右边的然后判断并输出,是 false 就停下来直接输出不会再运行后面的东西。

1.1.9:重载和重写的区别

1: 重载:发生在同一个类中,重载的方法名必须相同,参数类型或个数或顺序不同,方法返回值和访问修饰符可以不同。
2: 重写:重写发生在运行期,是子类对父类中允许访问的方法的实现过程进行重新编写。重写需满足下面几个要求:
1):返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
2):如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
3):构造方法无法被重写

tips: 图示表示更清楚些


Java开发部分面试题整理(一)

1.1.10:自动装箱和拆箱

装箱:将基本类型用它们对应的引用类型包装起来;拆箱:将包装类型转换为基本数据类型;
//eg:
装箱Integer i = new Integer(10);
//或
Integer i = 10;
//eg:
拆箱int n = i;

tips:基本数据类型对应的包装器类型


Java开发部分面试题整理(一)

tips:装箱拆箱延伸

public class InteTest {       
    public static void main(String[] args) {            
     Integer i = 10;// 装箱            
     int n = i;// 拆箱       
    }   
 }

//代码反汇编解析成的汇编指令

/**通过汇编指令可以看出int类型 装箱其实就是调用Integer的valueOf(int)方法,拆箱调用的是Integer的intValue方法同理其他基本数据类型**/public class com.example.others.InteTest {  
public com.example.others.InteTest();    
Code:       0: aload_0       
1: invokespecial #1                  // Method java/lang/Object."<init>":()V       
4: return  public static void main(java.lang.String[]);    
Code:       
0: bipush        10       
2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;       
5: astore_1       
6: aload_1       
7: invokevirtual #3                  // Method java/lang/Integer.intValue:()I      
10: istore_2      
11: return}


1.1.11 == 与 equals (!!!)

1: == : 判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。
2: equals() : 它的作用也是判断两个对象equals  分为两种情况,
1⃣️:类没有重写 equals() 方法。等价于通过“==”比较这两个对象。
2⃣️:类重写了equals() 方法。一般来说,我们基本上是覆盖 equals() 方法来完成比较两个对象的内容是否相等的功能

1.1.12: hashCode 与 equals(重写 equals 时必须重写 hashCode 方法)

// Object.hashCode方法//作用是获取码,它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置//哈希表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。
public native int hashCode();

tips:

// Object.hashCode方法//作用是获取码,它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置//哈希表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。
public native int hashCode();//如果两个对象相等,则 hashcode 一定也是相同的

1.1.13:深拷贝 vs 浅拷贝

1:浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝
2:深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容

1.2:序列化机制

序列化是把对象改成可以存到磁盘或通过网络发送到其他运行中的 Java 虚拟机的二进制格式的过程, 并可以通过反序列化恢复对象状态. 流 ObjectOutputStream.writeObject() 不指定 serialVersionUID的后果是,当你添加或修改类中的任何字段时, 则已序列化类将无法恢复, 因为为新类和旧序列化对象生成的 serialVersionUID 将有所不同。 transient:一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。

1.3:反射

反射就是程序运行期间JVM会对任意一个类洞悉它的属性和方法,对任意一个对象都能够访问它的属性和方法。依靠此机制,可以动态的创建一个类的对象和调用对象的方法。 其次就是反射相关的API,常用的,比如获取一个Class对象。Class.forName(完整类名)。通过Class对象获取类的构造方法,class.getConstructor。根据class对象获取类的方法,getMethod和getMethods。使用class对象创建一个对象,class.newInstance等。


反射的优点和缺点,优点就是增加灵活性,可以在运行时动态获取对象实例。 缺点是反射的效率很低,而且会破坏封装,通过反射可以访问类的私有方法,不安全。


获取class的三种方法: 1:知道具体类的情况下可以使用: Object.class 2:Class.forName(); 3:知道具体对象的情况下,可以使用,obj.getClass();

1.4:多线程

多线程图解


Java开发部分面试题整理(一)

多线程图解


1.4.1:为什么要用线程池?

1:降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

2:提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。

3:提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

1.4.2: Java主要提供了哪几种线程池,各自的使用场景是怎么样的?

主要提供了4种线程池 FixedThreadPool: 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 ScheduledThreadPoolExecutor: 主要用来在给定的延迟后运行任务,或者定期执行任务。ScheduledThreadPoolExecutor 又分为:ScheduledThreadPoolExecutor(包含多个线程)和 SingleThreadScheduledExecutor (只包含一个线程)两种。

4种线程池各自使用场景

FixedThreadPool: 适用于为了满足资源管理需求,而需要限制当前线程数量的应用场景。它适用于负载比较重的服务器;SingleThreadExecutor: 适用于需要保证顺序地执行各个任务并且在任意时间点,不会有多个线程是活动的应用场景; CachedThreadPool: 适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器; ScheduledThreadPoolExecutor: 适用于需要多个后台执行周期任务,同时为了满足资源管理需求而需要限制后台线程的数量的应用场景; SingleThreadScheduledExecutor: 适用于需要单个后台线程执行周期任务,同时保证顺序地执行各个任务的应用场景。

1.4.3:创建线程池的几种方式

(1) 使用 Executors 创建

(2) ThreadPoolExecutor 的构造函数创建

(3) 使用开源类库

1.4.4:synchronized 和 ReentrantLock 的区别

① 两者都是可重入锁 两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。 
② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API synchronized 是依赖于 JVM 实现的,虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 
③ ReentrantLock 比 synchronized 增加了一些高级功能 (决定了使用场景不同)主要来说主要有三点:
1:等待可中断;
2: 可实现公平锁;
3:可实现选择性通知(锁可以绑定多个条件)ReentrantLock 提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 ReentrantLock 可以指定是公平锁还是非公平锁。而 synchronized 只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock 默认情况是非公平的,可以通过 ReentrantLock 类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。 synchronized 关键字与 Object.wait()和 notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock 类当然也可以实现,但是需要借助于 Condition 接口与 newCondition() 方法。Condition 是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个 Lock 对象中可以创建多个 Condition 实例(即对象监视器),线程对象可以注册在指定的 Condition 中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用 notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用 ReentrantLock 类结合 Condition 实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而 synchronized 关键字就相当于整个 Lock 对象中只有一个 Condition 实例,所有的线程都注册在它一个身上。如果执行 notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而 Condition 实例的 signalAll()方法 只会唤醒注册在该 Condition 实例中的所有等待线程。 如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。 
4: 两者的性能已经相差无几 在 JDK1.6 之前,synchronized 的性能是比 ReentrantLock 差很多。具体表示为:synchronized 关键字吞吐量随线程数的增加,下降得非常严重。而 ReentrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReentrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReentrantLock 的文章都是错的!JDK1.6 之后,性能已经不是选择 synchronized 和 ReentrantLock 的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的 synchronized,所以还是提倡在 synchronized 能满足你的需求的情况下,优先考虑使用 synchronized 关键字来进行同步!优化后的 synchronized 和 ReentrantLock 一样,在很多地方都是用到了 CAS 操作。

1.4.5: wait和sleep的区别

1:sleep是Thread类的方法,wait是Object类中定义的方法
2: 当前线程是拥有锁的情况下,Thread.sleep不会让线程释放锁。Object.wait()会释放锁
3:Thread.sleep和Object.wait都会暂停当前的线程,表示它不再需要CPU的执行时间。区别是,调用wait后,需要别的线程执行notify/notifyAll才能够重新获得CPU执行时间。

1.4.7:CAS算法

即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。


CAS算法涉及到三个操作数 需要读写的内存值 V 进行比较的值 A 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作:重点)。一般情况下是一个自旋操作,即不断的重试。

tips :CAS 算法的问题

1;ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。

ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。 JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。

2:循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。

3: 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。 Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作

1.4.8:看过synchronized的源码没

tips: synchronized 反汇编解析出的汇编指令

//源代码public class SynchronizedTest {    
public static void main(String[] args) {        
    testSynchronized();   
    }    
 public static   void testSynchronized(){        
       synchronized (SynchronizedTest.class){
            System.err.println("hhh");
        }
    }
}

代码反汇编解析成的汇编指令(synchronized重点:testSynchronized方法中的 monitorenter 和monitorexit 指令)

终端输入 :javap -c SynchronizedTest.class > SynchronizedTest.txt

public class com.example.others.SynchronizedTest {
  public com.example.others.SynchronizedTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #2 // Method testSynchronized:()V
       3: return  public static void testSynchronized();
    Code:
       0: ldc 
          #3                  // class com/example/others/SynchronizedTest
       2: dup 
       3: astore_0
       4: monitorenter
       5: getstatic     #4                  // Field java/lang/System.err:Ljava/io/PrintStream;
       8: ldc           #5                  // String hhh
      10: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      13: aload_0      14: monitorexit      15: goto
          23      18: astore_1      19: aload_0      20: monitorexit
      21: aload_1
      22: athrow
      23: return    
Exception table:       from    to  target type           5    15    18   any          18    21    18   any}

1.4.9:Java锁种类,以及区别

1.4.9.1:乐观锁VS悲观锁

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

悲观锁

 synchronized public synchronized void testMethod() {
    // 操作同步资源
  } 
    // ReentrantLock   
private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁   
public void modifyPublicResources() {
     lock.lock();
     lock.unlock();
   }

乐观锁

private AtomicInteger atomicInteger = new AtomicInteger(); // 需要保证多个线程使用的是同一个
AtomicInteger atomicInteger.incrementAndGet(); //执行自增1

1.4.9.2: 自旋锁和非自旋锁

自旋锁:阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间,让当前线程进行自旋,避免切换线程的开销

自旋锁的缺点

自旋锁不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

1.4.9.3: 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁(专门针对synchronized,同锁升级状态流程)


1.4.9.4:公平锁和非公平锁

1:公平锁是指多个线程按照申请锁的顺序来获取锁

2:非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待

1.4.9.5: 可重入锁 VS 非可重入锁

可重入锁是指锁对象得是同一个对象或者class的前提下,在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁 ReentrantLock和synchronized都是可重入锁 优点是可以一定程度上避免死锁