【金三银四】每日一套面经(Java),看你是否能答对多少?

1、线程有几种状态?

Java中,线程的生命周期主要可以分为以下五种状态:

  1. 新建(New)
    当线程对象被创建但还未调用start()方法时,线程处于新建状态。
  2. 就绪(Runnable)
    当线程调用了start()方法,线程进入就绪状态,等待被线程调度器选中获取CPU的执行时间。
  3. 运行(Running)
    就绪状态的线程获得CPU时间片后,开始执行run()方法中的代码,此时线程处于运行状态。
  4. 阻塞(Blocked)
    线程因为某些原因放弃CPU执行权,暂停运行。阻塞状态可以细分为以下几种:
    等待阻塞:线程执行了某个对象的wait()方法,进入等待队列,等待其他线程执行notify()或notifyAll()方法唤醒。
    同步阻塞:线程尝试获取对象的同步锁失败,进入锁池,等待获取到锁。
    其他阻塞:线程执行了Thread.sleep(long ms)、join()方法或者进行I/O操作,暂时停止运行。
  5. 死亡(Terminated/Dead)
    线程的run()方法执行完成,或者因为异常退出了run()方法,线程结束生命周期。

这五种状态描述了一个线程从创建到消亡的整个生命周期。需要注意的是,实际中线程的状态可能会更加复杂,例如在操作系统层面,线程可能还会有其他的细分状态,但在Java语言层面,通常将线程状态简化为上述五种。

2、wait和 timed_wait 有啥区别?

在Java中,wait()wait(long timeout, int nanos)方法是Object类的方法,用于线程之间的通信。它们之间的主要区别在于等待的时间:

  1. wait(): 这是一个无限期等待的方法。当线程调用wait()时,它会一直等待,直到另一个线程调用同一个对象的notify()notifyAll()方法。等待的线程将被唤醒并可以继续执行。

  2. wait(long timeout, int nanos): 这是一个带超时的等待方法。线程会等待指定的毫秒数加上纳秒数。如果在指定的时间段内没有收到通知,线程将自动唤醒并继续执行。

在使用这些方法时,需要注意以下几点:

  • wait()wait(long timeout, int nanos)方法必须在同步块或方法内部调用,因为它们依赖于当前线程持有对象的锁。
  • 当线程被唤醒后,它需要重新检查条件是否满足,因为可能由于其他原因(如虚假唤醒)而被唤醒。
  • 这两个方法在等待时会释放当前线程持有的锁,使得其他线程可以获取该锁并执行相应的操作。当线程被唤醒并重新开始执行时,它会重新尝试获取锁。

3、Sleep 和 Wait有啥区别?

在Java中,sleep()wait()方法都可以用于线程控制,但它们之间有一些重要的区别:

  1. sleep()方法来自Thread类,而wait()方法来自Object类:

    • sleep()方法是Thread类的静态方法,可以让当前线程暂停执行一段时间,不释放对象锁。
    • wait()方法是Object类的实例方法,用于线程间的通信,让调用该方法的线程暂停执行,并释放对象锁。
  2. 锁的释放情况:

    • 在调用sleep()方法时,线程仍然持有对象锁,不会释放锁。
    • 而在调用wait()方法时,线程会释放对象锁,让其他线程可以获取该对象锁进行操作。
  3. 用法和场景:

    • sleep()方法通常用于控制线程的执行时间,例如在定时任务中间隔一段时间执行。
    • wait()方法通常与notify()notifyAll()一起使用,用于线程间的通信,等待特定条件满足后再继续执行。

总的来说,sleep()用于线程的暂停执行,而wait()用于线程之间的通信和同步。在使用时需要根据具体的情况选择合适的方法。

4、Synchronized锁的一定是对象吗?如果synchronized(Class)锁的是什么?

