并发编程(高并发、多线程)
并发编程
1.并发编程
1.1 Java程序中怎么保证多线程的运行安全?(难度:★★ 频率:★★★★★)
1.1.1 并发编程三要素
首先我们要了解并发编程的三要素
- 原子性
- 可见性
- 有序性
1.原子性
原子性是指一个操作是不可分割的单元,要么全部执行成功,要么全部失败。
在并发环境中,多个线程可能同时访问和修改共享的数据,为了确保数据的一致性,需要确保一组相关的操作是原子执行的。
例如,如果多个线程同时尝试更新同一个变量,需要使用锁或其他同步机制来确保原子性。
2.可见性
可见性是指一个线程对共享数据的修改应该对其他线程是可见的
在多处理器系统中,每个线程可能在不同的处理器上执行,它们有各自的缓存。因此,对一个线程的修改可能不会立即被其他线程看到。为了确保可见性,需要使用同步机制,例如锁或volatile变量,来通知其他线程共享数据的变化。
3.有序性
有序性是指程序的执行应该按照一定的顺序来进行,而不是随机的。
在多线程环境中,由于指令重排等原因,线程执行的顺序可能与程序中编写的顺序不同。为了确保有序性,需要使用同步机制来保持程序的预期执行顺序。
1.1.2 原子性、可见性、有序性问题的解决方法
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.缓存导致的可见性问题解决办法
- 使用volatile关键字
使用volatile关键字来修饰共享的变量。volatile保证了变量的可见性,即当一个线程修改了volatile变量的值,这个变化对其他线程是立即可见的 - 使用synchronized关键字
使用synchronized关键字来保护对共享数据的访问, 确保在进入和退出同步块时, 会刷新缓存, 从而保证可见性。 - 使用Lock接口
显式地使用Lock接口及其实现类, 如ReentrantLock, 以及ReadWriteLock可以提供更灵活的同步控制,确保在锁的释放时将数据的变化同步到主内存。 - 使用JUC(并发工具)
java.util.concurrent包提供了一些用于并发编程的工具,例如Atomic类、CountDownLatch、CyclicBarrier等。这些工具通常会处理可见性问题,避免了手动进行同步的复杂性。
3.编译优化带来的有序性问题解决办法
- 使用volatile关键字
volatile关键字不仅保证了变量的可见性,还防止了编译器对被volatile修饰的变量进行重排序。volatile变量的读写都会插入内存屏障,防止指令重排序。 - 使用synchronized关键字或锁
使用synchronized关键字或锁也能够防止指令重排序,因为在进入和退出同步块时都会插入内存屏障,确保了代码块的有序性。 - 使用final关键字
在Java中,final关键字除了用于声明常量外,还可以用于修饰字段、方法和类。对于字段,final关键字可以防止字段的写入重排序。 - 使用happens-before规则
在Java中,happens-before规则定义了在多线程环境下操作的顺序。通过正确使用同步、volatile等机制,可以利用happens-before规则来确保代码的正确有序执行。 - 使用JUC(并发工具)
java.util.concurrent包中的一些工具类,如CountDownLatch、CyclicBarrier等,也可以防止编译器对代码进行过度优化,确保有序性。
1.2 Synchronized(难度:★★ 频率:★★★)
1.2.1 synchronized的三种加锁方法
// 修饰普通方法
public synchronized void increase() {
}
// 修饰静态方法
public static synchronized void increase() {
}
// 修饰代码块
public Object synMethod(Object a1) {
synchronized(a1) {
// 操作
}
}
作为范围 | 锁对象 |
---|---|
普通方法 | 当前实例对象(this), 对于Class类的不同实例, 它们的实力方法是独立的, 可以同时执行 |
静态方法 | 整个类的Class对象, 对于Class类的所有实例,同一时间只能有一个线程执行该静态方法 |
代码块 | 指定对象 |
1.2.2 提高synchronized的并发性能
- 减小同步块的范围: 尽量缩小使用 synchronized 保护的代码块范围,以减少线程持有锁的时间。只在必要的代码块上使用同步。
- 使用局部变量代替共享变量: 尽量使用局部变量而不是共享变量,这可以减小锁的粒度,减少竞争
- 使用读写锁: 如果读操作远远多于写操作,可以考虑使用 ReentrantReadWriteLock,以允许多个线程同时读取而不互斥。
- 考虑锁分离: 将对象的锁分离,使用不同的锁来保护对象的不同部分,从而减小锁的争用。
扩展问题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.3 volatile(难度:★★ 频率:★★★★)
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,可以禁止特定类型的指令重排序,从而确保操作的顺序符合程序员的预期。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!