Java多线程——线程之间的协作

摘要:本文主要学习多线程之间是如何协作的,以及如何使用wait()方法与notify()/notifyAll()方法。

部分内容来自以下博客:

https://www.cnblogs.com/hapjin/p/5492645.html

https://www.cnblogs.com/sharpxiajun/p/2295677.html

https://www.cnblogs.com/90zeng/p/java_multithread_2.html

为什么线程之间需要进行协作

在多线程并发的情况下,如果都对共享资源进行操作,那么会导致线程安全问题,所以我们使用线程的同步机制来保证多线程环境下程序的安全性,但是使用同步机制只能保证线程安全,并不能在两个线程或者多个线程之间自由切换,线程的切换完全受CPU的影响。

如果使用同步机制让两个线程交替打印1到10的数字,代码如下:

 public class Demo {
public static void main(String[] args) {
DemoThread demoThread = new DemoThread();
Thread a = new Thread(demoThread, "线程A");
Thread b = new Thread(demoThread, "线程B");
a.start();
b.start();
}
} class DemoThread implements Runnable {
private int num = 1; @Override
public void run() {
while (num <= 10) {
synchronized (DemoThread.class) {
if (num <= 10) {
System.out.println(Thread.currentThread().getName() + " >>> " + num++);
}
}
}
}
}

运行结果如下:

 线程A >>> 1
线程A >>> 2
线程A >>> 3
线程A >>> 4
线程B >>> 5
线程B >>> 6
线程B >>> 7
线程B >>> 8
线程B >>> 9
线程B >>> 10

结果说明:

因为两个线程的调度完全受CPU时间片的影响,只有当一个线程运行时间结束后,另一个线程才能运行,并不能实现在线程运行的过程中进行切换。

如果我们想让两个线程交替打印数字,那么很显然同步机制是做不到的,这时候就需要两个线程的协作,让两个线程之间进行通信。

线程的等待唤醒机制

要达到上面所说的两个线程交替打印,需要两个线程进行通信,当第一个线程打印了之后,把自己锁起来,唤醒第二个线程打印,当第二个线程打印之后,也把自己锁起来,唤醒第一个线程,这样就可以实现两个线程的交替打印了。

线程的协作就是线程的通信,比如有A和B两个线程,A和B都可以独立运行,A和B有时也会做信息的交换,这就是线程的协作了。在Java里线程的协作是通过Object类里的wait()和notify()/和notifyAll()来实现的。

涉及的方法

◆ wait()

该方法会导致当前线程等待,直到其他线程调用了此线程的notify()或者notifyAll()方法。注意到wait()方法会抛出异常,所以在面我们的代码中要对异常进行捕获处理。

◆ wait(long timeout)

该方法与wait()方法类似,唯一的区别就是在指定时间内,如果没有notify或notifAll方法的唤醒,也会自动唤醒。wait(0)等效于wait()。

◆ nofity()

唤醒线程池中任意一个线程。

◆ notifyAll()

唤醒线程池中的所有线程。

方法的使用说明

这些方法都必须定义在同步中。因为这些方法是用于操作线程状态的方法,所以必须要明确到底操作的是哪个锁上的线程。

注意到上述操作线程的方法都是放在Object类中,这是因为方法都是同步锁的方法。而锁可以是任意对象,任意的对象都可以调用的方法一定定义在Object类中。

这些方法属于Object类的一部分而不是Thread类的一部分,这个咋看一下真的很奇怪,不过细细思考下,这种做法是有道理的,我们把锁机制授予对象会帮我们扩展线程应用的思路,至少把这几个方法用在任何的具有同步控制功能的方法中,而不用去考虑方法所在的类是继承了Thread还是实现了Runnable接口。
但是事实上使用这些方法还是要注意只能在同步控制方法或同步块里调用,因为这些操作都会使用到锁。

如果是在非同步的方法里调用这些方法,程序会编译通过,但是在运行时候程序会报出IllegalMonitorStateException异常,这个异常的含义是调用方法的线程在调用这些方法前必须拥有这个对象的锁,或者当前调用方法的对象锁不是之前同步时的那个锁。

使用唤醒等待实现两个线程交替执行

