206面试题(47~60)
208道Java面试题
47~60
- **208道Java面试题**
- **47. 在 Java 程序中怎么保证多线程的运行安全?**
- **48. 多线程中 synchronized 锁升级的原理是什么?**
- **49. 什么是死锁?**
- **50. 怎么防止死锁?**
- **51. ThreadLocal 是什么?有哪些使用场景?**
- **52. 说一下 synchronized 底层实现原理?**
- **53. synchronized 和 volatile 的区别是什么?**
- **54. synchronized 和 Lock 有什么区别?**
- **55. synchronized 和 ReentrantLock 区别是什么?**
- **56. 说一下 atomic 的原理?**
- **57. 什么是反射?**
- **58. 什么是 Java 序列化?什么情况下需要序列化?**
- **59. 动态代理是什么?有哪些应用?**
- **60. 怎么实现动态代理?**
-
47. 在 Java 程序中怎么保证多线程的运行安全?
1、使用同步(synchronization)
- 同步方法:使用synchronized关键1字修饰方法,使同一时间只有一个线程可以执行方法。
public synchronized void someMethod() {// 方法体 }
- 同步代码块:使用synchronized关键字修饰代码块,可以减少同步的范围,提高新能。
public void someMethod() {synchronized (this) {// 同步代码块} }
- 同步静态方法:使用 synchronized关键字修饰静态方法,使得同一时间只有一个线程可以执行该类的静态同步方法。
public static synchronized void someStaticMethod() {// 静态方法体 }
2、使用显式锁(Explicit Locking)
- 使用 java.util.concurrent.locks.Lock 接口及其实现类(如 ReentrantLock)来替代 synchronized关键字。显式锁提供了更多的灵活性和高级功能,如尝试获取锁、定时获取锁、中断获取锁等。
Lock lock = new ReentrantLock(); public void someMethod() {lock.lock();try {// 需要同步的代码} finally {lock.unlock();} }
3、使用原子变量(Atomic Variables)
- 使用
java.util.concurrent.atomic
包中的原子变量类(如AtomicInteger
、AtomicLong
、AtomicReference
等),这些类提供了在变量级别上的原子操作,避免了使用锁。
AtomicInteger atomicCounter = new AtomicInteger(0); public void increment() {atomicCounter.incrementAndGet(); }
4、使用并发1集合(Concurrent Collection):
- 使用
java.util.concurrent
包中的并发集合类(如ConcurrentHashMap
、CopyOnWriteArrayList
等),这些类内部已经实现了线程安全机制。
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); map.put("key", 1);
5、使用1不可变对象(lmmutable Objects)
- 不可变对象在创建后状态不能改变,因此线程天生就是安全的。可以通过将所有字段设置为
final
并确保对象创建时不允许外部修改来实现不可变性。通过使线程的使用对象为不可变来确保线程安全。
public final class ImmutablePoint {private final int x;private final int y;public ImmutablePoint(int x, int y) {this.x = x;this.y = y;}// Getter 方法public int getX() {return x;}public int getY() {return y;} }
6、使用线程安全的工具类
- 使用
java.util.concurrent
包中的线程安全工具类(如CountDownLatch
、CyclicBarrier
、Semaphore
、ExecutorService
等)来管理线程的执行顺序和资源访问。
7、减少或避免共享可变状态
- 通过减少线程之间的共享状态,多使用局部变量和线程本地变量来存储数据,减少线程之间的数据交互。
-
48. 多线程中 synchronized 锁升级的原理是什么?
在Java多线程编程中,synchronized关键字是常用的同步机制,用来保护共享资源,防止线程间的数据竞争。为了提升性能,Java虚拟机(JVM)引入了锁的优化机制,允许锁根据竞争情况进行升级。这些锁的升级机制包括偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)和重量级锁(Heavyweight Locking)。锁的升级是JVM在多线程环境下为了平衡性能和线程安全性而进行的一种优化手段。
1、锁的分类和概述
Java中,synchronized锁有三种主要的状态 :-
偏向锁(Biased Locking):主要针对无竞争的场景,优化单线程执行的锁操作。
-
轻量级锁(Lightweight Locking):适用于有短暂锁竞争的情况下,通过自旋避免上下文切换的开销。
-
重量级锁(Heavyweight Locking):适用于严重锁竞争的场景,使用操作系统的互斥量(mutex)机制,保证线程安全,但会引入较大的上下文切换开销。
2、偏向锁(Biased Locking)
偏向锁是Java 6引入的一种锁优化技术,主要用于减少无竞争情况下的锁开销。
核心思想:如果一个线程获得了锁,那么该锁就会偏向这个线程,之后该线程再次进入同步块时无需再进行任何同步操作(即不再进行CAS操作来竞争锁),直接执行同步块的代码。-
当一个线程第一次进入同步块时,JVM会将锁对象的Mark Word(对象头的一部分)记录为该线程的ID,表示该锁已经偏向这个线程。
-
如果同一个线程再次进入同步块,JVM检查Mark Word,发现锁已经偏向该线程,无需进行锁竞争,直接执行同步块的代码。
-
如果另一个线程尝试获取这个锁,JVM会撤销偏向锁,将其升级为轻量级锁或重量级锁,以适应多线程竞争的场景。
适用场景
-
线程基本无竞争,锁由同一个线程多次获得和释放。
-
场景中锁的操作较多,但大多数情况下只有一个线程在使用这些锁。
偏向锁的撤销
偏向锁不是永久的。当另一个线程尝试获取偏向锁式,JVM就会撤销偏向锁,并升级为轻量级锁或重量级锁。撤销偏向锁的操作会触发安全点(Safepoint),导致性能下降。
3、轻量级锁(Lightweight Locking)
轻量级锁是一种为了减少多线程环境中重量级锁的使用而引入的优化技术。它主要通过自旋(spin)方式来避免线程的阻塞和唤醒,从而减少上下文切换的开销。
轻量级锁的原理-
当一个线程尝试获取锁时,如果锁是无锁状态(Unlocked),JVM会创建一个称为锁记录(Lock Record)的栈帧,并将对象头中的Mark Word复制到锁记录中,然后尝试使用CAS操作将对象头的Mark Word更新为指向锁记录的指针,并将该线程设置为持有轻量级锁。
-
如果CAS操作成功,锁升级为轻量级锁,线程获得锁。
-
如果CAS操作失败,说明有其他线程竞争该锁,轻量级锁会膨胀为重量级锁,线程会被阻塞,等待锁的释放。
轻量级锁的适用场景
-
锁有短暂的竞争,多个线程可能短暂地竞争同一个锁。
-
自旋等待可以避免线程的阻塞和唤醒,从而减少上下文切换的开销。
自旋锁与锁膨胀
自旋锁是轻量级锁的一种实现方式。自旋锁通过自旋(循环等待一段时间)来尝试获取锁,而不是立即阻塞线程。自旋的目的是避免线程在短时间内的频繁阻塞和唤醒,减少上下文切换的开销。
注意:如果自旋等待时间过长或竞争激烈,自旋锁会膨胀为重量级锁,将线程挂起,等待锁释放。
4、重量级锁(Heavyweigth Locking)
重量级锁是最传统的锁实现方式,使用操作系统的互斥量(mutex)来实现线程的阻塞和唤醒,确保线程安全。
重量级锁的原理-
当锁膨胀为重量级锁时,JVM将对象头的Mark Word更新为指向重量级锁的指针,其他线程如果尝试获取锁,会被挂起进入阻塞状态,直到持有锁的线程释放锁。
-
重量级锁引入了操作系统的互斥量机制,线程的阻塞和唤醒涉及到用户态与内核态的切换,开销较大。
重量级锁的适用场景
-
竞争非常激烈,轻量级锁的自旋带来的开销大于阻塞和唤醒的开销。
-
需要确保绝对的线程安全,而不在乎性能损耗。
锁升级的过程
在多线程竞争逐渐加剧的情况下,为了平衡性能和线程安全性而进行的动态调整。-
初始状态:锁处于无锁状态(Unlocked),无线程竞争时,不需要任何锁定操作,直接执行代码。
-
偏向锁:第一个线程获取锁,JVM将锁标记为偏向锁,Mark Word中记录该线程的ID。后续相同线程再次进入同步块时,无需竞争锁。
-
轻量级锁:当另一个线程尝试获取偏向锁时,JVM撤销偏向锁,锁升级为轻量级锁。线程通过自旋尝试获取锁,避免阻塞。
-
重量级锁:如果自旋失败,或者竞争过于激烈,轻量级锁会膨胀为重量级锁,线程进入阻塞状态,等待锁释放。
锁升级对性能的影响
锁升级机制通过动态调整锁的状态,旨在减少同步操作的开销,提升多线程程序的执行效率。然而,不同的锁状态对性能的影响各不相同:-
偏向锁:适用于无竞争的场景,性能最佳。撤销偏向锁时需要额外的开销。
-
轻量级锁:适用于短暂的锁竞争,自旋锁避免了上下文切换,但自旋时间过长会降低性能。
-
重量级锁:用于严重的锁竞争场景,性能较差,因为它引入了线程阻塞和唤醒的开销。
偏向锁的禁用
在一些特殊场景下,偏向锁可能并不会带来性能提升,反而因为偏向锁的撤销带来额外开销。在这种情况下,可以通过JVM参数禁用偏向锁:-XX:-UseBiasedLocking
总结
Synchronized
锁的升级机制是Java虚拟机在多线程环境下优化锁性能的重要策略。通过偏向锁、轻量级锁和重量级锁的动态调整,JVM能够在不同的竞争强度下提供更好的性能平衡。 -
-
49. 什么是死锁?
概念
两个或多个进程(或线程)在执行过程中在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。每个进程(或线程)持有某种资源的同时又等待其他进程(或线程)释放它所需要的资源,从而导致这些进程(或线程)都无法继续执行下去。
死锁发生的四个必要条件-
互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
-
不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
-
请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
-
循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。
死锁的产生原因
-
竞争资源引起进程死锁:当系统中提供多个进程共享的资源,其数目不足以满足全部进程的需求式,会引起进程对资源的竞争而产生死锁。
-
可剥夺资源和不可剥夺资源:可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺。不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放。
-
竞争不可剥夺资源:可剥夺资源在已经被获得后任然可以再被其他进程或系统剥夺,不会引起死锁。在系统中所配置的不可剥夺资源,由于它们的数量不能满足诸进程运行的需要,会使进程在运行过程中,因争夺这些资源而陷于僵局。两个进程都在等待对方释放自己所需要的资源,但是它们又都因不能继续获得自己所需要的资源而不能继续推进,从而也不能释放自己所占有的资源,以致进入死锁状态。
-
竞争临时资源:临时资源是指由一个进程产生,被另一个进程使用,短时间后便无用的资源,故也称为消耗性资源。
-
-
50. 怎么防止死锁?
-
避免循环等待: 确保线程以相同的顺序请求资源,这样就不会形成循环等待。
-
资源分层: 将资源分配一个全局的顺序,并强制所有线程按照这个顺序请求资源。
-
一次性请求所有资源: 设计线程在开始工作前一次性请求所有需要的资源,而不是在工作过程中逐步请求。
-
使用超时: 当线程请求资源时,设置一个超时时间。如果在超时时间内无法获得所有资源,线程就释放已持有的资源并重试。
-
使用锁超时: 在请求锁时使用超时机制,如果无法在一定时间内获得锁,则释放已经持有的锁并重试。
-
使用死锁检测算法: 定期运行死锁检测算法来检测循环等待的情况,并在检测到死锁时采取措施,比如回滚事务或强制释放资源。
-
使用锁顺序: 如果多个锁必须同时被持有,定义一个锁的顺序,并确保所有线程都按照这个顺序获取锁。
-
避免嵌套锁: 尽量避免嵌套使用锁,如果必须嵌套,确保内层锁的粒度足够小,并且持有时间短。
-
使用条件变量: 使用条件变量与互斥锁配合使用,可以让线程在条件不满足时释放锁并等待,直到条件满足时被唤醒。
-
使用读写锁: 如果可能,使用读写锁代替互斥锁,允许多个读操作同时进行,但写操作需要独占锁。
-
最小化锁持有时间: 尽量缩短持有锁的代码段,减少锁的持有时间。
-
使用锁的封装: 使用锁的封装类或模板,确保在所有路径上都能正确地释放锁。
-
避免在持有锁时调用外部函数: 因为外部函数可能会尝试获取锁,这可能导致死锁。
-
使用死锁预防工具: 使用静态或动态分析工具来检测潜在的死锁。
-
破话互斥条件:如果可能,将必须互斥访问的资源改为允许多个进程(或线程)同时访问。然而,这通常不适用于所有资源,因为有些资源由于其性质(如打印机)需要互斥访问。
-
破话占有并等待条件:允许进程(或线程)只占有部分资源就开始运行,但在运行过程中,若需要使用更多资源,则必须先释放已占有的资源,然后再申请新的资源。这种方法会降低资源的利用率和系统的吞吐量,但可以避免死锁的发生。
-
破坏不可抢占条件:允许进程(或线程)强行从占有者那里夺取某些资源。这种方式通常实现起来比较困难,并可能导致数据的不一致性。
-
采用银行家算法
-
-
51. ThreadLocal 是什么?有哪些使用场景?
ThreadLocal是Java提供的一个非常实用的类,用于解决多线程环境下的变量共享问题。
定义
ThreadLocal,线程局部变量,是一个线程的本地变量,这意味着这个变量是线程独有的,不能与其他线程共享。ThreadLocal类用来提供线程内部的局部变量,使得不同的线程之间不会相互干扰。每个线程访问ThreadLocal时,都会有自己专属的变量副本,从而避免了并发访问时共享变量的竞争问题。
ThreadLocal的工作原理
ThreadLocal的核心机制是为每个线程创建一个独立的变量副本,并且这个副本是存储在线程自身的内部结构中,而不是ThreadLocal实例中。它主要依赖于Thread类中的ThreadLocalMap来实现这一功能。每个Thread对象内部维护了一个ThreadLocalMap,用于存储线程的本地变量。ThreadLocalMap是一个类似于哈希表的结构,其中ThreadLocal对象作为键,线程的本地变量副本作为值。每次线程调用ThreadLocal.set()方法时,实际上是将变量存储到该线程的ThreadLocalMap中。当线程调用ThreadLocal.get()方法时,会从当前线程的ThreadLocalMap中读取与ThreadLocal对象相关联的值。
ThreadLocal的使用场景
1、线程间数据隔离:
-
当多个线程同时操作同一个对象时,可以通过ThreadLocal为每一个线程分配一个独立的变量副本,从而避免多线程间的变量共享导致的数据不一致。
-
例如,在Web应用中,可以使用ThreadLocal来存储每个线程的会话信息,如用户ID、认证信息、角色等,保证线程间的会话信息独立。
2、数据库的连接管理
-
在一些数据库操作中,每个线程可能需要维护一个数据库连接。通过ThreadLocal,可以确保每个线程都有一个独立的数据库连接,避免了多个线程竞争同一个连接的问题。
-
Spring的事务管理器就使用了ThreadLocal来存储当前线程的事务上下文信息,包括数据库的连接。
3、简化代码
-
使用ThreadLocal可以简化多线程环境下的编程模型,使得线程局部变量的访问变得像访问普通变量一样简单。
-
在某些情况下,可以避免将线程相关的状态信息作为参数传递给每个方法,从而简化代码结构。
4、日志记录
- 在日志系统中,可以使用ThreadLocal存储线程级别的上下文信息, 比如requestId或traceId。这样在整个请求链中都可以记录统一的上下文信息,方便追踪日志。
-
-
52. 说一下 synchronized 底层实现原理?
synchronized的底层原理
synchronized的底层时通过Java的监视器锁来实现的。每个 Java 对象都有一个与之对应的监视器锁,当一个线程获取了该对象的监视器锁,就可以执行 synchronized 代码块或方法。其他线程只能等待该线程释放锁,才能获取该对象的监视器锁并执synchronized 代码块或方法。
Java中每一个对象都有一个与之关联的管程。管程包括两个部分:一个是对象头(Object Header),用于存储对象的元数据信息;另一个是监视器锁(Monitor Lock),用于实现线程同步。当一个线程获取了对象的监视器锁,就可以执行 synchronized 代码块或方法,其他线程只能等待该线程释放锁,才能获取该对象的监视器锁并执行 synchronized 代码块或方法。
当一个线程进入 synchronized 代码块或方法时,它会尝试获取对象的监视器锁。如果该锁没有被其他线程占用,则该线程获取锁并继续执行 synchronized 代码块或方法;如果该锁已经被其他线程占用,则该线程会进入锁池(Lock Pool)等待,直到该锁被其他线程释放。当一个线程释放对象的监视器锁时,它会唤醒锁池中的一个等待线程,让其获取锁并继续执行 synchronized 代码块或方法。如果锁池中有多个等待线程,那么唤醒哪个线程是不确定的,取决于 JVM 的实现。
-
53. synchronized 和 volatile 的区别是什么?
作用的位置与方式
1、synchronized:
-
位置:可以修饰方法,代码块或者整个类。
-
作用方式:通过加锁机制来保证同步,确保同一个时刻只有一个线程可以被执行或被执行。
2、volatile:
-
位置:只能修饰变量。
-
作用方式:通过内存屏障来保证变量的可见性,并禁止指令重排序。
功能特性
1、synchronized
-
可见性:在锁释放时,会将数据写入主内存,从而确保其他线程可以看到最新的变量值。
-
原子性:可以确保被修饰的代码块或方法中的操作是原子的,即不可被中断或分割。
-
阻塞性:可能会导致线程的阻塞,当某个线程持有锁时,其他线程必须等待锁释放后才能继续执行。
2、volatile
-
可见性:每次读取变量时都会去主内存中获取最新的值,从而确保变量的可见性。
-
原子性:无法保证原子性,即对于复合操作(如自增、自减等),需要使用其他同步机制(如
AtomicInteger
)来保证原子性。 -
非阻塞性:不会导致线程的阻塞,因为它只是通过内存屏障来确保变量的可见性。
性能开销
1、synchronized
-
有一定的性能开销,因为需要进行锁的申请释放等待等操作。
-
在高并发场景下,可能会导致线程的频繁上下文切换和阻塞,从而影响性能。
2、volatile
-
没有锁的开销,通过 CPU 的缓存一致性协议来实现变量的可见性。
-
性能开销相对较小,但在高并发场景下仍需谨慎使用,以避免潜在的问题。
适用场景
1、synchronized
-
适用于需要在多个线程之间同步共享变量的情况,例如对共享资源的读写操作。
-
适用于需要保证代码块或方法的原子性和可见性的场景。
2、volatile
-
适用于只需要保证某个变量的可见性,而不需要同步控制的场景,如状态标志位等。
-
适用于需要避免指令重排序的场景,以确保程序的正确性.
-
-
54. synchronized 和 Lock 有什么区别?
使用方式
1、synchronized
-
是 Java 中的一个关键字,用于修饰方法或代码块。
-
使用时无需显式地创建锁对象,系统会自动为其分配和管理锁。
2、Lock
-
是Java的一个接口,提供了显示的锁机制。
-
使用时需要显示的创建锁对象,并通过该对象来管理锁的状态。
功能特性
1、synchronized
-
在jvm的优化下synchronized的性能已经显著提高了。
-
在竞争不激烈的情况下,
synchronized
的性能往往优于Lock
,因为Lock
需要显式地管理锁,增加了额外的开销。
2、Lock
-
在竞争激烈的情况下,
Lock
提供了更灵活的锁机制,可以通过尝试获取锁、设置超时等方式来避免线程长时间等待锁而导致的性能问题。 -
但由于需要显式地管理锁,因此在代码编写和维护上可能更复杂。
灵活性
1、synchronized
-
使用方式相对简单,适用于简单的同步场景。
-
由于是Java的关键字,因此其使用受到Java语言的规范限制。
2、Lock
-
提供了更灵活的锁机制,可以根据具体需求选择合适的锁获取方式。
-
可以通过实现
Lock
接口来自定义锁的行为,满足特定的同步需求。
-
-
55. synchronized 和 ReentrantLock 区别是什么?
(一)用法的区别
1、synchronized
- 可以修饰普通方法、静态方法、和代码块。
基础使用
- 使用synchronized修饰代码块。
public void method() {// 加锁代码synchronized (this) {// ...} }
2、ReentrantLock
- 只能作用于代码块上。
基础使用
- ReentrantLock在使用前需要先创建ReentrantLock对象,在使用lock方法进行加锁,使用完之后在调用unlock方法解锁。
public class LockExample {// 创建锁对象private final ReentrantLock lock = new ReentrantLock();public void method() {// 加锁操作lock.lock();try {// ...} finally {// 释放锁lock.unlock();}} }
(二)获取锁和释放锁的方式区别
1、synchronized
- synchronized会自动加锁和释放锁。当进入 synchronized 修饰的代码块之后会自动加锁,当离开 synchronized 的代码段之后会自动释放锁。
演示
public class App{public void method(){int count = 0;synchronized(this){for(int i = 0;i < 10;i++){count++;}}System.out.println(count);} }
2、ReentrantLock
- ReentrantLock需要手动加锁和释放锁。
演示
public class LockExample{//创建锁对象private final ReentrantLock lock = new ReentrantLock();public void method(){//加锁操作lock.lock();try{//内容}finally{//释放锁lock.unlock();}} }
(三)锁类型不同
1、synchronized
- synchronized属于非公平锁。
源码
2、Reentrant Lock
- ReentrantLock既可以属于公平锁也可以属于非公平锁。
源码
(四)响应中断不同
1、synchronized
- synchronized不能响应中断,如果发生了死锁使用synchronized会一直等待。
2、ReentrantLock
- ReentrantLock可以使用lockInterruptibly获取锁并响应中断指令。如果发生了死锁,ReentrantLock可以响应中断并释放锁,从而解决死锁问题。
(五)底层实现不同
1、synchronized
- synchronized是Java层面通过监视器(Moniter)实现。
2、ReentrantLock
- ReentrantLock是通过AQS程序级别的API实现的。
-
56. 说一下 atomic 的原理?
在Java高并发编程中,Atomic类提供了一组用于执行原子操作的工具类,它们是Java并发包 (java.util.concurrent.atomic) 的一部分。这些类通过底层的CAS(Compare-And-Swap, 比较并交换)机制实现了无锁的线程安全操作,适用于多线程环境下对单个变量的操作。Atomic类主要包括 AtomicInteger、AtomicLong、AtomicReference、AtomicBoolean 等。
1、Atomic类的概述
Atomic
类提供了一种无锁的方式来更新单个变量,这使得它们在高并发环境下能够比使用传统的锁(如synchronized
)更高效。Atomic
类通过底层的CAS操作来实现线程安全的更新。CAS操作确保了在多线程环境中,变量的更新是原子性的,并且不会出现竞态条件。-
1.1常见的Atomic类
-
AtomicInteger:用于对int类型的变量进行原子操作。
-
AtomicLong:用于对long类型的变量进行原子操作。
-
AtomicBoolean:用于对Boolean类型的变量进行原子操作。
-
AtomicReference:用于对引用类型的变量进行原子操作。
-
AtomicStampedReference:用于解决CAC操作中的ABA问题,带有一个时间戳(或者版本号)。
-
2、Atomic类的工作原理
2.1CAS的基本思想
CAS操作包含的三个操作数。
-
内存位置(V):要被操作的 变量的内存位置。
-
预期值(A):操作前期望变量当前持有的值。
-
新值(B):准备更新到变量的新值。
CAS的流程
-
检查变量当前的值是否与预期值(A)相等。
-
如果相等,则将变量的值更新为新值(B)。
-
如果不相等,则说明在此期间变量的值已经被其他线程修改过了,更新操作失败。
2.2AtomicInteger的实现
public class AtomicInteger extends Number implements java.io.Serializable {private static final long serialVersionUID = 6214790243416807050L;// 使用Unsafe类直接操作内存private static final Unsafe unsafe = Unsafe.getUnsafe();private static final long valueOffset;static {try {valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }}private volatile int value;public final int get() {return value;}public final void set(int newValue) {value = newValue;}public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;} }
-
value 变量
AtomicInteger
中包含一个用volatile
修饰的int
变量value
。volatile
保证了该变量的可见性,即当一个线程修改了value
,其他线程立即可以看到这个修改。 -
compareAndSet()
方法:通过调用Unsafe
类的compareAndSwapInt()
方法,实现了 CAS 操作。compareAndSwapInt()
是一个本地方法,底层直接调用处理器的 CAS 指令,保证了操作的原子性。 -
incrementAndGet()
方法:该方法通过getAndAddInt()
实现,该方法首先获取当前值并增加1,然后返回结果。
2.3Unsafe类
Unsafe是Java中的一个很重要的类,它提供了直接操作内存和线程同步的功能,但由于这些操作比较危险,所以
Unsafe
类在 Java 的标准 API 中并未对外公开,通常只有在 JVM 内部或通过反射等方式才能访问到它。Atomic
类通过Unsafe
类来直接进行 CAS 操作,从而绕过了传统的锁机制,提高了执行效率。3、Atomic类的应用场景
Atomic
类在并发编程中有广泛的应用场景,尤其是在高并发、低延迟的场景中表现尤为突出。3.1高效计数器
AtomicInteger
和AtomicLong
通常用于实现高效的计数器。例如,统计请求数量、任务处理数量等。在这些场景下,使用AtomicInteger
比使用synchronized
或ReentrantLock
实现的计数器要高效得多。AtomicInteger counter = new AtomicInteger();public void increment() {counter.incrementAndGet(); }
3.2非阻塞算法
在实现非阻塞算法时,
AtomicReference
类被广泛应用。比如,在实现并发队列、栈等数据结构时,AtomicReference
可以用于指针的原子更新,避免锁的使用。AtomicReference<Node> top = new AtomicReference<>();public void push(T item) {Node newNode = new Node(item);Node oldTop;do {oldTop = top.get();newNode.next = oldTop;} while (!top.compareAndSet(oldTop, newNode)); }
3.3解决ABA问题
在使用 CAS 操作时,可能会出现 ABA 问题:即一个变量在
A -> B -> A
的过程中,虽然值恢复为A
,但实际上发生了变化。为了解决这个问题,可以使用AtomicStampedReference
,它在每次更新时不仅比较值,还比较版本号。AtomicStampedReference<Integer> stampedRef = new AtomicStampedReference<>(100, 0);int[] stampHolder = new int[1]; Integer value = stampedRef.get(stampHolder); int stamp = stampHolder[0];boolean updated = stampedRef.compareAndSet(value, 101, stamp, stamp + 1);
4、Atomic类的优缺点
4.1优点
-
1、高效性:
Atomic
类通过无锁的方式实现线程安全操作,避免了上下文切换和线程阻塞带来的开销。 -
2、简介性:
Atomic
类封装了底层的 CAS 操作,使得开发者可以方便地进行原子操作,而不需要处理复杂的同步机制。 -
3、适用广泛:适用于高并发、低延迟的场景,特别是在需要频繁更新共享变量的情况下。
4.2缺点
-
1、ABA问题:CAS 操作可能会出现 ABA 问题,需要使用带版本号的
AtomicStampedReference
或AtomicMarkableReference
来解决。 -
2、只适用于单个变量:
Atomic
类只能用于单个变量的原子操作,无法同时操作多个变量。在这种情况下,需要使用更复杂的同步机制或锁。 -
忙等待问题:在高竞争的情况下,CAS 操作可能会导致自旋等待,浪费 CPU 资源。
-
-
57. 什么是反射?
主要是指程序可以访问检测和修改它本身状态或行为的一种能力。
java反射
JAVA反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;它允程序在运行时(注意不是编译的时候)来进行自我检查并且对内部的成员进行操作。这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象。
反射就是把java类中的各种成分映射成一个个的Java对象 。
反射就是将Java中的各种成分映射成相应的Java类。
Java反射机制提供的主要功能
-
1、在运行时判断一个对象所属的类。
-
2、在运行时构造任意一个类的对象。
-
3、在运行时判断任意一个类所具有的成员变量和方法。
-
4、在运行时调用任意一个对象的方法。
Reflection
Reflection是Java被视为动态(准动态)语言的一个关键性质。
这个机制允许程序在运行时透过Reflection APIs取得任何一个已知名称的class的内部信息。
动态语言
动态语言的定义“程序运行时,允许改变程序结构或者变量类型,这种语言称为动态语言”。
在此定义下java不是动态语言,但是Java有一个非常突出的动态相关机制:Reflection。
在Java中指的是我们可以于运行时加载、探知、使用编译期间完全未知的class。
java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体、或对其fields设置、或唤起其methods。
这种“看透”class的能力被视为introspection。Reflection和introspection是常被提起的两个术语。
Java Reflection API简介
实现Java反射机制的类(除第一个,其他都在java.lang.reflect包中)。
-
Class类:代表一个类(Java.lang包下)。
-
Field类:代表一个类的成员变量(成员变量也称为类的属性)。
-
Method类:代表类的方法。
-
Constructor类:代表类的构造方法。
-
Array类:提供了动态创建数组,以及访问数组的元素的静态方法。
Class对象
要想使用反射,首先需要获得待操作的类所对应的Class对象。
Java中,无论生成某个类的多少个对象,这些对象都会对应于同一个Class对象。这个Class对象是由JVM生成的,通过它能够获悉整个类的结构。
1、获取Class类的静态方法。如
Class.forName("java.lang.String");
2、使用类的.class语法。如
String.class;
3、使用对象的getClass()方法。如
String str = "a"; str.getClass();
java中的编译类型:
-
静态编译:在编译时就确定类型,绑定对象即通过。
-
动态编译:运行时确定类型,绑定对象。动态编译最大限度地发挥了Java的灵活性,体现了多态的应用,可以降低类之间的耦合性。
一句话概括就是使用反射可以赋予jvm动态编译的能力,否则类的元数据信息只能用静态编译的方式实现,例如热加载,Tomcat的classloader等等都没法支持。
-
-
58. 什么是 Java 序列化?什么情况下需要序列化?
Java序列化的定义
-
序列化:将对象状态转换为字节流,以便可以保存或传输到另一个运行环境中的过程。序列化后的对象状态(即其字段和变量的值)被写入一个字节序列中。
-
反序列化:根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象的过程。
Java序列化的应用场景
-
1、数据持久化:序列化允许将对象保存到文件或数据库中,以便在程序重新启动时恢复先前的状态。这对于保存应用程序的配置信息、用户数据等是很有用的。例如,可以将用户登录信息、游戏进度等数据序列化为字节流并保存到文件中,以便在需要时重新加载。
-
2、网络通信:序列化使得对象可以在网络上传输,例如在客户端和服务器之间。通过将对象转换为字节流,可以轻松地在不同的系统之间进行数据交换。在远程方法调用(RMI)中,序列化也用于在服务器和客户端之间传输数据。
-
3、分布式系统:在分布式系统中,对象的序列化和反序列化使得可以将对象在不同的节点之间传递,实现分布式计算和通信。通过序列化,可以将对象转换为字节流,并通过进程间通信(IPC)机制(如管道、套接字等)进行传递。
-
4、缓存:序列化还可用于缓存对象。将对象转换为字节流后存储在缓存中,可以提高系统性能。当需要访问对象时,可以从缓存中读取字节流并反序列化为对象。
-
5、跨平台兼容性:序列化的字节流是平台无关的,因此可以在不同操作系统和编程语言之间进行数据交换。这使得Java序列化成为跨语言数据交换的一种有效手段。
Java序列化的注意事项
-
1、安全性:由于序列化涉及将对象转换为字节流并在可能不受信任的环境中传输,因此必须谨慎处理。不应对不信任的数据进行反序列化,因为这可能导致安全问题,如代码注入。
-
2、性能问题:序列化过程涉及将对象的内部状态转换为字节流,并可能需要执行一些额外的操作(如写入类描述符和对象引用等)。这可能会增加处理时间和内存消耗。因此,在性能敏感的应用程序中,可能需要考虑使用更高效的序列化机制或数据格式。
-
3、版本兼容性:序列化机制不保证跨版本兼容性。如果类的定义在序列化后发生了更改(例如,添加或删除了字段),那么反序列化时可能会遇到问题。为了确保序列化的兼容性,Java提供了一个
serialVersionUID
字段来验证序列化的对象的发送者和接收者是否为该对象加载了与序列化兼容的类版本。
演示
import java.io.Serializable;public class Person implements Serializable {private static final long serialVersionUID = 1L; // 推荐定义这个UID,以确保序列化兼容性private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}// Getter和Setter方法public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "Person{name='" + name + "', age=" + age + "}";} }
import java.io.*;public class SerializationDemo {public static void serialize(Object obj, String filename) {try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename))) {oos.writeObject(obj);System.out.println("Serialized data is saved in " + filename);} catch (IOException e) {e.printStackTrace();}}public static <T> T deserialize(String filename, Class<T> clazz) {T object = null;try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {object = clazz.cast(ois.readObject());} catch (IOException | ClassNotFoundException e) {e.printStackTrace();}return object;}public static void main(String[] args) {Person person = new Person("John Doe", 30);// 序列化对象serialize(person, "person.ser");// 反序列化对象Person deserializedPerson = deserialize("person.ser", Person.class);// 输出反序列化后的对象System.out.println("Deserialized Person: " + deserializedPerson);} }
-
-
59. 动态代理是什么?有哪些应用?
在Java开发中,动态代理是一种强大且灵活的技术,广泛应用于AOP(面向切面编程)、拦截器、装饰器等场景。通过动态代理,开发者可以在运行时动态地为对象生成代理类,实现方法拦截、日志记录、安全检查等功能。本文将深入探讨Java中的动态代理,包括其原理、实现方式及实际应用案例。
代理模型
代理模型是一种设计模式,其中代理类负责控制对实际对象的访问。代理类可以在调用实际对象的方法之前或之后,增加额外的功能,例如日志记录、安全检查等。
静态代理于动态代理
-
静态代理:代理类在编译时已经确定,需要手动编写代理类代码。
-
动态代理:代理类在运行时动态生成,不需要手动编写代理类代码。
Java的动态代理实现
-
动态代理主要有两种方式实现:
-
基于接口的动态代理(JDK动态代理)。
-
基于类的动态代理(CGLIB动态代理)。
-
JDK动态代理
JDK动态代理通过
java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口实现,适用于代理实现了接口的类。import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;// 业务接口 interface UserService {void addUser(String name); }// 业务实现类 class UserServiceImpl implements UserService {public void addUser(String name) {System.out.println("Adding user: " + name);} }// 代理处理器 class UserServiceProxyHandler implements InvocationHandler {private final Object target;public UserServiceProxyHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("Before method: " + method.getName());Object result = method.invoke(target, args);System.out.println("After method: " + method.getName());return result;} }// 测试类 public class DynamicProxyDemo {public static void main(String[] args) {UserService userService = new UserServiceImpl();UserServiceProxyHandler handler = new UserServiceProxyHandler(userService);UserService proxyInstance = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(),userService.getClass().getInterfaces(),handler);proxyInstance.addUser("John");} }
-
ervice.getClass().getClassLoader(),
userService.getClass().getInterfaces(),
handler
);
proxyInstance.addUser("John");}
}
CGLIB动态代理CGLIB动态代理通过生成子类来代理目标对象,不需要目标对象实现接口。它基于ASM(一个Java字节码操控框架)生成代理类。```java
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;// 业务类
class UserService {public void addUser(String name) {System.out.println("Adding user: " + name);}
}// 代理拦截器
class UserServiceInterceptor implements MethodInterceptor {@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {System.out.println("Before method: " + method.getName());Object result = proxy.invokeSuper(obj, args);System.out.println("After method: " + method.getName());return result;}
}// 测试类
public class CglibProxyDemo {public static void main(String[] args) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(UserService.class);enhancer.setCallback(new UserServiceInterceptor());UserService proxy = (UserService) enhancer.create();proxy.addUser("John");}
}
动态代理的实际应用
-
AOP(面向切面编程)
- AOP是动态代理最常见的应用之一,通过在方法执行的前后加入横切关注点(如日志记录、安全检查等),实现解耦和代码重用。Spring AOP就是基于动态代理实现的。
-
拦截器
- 在Web应用中,拦截器用于在请求处理前后执行特定逻辑,例如权限验证、日志记录等。动态代理可以简化拦截器的实现。
-
装饰器模式
- 装饰器模式通过动态代理为对象添加新功能,而不影响其原有功能。动态代理可以在运行时灵活地为对象添加或移除装饰功能。
动态代理的优缺点
优点
-
灵活性高:无需预定义代理类,可以在运行时动态生成代理类。
-
代码复用:通过统一的代理逻辑,实现方法拦截、日志记录等功能。
-
解耦:通过代理模a式,将核心业务逻辑与横切关注点分离,提升代码的可维护性。
缺点
-
性能开销:动态代理涉及反射机制,可能带来一定的性能开销。
-
调试困难:动态生成的代理类在调试时不直观,需要额外的工具或日志辅助调试。
-
60. 怎么实现动态代理?
动态代理实现的两种方式
-
基于接口的动态代理(JDK动态代理)。
-
基于类的动态代理(Cglib动态代理)。
JDK动态代理
JDK动态代理通过
java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口实现,适用于代理实现了接口的类。import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;// 业务接口 interface UserService {void addUser(String name); }// 业务实现类 class UserServiceImpl implements UserService {public void addUser(String name) {System.out.println("Adding user: " + name);} }// 代理处理器 class UserServiceProxyHandler implements InvocationHandler {private final Object target;public UserServiceProxyHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("Before method: " + method.getName());Object result = method.invoke(target, args);System.out.println("After method: " + method.getName());return result;} }// 测试类 public class DynamicProxyDemo {public static void main(String[] args) {UserService userService = new UserServiceImpl();UserServiceProxyHandler handler = new UserServiceProxyHandler(userService);UserService proxyInstance = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(),userService.getClass().getInterfaces(),handler);proxyInstance.addUser("John");} }
Cglib动态代理
CGLIB动态代理通过生成子类来代理目标对象,不需要目标对象实现接口。它基于ASM(一个Java字节码操控框架)生成代理类。
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;// 业务接口 interface UserService {void addUser(String name); }// 业务实现类 class UserServiceImpl implements UserService {public void addUser(String name) {System.out.println("Adding user: " + name);} }// 代理处理器 class UserServiceProxyHandler implements InvocationHandler {private final Object target;public UserServiceProxyHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("Before method: " + method.getName());Object result = method.invoke(target, args);System.out.println("After method: " + method.getName());return result;} }// 测试类 public class DynamicProxyDemo {public static void main(String[] args) {UserService userService = new UserServiceImpl();UserServiceProxyHandler handler = new UserServiceProxyHandler(userService);UserService proxyInstance = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(),userService.getClass().getInterfaces(),handler);proxyInstance.addUser("John");} }
-