在Java中,synchronized关键字用于创建同步块或同步方法,以确保多个线程在访问共享资源时的安全性。通常情况下,synchronized锁定的是对象,但也可以使用synchronized来锁定类。

  1. 锁定对象:

    • 当使用synchronized修饰实例方法或代码块时,锁定的是当前对象实例,即使用synchronized关键字的方法或代码块将锁定当前对象。
    • 例如:synchronized(this) { ... } 或者 synchronized(objectInstance) { ... }
  2. 锁定类:

    • 通过synchronized(Class)语法,可以锁定类而不是对象。这样做会锁定整个类,阻止其他线程同时访问该类的所有synchronized static方法。
    • 例如:synchronized(MyClass.class) { ... }

当使用synchronized(Class)时,它实际上锁定的是类的Class对象,因为每个类在内存中都有唯一的Class对象实例。这意味着对该类的所有实例和静态成员的访问都将受到影响,因为它们都共享同一个类级锁。

需要注意的是,当锁定类时,需要格外小心,因为类级锁的范围更大,可能会导致性能问题或死锁等情况。因此,在使用synchronized(Class)时,需要谨慎考虑并确保合适的使用场景。

5、Synchronized是如何保证原子性、有序性、可见性吗?

synchronized是Java中的一个关键字,用于实现线程同步,即在同一时刻,只有一个线程可以执行某个方法或代码块。以下是synchronized如何保证原子性、有序性和可见性的详细解释:

原子性

原子性指的是一个操作在其他线程看来是不可分割的,即在同一时刻,只有一个线程可以执行synchronized标记的方法或代码块。

在Java中,当线程尝试进入synchronized方法或代码块时,它会检查锁是否可用。如果锁已被占用,线程将被阻塞,直到锁被释放。这个机制保证了操作的原子性,因为一旦线程获得了锁,它会一直保持该锁直到synchronized方法或代码块执行完毕,其他线程无法打断该操作。

可见性

可见性指的是当一个线程修改了共享变量的值,这个新值对于其他线程来说应当是立即可见的。

synchronized通过以下方式保证可见性:

当线程进入synchronized代码块时,它会清空工作内存中的共享变量值,从主内存中重新读取。
当线程退出synchronized代码块时,它会将工作内存中的共享变量的最新值刷新回主内存。
这意味着,当一个线程在synchronized代码块中修改变量时,这个修改会在退出synchronized块时被写入主内存,从而对其他线程立即可见。

有序性

有序性指的是程序执行的顺序按照代码的先后顺序执行,防止指令重排序。

在Java中,synchronized块内的操作会建立一个“内存屏障”,这个屏障可以防止编译器和处理器对操作进行重排序。这意味着,在synchronized块内的代码执行顺序会严格按照代码的书写顺序,从而保证了操作的有序性。

具体来说,monitorenter和monitorexit这两个JVM指令,它们隐式地包含了对内存的读写屏障,保证了进入和退出synchronized块时的内存操作的顺序。

总结来说,synchronized通过锁机制保证了操作的原子性,通过内存屏障保证了操作的可见性和有序性,从而为多线程环境下的并发控制提供了重要保证。

6、原子性、有序性、可见性分别是什么?synchronized能禁止指令重排么?

在多线程编程中,通常会涉及到三个重要的概念:原子性、有序性和可见性。

  1. 原子性

    • 原子性指的是一个操作是不可分割的,要么全部执行成功,要么全部执行失败。在多线程环境下,某个操作要么被完整地执行,要么不执行,不会被中断。
    • 例如,在Java中,synchronized关键字可以确保代码块的原子性,即同一时间只有一个线程可以进入同步代码块。
  2. 有序性

    • 有序性指的是程序的执行顺序按照代码的先后顺序执行,不会出现乱序执行的情况。在多线程环境下,有序性可以保证线程内部的操作顺序是按照代码编写顺序执行的。
    • 在Java中,一般情况下,线程内部的操作是有序的,但在多线程环境下,线程间的操作顺序可能是不确定的。
  3. 可见性

    • 可见性指的是当一个线程对共享变量的修改对其他线程是可见的。如果一个线程修改了共享变量的值,其他线程应该能够立即看到这个修改。
    • 在Java中,使用volatile关键字可以确保共享变量的可见性,也可以通过synchronized关键字来保证可见性。

