草庐IT

我看谁还不懂多线程之间的通信+基础入门+实战教程+详细介绍+附源码

有点小白的菜鸟 2023-04-16 原文

一、多线程之间的通信(Java版本)

1、多线程概念介绍

多线程概念

  • 在我们的程序层面来说,多线程通常是在每个进程中执行的,相应的附和我们常说的线程与进程之间的关系。线程与进程的关系:线程可以说是进程的儿子,一个进程可以有多个线程。但是对于线程来说,只属于一个进程。再说说进程,每个进程的有一个主线程作为入口,也有自己的唯一标识PID,它的PID也就是这个主线程的线程ID

  • 对于我们的计算机硬件来说,线程是进程中的一部分,也是进程的的实际运作单位,它也是操作系统中的最小运算调度单位。多线程可以提高CPU的处理速度。当然除了单核CPU,因为单核心CPU同一时间只能处理一个线程。在多线程环境下,对于单核CP来说,并不能提高响应速度,而且还会因为频繁切换线程上下文导致性能降低。多核心CPU具有同时并行执行线程的能力,因此我们需要注意使用环境。线程数超出核心数时也会引起线程切换,并且操作系统对我们线程切换是随机的。

2、线程之间如何通信

引入

  • 对于我们Java语言来说,多线程编程也是它的特性之一。我们需要利用多线程操作同一共享资源,从而实现一些特殊任务。上面说了,多线程在进行切换时CPU随机调度的,假如我们直接运行多个线程操作共享资源的话,势必会引起一些不可控错误因素。
  • 接下来,我们就需要让这些不可控变为可控 !这个时候就引出了本文的重点线程通信。线程通信就是为了解决多线程对同一共享变量的争夺

Java 线程通信的方式

  • 共享内存机制
    • 比如说Java的volatile关键字就是基于内存屏障解决变量的可见性,从而实现其他线程访问共享变量都是必须从主存中获取(对应其他线程对变量的更新也得及时的刷新到主存)。
    • synchronized 关键字基于对象锁这种方式实现线程互斥,可以通知对方有其他的线程正在执行这部分代码。
  • 消息传递模式
    • wait() 和 notify()/notifyAll() 等待通知方式实现线程的阻塞就绪状态之间的转换。
    • park、unpark
    • join() 阻塞【底层也是依赖wait实现】。
    • interrupt()打断阻塞状态。
    • 管道输入/输出。

3、线程通信方法详细介绍

主要介绍wait/notify,也有ReentrantLock的Condition条件变量的await/signal,LockSupport的park/unpark方法,也能实现线程之间的通信。主要是阻塞/唤醒通信模式。

首先说明这种方法一般都是作用于调用方法的所在线程。比如在主线程执行wait方法,就是将主线程阻塞了。

wait/notify机制

  • wait()、notify方法在Java中是Object提供给我们的。又因为所有的类都默认隐式继承了Object类,进而我们的每一个对象都具有wait和notify。
    • wait方法含义:一个线程一旦调用了任意对象obj.wait()方法,它就释放了所持有的监视器对象(obj)上的锁,并转为非运行状态(阻塞)。
    • notify方法含义:一个线程若执行obj.notify方法,则随机唤醒obj对象上监视器(操作系统也称为管程)monitor的阻塞队列waitset中一个线程。
    • wait和notify方法的使用同时必须配合synchronized关键字使用。同时也需要成对出现。就是说wait和notify必须得在同步代码块内部使用,大致原因就是需要保证同时只有一个线程可以去执行wait,使该线程阻塞。

await/signal

  • 要想使用await/signal首先是需要借用Condition条件变量,要想获取Condition条件变量,就必须通过ReentrantLock锁获取。
  • ReentrantLock和Synchronized类似,都是可重入锁,并且大多都是当做重量级锁使用。
    • 区别:ReentrantLock是API层面实现的,我们可以根据自己随意调用定制,但是Synchronized是JVM底层实现,我们无需关心他上锁解锁的流程。
  • await/signal使用时需要配合ReentrantLock锁对象的lock和unlock方法加锁解锁。就像wait/notify在synchronized在同步代码块中使用一样。他们都需要保证当前线程是唯一执行这段逻辑的线程。防止出现多线程造成的线程安全问题。

