深入理解Java线程间通信

这篇具有很好参考价值的文章主要介绍了深入理解Java线程间通信。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


合理的使用Java多线程可以更好地利用服务器资源。一般来讲,线程内部有自己私有的线程上下文,互不干扰。但是当我们需要多个线程之间相互协作的时候,就需要我们掌握Java线程的通信方式。本文将介绍Java线程之间的几种通信原理。

锁与同步

在Java中,锁的概念都是基于对象的,所以我们又经常称它为对象锁。一个锁同一时间只能被一个线程持有。也就是说,一个锁如果被一个线程所持有,那其他线程如果需要得到这个锁,就得等这个线程释放该锁。

线程之间,有一个同步的概念。在多线程中,可能有多个线程试图访问一个有限的资源,必须预防这种情况的发生。

所以引入了同步机制:在线程使用一个资源时为其加锁,这样其他的线程便不能访问那个资源了,直到解锁后才可以访问。线程同步是线程之间按照一定的顺序执行。

我们先来看看一个无锁的程序:

public class NoLock {

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("Thread A " + i);
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("Thread B " + i);
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new ThreadA()).start();
        new Thread(new ThreadB()).start();
    }
}

执行这个程序,你会在控制台看到,线程A和线程B各自独立工作,输出自己的打印值。每一次运行结果都会不一样。如下是我的电脑上某一次运行的结果。

...
Thread B 74
Thread B 75
Thread B 76
Thread B 77
Thread B 78
Thread B 79
Thread B 80
Thread A 3
Thread A 4
Thread A 5
Thread A 6
Thread A 7
Thread A 8
Thread A 9
...

现在有一个需求,想等A先执行完之后,再由B去执行,怎么办呢?最简单的方式就是使用一个“对象锁”。

public class ObjLock {

    private static Object lock = new Object();

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("Thread A " + i);
                }
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("Thread B " + i);
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(10);
        new Thread(new ThreadB()).start();
    }
}

这里声明了一个名字为lock的对象锁。我们在ThreadAThreadB内需要同步的代码块里,都是用synchronized关键字加上了同一个对象锁lock

上文我们说到了,根据线程和锁的关系,同一时间只有一个线程持有一个锁,那么线程B就会等线程A执行完成后释放lock,线程B才能获得锁lock

这里在主线程里使用sleep方法睡眠了10毫秒,是为了防止线程B先得到锁。因为如果同时start,线程A和线程B都是出于就绪状态,操作系统可能会先让B运行。这样就会先输出B的内容,然后B执行完成之后自动释放锁,线程A再执行。

等待/通知机制

上面一种基于“锁”的方式,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会耗费服务器资源。
而等待/通知机制是另一种方式。

Java多线程的等待/通知机制是基于Object类的wait()方法和notify(), notifyAll()方法来实现的。

notify()方法会随机叫醒一个正在等待的线程,而notifyAll()会叫醒所有正在等待的线程。

前面我们讲到,一个锁同一时刻只能被一个线程持有。而假如线程A现在持有了一个锁lock并开始执行,它可以使用lock.wait()让自己进入等待状态。这个时候,lock这个锁是被释放了的。

这时,线程B获得了lock这个锁并开始执行,它可以在某一时刻,使用lock.notify(),通知之前持有lock锁并进入等待状态的线程A,说“线程A你不用等了,可以往下执行了”。

需要注意的是,这个时候线程B并没有释放锁lock,除非线程B这个时候使用lock.wait()释放锁,或者线程B执行结束自行释放锁,线程A才能得到lock锁。

用代码来实现一下:

public class WaitNotify {