关于synchronized能否禁止指令重排的问题,答案是可以。在Java中,synchronized关键字可以确保原子性、有序性和可见性,其中有序性可以防止指令重排。当一个线程进入synchronized代码块时,它会获取锁并清空工作内存,从主内存中重新读取变量的值,这样可以防止指令重排对程序的影响。因此,synchronized可以一定程度上禁止指令重排。

7、Synchronized的锁升级过程?偏向锁

Synchronized的锁升级过程是一个复杂的过程,主要包括以下几种锁状态:

  1. 无锁状态:最基础的状态,没有进行任何锁优化。

  2. 偏向锁:当对象头中没有记录线程ID时,JVM会尝试通过CAS操作将当前线程的ID记录在对象头中,实现偏向锁。偏向锁的目的是减少不必要的锁竞争,提高性能。

  3. 轻量级锁:当第二个线程尝试获取偏向锁时,偏向锁会升级为轻量级锁。轻量级锁通过在当前线程的栈帧中创建Lock Record记录锁信息,并通过CAS操作修改对象头中的指针,指向当前线程的Lock Record。

  4. 重量级锁:当锁竞争激烈时,轻量级锁会升级为重量级锁。重量级锁涉及到操作系统层面的线程状态转换,即线程阻塞和唤醒,性能较差。

偏向锁是锁升级过程中的一个优化手段,旨在减少锁竞争和提高性能。偏向锁的实现原理是使用CAS操作替换对象头中的Thread ID,成功则获得偏向锁,否则升级为轻量级锁。偏向锁的目的是偏向第一个获取锁的线程,使得该线程在后续访问时快速获得锁,提高性能。

8、CAS有什么优势和缺点?CAS一定自旋吗?ABA问题

CAS(Compare and Swap)是一种乐观锁技术,用于实现多线程环境下的并发控制。CAS操作包含三个参数:要更新的内存位置的值、预期值和新值。CAS操作仅在当前值等于预期值时才会将新值写入内存,并返回操作结果。CAS的优势和缺点如下:

优势:

  1. 无锁机制: CAS是一种无锁机制,相比传统的加锁方式,避免了线程阻塞和唤醒所带来的开销。
  2. 减少系统调用: CAS操作是在用户态完成的,不需要进入内核态,减少了系统调用的开销。
  3. 避免死锁: CAS不会导致线程阻塞,因此避免了死锁问题。
  4. 适用范围广: CAS适用于很多并发场景,能够确保原子性操作。

缺点:

  1. 自旋消耗CPU资源: 如果CAS操作失败,会一直进行自旋尝试,消耗CPU资源。
  2. ABA问题: 在CAS操作中,如果一个值原来是A,后来被改成了B,然后又被改回A,这时CAS会误认为数据没有变化,可能导致数据错误。
  3. 无法解决多线程并发问题: CAS只能解决单个变量的原子操作,无法解决复杂的多变量并发问题。

CAS是否一定自旋: CAS操作本身并不一定要自旋。在某些情况下,CAS操作可能成功,无需自旋。但在失败时,通常会进行一定次数的自旋重试,以尽量保证更新操作的成功。

ABA问题: ABA问题指的是在CAS操作中可能出现的一个值原来是A,后来被改成了B,然后又被改回A的情况,导致CAS操作误判。为了解决ABA问题,可以使用版本号或者标记位来辅助判断数据是否被修改过。

9、让一个类能序列化,需要怎么做?序列化成网络流?Serilazable?为啥实现了这个接口就能序列化?

要使一个类能够序列化,需要按照以下步骤进行:

  1. 让类实现java.io.Serializable接口:该接口是一个标记接口,没有任何方法,只是用来标识一个类可以被序列化。

  2. 确保类的所有成员变量都是可序列化的:如果一个类的成员变量是其他对象,那么这些对象也必须实现Serializable接口才能被序列化。

  3. 对于不想被序列化的成员变量,可以使用transient关键字进行标记,这样它们就不会被序列化。

  4. 定义一个无参构造方法:在反序列化过程中,需要调用类的无参构造方法来创建对象。