代码如下:

 public class Demo {
public static void main(String[] args) {
DemoThread demoThread = new DemoThread();
Thread a = new Thread(demoThread, "线程A");
Thread b = new Thread(demoThread, "线程B");
a.start();
b.start();
}
} class DemoThread implements Runnable {
private Integer num = 1; @Override
public void run() {
while (true) {
synchronized (DemoThread.class) {
DemoThread.class.notify();
if (num <= 10) {
System.out.println(Thread.currentThread().getName() + " >>> " + num++);
try {
DemoThread.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}

运行结果如下:

 线程A >>> 1
线程B >>> 2
线程A >>> 3
线程B >>> 4
线程A >>> 5
线程B >>> 6
线程A >>> 7
线程B >>> 8
线程A >>> 9
线程B >>> 10

线程的虚假唤醒

在使用wait()时,当被唤醒时有可能会被虚假唤醒,建议使用while而不是if来进行判断,即在循环中使用wait()方法。

在下面的代码中,没有在循环中使用wait()方法:

 public class Demo {
public static void main(String[] args) {
DemoThread demoThread = new DemoThread(); Thread a = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 4; i++) {
demoThread.increase();
}
}
}, "线程A");
Thread b = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 4; i++) {
demoThread.decrease();
}
}
}, "线程B");
Thread c = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 4; i++) {
demoThread.increase();
}
}
}, "线程C");
Thread d = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 4; i++) {
demoThread.decrease();
}
}
}, "线程D");
a.start();
b.start();
c.start();
d.start();
}
} class DemoThread {
private static Integer num = 1; public synchronized void increase() {
if (num > 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.print(num + " ");
this.notifyAll();
} public synchronized void decrease() {
if (num == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
System.out.print(num + " ");
this.notifyAll();
}
}

运行结果如下:

 0 1 0 1 2 1 0 1 2 1 0 1 2 1 2 1

可以看到即便使用了synchronized关键字,仍然出现了线程安全问题,原因如下:

在某一刻,一个负责增加的线程获得了资源,此时num为1,所以执行 this.wait(); 并等待。

下一刻,另一个负责增加的线程获得了资源,此时num仍然为1,所以再次执行 this.wait(); 并等待。

此后负责减少的线程将num减少到0并唤醒所有等待进程,两个负责增加的线程被唤醒,执行两次增加运算,导致num为2的情况产生。

解决办法就是将 if (num > 0) { 和 if (num == 0) { 中的if换成while。

wait()和sleep()方法的区别

两个方法声明的位置不同:Thread类中声明sleep() ,Object类中声明wait()。

使用方法不同:wait()可以指定时间,也可以不指定时间,sleep()必须指定时间。

调用的要求不同:sleep()可以在任何需要的场景下调用, wait()必须使用在同步代码块或同步方法中。

关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

生产者消费者问题

 public class Demo {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk);
Consumer consumer = new Consumer(clerk);
Thread productor1 = new Thread(productor, "生产者1");
Thread productor2 = new Thread(productor, "生产者2");
Thread consumer1 = new Thread(consumer, "消费者1");
Thread consumer2 = new Thread(consumer, "消费者2");
productor1.start();
productor2.start();
consumer1.start();
consumer2.start();
}
} class Clerk {// 店员
private int num = 0;// 产品数量 public synchronized void product() {// 生产产品
if (num < 2000) {
System.out.println(Thread.currentThread().getName() + ":生产了第" + ++num + "个产品");
notifyAll();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} public synchronized void consume() {// 消费产品
if (num > 0) {
System.out.println(Thread.currentThread().getName() + ":消费了第" + num-- + "个产品");
notifyAll();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} class Productor implements Runnable {// 生产者线程
Clerk clerk; public Productor(Clerk clerk) {
this.clerk = clerk;
} @Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始生产产品");
while (true) {
clerk.product();
}
}
} class Consumer implements Runnable {// 消费者线程
Clerk clerk; public Consumer(Clerk clerk) {
this.clerk = clerk;
} @Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始消费产品");
while (true) {
clerk.consume();
}
}
}

Java多线程——线程之间的协作的更多相关文章

  1. Java多线程——线程之间的同步

    Java多线程——线程之间的同步 摘要:本文主要学习多线程之间是如何同步的,如何使用volatile关键字,如何使用synchronized修饰的同步代码块和同步方法解决线程安全问题. 部分内容来自以 ...

  2. java多线程(七)-线程之间的 协作

    对于多线程之间的共享受限资源,我们是通过锁(互斥)的方式来进行保护的,从而避免发生受限资源被多个线程同时访问的问题.那么线程之间既然有互斥,那么也会有协作.线程之间的协作也是必不可少的,比如 盖个商场 ...

  3. Java并发编程,互斥同步和线程之间的协作

    互斥同步和线程之间的协作 互斥同步 Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLo ...

  4. java并发系列(二)-----线程之间的协作(wait、notify、join、CountDownLatch、CyclicBarrier)

    在java中,线程之间的切换是由操作系统说了算的,操作系统会给每个线程分配一个时间片,在时间片到期之后,线程让出cpu资源,由其他线程一起抢夺,那么如果开发想自己去在一定程度上(因为没办法100%控制 ...

  5. java 多线程—— 线程等待与唤醒

    java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 ...

  6. Java多线程--线程及相关的Java API

    Java多线程--线程及相关的Java API 线程与进程 进程是线程的容器,程序是指令.数据的组织形式,进程是程序的实体. 一个进程中可以容纳若干个线程,线程是轻量级的进程,是程序执行的最小单位.我 ...

  7. Java多线程——线程的优先级和生命周期

    Java多线程——线程的优先级和生命周期 摘要:本文主要介绍了线程的优先级以及线程有哪些生命周期. 部分内容来自以下博客: https://www.cnblogs.com/sunddenly/p/41 ...

  8. Java多线程——线程八锁案例分析

    Java多线程——线程八锁案例分析 摘要:本文主要学习了多线程并发中的一些案例. 部分内容来自以下博客: https://blog.csdn.net/dyt443733328/article/deta ...

  9. java 多线程—— 线程让步

    java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 ...

随机推荐

  1. Eclipse错误:Syntax error on tokens, delete these tokens问题解决

    错误:Syntax error on tokens, delete these tokens 出现这样的错误一般是括号.中英文字符.中英文标点.代码前面的空格,尤其是复制粘贴的代码,去掉即可.

  2. JSP的异常处理

    以下内容引用自http://wiki.jikexueyuan.com/project/jsp/exception-handling.html: 当写JSP代码的时候,有可能会留下一个编码错误,并且它会 ...

  3. 将mysql数据库数据导出为.sql文件

    打开Navicat ,在我们要到处的数据上面右击鼠标,然后弹出的快捷菜单上点击“转储SQL 文件”,在再次弹出的子菜单项中选择第一个“数据跟结构”.

  4. Windows下安Mac

    Windows PC下安装苹果系统 第一步: 準備2個新邏輯分區,一個6G(os),一個隨意(Mac),且不要格式化. 第二步: 启动硬盘助手,选择下载好的苹果镜像文件  .再选择6G(os)分區,寫 ...

  5. Cardboard虚拟现实开发初步(二)

    Google Cardboard 虚拟现实眼镜开发初步(二) Cardboard SDK for Unity的使用 上一篇文章作为系列的开篇,主要是讲了一些虚拟现实的技术和原理,本篇就会带领大家去看一 ...

  6. FlashBuilder 4.7 非正常关闭导致的不能启动的解决的方法

    停电.或者卡死.FB就不能正常启动了. 以下是老外给出的方法,好用: 进入.metadata/.plugins/org.eclipse.core.resources 文件夹 删除.snap文件 假设是 ...

  7. Android 圆形/圆角图片的方法

    Android 圆形/圆角图片的方法 眼下网上有非常多圆角图片的实例,Github上也有一些成熟的项目.之前做项目,为了稳定高效都是选用Github上的项目直接用.但这样的结束也是Android开发必 ...

  8. chrome.socket

    chrome.socket https://chajian.baidu.com/developer/apps/socket.html#method-create 描述: 使用 chrome.socke ...

  9. ZOJ 1806 (小数高精度)

    题意:八进制小数转化成十进制的小数. 0.d1d2d3 ... dk [8] = 0.D1D2D3 ... Dm [10] 例: 0.75 [8] = 7*8^-1+5*8^-2 = ( 5/8 + ...

  10. java.io.IOException: read failed, socket might closed or timeout, read ret: -1

    近期项目中连接蓝牙之后接收蓝牙设备发出的指令功能,在连接设备之后,创建RfcommSocket连接时候报java.io.IOException: read failed, socket might c ...