• 解密Java多线程同步:掌握线程间同步与互斥技巧


    哈喽,各位小伙伴们,你们好呀,我是喵手。

      今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

      我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

    小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

    前言

      在现代软件开发中,多线程是一项重要的技术,而线程间的同步与互斥是其中关键的一部分。本文将揭秘Java多线程同步的奥秘,帮助读者掌握线程间同步与互斥技巧。

    摘要

      本文将全面解析Java多线程同步技术,包括线程间通信、锁、条件变量等。我们将深入讨论如何实现线程的同步与互斥,以及应对线程安全问题的技巧。

    简介

      多线程编程中,线程间的同步与互斥是保证数据一致性和程序正确性的关键。在本节中,我们将简要介绍多线程编程的挑战和线程间同步的重要性。

    源代码解析

      通过源代码解析,我们将深入研究Java多线程同步的实现方式。我们将介绍关键的同步和互斥机制,如synchronized关键字、Lock、Condition等,并解释它们的用法和注意事项。

    应用场景案例

      本节将提供一些实际应用场景的案例,展示Java多线程同步技术在解决并发问题时的应用。包括生产者消费者模型、读写锁、线程池等常见场景的解决方案。

    应用场景案例

    生产者消费者模型

      生产者消费者模型是一个常见的并发场景,其中生产者线程负责生产数据并将其存入共享的缓冲区,而消费者线程从缓冲区中获取数据并进行处理。在这种情况下,使用合适的同步机制来保证生产者和消费者线程之间的同步和互斥是非常重要的。

    public class ProducerConsumerExample {
        public static void main(String[] args) {
            Buffer buffer = new Buffer();
            Thread producerThread = new ProducerThread(buffer);
            Thread consumerThread = new ConsumerThread(buffer);
            producerThread.start();
            consumerThread.start();
        }
    
        static class Buffer {
            private List<Integer> data = new ArrayList<>();
            private int maxSize = 5;
    
            public synchronized void produce(int value) throws InterruptedException {
                while (data.size() >= maxSize) {
                    wait();
                }
                data.add(value);
                System.out.println("Produced: " + value);
                notify();
            }
    
            public synchronized int consume() throws InterruptedException {
                while (data.size() == 0) {
                    wait();
                }
                int value = data.get(0);
                data.remove(0);
                System.out.println("Consumed: " + value);
                notify();
                return value;
            }
        }
    
        static class ProducerThread extends Thread {
            private Buffer buffer;
    
            public ProducerThread(Buffer buffer) {
                this.buffer = buffer;
            }
    
            @Override
            public void run() {
                try {
                    for (int i = 1; i <= 10; i++) {
                        buffer.produce(i);
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        static class ConsumerThread extends Thread {
            private Buffer buffer;
    
            public ConsumerThread(Buffer buffer) {
                this.buffer = buffer;
            }
    
            @Override
            public void run() {
                try {
                    for (int i = 1; i <= 10; i++) {
                        int value = buffer.consume();
                        Thread.sleep(2000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    代码解析:

      这里给大家详细解读下如上代码,该代码是一个生产者消费者模型的例子。生产者线程和消费者线程共享一个缓冲区,生产者线程向缓冲区中生产数据,消费者线程从缓冲区中消费数据。

    1. 在主函数中,创建了一个缓冲区对象和生产者线程对象、消费者线程对象,并启动了这两个线程。

    2. 缓冲区的produce方法用于生产数据。当缓冲区已满时,调用wait方法使该线程进入等待状态。当缓冲区有空闲位置时,生产数据并打印出来,然后调用notify方法唤醒可能在等待的其他线程。

    3. 缓冲区的consume方法用于消费数据。当缓冲区为空时,调用wait方法使该线程进入等待状态。当缓冲区有数据时,取出第一个数据并从缓冲区中移除,然后打印出来,最后调用notify方法唤醒可能在等待的其他线程。

    4. 生产者线程类和消费者线程类继承自Thread类,并在run方法中调用缓冲区的produceconsume方法,分别生产和消费数据。生产者线程每隔一秒生产一个数据,消费者线程每隔两秒消费一个数据。

    5. 整个程序的运行过程是,生产者线程先生产数据加入缓冲区,然后消费者线程从缓冲区中取出数据消费,然后再生产,再消费,循环往复。

    读写锁

    在某些场景下,读写操作的并发访问可能会导致数据不一致性和并发性能问题。为此,Java提供了ReentrantReadWriteLock类,它允许多个线程同时读取共享资源,但只允许一个线程进行写操作。

    public class ReadWriteLockExample {
        private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        private static String data = "";
    
        public static void main(String[] args) {
            Thread writer1 = new WriterThread("Thread 1");
            Thread reader1 = new ReaderThread("Thread 2");
            Thread reader2 = new ReaderThread("Thread 3");
    
            writer1.start();
            reader1.start();
            reader2.start();
        }
    
        static class WriterThread extends Thread {
            private String name;
    
            public WriterThread(String name) {
                this.name = name;
            }
    
            @Override
            public void run() {
                lock.writeLock().lock();
                try {
                    data += " Written by " + name;
                    System.out.println(data);
                } finally {
                    lock.writeLock().unlock();
                }
            }
        }
    
        static class ReaderThread extends Thread {
            private String name;
    
            public ReaderThread(String name) {
                this.name = name;
            }
    
            @Override
            public void run() {
                lock.readLock().lock();
                try {
                    System.out.println("Data read by " + name + ": " + data);
                } finally {
                    lock.readLock().unlock();
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    代码解析:

      这里给大家详细解读下如上代码,该代码演示了如何使用Java中的ReentrantReadWriteLock实现读写锁。

    1. 我们创建了一个ReentrantReadWriteLock对象,命名为lock,用于管理读写锁。然后,我们创建了一个静态字符串变量data,用于存储数据。在main方法中,我们创建了一个写线程writer1和两个读线程reader1和reader2。然后,我们启动这些线程。

    2. 写线程WriterThread是一个内部类,继承自Thread类。它接收一个名字作为参数,并重写了run方法。在run方法中,我们首先获取写锁(lock.writeLock().lock()),然后将数据写入data变量,并打印出来。最后,释放写锁(lock.writeLock().unlock())。

    3. 读线程ReaderThread是另一个内部类,也继承自Thread类。它接收一个名字作为参数,并重写了run方法。在run方法中,我们首先获取读锁(lock.readLock().lock()),然后打印出data变量的内容。最后,释放读锁(lock.readLock().unlock())。

      这里我们通过使用读写锁,我们可以实现多个线程同时读取数据,但只允许一个线程写入数据。这可以提高程序的并发性能。

      这里需要注意的是,在实际应用中,我们可以根据具体需求进行更多的错误处理和线程同步操作。例如,在数据读取过程中,如果写线程正在写入数据,我们可以通过使用Condition对象来挂起读线程。

    线程池

      当我们在需要处理大量任务的情况下,线程池是一种有效的方式来管理线程资源。Java提供了线程池框架,可以方便地创建和管理线程池,并可避免线程频繁创建和销毁的开销。

    public class ThreadPoolExample {
        public static void main(String[] args) {
            ExecutorService executor = Executors.newFixedThreadPool(3);
            for (int i = 0; i < 10; i++) {
                Task task = new Task(i);
                executor.execute(task);
            }
            executor.shutdown();
        }
    
        static class Task implements Runnable {
            private int id;
    
            public Task(int id) {
                this.id = id;
            }
    
            @Override
            public void run() {
                System.out.println("Task " + id + " is running on " + Thread.currentThread().getName());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    代码解析:

      这里给大家详细解读下如上代码,它具体演示了如何使用Java的Executor框架中的线程池来执行任务。

    1. 在主方法中创建了一个固定大小为3的线程池,即最多同时执行3个任务。

    2. 使用一个循环创建了10个任务,并将这些任务提交给线程池进行执行。每个任务都是一个Task对象,Task实现了Runnable接口,表示它是一个可以通过Thread运行的任务。当任务被执行时,它会打印出当前任务的ID和执行任务的线程的名称。

    3. 通过调用线程池的shutdown方法,告诉线程池不再接受新的任务,并且等待已经提交的任务执行完成。

      当所有的任务执行完成后,线程池会自动关闭。总的来说,这段代码展示了线程池的使用方式,通过将任务提交给线程池,可以实现并发执行任务的效果,并且可以控制并发的程度。

    小结

      我们在面对Java多线程同步,我们可以知道,它是实现高效并发编程的重要技术之一。通过掌握线程间同步与互斥的技巧,我们能够开发出性能优越、可靠稳定的多线程应用程序。在实际开发中,根据具体的场景和需求,选择合适的同步机制非常重要。希望本文能够帮助读者掌握Java多线程同步技术,高效应用于实际项目中,构建出高质量的并发应用程序。

    优缺点分析

      在本节中,我们将分析Java多线程同步技术的优点和缺点。我们将讨论性能开销、死锁和竞态条件等方面的考虑,以全面评估多线程同步的适用性。

    类代码方法介绍

      在本节中,我们将详细介绍Java中与线程同步相关的类和方法。我们将重点介绍synchronized关键字、Lock接口和Condition接口的用法和原理。

    具体的Java代码测试用例

      为了验证Java多线程同步的正确性和可靠性,我们将编写具体的Java代码测试用例。我们将模拟多线程并发访问共享资源的情况,观察同步机制的表现和效果。

    package com.example.javase.ms.threadDemo;
    
    /**
     * @Author ms
     * @Date 2023-12-16 18:05
     */
    public class ThreadSyncExample {
        public static void main(String[] args) {
            // 创建共享资源对象
            SharedResource resource = new SharedResource();
    
            // 创建多个线程并启动
            for (int i = 0; i < 5; i++) {
                // 传入共享资源对象
                Thread thread = new WorkerThread(resource);
                thread.start();
            }
        }
    
        static class SharedResource {
            // 共享资源数据和方法
        }
    
        static class WorkerThread extends Thread {
            private SharedResource resource;
    
            public WorkerThread(SharedResource resource) {
                this.resource = resource;
            }
    
            @Override
            public void run() {
                // 访问和操作共享资源
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    测试结果展示:

    根据如上测试用例,我本地执行结果如下:

    image.png

    代码解析:

      如下针对上述测试代码,给大家具体讲解下,仅供参考:

      如上代码我主要是演示了如何创建一个共享资源对象,并使用多个线程对这个共享资源进行访问和操作。

    1. 在main方法中,创建了一个SharedResource对象,并将其作为参数传递给WorkerThread的构造函数。然后使用一个循环创建了5个WorkerThread对象,并启动这些线程。

    2. 在SharedResource类中,可以定义共享资源的数据和方法,具体内容根据实际需求来确定。

    3. WorkerThread类继承自Thread类,并具有一个SharedResource对象作为成员变量。在run方法中,可以编写具体的代码来访问和操作共享资源。

      其中,这里需要注意的是,由于多个线程会同时访问共享资源,可能会导致竞态条件和数据不一致的问题。为了解决这个问题,可以使用同步(synchronization)机制来确保只有一个线程可以访问共享资源,从而避免并发访问的问题。具体的同步方式可以使用synchronized关键字或者Lock接口来实现。

      以上就是这段代码的简单解析,希望能够帮助同学们理解多线程编程中的共享资源访问和同步问题。

    全文小结

      在本节中,我们将对全文的要点进行小结,强调Java多线程同步的关键知识和技巧。我们将回顾线程同步的重要性和应用场景,并强调实践的重要性。

    总结

      通过本文的学习,我们深入了解了Java多线程同步的关键技术和技巧。掌握线程间同步与互斥的技术,对于保证程序的正确性和性能至关重要。我们鼓励读者深入学习和实践Java多线程同步技术,使用合适的同步机制解决并发编程中的问题。

      希望本文能够帮助读者全面理解和应用Java多线程同步技术,提升并发编程能力,构建高效可靠的多线程应用程序。通过灵活运用同步与互斥技巧,我们能够充分发挥多核处理器的性能,提高程序的并发性和响应能力。

    … …

    文末

    好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

    … …

    学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

    wished for you successed !!!


    ⭐️若喜欢我,就请关注我叭。

    ⭐️若对您有用,就请点赞叭。

    ⭐️若有疑问,就请评论留言告诉我叭。

  • 相关阅读:
    后端——缓存Cookie、缓存Session、cookies和Session的区别:
    SpringBoot系列之集成Redission入门与实践教程
    C++--map和set--1027
    ResNext架构解析:深度神经网络的聚合残差变换
    【云岚到家】-day02-2-客户管理-认证授权
    python获取本机IP
    卷积神经网络原理及其C++/Opencv实现(2)
    【线程的重要性】【线程和进程的区别】【多线程的运行优势,对比运行时间System.nanoTime();】
    杀疯了,GitHub疯传2022Java面试八股文解析+大厂面试攻略
    操作系统复习:进程间通信与常见IPC问题
  • 原文地址:https://blog.csdn.net/weixin_66592566/article/details/136818499