park/unpark

二、线程通信过程中需要注意的问题

1、唤醒丢失

如果一个线程先于被通知线程调用wait()前调用了notify(),等待的线程将错过这个信号。

  • 唤醒丢失主要是在我们使用wait 和 notify的过程中的时序问题。比如说我们线程二在执行某个对象notify的时候,线程一还没有执行该对象的wait方法。那么这次的唤醒就会丢失,我们就不能让线程二得notify方法起作用,自然而然线程一就不会被唤醒。
  • 举个例子吧,这就好比我们平常在宿舍每天都会有叫醒服务,但是这次 因为一些原因(通宵···)我一整晚都没有睡觉,而且当第二天早上的叫醒服务来的 时候也是醒着的。那么叫醒服务就会以为你已经醒来了,就会视而不见。没想到吧,叫醒服务刚走我就躺下来睡着了,所以我错过了这次叫醒服务。就能好好的睡亿觉了。这看起来没有什么大问题,但是你仔细想想若是每个睡着的人都需要被叫醒服务才能醒过来,外加上只有一次叫醒服务的机会。那么你就可以沉睡万年了,开心不。
  • 哈哈哈···
  • 这在程序中也是一样 的,如果错过notify那么就会一直wait。
    • 所以我们必须预防这种问题,比如说每隔一段时间去唤醒,也就是隔两分钟就去叫醒睡着的人。但是这种缺点就是太累了,对于程序来说是消耗性能和内存。实现也简单就是写入while循环体中,不停地尝试即可。
    • 我们也可以使用一个标志位完美的实现。初始化设置flag=FALSE表示还没wait,在wait之前将设置flag=TRUE,在notify之后设置flag=FALSE。每次notify唤醒之前都判断flag=true是否已经wait,在wait中判断flag=false是否已经notify。

核心代码演示

  • 首先使用线程池创建线程一使自己进入阻塞态,然后再调用LOCK1的notify方法唤醒线程一
	    // 线程一使用LOCK1对象调用wait方法阻塞自己
        executor.execute(new ThreadTest("线程一",LOCK1,LOCK2));

        synchronized (LOCK1) {
            System.out.println("main执行notify方法让线程一醒过来");
            LOCK1.notify();
        }
  • 但是他很有可能醒不来,因为主线程调用LOCK1对象的notify方法,可能主线程已经执行完了,上面线程还没创建完成,也就是没有进入wait状态。就醒不来了。

  • 解决方式:使用信号量标志进行判断是否已经进入wait

            synchronized (LOCK1) {
                while (true) {
                    if (FLAG.getFlag()) {
                        System.out.println("main马上执行notify方法让线程一醒过来" + "flag = " + FLAG.getFlag());
                        LOCK1.notify();
                        // 将标志位变为FALSE
                        FLAG.setFlag(Constants.WaitOrNoWait.NO_WAIT.getFlag());
                        System.out.println("main执行notify方法完毕" + "flag = " + FLAG.getFlag());
                        break;
                    }
                }
            }
    

2、假唤醒

由于莫名其妙的原因,线程有可能在没有调用过notify()和notifyAll()的情况下醒来。

  • 其实在上面的代码中已经解决了假唤醒的问题,因为我们只需要不断去尝试获取标志位信息即可。

