首先我们要了解并发编程的三要素
1.原子性
原子性是指一个操作是不可分割的单元,要么全部执行成功,要么全部失败。
在并发环境中,多个线程可能同时访问和修改共享的数据,为了确保数据的一致性,需要确保一组相关的操作是原子执行的。
例如,如果多个线程同时尝试更新同一个变量,需要使用锁或其他同步机制来确保原子性。
2.可见性
可见性是指一个线程对共享数据的修改应该对其他线程是可见的
在多处理器系统中,每个线程可能在不同的处理器上执行,它们有各自的缓存。因此,对一个线程的修改可能不会立即被其他线程看到。为了确保可见性,需要使用同步机制,例如锁或volatile变量,来通知其他线程共享数据的变化。
3.有序性
有序性是指程序的执行应该按照一定的顺序来进行,而不是随机的。
在多线程环境中,由于指令重排等原因,线程执行的顺序可能与程序中编写的顺序不同。为了确保有序性,需要使用同步机制来保持程序的预期执行顺序。
1.线程切换带来的原子性问题解决办法:
synchronized关键字
、ReentrantLock锁
确保一段代码在同一时刻只能被一个线程执行AtomicInteger
、AtomicLong
等, 原子类底层是通过CAS操作
来保证原子性的数据库事务
来确保一系列操作的原子性。数据库事务通常在开始和结束时设置边界,确保整个操作在一个原子性的单元中执行。ABA问题
)public class Example {
private AtomicReference<Data> dataReference = new AtomicReference<>();
public void updateData() {
Data currentData = dataReference.get();
// 在更新前检查数据是否被其他线程修改
// ...
// 更新数据
dataReference.compareAndSet(currentData, newData);
}
}
2.缓存导致的可见性问题解决办法
3.编译优化带来的有序性问题解决办法
// 修饰普通方法
public synchronized void increase() {
}
// 修饰静态方法
public static synchronized void increase() {
}
// 修饰代码块
public Object synMethod(Object a1) {
synchronized(a1) {
// 操作
}
}
作为范围 | 锁对象 |
---|---|
普通方法 | 当前实例对象(this), 对于Class类的不同实例, 它们的实力方法是独立的, 可以同时执行 |
静态方法 | 整个类的Class对象, 对于Class类的所有实例,同一时间只能有一个线程执行该静态方法 |
代码块 | 指定对象 |
扩展问题1: Synchronized修饰的方法在抛出异常时,会释放锁吗?
当一个线程执行一个被 synchronized 关键字修饰的方法时,如果发生异常,虚拟机会将锁释放,允许其他线程进入相同的方法或代码块。这样,其他线程有机会执行相应的同步代码,而不会被阻塞。
public class SynchronizedExample {
private static int counter = 0;
public synchronized void synchronizedMethod() {
System.out.println(Thread.currentThread().getName() + " entering synchronizedMethod.");
if (counter < 3) {
counter++;
System.out.println(Thread.currentThread().getName() + " Counter: " + counter);
throw new RuntimeException("Simulating an exception.");
}
System.out.println(Thread.currentThread().getName() + " exiting synchronizedMethod.");
}
public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
// 创建两个线程调用同一个对象的同步方法
Thread thread1 = new Thread(() -> {
example.synchronizedMethod();
});
Thread thread2 = new Thread(() -> {
example.synchronizedMethod();
});
thread1.start();
thread2.start();
}
}
扩展问题2: synchronized 是公平锁还是非公平锁?
synchronized关键字默认是非公平锁。这意味着在多个线程竞争同一个锁的情况下,无法保证线程获取锁的顺序与线程请求锁的顺序一致。
非公平锁的特点是,当一个线程释放锁时,下一个要获得锁的线程是任意选择的,不考虑这个线程是否在等待队列中等待更长的时间。
1.保证可见性
当一个变量被声明为volatile时,对该变量的读写操作都会直接在主内存中进行,而不会在线程的本地缓存中进行。这确保了一个线程对该变量的修改对其他线程是可见的。即使一个线程修改了volatile变量,其他线程立即看到的是最新的值。
public class VisibilityExample {
private static volatile boolean flag = false;
public static void main(String[] args) {
// 线程 A
new Thread(() -> {
System.out.println("Thread A started");
while (!flag) {
// 在没有使用 volatile 的情况下,可能会陷入无限循环,因为 flag 的修改对线程 A 不可见
}
System.out.println("Thread A finished");
}).start();
try {
Thread.sleep(1000); // 等待一段时间,确保线程 A 已经启动
} catch (InterruptedException e) {
e.printStackTrace();
}
// 线程 B
new Thread(() -> {
System.out.println("Thread B started");
flag = true; // 修改 flag 的值
System.out.println("Thread B set flag to true");
}).start();
}
}
2.禁止指令重排序
在 Java 中,编译器和处理器为了提高性能可能会对指令进行重排序。对于一些涉及多线程的代码,这种重排序可能导致意外的结果。通过使用 volatile,可以禁止特定类型的指令重排序,从而确保操作的顺序符合程序员的预期。