1. 什么是Java中的锁?请解释Java中的两种常用锁类型。
Java中的锁是一种用于控制并发访问资源的机制。它可确保在同一时间只有一个线程可以访问被锁定的资源。
常用的两种锁类型是:
- synchronized关键字:在方法或代码块上使用synchronized关键字,将其标记为同步的,一次只允许一个线程执行代码块或方法。synchronized关键字可用于实现互斥和同步。
- ReentrantLock类:Java的Lock接口的实现类,提供了更多灵活的锁定机制。ReentrantLock类具有更丰富的功能,例如锁定超时、可中断锁和公平性。
2. 什么是锁的可见性问题?如何解决锁的可见性问题?
锁的可见性问题是指当多个线程共享一个可写变量时,一个线程对变量进行修改后,其他线程无法立即感知到该变化。
解决锁的可见性问题的一种常用方法是使用volatile关键字。将变量声明为volatile可以确保当一个线程修改变量后,其他线程能够立即看到最新的值。volatile关键字通过禁止编译器和CPU对变量进行优化,直接从主内存中读取和写入变量的值,从而保证了可见性。
3. synchronized关键字和ReentrantLock类之间有何区别?
- synchronized关键字是Java内置的关键字,而ReentrantLock类是Java的Lock接口的具体实现类。
- synchronized关键字不需要手动解锁,当同步块或同步方法执行结束后,锁会自动释放。而ReentrantLock类需要手动调用lock()方法获得锁,并在使用完后调用unlock()方法释放锁。
- ReentrantLock类提供了额外的功能,例如锁定超时、可中断锁和公平性控制。
- synchronized关键字隐式地使用了内置锁(或监视器锁),而ReentrantLock类使用了显式锁。
4. synchronized关键字和Lock接口哪种更好?
这个问题没有绝对的答案,取决于具体的使用场景。
synchronized关键字是简单易用的,并且在性能方面优化得较好,适用于大多数情况。它可以隐式地获得和释放锁,不需要手动编写额外的代码。此外,synchronized关键字在JVM层面上做了一些优化,例如自适应自旋锁和锁升级等,使得其性能比Lock接口稍好。
然而,在某些特定的场景中,ReentrantLock类提供了更多的灵活性。例如,在需要实现可中断锁、锁定超时或公平性控制的情况下,ReentrantLock类是更好的选择。
5. 什么是可重入锁(ReentrantLock)?它为什么被称为可重入锁?
可重入锁是指同一个线程可以多次获得同一个锁,而不会造成死锁的情况。当线程已经持有锁时,它可以继续请求并获得相同的锁,而不会被自己所持有的锁所阻塞。
ReentrantLock之所以被称为可重入锁,是因为它允许一个线程在持有锁的情况下多次进入被锁定的代码块或方法。当线程再次进入被锁定的代码时,锁的计数器会增加,而不会进入阻塞状态。只有当线程退出所有相同的锁定代码块或方法后,锁的计数器会递减,直到计数器为0,锁才会真正被释放。
6. 什么是悲观锁和乐观锁?
- 悲观锁:悲观锁的基本思想是假设在并发情况下,多个线程会同时修改共享数据,因此每个线程执行操作之前都会先获取锁。悲观锁适合于并发写操作频繁的情况,可以避免数据的竞争和冲突。
- 乐观锁:乐观锁的基本思想是假设在并发情况下,多个线程修改共享数据的可能性较低,因此每个线程执行操作之前不会获取锁。乐观锁通常采用无锁技术(如CAS算法)来实现,并且依赖于版本号或时间戳来判断数据是否被修改。乐观锁适合于并发读操作频繁的情况,可以提高并发性能。
7. 什么是死锁?如何避免死锁?
死锁是指两个或多个线程无限地等待彼此持有的资源释放,导致程序无法继续执行的状态。死锁通常发生在多线程同时请求多个资源,且资源之间存在依赖关系时。
为了避免死锁,可以采取以下策略:
- 避免使用多个锁:当使用多个锁时,会增加发生死锁的可能性。可以尝试减少锁的使用,或者使用更细粒度的锁。
- 使用按顺序获取锁的策略:当多个线程需要获取多个锁时,按照统一的顺序获取锁,可以避免死锁的发生。
- 使用超时机制:当无法获取锁时,可以引入超时机制,防止线程一直等待锁的释放而无法继续执行。
- 监控和检测死锁:可以通过一些工具或技术来监控和检测死锁的发生,及时解决死锁问题。
8. 什么是公平锁和非公平锁?
- 公平锁:公平锁是指多个线程按照申请锁的顺序获得锁。当锁处于可用状态时,线程按照先来后到的顺序获得锁。公平锁保证了资源分配的公平性。
- 非公平锁:非公平锁是指多个线程争夺锁时,不按照申请锁的顺序获得锁。在锁释放之后,下一个等待的线程不一定是先到的线程。非公平锁允许后来的线程插队,可以在一定程度上提高并发性能。
公平锁和非公平锁的选择取决于对性能和公平性的权衡。公平锁可能会导致线程切换的开销,而非公平锁可能会导致某些线程长时间等待。
9. 什么是自旋锁和阻塞锁?
- 自旋锁:自旋锁是一种基于循环等待的锁机制。当一个线程请求自旋锁时,如果锁已经被其他线程持有,则该线程会自旋(不会进入阻塞等待状态)并不断地尝试获取锁,直到获取到锁或达到一定的尝试次数。自旋锁适用于锁持有时间较短,且线程竞争不激烈的情况。
- 阻塞锁:阻塞锁是一种基于线程阻塞和唤醒的锁机制。当一个线程请求阻塞锁时,如果锁已经被其他线程持有,则该线程会进入阻塞等待状态,直到持有锁的线程释放锁并通知等待的线程重新竞争锁。阻塞锁适用于锁持有时间较长,且线程竞争激烈的情况。
10. 什么是读写锁(ReadWriteLock)?它和互斥锁的区别是什么?
读写锁是一种特殊的锁,可以同时支持多个线程读取共享数据,但只能允许一个线程写入共享数据。读写锁在读操作频繁、写操作较少的场景中可以提高并发性能。
与互斥锁相比,读写锁的区别在于:
- 互斥锁在任意时间点只允许一个线程访问共享资源,无论是读还是写。而读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。
- 多个线程可以同时获取读锁,但只有持有写锁的线程才能写入共享资源。这样可以提高并发读的性能。
- 当一个线程持有写锁时,其他线程无法获取读锁或写锁,直到写锁被释放。
11. 什么是条件变量(Condition)?它与锁的关系是什么?
条件变量是一种线程间通信的机制,用于在特定条件下等待和通知线程。它通常与锁结合使用,用于在线程等待某个条件满足时暂时释放锁,并在条件满足时重新获取锁并恢复执行。
条件变量通过`await()`方法等待条件满足,当其他线程调用`signal()`或`signalAll()`方法时,等待的线程被唤醒,并重新争夺锁。只有获得锁的线程才能对条件变量进行操作。
条件变量可以用于解决生产者-消费者问题、线程间协作等场景,通过精确控制线程的等待和唤醒,实现线程的顺序执行和同步。
12. 什么是死锁?如何避免死锁?
- 死锁是指两个或多个线程互相持有对方需要的资源,并且由于无法获取对方持有的资源而无法继续执行的情况。为避免死锁,可以使用以下方法:
- 避免策略:确保线程按照相同的顺序获取锁资源。
- 超时策略:通过设置超时时间,在一段时间内等待获取锁,如果超过指定时间仍未获取到,释放已持有的锁并重新尝试获取。
- 死锁检测策略:通过监控线程的状态来检测死锁,并采取合适的措施解决。
13. 什么是乐观锁和悲观锁的优缺点?
- 乐观锁的优点是在读多写少的情况下,可以提高并发性能,减少线程的阻塞等待时间。缺点是在数据冲突频繁的情况下,可能会带来一定的开销。
- 悲观锁的优点是可以确保数据的一致性和安全性,适用于写多的场景。缺点是会带来较高的竞争和开销,容易导致线程的阻塞等待。
14. 什么是乐观锁的CAS操作?如何实现乐观锁?
- CAS(Compare and Swap)是一种无锁算法,是乐观锁的实现方式之一。它通过比较共享变量的值与预期值是否相等,如果相等则进行更新操作,否则重新尝试。
- 在Java中,`Atomic`类(如`AtomicInteger`、`AtomicLong`)通过底层的CAS操作实现了乐观锁。
15. 什么是可见性问题?如何解决可见性问题?
- 可见性问题是指当多个线程访问共享变量时,一个线程对共享变量的修改可能对其他线程不可见。解决可见性问题的方法包括:
- 使用`volatile`关键字:确保共享变量的修改对所有线程都是可见的。
- 使用锁:通过加锁和解锁操作来保证对共享变量的访问互斥性和可见性。
- 使用原子类:使用原子类(如`AtomicBoolean`、`AtomicInteger`)对共享变量的修改实现线程安全和可见性。
16. 谈谈你对线程安全和同步的理解。
- 线程安全是指多个线程同时访问共享资源时,不会出现竞态条件(Race Condition)和数据不一致的情况。同步是指为了保证线程安全,线程之间需要进行协作和互斥操作的机制。
- 实现线程安全可以通过以下方式:
- 互斥同步:使用锁机制(如`synchronized`、`ReentrantLock`)保证只有一个线程能够访问临界资源。
- 非互斥同步:使用原子类(如`AtomicBoolean`、`AtomicInteger`)和无锁算法(如CAS)保证对共享变量的访问是线程安全的。
- 不可变性:通过不可变对象来避免竞态条件和数据不一致的问题。
以上是一些更具挑战性的Java锁的面试问题及其答案。通过回答这些问题,您可以展示您对Java锁机制及相关概念的深入理解,并能够解释如何解决常见的并发问题。在回答问题时,尽量提供现实场景下的案例和示例来支持您的回答。