多线程安全问题和解决方案

多线程安全问题的原因

Java多线程安全问题主要源于多个线程同时访问共享资源时的不可控行为,出现多线程安全的根本原因是线程调度是随机的(抢占式执行),这是目前计算机设计的问题我们无法直接干预,我可从其他方面研究,具体原因包括:

  1. 竞态条件(Race Condition)

    • 多个线程以非原子方式操作共享数据

    • 执行结果依赖于线程执行的时序

  2. 内存可见性问题

    • 一个线程对共享变量的修改可能不会立即对其他线程可见

    • 由于CPU缓存、指令重排序等优化导致

  3. 指令重排序

    • 编译器和处理器可能会对指令进行重排序优化

    • 导致程序执行顺序与代码顺序不一致

常见线程安全问题表现

1.数据不一致

我们先看这样一个现象:

多线程安全问题和解决方案_第1张图片

按照自己的逻辑,我们得到的结果应该是10000,但是结果却不一致,这就是由于线程安全问题引起的。

2.死锁

//线程安全问题:死锁
public class Demo2 {
    static Object locker1 = new Object();
    static Object locker2 = new Object();

    public static void main(String[] args) throws InterruptedException{
        Thread t1 = new Thread(()->{
           synchronized (locker1) {
               System.out.println("t1 线程拿到locker1");
               // 此处的 sleep 目的是让 t1 和 t2 确实都已经拿到各自的 locker1 和 locker2
               // 然后再进行后序操作. 
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               synchronized (locker2) {
                   System.out.println("t2 线程拿到locker2");
               }
           }
        });
        Thread t2 = new Thread(()->{
           synchronized (locker2) {
               System.out.println("t2 线程拿到locker2");
               
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               synchronized (locker1) {
                   System.out.println("t1 线程拿到locker1");
               }
           }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

当我们执行这份代码的时候就会发现,程序一直在执行,不会停下来,执行结果是:

此时 t1 想要拿到 locker2 就要等 t2 解锁,t2 想要解锁就要先拿到 locker1 ,t2 想要拿到 locker1 就要等 t1 解锁,t1 要解锁就要先拿到 locker2 ,至此一个循环构成,也就是构成了死锁。

3.内存可见性

多线程安全问题和解决方案_第2张图片

当程序运行后我们看到当 t2 线程修改 flag 的值后,t1 线程并没有结束,而是一直在运行,这就是内存可见性,当 t1 线程循环、很多次发现 flag 的值没有变化就会优化执行过程,当 t2 线程修改 flag 后,t1 就无法及时获取到最新的值。

4.指令重排序

重排序是⼀个⽐较复杂的话题, 涉及到 CPU 以及编译器的⼀些底层⼯作原理,这里由于“博主”知识有限暂无法完全解释,有机会在细讲。

解决方案

1. 使用同步机制

// 同步方法
public synchronized void increment() {
    count++;
}

// 同步代码块
public void increment() {
    synchronized(this) {
        count++;
    }
}

2. 使用锁机制

// ReentrantLock
private final Lock lock = new ReentrantLock();

public void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}

3. 使用原子类(Atomic Classes)

private AtomicInteger count = new AtomicInteger();

public void increment() {
    count.incrementAndGet();
}

4. 使用volatile关键字

private volatile boolean flag = false;

// 保证flag的修改对所有线程立即可见

5. 使用线程安全容器

// ConcurrentHashMap
Map map = new ConcurrentHashMap<>();

// CopyOnWriteArrayList
List list = new CopyOnWriteArrayList<>();

6. 使用ThreadLocal

private ThreadLocal threadLocalCount = ThreadLocal.withInitial(() -> 0);

public void increment() {
    threadLocalCount.set(threadLocalCount.get() + 1);
}

7. 使用不可变对象

public final class ImmutableValue {
    private final int value;
    
    public ImmutableValue(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
}

最佳实践

  1. 尽量减少同步范围

  2. 优先使用不可变对象

  3. 考虑使用线程封闭技术

  4. 避免锁的嵌套使用以防死锁

  5. 使用高级并发工具而非自己实现

  6. 合理设置线程池大小

  7. 使用适当的并发级别

你可能感兴趣的:(python,开发语言,学习,java,数据结构,安全,c语言)