Java进阶——多线程之线程间的通信、同步、等待唤醒机制小结

版权声明:本文为博主原创文章,遵循 CC 4.0 BY 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/CrazyMo_/article/details/94486236

引言

在项目开发过程中,当多个线程同时操作一个共享资源时,如果不进行同步处理,运行以下代码

public class ThreadInteract {
	final static Object OBJ=new Object();
	public static void main(String[] args) {
		Resource res = new Resource();
		new Thread(new InputThread(res)).start();
		new Thread(new OutputThread(res)).start();
	}
}
//共享资源
class Resource {
	String name;
	String sex;
}
class InputThread implements Runnable {
	private Resource res;// 要操作的资源对象
	public InputThread(Resource res) {
		this.res = res;
	}
	public void run() {
		int flag = 0;
		while (true && !Thread.interrupted()) {
			if (flag == 0) {
				res.name = "CrazyMo";
				res.sex = "Man";
			} else {
				res.name = "钟红红";
				res.sex = "女女女";
			}
			flag = (flag + 1) % 2;
		}
	}
}
class OutputThread implements Runnable {
	private Resource res;// 要操作的资源对象
	public OutputThread(Resource res) {
		this.res = res;
	}
	public void run() {
		while (true && !Thread.interrupted()) {
			System.out.println("读取资源:[" + res.name + " , " + res.sex + "]");
		}
	}
}

就很可能导致如下的情况:
在这里插入图片描述

一、多线程之间的通讯与同步

为了保证多线程访问共享资源时数据安全,Java 提供了同步机制,在同步机制中对象就如同监视器,只有持有监视器的线程才可以在同步中执行,没有获得锁即使获取到了CPU的执行权也无法执行同步中的代码语句。其中监视器(即锁)是控制多个线程对共享资源进行访问的工具,通常来说锁也提供了对共享资源的独占访问权,一次只能有一个线程获得锁,对所有共享资源的访问都需要先获取锁,通俗来说是通过标志来判断的,有两个标记位0和1,默认为0代表锁住,当线程进入同步时,首先去判断标记位,符合则允许访问,反之则不允许,从而实现锁的功能。(仅仅是一个通俗地比喻事实上更为复杂些),

二、使用同步(synchonize)

从上面简单的例子可以看到,正确的操作的结果应该是橙色方框部分的,而红色部分的数据混乱了,根本原因就是线程之间是相互独立的,即使都对同一共享对象进行操作时,也无法得知彼此的状态,可能这个InputThread刚刚从共享对象res写入CrazyMo时,此时OutputThread 抢到CPU的执行权进行读取了(此时本应该还要写入sex字段的,但是被OutputThread 抢夺了资源从而导致InputThread还没来得及写入sex,直接读取了上一次的值),从而可能会导致线程安全性的问题,这就需要线程之间进行通信才能确保线程安全,而所谓线程之间的通讯,本质上就是不同的线程之间对同一公有资源进行不同的操作,接下来让我们借助同步把上面的例子进行逐步改造,我们都知道解决线程数据安全的简单的方法就是同步(synchronize)方法或者同步代码块,加入了synchronize 之后,共享资源对象就被”锁”住了:

class InputThread implements Runnable {
	public void run() {
		int flag = 0;
		while (true && !Thread.interrupted()) {
			synchronized (res) {
				if (flag == 0) {
					res.name = "CrazyMo";
					res.sex = "Man";
				} else {
					res.name = "钟红红";
					res.sex = "女女女";
				}
				flag = (flag + 1) % 2;
			}
		}
	}
}

class OutputThread implements Runnable {
	public void run() {
		while (true && !Thread.interrupted()) {
			synchronized (res) {
				System.out.println("读取资源:[" + res.name + " , " + res.sex + "]");
			}
		}
	}
}

在这里插入图片描述
使用同步机制时需要满足以下的前提:

  • 在所有线程中操作共享资源的代码块中都需要加入了同步机制
  • 操作共享资源对象的线程的同步中必须使用同一个锁,无论是Object锁、.class锁、类锁、对象锁功能上都是一样的,没有本质区别。

虽然使用synchronized 同步机制之后,就可以确保多个线程操作同一共享对象时线程安全。但是却不太符合实际的生产环境需求,我们想要的状态是设置资源一次,马上读取资源一次,如此交替执行,才能确保数据没有被覆盖或者丢失。假如InputThread先执行拿到CPU执行权时开始进入同步代码块并往共享对象写入值的某一时刻,OutputThread 抢夺了CPU执行权输出多次,而造成上面的原因是CPU切换引起的。为了更好的实现这样的需求,可以借助JDK 1.5之前的等待唤醒机制。

三、等待-唤醒机制

JDK 1.5之前提供的等待-唤醒机制,主要是通过Object类中的wait、notify、notifyAll方法实现的。

  • wait方法——调用这个方法的线程将被放入到系统的等待线程池中,相当于是主动放弃了CPU的执行权,该线程则进入了冻结状态,而且调用这个方法的线程必须要拥有对应的监视器(即锁),直到其他线程通过调用notify或notifyAll方法通知锁对应的线程醒来,只有重新获取锁之后才能继续执行,

  • notify——唤醒线程池中的第一个线程,唤醒了线程之后,仅仅是意味着重新获取争取CPU执行权的资格。

  • notifyAll——唤醒线程池中的所有线程

因为这些方法都是使用在同步机制中(因为需要对持有监视器的线程操作),无论是使用wait还是notify方法时都必须显式标示出调用这些方法的锁对象,只有同一个被wait的线程才能被同一个锁上notify方法唤醒即等待和唤醒必须是同一个锁,而锁可以是任意对象,因此定义在Object方法中,假如同步机制之后最终的代码如下:

public class ThreadInteract {
final static Object OBJ=new Object();
public static void main(String[] args) {
Resource res = new Resource();
new Thread(new InputThread(res)).start();
new Thread(new OutputThread(res)).start();
}
}

class Resource {
private String name;
private String sex;
boolean canRead=false;//默认为false 不可以读,需要等待写了之后才可以进行读操作

public synchronized void set(String name,String sex){
if(canRead){
try {
//表示调用这个方法后,会使得此时持有res对象锁的线程wait
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name=name;
this.sex=sex;
canRead=true;
this.notify();
}

public synchronized void output(){
if(!canRead){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("读取资源:[" + this.name + " , " + this.sex + "]");
canRead=false;
this.notify();
}
}

class InputThread implements Runnable {
private Resource res;// 要操作的资源对象

public InputThread(Resource res) {
this.res = res;
}

public void run() {
int flag = 0;
while (true && !Thread.interrupted()) {
if (flag == 0) {
res.set("CrazyMo","Man");
} else {
res.set("钟红红","女女女");
}
flag = (flag + 1) % 2;
}
}
}

class OutputThread implements Runnable {
private Resource res;// 要操作的资源对象

public OutputThread(Resource res) {
this.res = res;
}

public void run() {
// /Object obj=new Object();
while (true && !Thread.interrupted()) {
res.output();
}
}
}

在这里插入图片描述
此处是2个线程交互的例子,如果改成4个或者多个呢,以上实现是否能支持?预知后事如何请看下文。

猜你喜欢

转载自blog.csdn.net/CrazyMo_/article/details/94486236