极客Java进阶训练营6期2022最新
深刻理解Java线程间通信。
合理使用Java多线程可以更好的利用服务器资源。一般来说,线程都有自己的私有线程上下文,互不干扰。但是当我们需要多个线程相互协作时,就需要掌握Java线程的通信方式。本文将介绍Java线程之间的几个通信原则。
锁定同步
在Java中,锁的概念是基于对象的,所以我们经常称之为对象锁。一个锁一次只能被一个线程持有。也就是说,如果一个锁被一个线程持有,其他线程如果需要获得锁,就必须等待该线程释放锁。
线程之间,有一个同步的概念。在多线程中,可能有多个线程试图访问一个有限的资源,这是必须防止的。
因此,引入了一种同步机制:当一个线程使用一个资源时,它会将其锁定,以便其他线程无法访问该资源,直到它被解锁。同步是线程按照一定的顺序执行。
这里声明了一个名为lock的对象锁。在ThreadA和ThreadB中需要同步的代码块中,使用synchronized关键字添加了相同的对象锁。
如上所述,根据线程和锁的关系,同一时刻只有一个线程持有锁,所以线程B会在线程A完成执行后释放锁,然后线程B就可以获得锁的锁。
这里,主线程使用sleep方法休眠10毫秒,以防止线程B首先获得锁。因为线程A和线程B在同时启动时都处于就绪状态,所以操作系统可能会让B先运行。这样会先输出B的内容,B执行完后锁会自动释放,线程A会重新执行。
等待/通知机制
在上述基于“锁”的方法中,线程需要不断尝试获取锁,如果失败,则继续尝试。这可能会消耗服务器资源。
等待/通知机制是另一种方式。
Java多线程等待/通知机制是基于对象类的wait()方法和notify()和notify all()方法。
notify()方法将随机唤醒一个等待的线程,而notifyAll()将唤醒所有等待的线程。
如前所述,一个锁一次只能被一个线程持有。如果线程A现在持有一个锁并开始执行它,它可以使用lock.wait()将自己置于等待状态。这时,锁被打开了。
此时,线程B已经获得锁,并开始执行它。在某个时刻,它可以使用lock.notify()通知之前持有锁并进入等待状态的线程A,说“线程A,你不用等了,可以执行下来了”。
需要注意的是,线程B此时并没有释放锁锁。除非此时线程B使用lock.wait()释放锁,或者线程B在执行结束时自己释放锁,否则线程A可以获得锁lock。
线程A和线程B先打印出自己需要的东西,然后用notify()方法唤醒另一个等待的线程,再用wait()方法陷入等待,释放锁。
注意,等待/通知机制使用相同的对象锁。如果你的两个线程使用不同的对象锁,它们就不能通过等待/通知机制相互通信。
信号量-易失性
信号量(Semaphore):有时称为信号量,它是多线程环境中使用的一种工具,可用于确保两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获得一个信号量;一旦关键代码段完成,线程必须释放信号量。想要进入这个关键代码段的其他线程必须等到第一个线程释放信号量。
本文不是介绍这个类,而是介绍一个基于volatile关键字的自实现信号量通信。
volatile关键字可以确保内存的可见性。如果一个变量是用volatile关键字声明的,并且这个变量的值在一个线程中发生了变化,那么这个变化的值将立即被其他线程看到。
比如我现在有一个要求。我希望线程A输出0,然后线程B输出1,然后线程A输出2…以此类推。我该如何实现?
我们可以看到,使用了一个易变变量count来实现“信号量”的模型。这里需要注意的是,volatile变量需要原子操作,而count++不是原子操作。如果有必要,使用synchronized来“锁定”它,或者使用AtomicInteger之类的原子类。
信号量的应用场景
如果在一个停车场,车位是我们的公共资源,线程就像车辆,看门人扮演的是“旗语”的角色。
因为在这种场景下,多线程需要相互配合,所以我们使用简单的“锁”和“等待通知机制”就不是那么方便了。这时候信号量就可以用了。
输入/输出流
管道是一种基于“管道流”的通信方式。JDK提供PipedWriter、PipedReader、PipedOutputStream和PipedInputStream。
其中,前两种是基于字符的,后两种是基于字节流的。
下面的示例代码使用基于字符的
我们通过线程的构造函数传入了PipedWrite和PipedReader对象。您可以简要分析一下这个示例代码的执行过程:
线程阅读器thread开始执行,
线程ReaderThread使用管道reader.read()进入“块”,
WriterThread开始执行,
WriterThread用writer . write(“test”)将字符串写入管道,
WriterThread使用writer.close()结束管道写入并完成执行。
线程阅读器thread接收管道输出的字符串并打印出来,
线程读取器线程已完成执行。
管道通信的应用场景
这个很好理解。使用的大部分管道都与I/O流有关。当一个线程需要先向另一个线程发送消息(比如字符串)或文件时,我们需要使用管道通信。
Thread.join()方法
join()方法是Thread类的实例方法。它的作用是将当前线程置于“等待”状态,然后在join的这个线程执行完成后继续执行当前线程。
有时,主线程创建并启动子线程。如果子线程中需要大量耗时的操作,主线程往往会在子线程结束之前结束。
如果主线程希望等待子线程完成执行,并在子线程中获得一些已处理的数据,将使用join方法。
Profiler可以在耗时的方法统计功能中重用。begin()方法在方法入口之前执行,end()方法在方法调用之后执行。优点是这两个方法不会在同一个方法和类中调用。比如在AOP(面向方面编程)中,begin()方法可以在方法调用之前的切入点执行,end()方法在方法调用的切入点执行,这样仍然可以得到方法的执行时间。
ThreadLocal的应用场景
ThreadLocal最常见的使用场景是解决数据库连接、会话管理等。数据库连接和会话管理涉及多个复杂对象的初始化和关闭。如果在每个线程中声明一些私有变量来操作,那么这个线程就变得不那么“轻量级”了,需要频繁地创建和关闭连接。
总结
线程间通信使线程成为一个整体,提高了系统间的交互性,在提高CPU利用率的同时,可以有效地控制和监督线程任务。
极客Java进阶训练营6期2022最新
download链接:https://pan.baidu.com/s/19YvzMYY7GXtSY7Hfr_THeQ?pwd=74yp 
提取码:74yp 
--来自百度网盘超级会员V5的分享