首页 > 编程语言 >Java 中常见的三类线程安全问题:解决方案与实例分析

Java 中常见的三类线程安全问题:解决方案与实例分析

时间:2024-11-17 08:45:25浏览次数:3  
标签:初始化 Java Thread students 实例 线程 new public

在 Java 并发编程中,线程安全是一个非常重要的概念。如果多个线程同时访问一个共享资源而不进行适当的同步,就会出现线程安全问题,导致程序行为异常。根据不同的场景,线程安全问题可以分为 运行结果错误发布和初始化导致的线程安全问题活跃性问题。本文将详细探讨这三类线程安全问题,并通过实例分析它们的产生原因和解决方案。

1. 运行结果错误

问题分析:

运行结果错误通常发生在多个线程并发执行时,其中一个线程对共享变量进行修改,而其他线程在修改过程中也对该变量进行读写操作。由于操作不具备原子性,导致最终结果不符合预期。

代码示例:

public class WrongResult {
    volatile static int i;
    
    public static void main(String[] args) throws InterruptedException {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 10000; j++) {
                    i++;  // 注意:这不是原子操作,分为 读取 -> 加1 -> 保存 三个步骤
                }
            }
        };
        Thread thread1 = new Thread(r);
        thread1.start();
        Thread thread2 = new Thread(r);
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(i);  // 本来想要输出 20000,但实际结果不一定是 20000
    }
}

问题原因:

  • i++ 其实是三步操作:读取 i、增加 1、写回 i。这三步操作并不是原子操作,因此在线程切换的过程中,可能会发生线程间的竞争,导致丢失更新。
解决方案:
  • 使用 synchronizedAtomic 类来保证操作的原子性。例如,可以使用 AtomicInteger 类来替代普通的 int,确保线程安全。
import java.util.concurrent.atomic.AtomicInteger;

public class CorrectResult {
    static AtomicInteger i = new AtomicInteger(0);
    
    public static void main(String[] args) throws InterruptedException {
        Runnable r = () -> {
            for (int j = 0; j < 10000; j++) {
                i.incrementAndGet();  // 原子操作,保证线程安全
            }
        };
        
        Thread thread1 = new Thread(r);
        thread1.start();
        Thread thread2 = new Thread(r);
        thread2.start();
        
        thread1.join();
        thread2.join();
        
        System.out.println(i);  // 现在会正确输出 20000
    }
}

2. 发布和初始化导致的线程安全问题

问题分析:

当一个对象在多线程环境中被多个线程访问时,必须确保对象的初始化操作是线程安全的。如果在对象初始化的过程中,其他线程就开始访问该对象,可能会导致未初始化完成的对象被使用,从而产生线程安全问题。

代码示例:
public class WrongInit {
    private Map<Integer, String> students;
    
    public WrongInit() {
        new Thread(() -> {
            students = new HashMap<>();
            students.put(1, "王小美");
            students.put(2, "钱二宝");
            students.put(3, "周三");
            students.put(4, "赵四");
        }).start();
    }
    
    public Map<Integer, String> getStudents() {
        return students;
    }

    public static void main(String[] args) throws InterruptedException {
        WrongInit wrongInit = new WrongInit();
        System.out.println(wrongInit.getStudents().get(1));  // 可能抛出空指针异常
    }
}

问题原因:

  • studentsWrongInit 类的构造方法中被异步初始化。如果在初始化过程中调用了 getStudents() 方法,可能会得到一个尚未初始化完成的对象,从而导致 NullPointerException 或其他异常。
解决方案:
  • 确保对象在多线程访问前已完全初始化。可以通过 volatilesynchronized 保证对象的正确发布,或采用 final 修饰来确保对象初始化完成后不会被修改。
public class CorrectInit {
    private volatile Map<Integer, String> students;
    
    public CorrectInit() {
        new Thread(() -> {
            students = new HashMap<>();
            students.put(1, "王小美");
            students.put(2, "钱二宝");
            students.put(3, "周三");
            students.put(4, "赵四");
        }).start();
    }
    
    public Map<Integer, String> getStudents() {
        return students;
    }

    public static void main(String[] args) throws InterruptedException {
        CorrectInit correctInit = new CorrectInit();
        // 使用阻塞等待或其他方式确保对象已完全初始化
        Thread.sleep(500);
        System.out.println(correctInit.getStudents().get(1));  // 不会抛出空指针异常
    }
}

3. 活跃性问题

活跃性问题是指线程无法按预期完成任务或获取资源,通常表现为 死锁活锁饥饿

死锁(Deadlock)

死锁发生时,两个或多个线程因互相等待对方释放资源而导致程序无法继续执行。

代码示例:死锁
public class MayDeadLock {
    Object o1 = new Object();
    Object o2 = new Object();

    public void thread1() throws InterruptedException {
        synchronized (o1) {
            Thread.sleep(500);
            synchronized (o2) {
                System.out.println("线程1成功拿到两把锁");
            }
        }
    }

    public void thread2() throws InterruptedException {
        synchronized (o2) {
            Thread.sleep(500);
            synchronized (o1) {
                System.out.println("线程2成功拿到两把锁");
            }
        }
    }

    public static void main(String[] args) {
        MayDeadLock mayDeadLock = new MayDeadLock();
        new Thread(() -> {
            try {
                mayDeadLock.thread1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            try {
                mayDeadLock.thread2();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

死锁产生的条件:

  1. 互斥:至少有一个资源处于被占用状态。
  2. 不可剥夺:资源不能强行抢占。
  3. 请求与保持:线程已经持有某些资源,且正在等待其他资源。
  4. 循环等待:形成了资源请求的环状链条。
解决方案:
  • 避免线程持有多个锁。
  • 采用定时锁等待(tryLock())来避免死锁。

活锁(Livelock)

活锁与死锁非常相似,区别在于线程不断改变状态,但永远无法完成任务。尽管线程没有被阻塞,但它们无法获得所需的资源。

饥饿(Starvation)

饥饿发生时,某些线程始终得不到 CPU 资源,导致无法执行。常见的原因是线程优先级不均或锁竞争过于激烈。

总结

线程安全问题在多线程环境中经常出现,理解和处理这些问题是并发编程的关键。常见的线程安全问题包括:

  1. 运行结果错误:如竞争条件导致的错误结果,解决方法是使用原子操作或同步机制。
  2. 发布和初始化导致的线程安全问题:确保对象在被使用前已完全初始化。
  3. 活跃性问题:如死锁、活锁和饥饿,需要避免复杂的锁嵌套,并合理调度线程。

掌握这些线程安全问题及其解决方案,能够帮助你写出更高效且可靠的并发程序。

标签:初始化,Java,Thread,students,实例,线程,new,public
From: https://blog.csdn.net/fulai00/article/details/143801502

相关文章