当一个类实现了java.io.Serializable接口时,它表明该类可以进行序列化操作。Java序列化机制会通过检查类是否实现了Serializable接口来确定一个对象是否可被序列化。

实现Serializable接口的作用是告诉Java虚拟机,这个类是可序列化的,可以进行对象的序列化和反序列化操作。在序列化过程中,Java虚拟机会通过读取对象的状态信息,将对象转化为字节流,可以保存到文件或通过网络传输。而在反序列化过程中,Java虚拟机会根据字节流重建对象的状态。

值得注意的是,Serializable接口只是一个标记接口,并没有定义具体的序列化和反序列化的实现。序列化和反序列化的具体实现是由Java序列化机制自动完成的。

10、异常处理有关的关键字?finally一定执行吗?

在 Java 中,异常处理相关的关键字包括:try、catch、finally、throw 和 throws。

  • try: 用于包裹可能会抛出异常的代码块,在 try 块中的代码执行过程中如果发生异常,会跳转到相应的 catch 块进行异常处理。

  • catch: 用于捕获 try 块中抛出的异常,可以指定捕获特定类型的异常,并给出相应的处理逻辑。

  • finally: finally 块中的代码无论是否发生异常都会被执行。通常用于释放资源、关闭连接等操作,确保资源得到正确释放。但在某些情况下,finally 块可能不会执行,比如在 System.exit() 被调用时,程序会直接退出而不执行 finally 块。

  • throw: 用于手动抛出一个异常对象,可以在代码中主动触发异常。

  • throws: 用于声明方法可能会抛出的异常类型,告诉调用者需要处理这些可能的异常。

关于 finally 块一定执行的问题,一般情况下 finally 块中的代码都会执行,即使 try 或 catch 块中有 return 语句,也会在方法返回之前执行 finally 块中的代码。但有一些情况下 finally 块不会执行,比如:

  1. 在执行 finally 块之前 JVM 被意外终止。
  2. 在执行 finally 块之前调用了 System.exit() 方法。
  3. 在执行 finally 块之前发生了死锁。

总的来说,finally 块大部分情况下是会执行的,用于确保资源的正确释放,但在极端情况下可能不会执行。

11、Java是值传递还是引用传递?如何区分值传递还是引用传递?在 Java 中,参数传递是值传递。这意味着当你将一个变量作为参数传递给一个方法时,实际上传递的是这个变量的值,而不是变量本身。这种机制决定了无论是基本数据类型还是对象引用,方法内部对参数的改变都不会影响到原始变量的值或引用。

区分值传递和引用传递:

  1. 值传递: 在值传递中,传递的是变量的副本,对副本的修改不会影响原始变量的值。当传递基本数据类型(如int、float、char等)时,一定是值传递。

    void modifyValue(int x) {
        x = x + 10;
    }
    
    int num = 5;
    modifyValue(num);
    System.out.println(num); // 输出为5,原始变量未改变
    
  2. 引用传递: 在引用传递中,传递的是对象的引用,即指向对象的地址。对于引用传递来说,改变对象的属性会影响原始对象。

    class Person {
        String name;
    
        Person(String name) {
            this.name = name;
        }
    }
    
    void modifyReference(Person person) {
        person.name = "Alice";
    }
    
    Person person = new Person("Bob");
    modifyReference(person);
    System.out.println(person.name); // 输出为"Alice",原始对象被修改
    

在 Java 中,虽然对象的引用是通过值传递的方式传递的,但要注意的是,对于对象引用来说,它指向的是对象在堆内存中的地址,因此在方法中可以修改对象的状态,但不能修改对象的引用。

12、Char能存储中文么?字符串能存储中文吗?字符串底层是啥实现的?

