synchronized
一,介绍
Java中的synchronized关键字用于实现线程同步,可以修饰方法或代码块。
1. 修饰方法:当一个方法被synchronized修饰时,只有获得该方法的锁的线程才能执行该方法。其他线程需要等待锁的释放才能执行该方法。
2. 修饰代码块:当某个对象被synchronized修饰时,任何线程在执行该对象中被synchronized修饰的代码块时,必须先获得该对象的锁。其他线程需要等待锁的释放才能执行同步代码块。Java中的每个对象都有一个内置锁,当一个对象被synchronized修饰时,它的内置锁就起作用了。只有获得该锁的线程才能访问被synchronized修饰的代码段。使用synchronized可以保证多个线程对共享数据的访问顺序,避免了竞态条件和数据不一致的问题。注意:synchronized关键字只能保证同一对象内部的线程同步,不能保证多个对象的同步。如果需要多个对象之间的同步,可以考虑使用Lock接口的实现类。
二,锁升级
按照锁的升级顺序,可以将锁状态和过渡描述为以下几个步骤:
- 无锁状态(Unlocked):初始时,资源处于无锁状态,没有线程正在使用它。
- 偏向锁(Biased Locking):当一个线程第一次进入同步代码块时,会尝试获取偏向锁。如果成功,该线程会标记为拥有偏向锁,并记录下自己的线程 ID。这样,在未发生竞争的情况下,该线程后续访问同步代码块时可以快速获取锁,避免了重量级锁的开销。
- 轻量级锁(Lightweight Locking):当两个或多个线程开始竞争同一个锁时,偏向锁会升级为轻量级锁。此时,JVM 会使用 CAS(Compare and Swap)操作来尝试获取锁。如果成功,当前线程会获得轻量级锁,并继续执行临界区代码;如果失败,表示有其他线程持有锁,需要进行锁的膨胀。
- 自旋锁(Spin Locking):在轻量级锁的基础上,为了避免线程阻塞和降低线程切换的开销,如果获取轻量级锁失败,当前线程会选择进行自旋,即忙等待一段时间。它会尝试多次使用 CAS 操作来获取锁,期望其他线程能够快速释放锁。如果在自旋期间成功获取了锁,线程可以继续执行临界区代码;否则,会进一步升级为重量级锁。
- 重量级锁(Heavyweight Locking):当自旋锁无法获取到锁时或自旋次数达到一定阈值,轻量级锁会膨胀为重量级锁。此时,JVM 使用操作系统的互斥量(Mutex)来实现锁,并将当前线程阻塞,直到获取到锁为止。其他线程需要等待持有锁的线程释放锁后才能获取锁并执行临界区代码。
这是锁状态的一个常见升级顺序,它描述了锁的不同阶段和相应的过渡。但需要注意的是,在具体实现中,JVM 和不同的 Java 版本可能会有一些优化和变化,因此实际的锁升级过程可能略有不同。
锁会升级的主要原因是为了解决并发环境下的线程竞争和保证数据的一致性。当多个线程同时竞争同一个锁时,如果使用低级别的锁(如偏向锁或轻量级锁),可能会导致一些问题,例如:
- 竞争激烈:如果多个线程频繁地竞争同一个锁,轻量级锁的 CAS 操作可能会失败,导致自旋失败,增加了线程切换的开销。
- 锁撤销:偏向锁只能保证在没有竞争的情况下加锁的效率,一旦有其他线程竞争锁,就需要将偏向锁撤销,转而升级为更高级别的锁。
- 锁膨胀:对于长时间持有锁的线程,自旋可能会浪费大量的 CPU 时间,不仅影响性能,还会导致其他线程无法及时获取锁。此时,将轻量级锁膨胀为重量级锁,可以将持有锁的线程阻塞,避免自旋带来的性能损耗。
因此,为了提高并发的效率和线程安全性,锁会根据实际情况进行升级。锁的升级过程就是将低级别的锁转换为高级别的锁,以应对竞争激烈、长时间持有锁的情况,从而保证线程的顺序执行和数据的一致性。锁升级的原则是在减少线程切换开销、提高吞吐量和保证线程安全之间找到一个平衡点,以最优的方式处理并发情况。
三,什么是CAS
CAS(Compare and Swap)是一种并发算法,用于实现多线程环境下的原子操作。它是一种乐观锁策略,通过比较内存中的值与期望值是否相等来判断数据是否被修改,从而进行原子操作。
CAS 操作通常涉及三个参数:内存地址(或变量),期望值和新值。CAS 操作会先读取内存中的值与期望值进行比较,如果相等,则将新值写入内存中,并返回操作成功;如果不相等,则说明其他线程已经修改了内存的值,CAS 操作失败,需要重新尝试。
CAS 操作是原子性的,因为它在执行期间不会被中断或打断。它可以提供非阻塞的原子性操作,避免了使用锁造成的线程阻塞和上下文切换的开销。相比传统的加锁操作,CAS 操作通常具有更高的并发性能。
CAS 在并发编程中有广泛的应用,例如乐观锁、无锁数据结构和线程安全的计数器等。但需要注意的是,CAS 操作虽然能够解决一些并发问题,但在高并发环境下可能存在ABA问题(即在修改期间,数据经过了一系列变化又回到了原始状态),需要额外的手段来解决。
四,synchronized示例代码
以下是使用 Java 中的 `synchronized` 关键字实现同步的示例代码:
1. 同步方法示例:
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized void decrement() {
count--;
}
}
在上面的示例中,`increment()` 和 `decrement()` 方法都被声明为 `synchronized`,这意味着一次只能有一个线程访问这些方法。当一个线程正在执行其中一个方法时,其他线程必须等待。
2. 同步代码块示例:
public class SynchronizedExample {
private int count = 0;
private Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public void decrement() {
synchronized (lock) {
count--;
}
}
}
在上面的示例中,我们使用一个对象 `lock` 作为锁来创建同步代码块。在 `increment()` 和 `decrement()` 方法中,只有一个线程可以同时进入同步代码块,并且执行完同步代码块后会释放锁。
需要注意的是,在使用 `synchronized` 时,应该选择合适的对象作为锁,以避免不必要的竞争。通常,我们可以使用类的某个成员变量或者专门为同步目的创建一个 `Object` 对象作为锁。
以上示例代码演示了如何在 Java 中使用 `synchronized` 关键字进行同步。然而,Java 还提供了其他同步机制,如 `ReentrantLock`、`Semaphore` 等,可以根据具体的需求选择合适的同步方式。