synchronized
一,介绍
在Java中,synchronized
关键字用于解决多线程并发访问共享资源时可能出现的线程安全问题。当多个线程同时访问共享资源时,如果没有合适的同步机制,可能会导致以下问题:
-
竞态条件(Race Condition):多个线程在没有适当同步的情况下同时访问共享资源,由于执行顺序不确定,可能导致结果的不确定性或错误。
-
数据不一致性:由于并发访问导致数据的部分修改或读取,可能导致数据的不一致性。
-
内存可见性:由于线程之间的工作内存与主内存的不同步,一个线程对共享变量的修改可能对其他线程不可见,导致读取到旧的值。
synchronized
关键字可以解决上述问题,它提供了两种主要的同步机制:
-
互斥访问:
synchronized
确保在同一时刻只有一个线程可以进入被synchronized
修饰的代码块或方法,从而避免了多个线程同时修改共享资源的情况,解决了竞态条件问题。 -
内存可见性:
synchronized
不仅保证了互斥访问,还保证了同步代码块中共享变量的修改对其他线程可见,即确保了线程之间的内存可见性。
通过使用synchronized
,开发人员可以有效地确保多线程环境下共享资源的安全访问,避免了潜在的线程安全问题。然而,需要注意的是,过度使用synchronized
可能会导致性能下降,因此在使用时应谨慎权衡。
Java中的synchronized关键字用于实现线程同步,可以修饰方法或代码块。
1. 修饰方法:
当一个方法被synchronized修饰时,只有获得该方法的锁的线程才能执行该方法。其他线程需要等待锁的释放才能执行该方法。
我们将创建多个线程来同时增加和减少计数器的值。
下面是修改后的代码:
public class Counter {
private int count = 0;
// 同步方法,使用 synchronized 修饰
public synchronized void increment() {
count++;
}
// 同步方法,使用 synchronized 修饰
public synchronized void decrement() {
count--;
}
// 非同步方法
public int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
// 创建并启动增加计数器值的线程
Thread incrementThread = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
// 创建并启动减少计数器值的线程
Thread decrementThread = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.decrement();
}
});
// 启动线程
incrementThread.start();
decrementThread.start();
// 等待两个线程执行完毕
try {
incrementThread.join();
decrementThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出计数器的值
System.out.println("Final count: " + counter.getCount());
}
}
在这个示例中,我们创建了两个线程:一个增加计数器值的线程incrementThread
和一个减少计数器值的线程decrementThread
。这两个线程会同时运行,并且通过调用Counter
类中的同步方法来修改计数器的值。
最后,我们等待这两个线程执行完毕,并输出最终的计数器值。由于我们使用了synchronized
修饰方法来确保对计数器的安全访问,所以最终输出的计数器值应该是预期的结果。
3. 修饰代码块:
当某个对象被synchronized修饰时,任何线程在执行该对象中被synchronized修饰的代码块时,必须先获得该对象的锁。其他线程需要等待锁的释放才能执行同步代码块。Java中的每个对象都有一个内置锁,当一个对象被synchronized修饰时,它的内置锁就起作用了。只有获得该锁的线程才能访问被synchronized修饰的代码段。使用synchronized可以保证多个线程对共享数据的访问顺序,避免了竞态条件和数据不一致的问题。注意:synchronized关键字只能保证同一对象内部的线程同步,不能保证多个对象的同步。如果需要多个对象之间的同步,可以考虑使用Lock接口的实现类。
在Java中,synchronized
关键字用于实现线程同步,可以修饰代码块、方法以及静态方法。当synchronized
修饰一个代码块时,它会锁定一个对象,确保同时只有一个线程可以进入被修饰的代码块,从而保证了线程安全。
语法格式为:
synchronized (对象) {
// 需要同步的代码块
}
在这个语法中,括号内的对象是一个锁对象,每个Java对象都可以作为锁对象。当一个线程进入synchronized
代码块时,它会尝试获取锁对象,如果锁对象被其他线程占用,则当前线程会被阻塞,直到获取到锁对象后才能继续执行。当线程执行完synchronized
代码块后会释放锁对象,其他线程就可以获取锁对象继续执行synchronized
代码块。
下面是一个简单的示例:
public class Example {
private static final Object lock = new Object();
public void method() {
synchronized (lock) {
// 同步代码块
// 在这里访问共享资源
}
}
}
在这个示例中,lock
对象被用作锁对象,只有获取了这个锁对象的线程才能进入synchronized
代码块。
二,锁升级
按照锁的升级顺序,可以将锁状态和过渡描述为以下几个步骤:
1.无锁状态(Unlocked):
初始时,资源处于无锁状态,没有线程正在使用它。
偏向锁(Biased Locking):当一个线程第一次进入同步代码块时,会尝试获取偏向锁。如果成功,该线程会标记为拥有偏向锁,并记录下自己的线程 ID。这样,在未发生竞争的情况下,该线程后续访问同步代码块时可以快速获取锁,避免了重量级锁的开销。
2.轻量级锁(Lightweight Locking):
当两个或多个线程开始竞争同一个锁时,偏向锁会升级为轻量级锁。此时,JVM 会使用 CAS(Compare and Swap)操作来尝试获取锁。如果成功,当前线程会获得轻量级锁,并继续执行临界区代码;如果失败,表示有其他线程持有锁,需要进行锁的膨胀。
3.自旋锁(Spin Locking):
在轻量级锁的基础上,为了避免线程阻塞和降低线程切换的开销,如果获取轻量级锁失败,当前线程会选择进行自旋,即忙等待一段时间。它会尝试多次使用 CAS 操作来获取锁,期望其他线程能够快速释放锁。如果在自旋期间成功获取了锁,线程可以继续执行临界区代码;否则,会进一步升级为重量级锁。
4.重量级锁(Heavyweight Locking):
当自旋锁无法获取到锁时或自旋次数达到一定阈值,轻量级锁会膨胀为重量级锁。此时,JVM 使用操作系统的互斥量(Mutex)来实现锁,并将当前线程阻塞,直到获取到锁为止。其他线程需要等待持有锁的线程释放锁后才能获取锁并执行临界区代码。
这是锁状态的一个常见升级顺序,它描述了锁的不同阶段和相应的过渡。但需要注意的是,在具体实现中,JVM 和不同的 Java 版本可能会有一些优化和变化,因此实际的锁升级过程可能略有不同。
锁会升级的主要原因是为了解决并发环境下的线程竞争和保证数据的一致性。当多个线程同时竞争同一个锁时,如果使用低级别的锁(如偏向锁或轻量级锁),可能会导致一些问题,例如:
竞争激烈:如果多个线程频繁地竞争同一个锁,轻量级锁的 CAS 操作可能会失败,导致自旋失败,增加了线程切换的开销。
锁撤销:偏向锁只能保证在没有竞争的情况下加锁的效率,一旦有其他线程竞争锁,就需要将偏向锁撤销,转而升级为更高级别的锁。
锁膨胀:对于长时间持有锁的线程,自旋可能会浪费大量的 CPU 时间,不仅影响性能,还会导致其他线程无法及时获取锁。此时,将轻量级锁膨胀为重量级锁,可以将持有锁的线程阻塞,避免自旋带来的性能损耗。
因此,为了提高并发的效率和线程安全性,锁会根据实际情况进行升级。锁的升级过程就是将低级别的锁转换为高级别的锁,以应对竞争激烈、长时间持有锁的情况,从而保证线程的顺序执行和数据的一致性。锁升级的原则是在减少线程切换开销、提高吞吐量和保证线程安全之间找到一个平衡点,以最优的方式处理并发情况。
三,什么是CAS
CAS(Compare and Swap)是一种并发算法,用于实现多线程环境下的原子操作。它是一种乐观锁策略,通过比较内存中的值与期望值是否相等来判断数据是否被修改,从而进行原子操作。
CAS 操作通常涉及三个参数:内存地址(或变量),期望值和新值。CAS 操作会先读取内存中的值与期望值进行比较,如果相等,则将新值写入内存中,并返回操作成功;如果不相等,则说明其他线程已经修改了内存的值,CAS 操作失败,需要重新尝试。
CAS 操作是原子性的,因为它在执行期间不会被中断或打断。它可以提供非阻塞的原子性操作,避免了使用锁造成的线程阻塞和上下文切换的开销。相比传统的加锁操作,CAS 操作通常具有更高的并发性能。
CAS 在并发编程中有广泛的应用,例如乐观锁、无锁数据结构和线程安全的计数器等。但需要注意的是,CAS 操作虽然能够解决一些并发问题,但在高并发环境下可能存在ABA问题(即在修改期间,数据经过了一系列变化又回到了原始状态),需要额外的手段来解决。
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) {
// 模拟多个线程同时对计数器进行累加操作
Thread thread1 = new Thread(new CounterRunnable());
Thread thread2 = new Thread(new CounterRunnable());
thread1.start();
thread2.start();
// 等待两个线程执行完毕
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出最终的计数器值
System.out.println("Final Counter Value: " + counter.get());
}
static class CounterRunnable implements Runnable {
@Override
public void run() {
// 对计数器进行1000次累加操作
for (int i = 0; i < 1000; i++) {
// 使用CAS操作进行原子性累加
while (true) {
int currentValue = counter.get();
int newValue = currentValue + 1;
if (counter.compareAndSet(currentValue, newValue)) {
break; // CAS操作成功,退出循环
}
// CAS操作失败,继续尝试
}
}
}
}
}