java基础-线程通信
作者:互联网
目录
1、通信的方式:
要想实现多个线程之间的协同,如:线程的执行顺序、获取某个线程的执行结果等。
涉及到线程之间的通信,分为下面四类:
①文件共享
②网络共享
③共享变量
④JDK提供的线程协调API:suspend/resume、wait/notify、park/unpark
1.1、文件共享
线程1写数据到文件里,线程2读取文件中的数据内容,实现数据的交换
代码示例:
import java.nio.file.Files;
import java.nio.file.Paths;
public class Text {
//共享文件
public static String filePath= "text.txt";
public static void main(String[] args) throws Exception {
//线程1写入数据
new Thread(()->{
try{
while(true){
Files.write(Paths.get(filePath),("当前时间" + String.valueOf(System.currentTimeMillis())).getBytes());
Thread.sleep(1000L);
}
}catch (Exception e){
e.printStackTrace();
}
}).start();
//线程2,读数据
new Thread(() ->{
try{
while(true){
Thread.sleep(1000L);
byte[] allBytes = Files.readAllBytes(Paths.get(filePath));
System.out.println(new String(allBytes));
}
}catch (Exception e){
e.printStackTrace();
}
}).start();
}
}
1.2、变量共享
线程1写数据到内存里(某个变量),线程2读取内存中(某个变量)的数据内容,实现数据的交换
代码示例:
public class Text {
//共享变量
public static String content = "";
public static void main(String[] args) throws Exception {
//线程1写入数据
new Thread(()->{
try{
while(true){
content = "当前时间" + String.valueOf(System.currentTimeMillis());
Thread.sleep(1000L);
}
}catch (Exception e){
e.printStackTrace();
}
}).start();
//线程2,读数据
new Thread(() ->{
try{
while(true){
Thread.sleep(1000L);
System.out.println(new String(content));
}
}catch (Exception e){
e.printStackTrace();
}
}).start();
}
}
1.3、线程协作-JDK API
JDK中对于需要多线程协作完成某一任务场景,提供了对于API支持。
多线程协作的典型场景:生产者-消费者模型。(线程阻塞,线程唤醒)
示例1:线程1去买包子,没有包子,则不再执行。线程2生产包子,通知线程1继续执行。
场景:线程1买包子,包子店没有包子则等待。线程2生产包子,并通知线程1可以买包子了。
1.3.1suspend/remuse
API-被弃用的suspend挂起目标线程,通过remuse可以恢复线程执行。
太容易产生死锁,所以被弃用
正常用法:
public class Text {
//共享变量
public static Object baozidian = null;
public static void main(String[] args) throws Exception {
Thread consumerThread = new Thread(()->{
try{
if (baozidian == null){
System.out.println("暂时没有包子,进入等待...");
Thread.currentThread().suspend();//消费者卡在这,等待通知
}
System.out.println("买到包子 回家!");
}catch (Exception e){
e.printStackTrace();
}
});
consumerThread.start();
//主线程等待3秒,再生产包子。 让thread线程先执行
Thread.sleep(3000L);
baozidian = new Object();
consumerThread.resume();
System.out.println("生产了包子,通知消费者可以购买!");
}
}
结果:
suspend/remuse死锁写法:
第一种:
死锁的suspend/resume。 suspend并不会像wait一样释放锁,故此容易写出死锁代码
public class Text {
//共享变量
public static Object baozidian = null;
public static void main(String[] args) throws Exception {
Thread consumerThread = new Thread(()->{
try{
if (baozidian == null){
System.out.println("暂时没有包子,进入等待...");
synchronized(Text.class){ //拿到锁,
Thread.currentThread().suspend();//挂起阻塞,并没有释放锁
}
}
System.out.println("买到包子 回家!");
}catch (Exception e){
e.printStackTrace();
}
});
consumerThread.start();
//主线程等待3秒,再生产包子。 让consumerThread线程先执行
Thread.sleep(3000L);
baozidian = new Object();
synchronized(Text.class){ //consumerThread没有释放锁,拿不到锁。
consumerThread.resume();//拿不到锁 唤醒不了consumerThread。产生死锁
}
System.out.println("生产了包子,通知消费者可以购买!");
}
}
结果:
consumerThread 线程拿到锁,没有释放就挂起。主线程拿不到锁,不能唤醒consumerThread线程,从而 产生死锁
suspend/remuse死锁写法:
第二种:
remuse先通知唤醒线程consumerThread。suspend后面又使线程consumerThread挂起。得不到通知,导致永久挂起阻塞不能执行。
public class Text {
//共享变量
public static Object baozidian = null;
public static void main(String[] args) throws Exception {
Thread consumerThread = new Thread(()->{
if (baozidian == null){
System.out.println("暂时没有包子,进入等待...");
try{
Thread.sleep(5000L); //模拟处理时间,等待5秒
}catch (Exception e){
e.printStackTrace();
}
Thread.currentThread().suspend();//挂起阻塞
}
System.out.println("买到包子 回家!");
});
consumerThread.start();
//主线程等待3秒,再生产包子。 让consumerThread线程先执行
Thread.sleep(3000L);
baozidian = new Object();
consumerThread.resume();//通知 consumerThread 执行
System.out.println("生产了包子,通知消费者可以购买!");
}
}
1.3.2 wait/notify机制
这个方法只能由同一对象锁的持有者线程调用,也就是写在同步块里,否则会抛出illegalmonitorStateException异常。
wait 导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁。
notify/notifyAll 方法唤醒一个或所有正在等待这个对象锁的线程。
推荐理由:wait可以自动解锁,但是对顺序执行有要求,需要先wait后notify
注意:
虽然会wait自动解锁,但是对顺序有要求,如果在notify被调用之后,才开始wait方法的调用,线程会永远处于WAITING状态。
public class Text {
public static Object baozidian = null;
/** 正常的wait/notify */
public void waitNotifyTest() throws Exception {
// 启动线程
new Thread(() -> {
if (baozidian == null) { // 如果没包子,则进入等待
synchronized (this) {
try {
System.out.println("1、进入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2、买到包子,回家");
}).start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
synchronized (this) {
this.notifyAll();
System.out.println("3、通知消费者");
}
}
public static void main(String[] args) throws Exception {
Text text = new Text();
text.waitNotifyTest();;
}
}
上述代码:先执行wait方法使线程等待,后执行notifyAll方法唤醒所有等待的线程。
导致程序永久等待的wait/notify
notify先通知唤醒,后面执行wait使线程等待。notify已经执行,不再唤醒,永远等待
但是wait释放锁,所以比较好
public class Text {
public static Object baozidian = null;
/** 会导致程序永久等待的wait/notify */
public void waitNotifyDeadLockTest() throws Exception {
// 启动线程
new Thread(() -> {
if (baozidian == null) { // 如果没包子,则进入等待
try {
Thread.sleep(5000L);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
synchronized (this) {
try {
System.out.println("1、进入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2、买到包子,回家");
}).start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
synchronized (this) {
this.notifyAll();
System.out.println("3、通知消费者");
}
}
public static void main(String[] args) throws Exception {
Text text = new Text();
text.waitNotifyDeadLockTest();
}
}
1.3.3 park/unpark机制
线程调用park则等待“”许可”,unpark方法为指定线程提供“许可(permit)”。
不要求park/unpark的调用顺序。
多次调用unpark之后,再调用park,线程会直接运行。
多次调用unpark,只能得到一次“许可”,不会叠加许可。再调用park获得“许可”,直接运行,消耗一次许可。再调用park,线程则进入等待状态。当unpark又提供一次许可。则线程继续执行一次。
正常的park/unpark
先LockSupport.park()将当前线程,也就是消费者线程挂起,3秒之后,主线程 LockSupport.unpark(consumerThread);指定消费者线程继续执行。
执行的先后顺序,不影响。
/** 正常的park/unpark */
public void parkUnparkTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (baozidian == null) { // 如果没包子,则进入等待
System.out.println("1、进入等待");
LockSupport.park();
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
LockSupport.unpark(consumerThread);
System.out.println("3、通知消费者");
}
死锁的park/unpark
park并不是基于监视器锁的方式实现的,jvm底层提供的另外一种线程挂起方式。
park拿到锁,使当前消费者线程挂起。 unpark拿不到锁,无法是消费者线程继续执行
/** 死锁的park/unpark */
public void parkUnparkDeadLockTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (baozidian == null) { // 如果没包子,则进入等待
System.out.println("1、进入等待");
// 当前线程拿到锁,然后挂起
synchronized (this) {
LockSupport.park();
}
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
// 争取到锁以后,再恢复consumerThread
synchronized (this) {
LockSupport.unpark(consumerThread);
}
System.out.println("3、通知消费者");
}
2、总结
在同步代码块中:
suspend/resume用法:(被弃用)
对同步锁的使用有要求,不会释放锁,容易死锁。对顺序有要求,先调用suspend后resume。容易导致永久挂起。
wait/notify用法:
对同步锁没有要求,wait方法会自动释放锁。对顺序有要求,先调用wait后notify。容易导致永久挂起。
park/unpark用法:
对同步锁的使用有要求,不会释放锁,容易死锁。对顺序没有要求,park挂起消费者线程,unpak给消费者线程执行“许可”后,消费者线程继续执行。
虽然都有缺陷,但是都比已经弃用的suspend/resume要好。
3、伪唤醒
警告!之前代码用if语句来判断,是否进入等待状态,是错误的。
官方建议应该在循环中检查等待条件,原因是处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。
伪唤醒是指:线程并非是notify、notifyAll、unpark等api调用而唤醒的,是更底层的原因导致的。
在上面所有例子中,将if改为while,防止伪唤醒。
4、结语
本章内容,设计很多JDK多线程开发工具类,它底层实现的原理。
标签:java,Thread,包子,通信,System,consumerThread,线程,public 来源: https://blog.csdn.net/LemonSnm/article/details/122219055