多线程和并发(1)—等待/通知模型

简介: 多线程和并发(1)—等待/通知模型

一、进程通信和进程同步

1.进程通信的方法

同一台计算机的进程通信称为IPC(Inter-process communication),不同计 算机之间的进程通信被称为 RPC(Romote process communication),需要通过网络,并遵守共同的协议。进程通信解决的问题是两个或多个进程间如何交换数据的问题。常用的进程通信的方法如下:

  1. 管道:分为匿名管道(pipe)及命名管道(named pipe):匿名管道可用 于具有亲缘关系的父子进程间的通信,命名管道除了具有管道所具有的功能外, 它还允许无亲缘关系进程间的通信。
  2. 信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较 复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器 收到一个中断请求效果上可以说是一致的。
  3. 消息队列(message queue):消息队列是消息的链接表,它克服了上两 种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息 队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。 4. 共享内存(shared memory):可以说这是最有用的进程间通信方式。它 使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共 享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
  4. 信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间的同步和互斥手段。
  5. 套接字(socket):这是一种更为一般得进程间通信机制,它可用于网络 中不同机器之间的进程间通信,应用非常广泛。同一机器中的进程还可以使用 Unix domain socket(比如同一机器中 MySQL 中的控制台 mysql shell 和 MySQL 服 务程序的连接),这种方式不需要经过网络协议栈,不需要打包拆包、计算校验 和、维护序号和应答等,比纯粹基于网络的进程间通信肯定效率更高。

2.线程同步的方法

线程同步解决的问题是多个线程在并发执行过程中需要保持数据一致性和顺序性等问题。常见的线程同步方法如下:

  1. 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
  2. 互斥量:为协调共同对一个共享资源的单独访问而设计的。互斥量跟临界区很相似,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限。
  3. 信号量:为控制一个具有有限数量用户资源而设计。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。互斥量是信号量的一种特殊情况,当信号量的最大资源数=1就是互斥量了。
  4. 事件: 用来通知线程有一些事件已发生,从而启动后继任务的开始。

二、创建线程的方法

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Java可以用四种方式来创建线程,如下所示:

  1. 继承Thread类创建线程
  2. 实现Runnable接口创建线程
  3. 使用Callable和Future创建线程
  4. 使用线程池例如用Executor框架

重点说明(3)使用Callable和Future创建线程。和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大,其实现了(1)call()方法可以有返回值;(2)call()方法可以声明抛出异常;

Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。

3D8020B4-B418-4DD0-BE90-0B75DB4AA5DB

FutureTask的常见方法如下:

  • boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务
  • V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
  • V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException
  • boolean isDone():若Callable任务完成,返回True
  • boolean isCancelled():如果在Callable任务正常完成前被取消,返回True

介绍了相关的概念之后,创建并启动有返回值的线程的步骤如下:

  1. 创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
  3. 使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

代码实现:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class MyCallableTest {
   
   
    public static void main(String[] args) throws ExecutionException, InterruptedException {
   
   
        FutureTask futureTask = new FutureTask<>(new MyCallableThread());
        Thread thread = new Thread(futureTask);
        thread.start();
        String result = futureTask.get();
        System.out.println(result);
    }
}

class MyCallableThread implements Callable {
   
   
    @Override
    public String call() throws Exception {
   
   
        System.out.println("thread running");
        Thread.sleep(3000);
        return "thread returned";
    }
}

三、线程中run()方法和执行线程start()的区别

Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread()其实只是new出一个Thread的实例,还没有操作系统中真正的线程关联起来。只有执行了start()方法后,才实现了真正意义上的启动线程。

从Thread的源码可以看到,Thread的start方法中调用了start0()方法,而start0()是个native方法,这就说明Thread#start一定和操作系统是密切相关的。

Thread类中的run()方法中说明的是任务的处理逻辑,执行线程的start()方法让一个线程进入就绪队列等待分配cpu,分到cpu后才调用实现的run()方法,执行任务的处理逻辑,start()方法不能重复调用,如果重复调用会抛出异常。而run方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。

四、线程中断的方法

1.自然终止

要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。

2.调用stop等方法

暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的。不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。

public class MyThreadTest1 {
   
   
    public static void main(String[] args) {
   
   

        Thread thread = new Thread(new MyTask());
        thread.start();

        try {
   
   
            Thread.sleep(5000);
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }
        thread.stop();
    }
}

class MyTask implements Runnable{
   
   
    @Override
    public void run() {
   
   

        while (true) {
   
   
            System.out.println("thread runing: " + Thread.currentThread().getName());
            try {
   
   
                Thread.sleep(2000);
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            }
        }
    }
}

3.使用thread.interrupt()中断方法

安全的停止线程方式是使用thread.interrupt()中断来停止。在主线程中调用thread.interrupt()方法,能够将thread的中断标识设置为false,再在当前线程中调用Thread.currentThread().isInterrupted()判断是否中断,从而判断是否结束线程。

值得注意的是如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait等),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false,所以需要Thread.currentThread().interrupt()重新再设置一下。

代码实现:

public class MyTest1 {
   
   
    public static void main(String[] args) {
   
   
        Thread thread = new Thread(new MyTaskTest());
        thread.start();
        try {
   
   
            Thread.sleep(5000);
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }
        thread.interrupt();
        System.out.println("mainThread end");
    }
}