    private static Object lock = new Object();

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    try {
                        System.out.println("ThreadA: " + i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    try {
                        System.out.println("ThreadB: " + i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }
}

// 输出:
ThreadA: 0
ThreadB: 0
ThreadA: 1
ThreadB: 1
ThreadA: 2
ThreadB: 2
ThreadA: 3
ThreadB: 3
ThreadA: 4
ThreadB: 4

线程A和线程B首先打印出自己需要的东西,然后使用notify()方法叫醒另一个正在等待的线程,然后自己使用wait()方法陷入等待并释放lock锁。

需要注意的是等待/通知机制使用的是使用同一个对象锁,如果你两个线程使用的是不同的对象锁,那它们之间是不能用等待/通知机制通信的。

信号量–Volatile

信号量(Semaphore) :有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。

本文不是要介绍这个类,而是介绍一种基于volatile关键字的自己实现的信号量通信。

volatile关键字能够保证内存的可见性,如果用volatile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的。

比如我现在有一个需求,我想让线程A输出0,然后线程B输出1,再然后线程A输出2…以此类推。我应该怎样实现呢?

public class Count {
    private static volatile int count = 0;

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            while (count < 5) {
                if (count % 2 == 0) {
                    System.out.println("threadA: " + count);
                    synchronized (this) {
                        count++;
                    }
                }
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            while (count < 5) {
                if (count % 2 == 1) {
                    System.out.println("threadB: " + signal);
                    synchronized (this) {
                        count++;
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }
}

// 输出:
threadA: 0
threadB: 1
threadA: 2
threadB: 3
threadA: 4

我们可以看到,使用了一个volatile变量count来实现了“信号量”的模型。这里需要注意的是,volatile变量需要进行原子操作,而count++并不是一个原子操作,根据需要使用synchronized给它“上锁”,或者是使用AtomicInteger等原子类。

信号量的应用场景

假如在一个停车场中,车位是我们的公共资源,线程就如同车辆,而看门的管理员就是起的“信号量”的作用。
因为在这种场景下,多个线程需要相互合作,我们用简单的“锁”和“等待通知机制”就不那么方便了。这个时候就可以用到信号量。

管道输入/输出流

管道是基于“管道流”的通信方式。JDK提供了PipedWriterPipedReaderPipedOutputStreamPipedInputStream

其中,前面两个是基于字符的,后面两个是基于字节流的。

以下示例代码使用的是基于字符的:

public class Pipe {
    static class ReaderThread implements Runnable {
        private PipedReader reader;

        public ReaderThread(PipedReader reader) {
            this.reader = reader;
        }

        @Override
        public void run() {
            System.out.println("this is reader");
            int receive = 0;
            try {
                while ((receive = reader.read()) != -1) {
                    System.out.print((char)receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    static class WriterThread implements Runnable {

        private PipedWriter writer;

        public WriterThread(PipedWriter writer) {
            this.writer = writer;
        }

        @Override
        public void run() {
            System.out.println("this is writer");
            int receive = 0;
            try {
                writer.write("test");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        PipedWriter writer = new PipedWriter();
        PipedReader reader = new PipedReader();
        writer.connect(reader);

        new Thread(new ReaderThread(reader)).start();
        Thread.sleep(1000);
        new Thread(new WriterThread(writer)).start();
    }
}

// 输出:
this is reader
this is writer
test

我们通过线程的构造函数,传入了PipedWritePipedReader对象。可以简单分析一下这个示例代码的执行流程:

  1. 线程ReaderThread开始执行,
  2. 线程ReaderThread使用管道reader.read()进入”阻塞“,
  3. 线程WriterThread开始执行,
  4. 线程WriterThread用writer.write(“test”)往管道写入字符串,
  5. 线程WriterThread使用writer.close()结束管道写入,并执行完毕,
  6. 线程ReaderThread接受到管道输出的字符串并打印,
  7. 线程ReaderThread执行完毕。

管道通信的应用场景

这个很好理解。使用管道多半与I/O流相关。当我们一个线程需要先另一个线程发送一个信息(比如字符串)或者文件等等时,就需要使用管道通信了。

Thread.join()方法

join()方法是Thread类的一个实例方法。它的作用是让当前线程陷入“等待”状态,等join的这个线程执行完成后,再继续执行当前线程。

有时候,主线程创建并启动了子线程,如果子线程中需要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。

如果主线程想等待子线程执行完毕后,获得子线程中的处理完的某个数据,就要用到join方法了。

示例代码:

public class Join {
    static class ThreadA implements Runnable {

        @Override
        public void run() {
            try {
                System.out.println("我是子线程,我先睡一秒");
                Thread.sleep(1000);
                System.out.println("我是子线程,我睡完了一秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ThreadA());
        thread.start();
        thread.join();
        System.out.println("如果不加join方法,我会先被打出来,加了就不一样了");
    }
}

ThreadLocal类

ThreadLocal是一个本地线程副本变量工具类。内部是一个弱引用的Map来维护。

严格来说,ThreadLocal类并不属于多线程间的通信,而是让每个线程有自己”独立“的变量,线程之间互不影响。它为每个线程都创建一个副本,每个线程可以访问自己内部的副本变量。

ThreadLocal类最常用的就是set方法和get方法。示例代码:

public class Profiler {

    private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() {
        protected Long initialValue() {
            return System.currentTimeMillis();
        }
    };

    public static void begin() {
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    public static long end() {
        return System.currentTimeMillis() - TIME_THREADLOCAL.get();
    }

    public static void main(String[] args) throws InterruptedException {
        Profiler.begin();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("耗时:" + Profiler.end() + "mills");
    }
}

// 输出:
耗时:1001mills

Profiler 可以被复用在方法的耗时统计的功能上,在方法的入口前执行begin()方法,在方法调用后执行end()方法,好处是两个方法的调用不用在一个方法和类中,比如在AOP(面向切面编程)中,可以在方法调用前的切入点执行begin()方法,而在方法调用切入点执行end()方法,这样依旧可以获得方法的执行耗时。

ThreadLocal的应用场景

最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。数据库连接和Session管理涉及多个复杂对象的初始化和关闭。如果在每个线程中声明一些私有变量来进行操作,那这个线程就变得不那么“轻量”了,需要频繁的创建和关闭连接。

总结

线程间通信使线程成为一个整体,提高系统之间的交互性,在提高CPU利用率的同时可以对线程任务进行有效的把控与监督。文章来源地址https://www.toymoban.com/news/detail-483872.html

到了这里,关于深入理解Java线程间通信的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【多线程系列-03】深入理解java中线程的生命周期,任务调度

    多线程系列整体栏目 内容 链接地址 【一】深入理解进程、线程和CPU之间的关系 https://blog.csdn.net/zhenghuishengq/article/details/131714191 【二】java创建线程的方式到底有几种?(详解) https://blog.csdn.net/zhenghuishengq/article/details/127968166 【三】深入理解java中线程的生命周期,任务调度 ht

    2024年02月17日
    浏览(49)
  • 深入理解 Java 多线程、Lambda 表达式及线程安全最佳实践

    线程使程序能够通过同时执行多个任务而更有效地运行。 线程可用于在不中断主程序的情况下在后台执行复杂的任务。 创建线程 有两种创建线程的方式。 扩展Thread类 可以通过扩展Thread类并覆盖其run()方法来创建线程: 实现Runnable接口 另一种创建线程的方式是实现Runnable接口

    2024年03月15日
    浏览(54)
  • 深入理解Java线程池ThreadPoolExcutor实现原理、数据结构和算法(源码解析)

    什么是线程池?         线程池主要是为了解决执行新任务执行时,应用程序为减少为任务创建一个新线程和任务执行完毕时销毁线程所带来的开销。通过线程池,可以在项目初始化时就创建一个线程集合,然后在需要执行新任务时重用这些线程而不是每次都新建一个线

    2024年02月07日
    浏览(45)
  • 华为云出品《深入理解高并发编程:Java线程池核心技术》电子书发布

    系统拆解线程池核心源码的开源小册 透过源码看清线程池背后的设计和思路 详细解析AQS并发工具类 点击下方链接进入官网,右上角搜索框搜索“《深入理解高并发编程:Java线程池核心技术》” 即可获取下载。 https://auth.huaweicloud.com/authui/login.html?locale=zh-cnservice=https%3A%2F%2F

    2024年02月16日
    浏览(38)
  • 深入理解多线程编程

    title: 深入理解多线程编程 date: 2024/4/25 17:32:02 updated: 2024/4/25 17:32:02 categories: 后端开发 tags: 线程同步 互斥锁 死锁避免 竞态条件 线程池 异步编程 性能优化 线程 :在操作系统中,一个程序可以被划分为多个执行流,每个执行流就是一个独立的线程。线程是进程中的一个执行实

    2024年04月25日
    浏览(37)
  • 深入理解Java LinkedList:使用场景与实际应用

    在Java的世界里,集合框架是不可或缺的一部分,它提供了一系列用于存储和操作数据集合的接口和类。其中, LinkedList 作为List接口的一个实现,经常被拿来与 ArrayList 做对比。尽管在实际业务开发中 LinkedList 的使用频率可能不如 ArrayList 高,但这并不意味着它是一个冷门或无

    2024年01月24日
    浏览(57)
  • 深入理解MySQL InnoDB线程模型

    当我们谈论数据库性能时,存储引擎的线程模型是一个不可忽视的方面。MySQL的InnoDB存储引擎,作为目前最受欢迎的存储引擎之一,其线程模型的设计对于实现高并发、高性能的数据操作至关重要。在本文中,我们将深入探讨MySQL InnoDB线程模型的工作原理和关键组件。 在Inn

    2024年01月25日
    浏览(37)
  • 深入理解Flink Mailbox线程模型

    Mailbox线程模型通过引入阻塞队列配合一个Mailbox线程的方式,可以轻松修改StreamTask内部状态的修改。Checkpoint、ProcessingTime Timer的相关操作(Runnable任务),会以Mail的形式保存到Mailbox内的阻塞队列中。StreamTask在invoke阶段的runMailboxLoop时期,就会轮询Mailbox来处理队列中保存的M

    2024年02月12日
    浏览(41)
  • 深入理解JVM:Java使用new创建对象的流程

            ①new 对象         ②反射         ③对象的复制         ④反序列化 先看看常量池里面有没有,如果有,就用常量池的 看这个类有没有被加载过,如果没有,就执行类加载以及类的初始化。(对象的大小,在类加载的时候就确定了)。 对象在堆内存

    2024年02月15日
    浏览(66)
  • 深入理解高并发编程 - 线程的执行顺序

    在Java中,线程的执行顺序是由操作系统的调度机制决定的,具体顺序是不确定的,取决于多个因素,如操作系统的调度策略、线程的优先级、线程的状态转换等。因此,不能对线程的执行顺序做出可靠的假设。 以下是一个简单的Java代码示例,演示了多个线程的执行顺序是不

    2024年02月14日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包