多线程和并发(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();
    }
}
目录
相关文章
|
2天前
|
安全 Java
java线程之List集合并发安全问题及解决方案
java线程之List集合并发安全问题及解决方案
8 1
|
6天前
|
Java
Java Socket编程与多线程:提升客户端-服务器通信的并发性能
【6月更文挑战第21天】Java网络编程中,Socket结合多线程提升并发性能,服务器对每个客户端连接启动新线程处理,如示例所示,实现每个客户端的独立操作。多线程利用多核处理器能力,避免串行等待,提升响应速度。防止死锁需减少共享资源,统一锁定顺序,使用超时和重试策略。使用synchronized、ReentrantLock等维持数据一致性。多线程带来性能提升的同时,也伴随复杂性和挑战。
|
15天前
|
安全 Java API
Java并发基础-启动和终止线程
Java并发基础-启动和终止线程
23 0
|
15天前
|
Java 调度
Java并发基础-线程简介(状态、常用方法)
Java并发基础-线程简介(状态、常用方法)
17 0
|
3天前
|
Java
Java中的`synchronized`关键字是一个用于并发控制的关键字,它提供了一种简单的加锁机制来确保多线程环境下的数据一致性。
【6月更文挑战第24天】Java的`synchronized`关键字确保多线程数据一致性,通过锁定代码块或方法防止并发冲突。同步方法整个方法体为临界区,同步代码块则锁定特定对象。示例展示了如何在`Counter`类中使用`synchronized`保证原子操作和可见性,同时指出过度使用可能影响性能。
18 4
|
8天前
|
监控 程序员 调度
协程实现单线程并发(入门)
协程实现单线程并发(入门)
12 1
|
1天前
|
缓存 并行计算 安全
【并发编程系列一】并发编年史:线程的双刃剑——从优势到风险的全面解析
【并发编程系列一】并发编年史:线程的双刃剑——从优势到风险的全面解析
|
1天前
|
关系型数据库 MySQL 调度
深入理解MySQL InnoDB线程模型
深入理解MySQL InnoDB线程模型
7 0
|
1天前
|
安全 Linux 数据安全/隐私保护
【linux】线程同步和生产消费者模型
【linux】线程同步和生产消费者模型
5 0
|
3天前
|
Java
synchronized关键字在Java中为多线程编程提供了一种简便的方式来管理并发,防止数据竞争和死锁等问题
Java的`synchronized`关键字确保多线程环境中的数据一致性,通过锁定代码段或方法防止并发冲突。它可修饰方法(整个方法为临界区)或代码块(指定对象锁)。例如,同步方法只允许一个线程执行,同步代码块则更灵活,可锁定特定对象。使用时需谨慎,以避免性能影响和死锁。
9 0

热门文章

最新文章

相关实验场景

更多