问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

多线程同步机制:如何防止程序崩溃?

创作时间:
作者:
@小白创作中心

多线程同步机制:如何防止程序崩溃?

引用
CSDN
9
来源
1.
https://blog.csdn.net/qq_41973632/article/details/140170291
2.
https://blog.csdn.net/feiying101/article/details/139032196
3.
https://cloud.baidu.com/article/3305313
4.
https://blog.csdn.net/AdolphMacDonald/article/details/137961431
5.
https://blog.csdn.net/codingexpert404/article/details/145105045
6.
https://blog.csdn.net/qq_27471405/article/details/137129243
7.
https://www.cnblogs.com/dxflqm/p/18022824
8.
https://www.cnblogs.com/wgjava/p/18311697
9.
https://www.cnblogs.com/dxflqm/p/18069901

在多线程编程中,同步机制是确保程序稳定运行的关键。不当的线程同步不仅会导致数据不一致,还可能引发程序崩溃。本文将深入探讨Java多线程环境下的同步机制,重点介绍volatile变量和synchronized关键字如何防止程序崩溃,并对比其他同步机制如ReentrantLock和Semaphore的使用场景。

01

多线程程序崩溃的常见原因

在多线程环境中,程序崩溃往往源于以下几种情况:

  1. 数据竞争:多个线程同时访问并修改共享资源,导致数据不一致。
  2. 死锁:两个或多个线程互相等待对方释放资源,导致程序陷入永久等待状态。
  3. 线程饥饿:某些线程长期无法获得CPU时间片,无法执行。
  4. 活锁:线程虽然没有陷入死锁,但因某些条件无法满足,导致无法继续执行。

其中,死锁是最常见的导致程序崩溃的原因。死锁的产生需要满足四个必要条件:

  • 互斥条件:一个资源每次只能被一个线程占用。
  • 持有和等待:一个线程已持有至少一个资源,同时等待其他线程释放资源。
  • 不可剥夺:线程获得的资源在未使用完之前,不能强行剥夺。
  • 环路等待:多个线程形成环路,每个线程等待下一个线程释放资源。

下面是一个典型的死锁示例:

public class DeadlockExample {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1: Holding lock 1...");
                try { Thread.sleep(10); } catch (InterruptedException e) {}
                System.out.println("Thread 1: Waiting for lock 2...");
                synchronized (lock2) {
                    System.out.println("Thread 1: Holding lock 1 & 2...");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2: Holding lock 2...");
                try { Thread.sleep(10); } catch (InterruptedException e) {}
                System.out.println("Thread 2: Waiting for lock 1...");
                synchronized (lock1) {
                    System.out.println("Thread 2: Holding lock 2 & 1...");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

在这个例子中,线程t1和t2分别持有lock1和lock2,然后尝试获取对方持有的锁,从而形成死锁。

02

volatile关键字:解决可见性问题

volatile关键字主要用于解决多线程环境下的变量可见性问题。当一个变量被volatile修饰时,JVM会确保该变量的更新对所有线程都是立即可见的。

例如,在以下代码中,如果没有volatile关键字,线程threadA可能永远无法退出循环:

public class DataEntity {
    private boolean isRunning = true;

    public void addCount(){
        System.out.println("线程运行开始....");
        while (isRunning){ }
        System.out.println("线程运行结束....");
    }

    public boolean isRunning() {
        return isRunning;
    }

    public void setRunning(boolean running) {
        isRunning = running;
    }
}

public class MyThread extends Thread {
    private DataEntity entity;

    public MyThread(DataEntity entity) {
        this.entity = entity;
    }

    @Override
    public void run() {
        entity.addCount();
    }
}

public class MyThreadTest {
    public static void main(String[] args) throws InterruptedException {
        DataEntity entity = new DataEntity();
        MyThread threadA = new MyThread(entity);
        threadA.start();
        Thread.sleep(1000);
        entity.setRunning(false);
    }
}

但是,volatile并不能保证操作的原子性。例如,在以下代码中,即使count变量被volatile修饰,多个线程同时执行addCount方法时,仍然可能出现数据不一致的情况:

public class DataEntity {
    private volatile int count = 0;

    public void addCount(){
        for (int i = 0; i < 100000; i++) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

因此,volatile适用于简单的状态标志或计数器场景,但不适合复杂的原子性操作。

03

synchronized关键字:保证可见性和原子性

synchronized关键字提供了更全面的线程同步机制,它不仅能保证变量的可见性,还能确保代码块的原子性执行。当一个线程进入synchronized代码块时,其他线程必须等待,直到该线程释放锁。

例如,使用synchronized可以轻松解决上述DataEntity类中的线程安全问题:

public class DataEntity {
    private int count = 0;

    public synchronized void addCount(){
        for (int i = 0; i < 100000; i++) {
            count++;
        }
    }

    public synchronized int getCount() {
        return count;
    }
}

但是,synchronized的缺点是可能导致性能开销较大,特别是在高并发场景下。此外,不当的锁使用还可能引发死锁。

04

ReentrantLock:更灵活的锁机制

ReentrantLock是Java并发包中提供的显式锁,相比synchronized,它提供了更丰富的功能,如可中断的锁获取、锁投票、定时锁等候以及公平性选项。

例如,使用ReentrantLock可以实现更复杂的锁控制:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private final Lock lock = new ReentrantLock();
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

ReentrantLock的内部实现基于AQS(AbstractQueuedSynchronizer)框架,提供了更细粒度的锁控制。但是,使用ReentrantLock需要显式地进行锁的获取和释放,如果处理不当可能会导致死锁。

05

最佳实践:选择合适的同步机制

在实际开发中,选择合适的同步机制至关重要:

  1. 简单状态标志:使用volatile,因为它轻量且性能高。
  2. 复杂同步需求:使用synchronized或ReentrantLock,具体取决于是否需要更细粒度的锁控制。
  3. 避免死锁:遵循锁的获取顺序,尽量减少锁的嵌套。
  4. 性能考虑:在高并发场景下,考虑使用读写锁(ReadWriteLock)来提高吞吐量。

通过合理选择和使用这些同步机制,可以有效防止多线程程序崩溃,确保系统稳定运行。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号