• java技术文档--多线程(3)--线程同步于互斥


    并发编程中的共享资源问题

            在并发编程中,多个线程同时访问和修改共享资源可能会导致数据不一致或者出现竞态条件的问题。为了解决这些问题,Java提供了同步和互斥机制来保证多个线程对共享资源的安全访问。

            同步:

    通过使用关键字synchronized或者使用锁(Lock)来实现线程的同步。同步可以保证在同一时刻只有一个线程可以访问共享资源,其他线程需要等待当前线程释放锁才能继续执行。这样可以避免多个线程同时修改共享资源而引发的数据不一致问题        

    1. public class SharedResource {
    2. private int count;
    3. public synchronized void increment() {
    4. count++;
    5. }
    6. }

             互斥:

            通过使用临界区(Critical Section)来保护共享资源的访问。临界区是指一段代码,在同一时刻只能有一个线程执行该代码块,其他线程必须等待。可以使用synchronized关键字或者Lock对象来实现互斥机制。

    1. public class SharedResource {
    2. private int count;
    3. private final Object lock = new Object();
    4. public void increment() {
    5. synchronized (lock) {
    6. count++;
    7. }
    8. }
    9. }

             在同步和互斥机制下,线程在访问共享资源之前会获得锁,并在完成后释放锁,确保对共享资源的安全访问。这样可以避免多个线程同时修改共享资源而引发的数据竞争和不一致性问题。

            需要注意的是,过度的同步可能会导致性能下降。因此,在进行并发编程时,需要合理地选择同步粒度和锁的范围,避免不必要的同步操作,以提高程序的并发性能。

    扩展:保证线程有序执行

    有以下几种方式可以保证线程的有序执行:

    1. 使用锁机制:使用锁(如synchronized关键字或Lock接口的实现类)来控制线程的访问顺序。通过在临界区代码中获取和释放锁,可以确保只有一个线程能够进入临界区,从而保证线程的有序执行。

    2. 使用等待和通知机制:通过使用wait()notify()notifyAll()方法来进行线程间的协调和通信。线程可以在某个条件满足之前等待,当满足条件时通过通知其他线程来唤醒它们,从而实现线程的有序执行。

    3. 使用线程的join()方法:通过调用线程的join()方法可以使得一个线程等待另一个线程执行完毕。在线程A中调用线程B的join()方法,线程A将会阻塞直到线程B执行完毕,然后线程A才会继续执行。

    4. 使用线程池:通过使用线程池来管理线程的执行顺序。线程池可以按照任务的提交顺序来执行,保证线程的有序执行。

    需要根据具体的需求来选择适合的方式来保证线程的有序执行。锁机制和等待/通知机制可以精确地控制线程的执行顺序,但需要更多的手动管理。而使用线程的join()方法和线程池可以简化线程的有序执行,但可能会失去一些精细的控制能力。

    synchronized关键字和锁的机制

    Synchronized关键字和锁的机制是Java中实现线程同步和互斥的重要手段。

    • synchronized关键字:
      • 修饰方法:当synchronized关键字用于方法时,它将整个方法体视为临界区,保证在同一时间只有一个线程可以执行该方法。其他线程需要等待当前线程执行完毕才能进入该方法。
      • 修饰代码块:当synchronized关键字用于代码块时,它指定了所谓的“锁对象”,只有获得了该对象上的锁(也称为监视器锁)的线程才能执行这段被synchronized修饰的代码。其他线程需要等待该锁的释放才能进入该代码块。
    1. // 示例1:synchronized修饰方法
    2. public synchronized void synchronizedMethod() {
    3. // 临界区代码
    4. }
    5. // 示例2:synchronized修饰代码块
    6. public void synchronizedBlock() {
    7. synchronized (lockObject) {
    8. // 临界区代码
    9. }
    10. }
    • 锁的机制:Java提供了多种锁的机制来实现线程的同步和互斥,其中常见的锁包括:
      • 内置锁(Intrinsic Lock)也称为监视器锁或对象锁,是由synchronized关键字实现的。每个Java对象都与一个内置锁相关联,多个线程可以竞争该对象上的锁。
      • Lock接口:Java并发包(java.util.concurrent)提供了Lock接口及其实现类来提供更灵活的锁机制。通过使用Lock接口的实现类(如ReentrantLock),可以显式地获得和释放锁,并支持更高级的特性,如可重入、公平性等。
    1. // 示例:使用内置锁实现同步
    2. public class SharedResource {
    3. private int count;
    4. public synchronized void increment() {
    5. count++;
    6. }
    7. }
    8. // 示例:使用Lock接口实现同步
    9. public class SharedResource {
    10. private int count;
    11. private Lock lock = new ReentrantLock();
    12. public void increment() {
    13. lock.lock();
    14. try {
    15. count++;
    16. } finally {
    17. lock.unlock();
    18. }
    19. }
    20. }

            无论是使用synchronized关键字还是锁机制,它们都能确保在同一时间只有一个线程可以访问共享资源,从而避免数据竞争和不一致性问题。选择哪种方式取决于需求的复杂性、粒度控制和性能要求。 

    使用volatile关键字保证可见性

    在多线程编程中,可见性是指当一个线程修改了共享变量的值之后,其他线程可以立即看到这个变化。然而,由于线程间的执行是并发的,每个线程都有自己的本地缓存,这可能会导致一个线程对共享变量的修改对其他线程不可见,从而引发一些问题。

    为了解决这个问题,Java提供了volatile关键字。volatile关键字可以用来修饰共享变量,确保对该变量的读写操作具有可见性。具体来说,使用volatile关键字修饰的变量在每次被线程访问时,都会强制从主内存中重新加载其值,而不是使用线程的本地缓存。同时,每次对volatile变量的写入操作都会立即刷新到主内存中,以便其他线程可以立即看到最新的值。

    下面是volatile关键字保证可见性的几个方面:

    1. 可见性保证:使用volatile关键字修饰的变量,对它的修改对其他线程是可见的。如果一个线程修改了该变量的值,其他线程将立即看到最新的值,而不会使用过期或无效的缓存数据。

    2. 禁止重排序:volatile关键字禁止编译器和处理器对其修饰的变量进行指令重排序优化。这样可以确保变量的读写操作按照程序的顺序执行,从而避免出现意想不到的结果。

    3. 原子性限制:注意,volatile关键字只能确保对单个变量的读写操作具有原子性,不能保证复合操作的原子性。如果多个线程同时访问共享变量并对其进行复合操作(例如自增或自减),则可能会导致竞态条件和错误的结果。要保证复合操作的原子性,需要使用其他的同步机制,如synchronized关键字或java.util.concurrent.atomic包下的原子类。

    总结来说,volatile关键字通过强制从主内存读取变量的值,禁止重排序,以及立即刷新写入操作到主内存,保证了共享变量的可见性。然而,它并不能保证复合操作的原子性,需要额外的同步机制进行保护。

    使用Lock和Condition进行显示锁定和条件控制

    在Java等多线程编程语言中,我们经常使用LockCondition来进行线程间的同步和通信。LockCondition是Java的并发库中的重要组成部分,它们可以帮助我们避免线程间的竞争,并确保线程在正确的时机访问共享资源。

    下面,我将详细解释一下LockCondition的基本概念以及如何在多线程环境中使用它们。

    Lock (锁)

    Lock是一种同步机制,用于控制多个线程对共享资源的访问。它提供了比synchronized关键字更为灵活的线程同步方法。在Java中,我们可以使用java.util.concurrent.locks.Lock接口实现锁。

    以下是一个简单的例子:

    1. import java.util.concurrent.locks.Lock;
    2. import java.util.concurrent.locks.ReentrantLock;
    3. public class Counter {
    4. private final Lock lock = new ReentrantLock();
    5. private int count = 0;
    6. public void increment() {
    7. lock.lock(); // 获取锁
    8. try {
    9. count++;
    10. } finally {
    11. lock.unlock(); // 释放锁
    12. }
    13. }
    14. public int getCount() {
    15. return count;
    16. }
    17. }

    在这个例子中,我们使用ReentrantLock来实现锁。ReentrantLock是可重入的,意味着一个线程可以多次获取同一把锁,只要它每次都通过调用unlock()来释放锁。

    Condition (条件)

    Condition是线程之间的通知机制。它允许一个或多个线程等待某个条件成立,然后被唤醒并继续执行。在Java中,我们可以使用java.util.concurrent.locks.Condition接口实现条件。通常,我们会在一个循环中使用await()方法等待条件成立,在条件成立时使用signal()signalAll()方法唤醒等待的线程。

    以下是一个使用条件变量的例子:

    1. import java.util.concurrent.locks.Condition;
    2. import java.util.concurrent.locks.ReentrantLock;
    3. public class ProducerConsumer {
    4. private final Lock lock = new ReentrantLock();
    5. private final Condition producerCondition = lock.newCondition();
    6. private final Condition consumerCondition = lock.newCondition();
    7. private int itemsInStock = 0;
    8. private final Queue stock = new LinkedList<>();
    9. public void produce(int item) throws InterruptedException {
    10. lock.lock();
    11. try {
    12. while (itemsInStock == 0) { // 如果库存为空,生产者线程等待
    13. producerCondition.await();
    14. }
    15. itemsInStock--;
    16. stock.add(item);
    17. consumerCondition.signal(); // 通知消费者线程有新的商品可以取走
    18. } finally {
    19. lock.unlock();
    20. }
    21. }
    22. public int consume() throws InterruptedException {
    23. lock.lock();
    24. try {
    25. while (itemsInStock == 0) { // 如果库存为空,消费者线程等待
    26. consumerCondition.await();
    27. }
    28. int item = stock.poll(); // 取走一个商品
    29. itemsInStock++;
    30. producerCondition.signal(); // 通知生产者线程可以继续生产新的商品了
    31. return item;
    32. } finally {
    33. lock.unlock();
    34. }
    35. }
    36. }

    在这个例子中,生产者线程和消费者线程共享了一个库存和一个条件变量对。生产者线程在库存有空间时生产商品并将商品放入库存,然后唤醒等待的消费者线程。消费者线程在库存有商品时将商品取走并唤醒等待的生产者线程。通过使用锁和条件,我们确保了当库存为空时消费者线程不会尝试取走商品,同时当库存已满时生产者线程不会尝试生产新的商品。

    阿丹的业务场景:

            之前做了一个收集项目的对接工作因为数据并发量是蛮大的,所以手撸了一个缓冲池。去动态的切换缓冲池,缓冲池之后会专门出文章来给大家分享。那么当缓冲池没有数据了还需要再开线程去拿数据吗?当然不是,那我就可以使用Condition这个方法来将线程放掉。或者通知唤醒线程来去缓冲池中拿取数据。只能说!妙啊~~

    Condition详细

    Condition是Java中用于线程同步的条件变量,它允许线程等待某个条件成立,然后被唤醒并继续执行。下面是Condition在多线程中的一些常见业务场景和代码示例:

    • 生产者-消费者模型:这是一个常见的多线程模式,生产者线程生成数据并将其放入缓冲区,消费者线程从缓冲区中取出数据并处理。在这个模型中,可以使用Condition来控制生产者和消费者线程的同步。
    1. import java.util.concurrent.locks.Condition;
    2. import java.util.concurrent.locks.ReentrantLock;
    3. public class ProducerConsumerExample {
    4. private final ReentrantLock lock = new ReentrantLock();
    5. private final Condition producerCondition = lock.newCondition();
    6. private final Condition consumerCondition = lock.newCondition();
    7. private int itemsInStock = 0;
    8. private final Queue stock = new LinkedList<>();
    9. public void produce(int item) throws InterruptedException {
    10. lock.lock();
    11. try {
    12. while (itemsInStock ==缓冲区大小) { // 如果缓冲区已满,生产者线程等待
    13. producerCondition.await();
    14. }
    15. stock.add(item);
    16. itemsInStock++;
    17. consumerCondition.signal(); // 通知消费者线程有新的商品可以取走
    18. } finally {
    19. lock.unlock();
    20. }
    21. }
    22. public int consume() throws InterruptedException {
    23. lock.lock();
    24. try {
    25. while (itemsInStock == 0) { // 如果缓冲区为空,消费者线程等待
    26. consumerCondition.await();
    27. }
    28. int item = stock.poll(); // 取走一个商品
    29. itemsInStock--;
    30. producerCondition.signal(); // 通知生产者线程可以继续生产新的商品了
    31. return item;
    32. } finally {
    33. lock.unlock();
    34. }
    35. }
    36. }
    • 资源分配与释放:在资源分配与释放场景中,多个线程可能同时请求相同的资源。为了避免资源冲突和竞争条件,可以使用Condition来控制资源的分配和释放。
    1. import java.util.concurrent.locks.Condition;
    2. import java.util.concurrent.locks.ReentrantLock;
    3. public class ResourceAllocator {
    4. private final ReentrantLock lock = new ReentrantLock();
    5. private final Condition resourceCondition = lock.newCondition();
    6. private int resourcesAvailable = 10;
    7. public void allocateResource() throws InterruptedException {
    8. lock.lock();
    9. try {
    10. while (resourcesAvailable == 0) { // 如果资源已用完,请求线程等待
    11. resourceCondition.await();
    12. }
    13. resourcesAvailable--;
    14. } finally {
    15. lock.unlock();
    16. }
    17. }
    18. public void releaseResource() {
    19. lock.lock();
    20. try {
    21. resourcesAvailable++; // 释放一个资源
    22. resourceCondition.signal(); // 通知等待的线程有可用资源了
    23. } finally {
    24. lock.unlock();
    25. }
    26. }
    27. }
    • 线程间的协同工作:有时需要多个线程协同完成某项任务,比如多线程下载。在这种情况下,可以使用Condition来控制每个线程的工作流程和顺序。
    1. import java.util.concurrent.locks.Condition;
    2. import java.util.concurrent.locks.ReentrantLock;
    3. public class DownloadTask {
    4. private final ReentrantLock lock = new ReentrantLock();
    5. private final Condition downloadCondition = lock.newCondition();
    6. private boolean downloadStarted = false;
    7. private boolean downloadCompleted = false;
    8. public void download() throws InterruptedException {
    9. lock.lock();
    10. try {
    11. while (!downloadStarted) { // 如果下载未开始,线程等待
    12. downloadCondition.await();
    13. }
    14. // 执行下载操作...
    15. downloadCompleted = true; // 标记下载已完成
    16. downloadCondition.signalAll(); // 通知其他等待的线程可以开始下载了
    17. } finally {
    18. lock.unlock();
    19. }
    20. }
    21. public void startDownload() {
    22. lock.lock();
    23. try {
    24. if (downloadStarted) { // 如果下载已经开始,直接返回
    25. return;
    26. }
    27. downloadStarted = true; // 标记下载已开始
    28. downloadCondition.signalAll(); // 通知等待的线程可以开始下载了
    29. } finally {
    30. lock.unlock();
    31. }

    原子操作和原子类的使用

    在Java中,原子操作和原子类是用于处理多线程编程中线程安全问题的工具。下面是对这两者的详细讲解:

    原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何context switch(切换到另一个线程)。在Java中,原子操作主要通过java.util.concurrent.atomic包中的一系列原子操作类来实现。

    原子类是Java提供的一种高级线程安全的方式,它们是一组提供原子操作的类。这些类包括:AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference等。这些原子类底层都是基于CPU的CAS(Compare-and-Swap)操作实现的,因此效率非常高。

    以下是一个使用AtomicInteger的示例:

    1. import java.util.concurrent.atomic.AtomicInteger;
    2. public class AtomicExample {
    3. private AtomicInteger atomicInt = new AtomicInteger(0);
    4. public void increment() {
    5. atomicInt.incrementAndGet();
    6. }
    7. public int get() {
    8. return atomicInt.get();
    9. }
    10. }

    在这个例子中,increment()方法使用了AtomicInteger的incrementAndGet()方法,它提供了一个原子性的增加操作。在多线程环境下,使用这个方法可以保证对整数的增加操作的原子性,避免并发问题。

    总的来说,原子操作和原子类在Java中为我们提供了线程安全性的保障,让我们在多线程环境下进行复杂的并发操作也能够保证数据的一致性和操作的原子性。

  • 相关阅读:
    串口转TCP/IP方案选型
    MyBatis
    [创业-40]:-优秀人与普通人的区别
    CSS圆形旋转边框
    Flutter横屏实践
    读后:水浒的水有多深
    [JavaWeb] web的基本概念
    Java基础- 浅谈javac和javap
    计算机设计大赛 题目:基于python的验证码识别 - 机器视觉 验证码识别
    【SSM】SpringMVC系列——SpringMVC概述
  • 原文地址:https://blog.csdn.net/weixin_72186894/article/details/133644886