并发编程(高并发、多线程)

2023-12-29 18:37:43

1.并发编程

1.1 Java程序中怎么保证多线程的运行安全?(难度:★★ 频率:★★★★★)

1.1.1 并发编程三要素

首先我们要了解并发编程的三要素

  • 原子性
  • 可见性
  • 有序性

1.原子性
原子性是指一个操作是不可分割的单元,要么全部执行成功,要么全部失败。

在并发环境中,多个线程可能同时访问和修改共享的数据,为了确保数据的一致性,需要确保一组相关的操作是原子执行的。

例如,如果多个线程同时尝试更新同一个变量,需要使用锁或其他同步机制来确保原子性。

2.可见性
可见性是指一个线程对共享数据的修改应该对其他线程是可见的

在多处理器系统中,每个线程可能在不同的处理器上执行,它们有各自的缓存。因此,对一个线程的修改可能不会立即被其他线程看到。为了确保可见性,需要使用同步机制,例如锁或volatile变量,来通知其他线程共享数据的变化。

3.有序性
有序性是指程序的执行应该按照一定的顺序来进行,而不是随机的。

在多线程环境中,由于指令重排等原因,线程执行的顺序可能与程序中编写的顺序不同。为了确保有序性,需要使用同步机制来保持程序的预期执行顺序。

1.1.2 原子性、可见性、有序性问题的解决方法

1.线程切换带来的原子性问题解决办法:

  • 同步机制
    使用synchronized关键字ReentrantLock锁确保一段代码在同一时刻只能被一个线程执行
  • 原子类
    使用AtomicIntegerAtomicLong等, 原子类底层是通过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的并发性能

  1. 减小同步块的范围: 尽量缩小使用 synchronized 保护的代码块范围,以减少线程持有锁的时间。只在必要的代码块上使用同步。
  2. 使用局部变量代替共享变量: 尽量使用局部变量而不是共享变量,这可以减小锁的粒度,减少竞争
  3. 使用读写锁: 如果读操作远远多于写操作,可以考虑使用 ReentrantReadWriteLock,以允许多个线程同时读取而不互斥。
  4. 考虑锁分离: 将对象的锁分离,使用不同的锁来保护对象的不同部分,从而减小锁的争用。

扩展问题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,可以禁止特定类型的指令重排序,从而确保操作的顺序符合程序员的预期。

文章来源:https://blog.csdn.net/qq_24099547/article/details/135289504
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。