3、多线程唤醒

  • 多个线程执行时,防止notifyAll全部唤醒之后就结束运行,我们的需求是只能唤醒一个线程,当其他线程被唤醒之后需要重新判断标志位是否为FALSE,也就是需要判断是否有其他线程执行了唤醒操作,因为一次只能叫醒一个人,需要排队,他们就可以继续自旋判断。
		synchronized (waitName) {
            while (!flag.getFlag()) {
                try {
                    // 将标志位设置为TRUE
                    flag.setFlag(Constants.WaitOrNoWait.WAIT.getFlag());
                    System.out.println("name;"+name+" 我睡着了进入阻塞状态" + "flag = " + flag.getFlag());
                    waitName.wait();
                    System.out.println("name;"+name+" 我醒来了" + "flag = " + flag.getFlag());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
  • 大家如果使用的是new Thread()方式创建线程的话,要想保证安全的话还可以给该标志位加上volatile关键字,可以时刻保证该标志位的可见性。
  • 我这里使用的标志位是使用传递引用的方式,使用同一个对象,将标志位定义为该对象中的属性,然后再结合枚举类进行设置标志位的值。因为我使用线程池创建对象,并且自定义线程类,这里是无法设置全局变量,传递给线程类。包装类也不行哦。(感兴趣可以亲自试一下)
  • 大体代码结构如下所示:
	private final static Object LOCK1 = new Object();
    private final static Object LOCK2 = new Object();
    private final  static Constants.WaitStatus FLAG = new Constants.WaitStatus(false);
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 1, TimeUnit.DAYS, new ArrayBlockingQueue<>(4), new ThreadPoolExecutor.AbortPolicy());
        executor.execute(new ThreadTest("线程一",LOCK1,LOCK2, FLAG));
        // ···唤醒
    }

class ThreadTest implements Runnable { //阻塞··· }

完整代码可以看这[Gitee仓库完整代码][https://gitee.com/malongfeistudy/javabase/tree/master/Java多线程_Study/src/main/java/com/mlf/thread/demo_wait_notify]

三、线程通信实战

前置知识:线程池的使用方法

  • 首先复习一下创建线程的几种方式和其的优缺点:

    • 通过new Thread()
    • 继承Thread():和new Thread没啥区别,就是耦合度低了
      • 定义线程类继承Thread类并且重写run方法即可。
      • 优点是简洁方便
      • 缺点是占用了该类的单继承位置,无法继承其他父类
    • 实现Runnable接口
    • 实现Callable接口
      • 和实现Runnable接口类似
      • 优点:
        • 实现接口,不占用继承的位置;
        • 耦合度降低,并且可定化程度提高。各个模块之间的调用关系更加清晰
      • 缺点:
        • 实现起来稍微麻烦
  • 使用线程池的步骤

    • 线程池初始化方式:
      • 使用Executor初始化线程池
        • 优点:方便快捷,适用于自己测试时使用
        • 缺点:在实际开发中无法判断细节
      • new ThreadPoolExecutor()构造器创建(本文使用方式)
        • 优点:可以清晰地定制出适合自己的线程池,不会造成资源浪费
        • 缺点:麻烦
  • 在主线程自定义线程池使用实例,这里需要根据实际情况定义锁对象,因为我们需要使用这些锁对象控制多线程之间的运行顺序以及线程之间的通信。在Java中每个对象都会在初始化的时候拥有一个监视器,我们需要利用好他进行并发编程。这种创建线程池的方法也是阿里巴巴推荐的方式,想想以阿里的体量多年总结出来的总没有错,大家还是提前约束自己的编码习惯等。安装一个阿里代码规范的插件对自己的程序员道路是比较nice的。

    /**
     * 每个使用对应唯一的对象作为监视器对象锁。
     */
    public static final Object A_O = new Object();
    public static final Object B_O = new Object(); 
        /** 参数:
         * int corePoolSize,                     核心线程数
         * int maximumPoolSize,                  最大线程数
         * long keepAliveTime,                   救急存活时间
         * TimeUnit unit,                        单时间位
         * BlockingQueue<Runnable> workQueue,    阻塞队列
         * RejectedExecutionHandler handler      拒绝策略
         **/
        // 使用阿里巴巴推荐的创建线程池的方式
        // 通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                3,
                5,
                1,
                TimeUnit.DAYS,
                new ArrayBlockingQueue<>(2),
                new ThreadPoolExecutor.AbortPolicy());
  • 接下来需要自定义线程类,我们可以自定义线程类,并且在该线程类中定义自己需要的共享资源(锁对象,属性等),在run方法中写尽自己的线程运行逻辑即可。
class ThreadDiy implements Runnable {

    private final String name;

    /**
     * 阻塞锁对象  等待标记
     **/
    private final Object waitFor;

    /**
     * 执行锁对象  下一个标记
     **/
    private final Object next;

    public AlternateThread(String name, Object waitFor, Object next) {
    }

    @Override
    public void run() {
        // 线程的代码逻辑···
    }

}

1、控制两个线程之间的执行顺序

题目:现在有两个线程,不论线程的启动顺序,我需要指定线程一先执行,然后线程二再执行。

  • 初始化两个对象锁作为线程监视器。

        private final static Object ONE_LOCK = new Object();
        private final static Object TWO_LOCK = new Object();
    
  • 接下来初始化线程池,上面有具体的介绍,在这就不多说了

  • 使用线程池去执行我们的两个线程,在这里我们需要分析的是

        // 使用线程池创建线程
        executor.execute(new DiyThread(1, ONE_LOCK, TWO_LOCK));
        executor.execute(new DiyThread(2, TWO_LOCK, ONE_LOCK));

        synchronized (ONE_LOCK) {
            ONE_LOCK.notify();
        }

创建线程类

  • 我们使用继承Runnable的方式去创建线程对象,需要在这个类中实现每个线程执行的逻辑,我们根据题目可以得出,我们要控制每个线程的执行顺序,怎么办?那么就要实现所有线程之间的通信,通信方式采用wait-notify的方式即可。我们使用wait-notify的时候必须结合synchronized,那么就需要控制两个对象锁。因为我们不光是控制自己,还有另一个线程。

  • 我们再分析一下题意,首先需要指定先后执行的顺序,那么就需要实现两个线程之间的通信。其次呢,我们得控制两个线程,那么就需要两个监视器去监视这两个线程。

  • 我们定义这两个监视器对象为own和other。然后再新增一个属性threadId来标识自己。

        private final int threadId;
        private final Object own;
        private final Object other;
    
  • 接下来就是编写Run方法了

  • 每个线程首先需要阻塞自己,等待唤醒。然后唤醒之后,再去唤醒另外一个线程。这样就实现了自定义顺序。至于先唤醒哪个线程,交给我们的主线程去完成。

  • 这里需要注意的是,如果我们只是单纯地执行了多个线程对象,但是主线程没有主动去唤醒其中一个,这样就会形成类似于死锁的循环等待。你需要我唤醒,我需要你唤醒。这个时候需要主线程去插手唤醒其中的任意一个线程。

    • 第一步阻塞自己own

              synchronized (own) {
                  try {
                      own.wait();
                      System.out.println(num);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
      
    • 第二步唤醒other

              synchronized (other) {
                  other.notify();
              }
      

2、多线程交替打印输出

题目需求:现在需要使用三个线程轮流打印输出。说白了也就是多线程轮流执行罢了,和问题一控制两个线程打印顺序没什么区别

  • 还是老步骤,首先需要定义线程类,我们需要控制当前线程和下一个线程即可。我们这里需要两个对象,一个是阻塞锁对象用来阻塞当前线程。另一个是唤醒锁对象,用来唤醒下一个对象。
    /**
     * 阻塞锁对象  等待标记
     **/
    private final Object waitFor;
    /**
     * 唤醒锁对象  下一个标记
     **/
    private final Object next;
  • run方法的逻辑和上面的基本一样。 一个线程一旦调用了任意对象的wait()方法,它就释放了所持有的监视器对象上的锁,并转为非运行状态。

  • 每个线程首先会调用 waitFor对象的 wait()方法,随后该线程进入阻塞状态,等待其他线程执行自己引用的该 waitFor对象的 notify()方法即可。

    		while (true) {
                synchronized (waitFor) {
                    try {
                        waitFor.wait();
                        System.out.println(name + " 开始执行");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                synchronized (next) {
                    next.notify();
                }
            }
    
  • 主线程需要初始化线程池、执行三个线程,并且最后需要打破僵局,因为此时每个线程都是阻塞状态,他们没法阻塞/唤醒循环下去。

            synchronized (A_O) {
                A_O.notify();
            }
    
  • 模拟执行流程

/**
 * 模拟执行流程
 * 打印名(name)    等待标记(waitFor)   下一个标记(next)
 *      1                 A                  B
 *      2                 B                  C
 *      3                 C                  A
 * 
 * 像不像Spring的循环依赖:确实很像,Spring中的循环依赖就是 BeanA 依赖 BeanB,BeanB 依赖 BeanA;
 * 他们实例化过程中都需要先属性注入对方的实例,倘若刚开始的时候都没有实例化,初始化就会死等。类似于死锁。
 **/

3、多线程顺序打印同一个自增变量

使用多线程轮流打印 01234····

  • 思路:使用自增原子变量AtomicInteger和多线程配合打印。

具体代码请移步到Gitee仓库:[顺序打印自增变量][https://gitee.com/malongfeistudy/javabase/blob/master/Java多线程_Study/src/main/java/com/mlf/thread/print/AddNumberPrint2.java]

条件变量Condition的使用

  • Condition是一个 LOCK 实例出来的,他们获取的都是一个 LOCK 的锁,而如果要调用 object的 wait和notify 方法,首先要获取对应的object的锁,如果要调用Condition 的await、signal方法,必须先获取Lock锁(Lock.lock)。
  • 多线程的初衷就是操作共享资源,然后我们需要保证共享资源同一时刻只能被一个线程所修改。那么就需要一把锁来控制这些线程之间互斥条件。这里使用一个ReentrantLock锁作为我们的Lock对象。通过同一个 Lock锁 获取的每个Condition 就可以作为每个线程自己的阻塞条件和唤醒条件。

如有问题,请留言评论。

有关我看谁还不懂多线程之间的通信+基础入门+实战教程+详细介绍+附源码的更多相关文章

  1. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  2. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  3. ruby - #之间? Cooper 的 *Beginning Ruby* 中的错误或异常 - 2

    在Cooper的书BeginningRuby中,第166页有一个我无法重现的示例。classSongincludeComparableattr_accessor:lengthdef(other)@lengthother.lengthenddefinitialize(song_name,length)@song_name=song_name@length=lengthendenda=Song.new('Rockaroundtheclock',143)b=Song.new('BohemianRhapsody',544)c=Song.new('MinuteWaltz',60)a.betwee

  4. ruby-on-rails - `a ||= b` 和 `a = b if a.nil 之间的区别? - 2

    我正在检查一个Rails项目。在ERubyHTML模板页面上,我看到了这样几行:我不明白为什么不这样写:在这种情况下,||=和ifnil?有什么区别? 最佳答案 在这种特殊情况下没有区别,但可能是出于习惯。每当我看到nil?被使用时,它几乎总是使用不当。在Ruby中,很少有东西在逻辑上是假的,只有文字false和nil是。这意味着像if(!x.nil?)这样的代码几乎总是更好地表示为if(x)除非期望x可能是文字false。我会将其切换为||=false,因为它具有相同的结果,但这在很大程度上取决于偏好。唯一的缺点是赋值会在每次运行

  5. Unity 热更新技术 | (三) Lua语言基本介绍及下载安装 - 2

    ?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------

  6. postman接口测试工具-基础使用教程 - 2

    1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,

  7. 软件测试基础 - 2

    Ⅰ软件测试基础一、软件测试基础理论1、软件测试的必要性所有的产品或者服务上线都需要测试2、测试的发展过程3、什么是软件测试找bug,发现缺陷4、测试的定义使用人工或自动的手段来运行或者测试某个系统的过程。目的在于检测它是否满足规定的需求。弄清预期结果和实际结果的差别。5、测试的目的以最小的人力、物力和时间找出软件中潜在的错误和缺陷6、测试的原则28原则:20%的主要功能要重点测(eg:支付宝的支付功能,其他功能都是次要的)80%的错误存在于20%的代码中7、测试标准8、测试的基本要求功能测试性能测试安全性测试兼容性测试易用性测试外观界面测试可靠性测试二、质量模型衡量一个优秀软件的维度①功能性功

  8. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  9. 微信小程序开发入门与实战(Behaviors使用) - 2

    @作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors    1、什么是behaviors    2、behaviors的工作方式    3、创建behavior    4、导入并使用behavior    5、behavior中所有可用的节点    6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors    1、什么是behaviorsbehaviors是小程序中,用于实现

  10. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

随机推荐