1. 为什么非公平锁的吞吐量大于公平锁?
公平锁:公平锁的获取遵循先来先服务的原则。线程在获取锁时,如果锁被其他线程占用,它会进入队列等待,当锁可用时,队列中的第一个线程会获取到锁。这种机制保证了每个线程按照请求锁的顺序依次获得锁,但是频繁的队列操作(如入队、出队)会带来一定的性能开销。
非公平锁:非公平锁在获取锁时,不会考虑线程的等待顺序。当锁释放时,新请求锁的线程有机会直接获取锁,而不是先检查等待队列。这样减少了线程切换和队列操作的开销。因为它允许插队行为,在高并发场景下,线程有更多机会直接获取锁并执行任务,从而减少了等待时间,提高了整体的吞吐量。
2. synchronized的原理是什么?
在Java中,synchronized是一种内置的锁机制。它可以修饰方法或者代码块。
当修饰方法时,字节码层面会有一个ACC_SYNCHRONIZED标志位。当方法被调用时,执行线程会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会先获取锁,然后执行方法体,方法执行完后再释放锁。
当修饰代码块时,通过monitorenter和monitorexit指令实现。monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置和异常处理的位置。线程执行到monitorenter指令时,会尝试获取对象的监视器(monitor)锁。如果获取成功,就可以执行同步代码块,执行完后通过monitorexit指令释放锁。每个对象都有一个与之关联的监视器,它就像一个特殊的区域,同一时刻只能有一个线程持有该对象的监视器锁。
3. ReentrantLock的原理是什么?
ReentrantLock是一个可重入的互斥锁。它实现了Lock接口。
内部通过一个抽象的同步器(AbstractQueuedSynchronizer,AQS)来实现。AQS维护了一个等待队列,当线程尝试获取锁时,如果锁已经被其他线程持有,该线程会被包装成一个节点加入到等待队列中。
它使用state变量来表示锁的状态,state为0表示锁未被占用,大于0表示锁被占用,并且记录了重入的次数。当一个线程获取锁时,通过CAS(Compare - And - Swap)操作尝试将state从0变为1,如果成功,就获取到了锁。如果是已经获取到锁的线程再次获取锁,state会递增,表示重入。
当线程释放锁时,会通过CAS操作将state递减,当state变为0时,锁才真正被释放,并且会唤醒等待队列中的一个线程。
4. 什么是分段锁?
分段锁是一种用于提高并发性能的锁机制。主要应用在如ConcurrentHashMap等数据结构中。
它将数据结构分成多个段(segment),每个段都有自己独立的锁。例如,在ConcurrentHashMap中,它内部默认将数据分成16个段。
当多个线程对不同段的数据进行操作时,它们可以并发地进行,因为不同段的锁是相互独立的。这样可以大大提高在高并发场景下对数据结构的操作效率,相比于对整个数据结构使用一个锁,分段锁能够在保证线程安全的同时,允许更高程度的并发访问。
5. 在什么时候应该使用可重入锁?
当需要手动控制锁的获取和释放时,ReentrantLock(可重入锁)是一个很好的选择。例如,在复杂的业务逻辑中,需要在方法的不同部分灵活地获取和释放锁。
当需要实现公平锁或非公平锁的特性时,ReentrantLock可以通过构造函数来指定是公平锁还是非公平锁。如果业务场景对锁的获取顺序有要求,比如需要按照请求的先后顺序来获取锁,就可以使用公平锁模式的ReentrantLock。
在需要进行一些高级功能如尝试获取锁(tryLock)、可中断地获取锁(lockInterruptibly)的场景下。例如,在一个线程可能需要等待一定时间获取锁,如果超时则放弃等待,或者线程在等待锁的过程中可以被中断的情况下,ReentrantLock的这些功能就很有用。
6. synchronized和volatile的区别是什么?
语义方面:
synchronized用于保证在同一时刻只有一个线程可以访问被它修饰的方法或者代码块,实现了互斥访问,从而保证了原子性、可见性和有序性。
volatile主要用于保证变量的可见性和禁止指令重排序。它确保一个线程对共享变量的修改能及时被其他线程看到,但不能保证原子性。
应用场景方面:
synchronized适用于多个线程访问共享资源,并且需要对共享资源进行复杂的操作,如读写操作都包含的情况。
volatile适用于多个线程共享一个变量,并且这个变量的修改操作比较简单,如只有一个线程进行写操作,其他线程进行读操作的场景。
性能方面:
在简单的变量共享场景下,volatile的性能通常比synchronized要好,因为它没有像synchronized那样涉及到锁的获取和释放等复杂操作。但是在复杂的并发场景下,synchronized提供的完整的同步机制可能是更合适的选择。
7. 多线程synchronized锁升级的原理是什么?
在Java中,synchronized锁有偏向锁、轻量级锁和重量级锁三种状态,会根据不同的竞争情况进行升级。
偏向锁:偏向锁的目的是在大多数情况下,锁总是由同一线程多次获取。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID。以后该线程进入和退出同步块时,不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。偏向锁的获取和释放几乎没有性能开销。
轻量级锁:当有另外一个线程尝试竞争偏向锁时,偏向锁会升级为轻量级锁。轻量级锁通过CAS操作在对象头和栈帧中的锁记录之间交换数据来实现加锁。如果CAS操作成功,线程就获取到了轻量级锁。轻量级锁适用于线程交替执行同步块的情况,它的开销比重量级锁小,因为它避免了进入操作系统的内核态来实现互斥。
重量级锁:当多个线程竞争轻量级锁,且自旋一定次数后仍然无法获取锁时,轻量级锁会升级为重量级锁。此时,线程会通过操作系统的互斥量(mutex)来实现互斥,这涉及到线程阻塞和唤醒等操作,会有比较大的性能开销。重量级锁主要用于高竞争的场景,保证线程安全。