大厂面试题详解:synchronized的偏向锁和自旋锁怎么实现的
理解 synchronized 关键字
在 Java 中,synchronized 关键字是实现并发控制的重要工具之一。它用于实现对共享资源的互斥访问,确保在同一时刻只有一个线程可以进入同步代码块或方法。
synchronized 的基本用法
public class SynchronizedExample { private int count = 0; public synchronized void increment() { count++; } }
在上述示例中,increment() 方法被 synchronized 修饰,保证了对 count 的操作是原子的,即线程安全的。
synchronized 的性能问题
传统的 synchronized 在竞争激烈的情况下可能会导致性能问题,因为它会涉及到线程的上下文切换和用户态与内核态的切换。
偏向锁的引入
为了解决 synchronized 在单线程情况下的性能问题,JDK 1.6 引入了偏向锁的概念。偏向锁是一种针对单线程场景进行优化的锁机制,它可以减少无竞争情况下的同步操作开销。
偏向锁的实现原理
public class SynchronizedExample { private int count = 0; public synchronized void increment() { count++; } }
在单线程访问的情况下,synchronized 会尝试偏向于这个线程。当有其他线程尝试访问这个锁时,偏向锁就会升级为常规锁,保证多线程下的正确性。
自旋锁的引入
自旋锁是一种基于循环等待的锁,线程在获取锁时不会被挂起,而是不断地尝试获取锁。
自旋锁的实现原理
import java.util.concurrent.atomic.AtomicBoolean; public class SpinLockExample { private AtomicBoolean locked = new AtomicBoolean(false); public void lock() { while (!locked.compareAndSet(false, true)) { // 自旋等待锁释放 } } public void unlock() { locked.set(false); } }
在自旋等待期间,线程会一直占用 CPU 资源,如果等待时间过长,会导致性能问题。因此,自旋锁适用于短时间内持有锁的情况。
应用场景和最佳实践
synchronized 的适用场景
- 单线程访问或轻量级的并发情况下,可以使用 synchronized,它简单易用,且性能较好。
- 对于竞争激烈的场景或高并发环境,考虑使用 ReentrantLock 或者 ConcurrentHashMap 等并发工具类。
偏向锁和自旋锁的应用场景
- 偏向锁适用于单线程访问的场景,可以提升同步操作的性能。
- 自旋锁适用于短时间内持有锁的情况,可以减少线程上下文切换的开销。
高级应用和性能优化
偏向锁的优化
偏向锁在单线程场景下提供了很好的性能优化,但在多线程竞争激烈的情况下,偏向锁的性能会下降。为了解决这个问题,JVM 会根据一定的规则自动取消偏向锁。
偏向锁的实现机制涉及到锁标识位的设置与撤销,其主要流程包括偏向锁的获取、撤销和升级。
示例代码:
public class BiasedLockExample { private int value = 0; public synchronized void increment() { value++; } }
上述代码中的 increment() 方法使用了 synchronized 关键字,这意味着该方法是一个同步方法,会使用偏向锁进行优化。
自旋锁的优化
自旋锁的性能受到 CPU 核心数和线程竞争情况的影响。当线程竞争不激烈或锁持有时间较短时,自旋锁可以提升性能。但如果自旋等待时间过长,会造成资源浪费。
自旋锁通过循环重试的方式尝试获取锁,避免了线程阻塞和切换的开销。
示例代码:
import java.util.concurrent.atomic.AtomicBoolean; public class SpinLock { private AtomicBoolean locked = new AtomicBoolean(false); public void lock() { while (!locked.compareAndSet(false, true)) { // 自旋等待锁释放 } } public void unlock() { locked.set(false); } }
上述代码中,lock() 方法通过 AtomicBoolean 类型的状态标识来实现自旋锁的获取,unlock() 方法用于释放锁。
使用 synchronized 关键字
尽管 ReentrantLock 提供了更多的灵活性和功能,但在绝大多数情况下,synchronized 关键字已经能够满足需求,并且使用更为简单和方便。
synchronized 关键字的使用简单直接,JVM 对其进行了高度优化,性能较为优秀。在绝大多数情况下,synchronized 能够满足锁的需求,并且使用更为简单和方便。
示例代码:
public class SynchronizedExample { private int count = 0; public synchronized void increment() { count++; } }
上述代码中的 increment() 方法使用了 synchronized 关键字,保证了方法的原子性,避免了多线程并发访问导致的数据不一致问题。
应用场景
偏向锁的应用场景: 偏向锁适用于存在大量读线程的场景,通过偏向锁可以减少无谓的竞争,提高性能。
假设有一个数据结构,被多个线程读取,但只有少数线程会修改数据。在这种场景下,偏向锁可以提供显著的性能优势,因为大多数情况下读线程无需争夺锁,可以直接访问数据,而修改数据的线程会在获得锁后进行修改。
/** * 偏向锁的应用场景: * 偏向锁适用于存在大量读线程的场景,通过偏向锁可以减少无谓的竞争,提高性能。 * * 假设有一个数据结构,被多个线程读取,但只有少数线程会修改数据。在这种场景下,偏向锁可以提供显著的性能优势, * 因为大多数情况下读线程无需争夺锁,可以直接访问数据,而修改数据的线程会在获得锁后进行修改。 */ public class BiasedLockExample { // 定义共享的数据 private int data = 0; /** * 读取数据的方法 */ public synchronized void readData() { System.out.println("Reading data: " + data); } /** * 修改数据的方法 * @param newData 新数据 */ public synchronized void modifyData(int newData) { // 修改数据 data = newData; System.out.println("Data modified to: " + data); } }
在上述代码中,我展示了一个简单的应用场景,适合使用偏向锁进行性能优化。在这个示例中,多个线程可以同时调用 readData() 方法读取数据,而只有在调用 modifyData(int newData) 方法时才需要竞争偏向锁进行数据修改。
通过使用偏向锁,大多数情况下读取数据的线程无需竞争锁资源,可以直接访问数据,而修改数据的线程则会在获得锁之后进行数据修改操作,有效减少了竞争和锁的开销,提高了系统的性能和吞吐量。
自旋锁的应用场景: 自旋锁适用于锁的持有时间短、线程并发数不高的场景,可以有效减少线程阻塞和切换带来的开销。
自旋锁适用于并发竞争不激烈、锁竞争时间较短的场景,例如某个资源被多个线程轮流访问的情况,可以有效减少线程切换的开销。
/** * 自旋锁的实现: * 自旋锁是一种基于循环等待的锁,线程在获取锁时不会被挂起,而是不断地尝试获取锁。 * * 自旋锁适用于锁的持有时间较短、线程竞争不激烈的情况下,可以有效减少线程阻塞和切换带来的开销。 */ import java.util.concurrent.atomic.AtomicBoolean; public class SpinLock { // 使用AtomicBoolean作为状态标志,初始化为false表示未锁定状态 private AtomicBoolean locked = new AtomicBoolean(false); /** * 加锁方法,使用自旋等待获取锁 */ public void lock() { // 使用CAS操作尝试获取锁,直到成功为止 while (!locked.compareAndSet(false, true)) { // 自旋等待锁释放 // 在自旋过程中,当前线程不会被挂起,而是一直循环尝试获取锁 } } /** * 解锁方法,将锁标志位设置为false */ public void unlock() { // 将锁标志位设置为false,表示释放锁 locked.set(false); } }
在上述代码中,我实现了一个简单的自旋锁,采用了基于CAS(Compare And Set)操作的方式来实现加锁和解锁的过程。在 lock() 方法中,线程会不断地循环尝试获取锁,直到成功获取为止。而在 unlock() 方法中,线程会将锁的状态标志位设置为false,表示释放锁。
自旋锁适用于锁的持有时间较短、线程竞争不激烈的情况下,可以有效减少线程阻塞和切换带来的开销,提高系统的性能和吞吐量。
synchronized 关键字的应用场景: synchronized 关键字适用于绝大多数的同步场景,尤其是对于简单的同步操作,synchronized 提供了更为简单和便捷的使用方式。
synchronized 关键字是 Java 中最常用的同步机制,它可以用于任何对象和方法,提供了一种简单而强大的同步方式。
示例代码:
/** * synchronized 关键字的应用场景: * synchronized 关键字适用于绝大多数的同步场景,尤其是对于简单的同步操作,synchronized 提供了更为简单和便捷的使用方式。 * synchronized 关键字是 Java 中最常用的同步机制,它可以用于任何对象和方法,提供了一种简单而强大的同步方式。 */ public class SynchronizedCounter { // 定义一个计数器变量 private int count = 0; /** * synchronized 关键字修饰的方法,用于增加计数器的值 */ public synchronized void increment() { // 在方法内部使用 synchronized 关键字,确保了对 count 变量的操作是原子的,即线程安全的 count++; } /** * synchronized 关键字修饰的方法,用于获取计数器的值 */ public synchronized int getCount() { // 在方法内部使用 synchronized 关键字,确保了对 count 变量的读取操作是线程安全的 return count; } }
在上述代码中,我定义了一个简单的计数器类 SynchronizedCounter,其中的 increment() 和 getCount() 方法都使用了 synchronized 关键字修饰,这样可以确保对计数器变量 count 的操作是线程安全的。无论是增加计数器的值还是获取计数器的值,都能保证在多线程环境下的正确性和一致性。
synchronized 关键字适用于大多数的同步场景,特别是对于简单的同步操作,它提供了一种简单而强大的同步方式。因此,在绝大多数情况下,我可以使用 synchronized 关键字来确保线程安全,而无需引入更复杂的同步机制。