class MyTaskTest implements Runnable{
   
   
    @Override
    public void run() {
   
   
        boolean interrupted = Thread.currentThread().isInterrupted();

        while (!interrupted) {
   
   
            interrupted = Thread.currentThread().isInterrupted();
            System.out.println("interrupted = " + interrupted);

            System.out.println("subThread running");
            try {
   
   
                Thread.sleep(2000);
            } catch (InterruptedException e) {
   
   
//                e.printStackTrace();
                Thread.currentThread().interrupt();
                System.out.println("interrupted = " + interrupted);
            }
        }
    }
}

五、多线程中的等待/通知模式

thread.join()

join()是进行线程同步的方法,通过在当前线程中执行另一个线程的thread.join()方法,可以等待另一个线程执行完成之后,当前线程才执行,通过这样的方式来控制两个线程的先后顺序。

以下代码通过join()方法实现了thread1-->thread2-->thread3按照顺序来执行的逻辑。

public class MyJoinTest {
   
   
    public static void main(String[] args) throws InterruptedException {
   
   
        Thread thread1 = new Thread(new MyTask1());
        Thread thread2 = new Thread(new MyTask2(thread1));
        Thread thread3 = new Thread(new MyTask3(thread2));

        thread1.start();
        thread2.start();
        thread3.start();

        thread3.join();
        System.out.println("main end");

    }
}

class MyTask1 implements Runnable {
   
   

    @Override
    public void run() {
   
   
        System.out.println("thread:" + Thread.currentThread().getName());
        try {
   
   
            Thread.sleep(3000);
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }
    }
}

class MyTask2 implements Runnable {
   
   

    Thread thread;

    public MyTask2(Thread thread) {
   
   
        this.thread = thread;
    }

    @Override
    public void run() {
   
   
        try {
   
   
        thread.join();
        System.out.println("thread:" + Thread.currentThread().getName());
        Thread.sleep(3000);
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }
    }
}

class MyTask3 implements Runnable {
   
   

    Thread thread;

    public MyTask3(Thread thread) {
   
   
        this.thread = thread;
    }

    @Override
    public void run() {
   
   
        try {
   
   
            thread.join();
            System.out.println("thread:" + Thread.currentThread().getName());
            Thread.sleep(3000);
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }
    }
}

wait()/notiyf()

通过wait()/notiyf()来实现等待/通知模型,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

wait()方法:调用该方法的线程进入 WAITING状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方法后,会释放对象的锁

notify()方法:通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态。

等待/通知模型说明

等待方:
(1) 获取对象的锁。
(2) 如果条件不满足,那么调用对象的wait()方法,此时会释放锁。
(3) 竞争到锁并且条件满足,则执行对应的逻辑。
synchronized(lock){
    while(条件不满足){
    lock.wait()
    }
    业务逻辑
}

通知方:
(1) 获得对象的锁。
(2) 改变条件。
(3) 通知所有等待在对象上的线程,并释放锁。
synchronized(lock){
    改变条件,满足条件
    lock.notify()
}

以下代码实现:等待方等待通知方改变条件,满足条件后才执行后面的业务逻辑

public class MyWaitNotifyTest {
   
   
    public static Object lock = new Object();
    public static boolean flag = false;

    public static void main(String[] args) {
   
   
        Thread thread1 = new Thread(new Runnable() {
   
   
            @Override
            public void run() {
   
   
                synchronized (lock) {
   
   
                    System.out.println("等待方:我想要执行");
                    while (!flag) {
   
   
                        try {
   
   
                            lock.wait();
                        } catch (InterruptedException e) {
   
   
                            e.printStackTrace();
                        }
                    }
                    System.out.println("等待方:正常执行了");
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
   
   
            @Override
            public void run() {
   
   
                synchronized (lock) {
   
   
                    flag = true;
                    lock.notify();
                    System.out.println("通知方:可以执行了");
                }
            }
        });

        thread1.start();
        try {
   
   
            Thread.sleep(5000);
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }
        thread2.start();
    }
}
目录
相关文章
|
1月前
|
并行计算 Java 数据处理
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
179 0
|
28天前
|
安全
List并发线程安全问题
【10月更文挑战第21天】`List` 并发线程安全问题是多线程编程中一个非常重要的问题,需要我们认真对待和处理。只有通过不断地学习和实践,我们才能更好地掌握多线程编程的技巧和方法,提高程序的性能和稳定性。
135 59
|
3月前
|
编解码 网络协议 API
Netty运行原理问题之Netty的主次Reactor多线程模型工作的问题如何解决
Netty运行原理问题之Netty的主次Reactor多线程模型工作的问题如何解决
|
7天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
|
28天前
|
并行计算 JavaScript 前端开发
单线程模型
【10月更文挑战第15天】
|
19天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
30天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
19 1
|
2月前
|
消息中间件 存储 NoSQL
剖析 Redis List 消息队列的三种消费线程模型
Redis 列表(List)是一种简单的字符串列表,它的底层实现是一个双向链表。 生产环境,很多公司都将 Redis 列表应用于轻量级消息队列 。这篇文章,我们聊聊如何使用 List 命令实现消息队列的功能以及剖析消费者线程模型 。
99 20
剖析 Redis List 消息队列的三种消费线程模型
|
1月前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
29 1
|
1月前
|
NoSQL Redis 数据库
Redis单线程模型 redis 为什么是单线程?为什么 redis 单线程效率还能那么高,速度还能特别快
本文解释了Redis为什么采用单线程模型,以及为什么Redis单线程模型的效率和速度依然可以非常高,主要原因包括Redis操作主要访问内存、核心操作简单、单线程避免了线程竞争开销,以及使用了IO多路复用机制epoll。
49 0
Redis单线程模型 redis 为什么是单线程?为什么 redis 单线程效率还能那么高,速度还能特别快