文章目录
- 1. synchronized 是什么
- 2. synchronized 可以实现的锁
- 3. synchronized 使用
- 4. synchronized 底层原理
- 4.1 作用于同步代码块
- 4.2 作用于方法
1. synchronized 是什么
synchronized 是 Java 中实现线程同步的关键字,用于保护共享资源的访问,确保在多线程环境中同一时间只有一个线程能够访问特定的代码段或方法。它提供了互斥性、可见性和有序性三个基本特性,确保了线程间操作的原子性和数据的一致性。
-
互斥性: 当一个线程进入了 synchronized 代码块或方法时,其他试图进入该同步区域的线程必须等待,直至拥有锁的线程执行完毕并释放锁。这确保了在同一时间只能有一个线程访问共享资源,避免了竞态条件和数据不一致的问题。
-
可见性: synchronized 关键字还确保了线程间的数据可见性。一旦一个线程在 synchronized 块中修改了共享变量的值,其他随后进入同步区域的线程可以看到这个更改。这是因为解锁过程会将工作内存中的最新值刷新回主内存,而加锁过程则会强制从主内存中重新加载变量的值。
-
有序性: synchronized 提供的第三个特性是有序性,它可以确保在多线程环境下,对于同一个锁的解锁操作总是先行于随后对同一个锁的加锁操作。这就建立了线程之间的内存操作顺序关系,有效地解决了由于编译器和处理器优化可能带来的指令重排序问题。
2. synchronized 可以实现的锁
-
可重入锁(Reentrant Lock): synchronized 实现的锁是可重入的,允许同一个线程多次获取同一个锁,而不会造成死锁。这意味着线程在持有锁的情况下可以再次获取相同的锁,而不会被阻塞。
-
排它锁/互斥锁/独占锁: synchronized 实现的锁是排它的,也就是说,在任意时刻只有一个线程能够获取到锁,其他线程必须等待该线程释放锁才能继续执行。这确保了同一时刻只有一个线程可以访问被锁定的代码块或方法,从而保证了数据的一致性和完整性。
-
悲观锁: synchronized 实现的锁属于悲观锁,因为它默认情况下假设会发生竞争,并且会导致其他线程阻塞,直到持有锁的线程释放锁。悲观锁的特点是对并发访问持保守态度,认为会有其他线程来竞争共享资源,因此在访问共享资源之前会先获取锁。
-
非公平锁: 在早期的 Java 版本中,默认实现的 synchronized 是非公平锁,也就是说,线程获取锁的顺序并不一定按照它们请求锁的顺序来进行,而是允许“插队”,即已经在等待队列中的线程可能被后来请求锁的线程抢占。
3. synchronized 使用
synchronized关键字可以修饰方法、代码块或静态方法,用于确保同一时间只有一个线程可以访问被synchronized修饰的代码片段。
- 修饰实例方法: 当synchronized修饰实例方法时,锁住的是当前实例对象(this)。这意味着在同一时刻,只能有一个线程访问此方法,其他线程需要等待当前线程执行完毕才能执行该方法。修饰实例方法的方式可以确保对实例变量的访问是线程安全的。
public synchronized void methodName() {
// synchronized 代码块
}
- 修饰静态方法: 当synchronized修饰静态方法时,锁住的是类的Class对象。因此,无论多少个该类的实例存在,同一时刻也只有一个线程能够访问此静态同步方法。这种方式可以确保对静态变量的访问是线程安全的。
public static synchronized void staticMethodName() {
// synchronized 代码块
}
- 修饰代码块: 使用 synchronized 关键字修饰一个代码块,将需要同步的代码包裹在 synchronized 关键字所修饰的代码块中。通过指定对象作为锁,可以更精确地控制同步范围,只有持有该对象锁的线程才能执行被synchronized修饰的代码块。
synchronized (obj) {
// 需要同步的代码块
}
synchronized关键字确保了对共享资源的访问是线程安全的。但过多地使用synchronized可能会导致性能问题,因此在设计并发程序时需要权衡考虑。
4. synchronized 底层原理
synchronized 的底层原理涉及到 Java 对象头(Object Header)和 Monitor(监视器)两个关键概念。
-
Java 对象头:
在 Java 虚拟机中,每个对象都有一个对象头,用于存储对象的元数据信息,包括对象的哈希码、GC 相关信息、锁状态等。对象头通常包含一个标记字段(Mark Word),用于标识对象的锁状态。 -
Monitor(监视器):
Monitor 是一种同步机制,负责管理对象的锁。每个对象都与一个 Monitor 相关联。当一个线程尝试进入一个被synchronized修饰的代码块或方法时,它会尝试获取对象的 Monitor。如果 Monitor 处于无锁状态,则当前线程会尝试将其锁定;如果 Monitor 已经被其他线程锁定,则当前线程会进入阻塞状态,直到持有锁的线程释放锁。
在 JDK 1.6 之前,synchronized 关键字的实现确实被认为是重量级锁。其原理基于操作系统提供的互斥量(Mutexes)来实现线程间的同步,这涉及了从用户态到内核态的切换以及线程上下文切换等相对昂贵的操作。一旦一个线程获得了锁,其他试图获取相同锁的线程将会被阻塞,这种阻塞操作会导致线程状态的改变和 CPU 资源的消耗。
具体而言,在 JDK 1.6 之前,synchronized 的实现过程大致如下:
- 当线程尝试进入 synchronized 代码块时,它会尝试获取对象的 Monitor(监视器)。
- 如果 Monitor 的锁状态是无锁状态(Unlocked),则当前线程将尝试获取锁。如果获取成功,则进入临界区执行代码。
- 如果锁已经被其他线程持有,则当前线程将进入阻塞状态,等待锁被释放。
- 当持有锁的线程退出临界区并释放锁时,等待的线程将被唤醒,重新尝试获取锁。
这种实现方式存在以下问题:
- 需要从用户态到内核态的切换,以及线程上下文的切换,导致性能开销较大。
- 对于竞争不激烈的情况,阻塞等待锁的线程可能会浪费大量的 CPU 时间。
- 线程在竞争锁的过程中可能会发生多次上下文切换,影响性能。
- 因此,在高并发、低锁竞争的情况下,早期版本的 synchronized 实现可能成为性能瓶颈。
为了解决这些问题,在 JDK 1.6 中引入了偏向锁(Biased Locking)和轻量级锁(Lightweight Locking)等优化机制,提高了 synchronized 的性能和并发能力。
底层实现机制如下:
-
偏向锁(Bias Locking):当一个线程第一次访问一个同步块时,它会尝试获取对象的偏向锁。如果对象没有被其他线程锁定,那么当前线程会尝试将对象的偏向锁设置为自己。之后,当该线程再次访问同步块时,不需要再次获取锁,直接进入同步块执行。这样可以提高同步操作的性能,减少不必要的竞争。
-
轻量级锁(Lightweight Locking):如果对象已经被其他线程获取了偏向锁,但当前线程又想要获取锁,就会升级为轻量级锁。轻量级锁的获取过程包括尝试将对象头的 Mark Word 设置为指向当前线程的锁记录(Lock Record)和CAS(Compare and Swap)操作。如果锁竞争激烈,会升级为重量级锁。
-
重量级锁(Heavyweight Locking):如果对象的锁被多个线程竞争,那么会升级为重量级锁。重量级锁使用操作系统的互斥量(Mutex)来实现同步,涉及到用户态和内核态的切换,性能相对较低,但能够确保线程的互斥访问。
4.1 作用于同步代码块
当synchronized关键字作用于同步代码块时,Java 虚拟机使用 monitorenter 和 monitorexit 指令来实现同步。具体的工作流程如下:
- 当线程进入同步代码块时,首先执行 monitorenter 指令,该指令尝试获取对象的 Monitor(即锁)。
- 如果对象的 Monitor 处于无锁状态,则当前线程将成功获取锁,并继续执行同步代码块中的逻辑;同时,Monitor 的拥有者标记为当前线程。
- 如果对象的 Monitor 已经被其他线程锁定,则当前线程将进入阻塞状态,直到获取到锁为止。
- 当线程退出同步代码块时,执行 monitorexit 指令,释放对象的 Monitor,并清空拥有者标记。
4.2 作用于方法
而当synchronized关键字作用于方法时,Java 虚拟机会通过在方法的访问标志中设置 ACC_SYNCHRONIZED 标志来实现方法级别的同步。具体的工作流程如下:
- 当线程调用同步方法时,首先尝试获取方法所属对象的 Monitor。
- 如果方法所属对象的 Monitor 处于无锁状态,则当前线程将成功获取锁,并执行方法中的逻辑;同时,Monitor 的拥有者标记为当前线程。
- 如果方法所属对象的 Monitor 已经被其他线程锁定,则当前线程将进入阻塞状态,直到获取到锁为止。
- 当方法执行结束时,释放方法所属对象的 Monitor,并清空拥有者标记。
- 需要注意的是,对于静态同步方法,Java 虚拟机会使用类的 Class 对象作为 Monitor,而对于实例同步方法,则会使用实例对象的 Monitor。这样可以确保静态方法和实例方法之间的互斥性。