在 Java 中,char 类型是用来表示单个字符的数据类型,它使用 Unicode 编码,可以存储中文字符。每个 char 类型变量可以存储一个 Unicode 字符,包括中文字符在内。

字符串在 Java 中是用 String 类表示的,可以存储中文字符。String 类底层实际上是通过字符数组 char[] 来实现的,其中每个字符对应数组中的一个元素。Java 中的字符串是不可变的,即一旦创建就不能被修改,任何对字符串的操作实际上是返回一个新的字符串对象。

字符串的不可变性带来了很多优点,例如线程安全、安全性等。Java 中的字符串常量池(String Pool)也利用了字符串的不可变性,相同的字符串只会在内存中保存一份,以节省内存空间。

当我们创建一个字符串时,如果这个字符串已经存在于字符串常量池中,那么将返回常量池中的引用;如果不存在,则会在常量池中创建一个新的字符串对象。这也解释了为什么使用 == 比较两个字符串对象时,实际上比较的是它们在内存中的引用地址,而不是内容是否相同。

13、在数据库中char(10)和 varchar(10)有啥区别?

在数据库中,CHAR(10)VARCHAR(10) 是用来定义字段的数据类型,它们之间有以下区别:

  1. 存储方式:

    • CHAR(10): 会固定地存储指定长度的字符,不足长度时会使用空格进行填充。即使存储的实际内容不足 10 个字符,也会占用 10 个字符的存储空间。
    • VARCHAR(10): 会根据实际存储的内容动态地分配存储空间,只会占用实际内容所需的存储空间加上额外的一两个字节用于存储长度信息。
  2. 空间利用:

    • CHAR(10): 因为会固定占用指定长度的存储空间,所以可能会浪费存储空间,特别是对于存储内容长度变化较大的字段。
    • VARCHAR(10): 根据实际存储内容动态分配空间,可以更有效地利用存储空间。
  3. 性能:

    • CHAR(10): 由于固定长度,检索速度可能会比 VARCHAR 快一些,但对于存储内容长度变化较大的字段,可能会引起存储和检索效率问题。
    • VARCHAR(10): 可以节省存储空间,但在某些情况下可能会降低检索速度。
  4. 默认值:

    • CHAR(10): 如果插入的字符串长度小于 10,会在右侧自动填充空格,保证存储长度为 10。
    • VARCHAR(10): 只会存储实际长度的字符,不会填充空格。

一般来说,如果字段长度固定且长度相对较短,并且对存储空间要求比较严格,可以选择使用 CHAR 类型;如果字段长度可变或长度较长,建议使用 VARCHAR 类型以节省存储空间。

14、Select * 会用到事务?count(1)、count(*)、count(字段)的区别?用哪个?

在数据库中,SELECT * 语句本身并不会启动或涉及事务。SELECT 语句用于从数据库中检索数据,它可以在事务内部或外部使用,但并不会自动创建事务。

关于 count(1)count(*)count(字段) 的区别如下:

  1. count(1):这种写法会统计结果集的行数,它实际上是在统计每一行中的值是否为非空(即存在),因为传入 COUNT 函数的参数是固定的常量值 1。这种写法通常用于统计行数而不关心具体的字段值。

  2. count(*):这种写法也用于统计结果集的行数,它会统计符合条件的行数,不论字段的值是否为 NULL。这是最常见的用法,因为它简洁且效率较高。

  3. count(字段):这种写法会统计指定字段的非空值的数量,忽略 NULL 值。如果想统计某个特定字段的非空值数量,可以使用这种写法。

通常情况下,建议使用 count(*) 来统计行数,因为它更通用,同时在大多数数据库管理系统中性能也更好。如果需要统计特定字段的非空值数量,则可以使用 count(字段)。而 count(1) 虽然也能实现相同的功能,但通常不推荐使用,因为它并不直观,容易引起误解。

总结:对于一般的行数统计,使用 count(*) 是最常见和推荐的做法;对于特定字段的非空值数量统计,可以使用 count(字段)

  • 23
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值