【从0开始深入理解并发、线程与等待通知机制】

一、进程与线程

1.1、什么是进程

我们电脑或手机上的应用程序,如果在不运行时,其实是由指令和数据组成,这些数据是存在磁盘上的一些二进制代码,但是如果我们运行程序之后,这些指令要运行、数据也要进行读写,就必须要将这些指令加载到CPU,数据加载到内存中,甚至在指令运行途中,可能会涉及到磁盘、网络设备,所以,从这个角度来说:进程就是用来加载指令、管理内存、管理IO的,我们可以将进程看做是上述应用程序的实例,例如:Java中的Student对象,就相当于这里的应用程序,我们在new Student()操作时,得到的结果就相当于这的进程,站在操作系统角度来说:进程是操作系统为应用程序运行时分配资源(主要是内存)的最小单位

1.2、什么是线程

在众多的应用程序中,CPU资源是有限的,如何让CPU运行众多的应用程序==>那就需要一种介质来负责这个在程序之间进行协调,也称为CPU资源的调度,线程就是这种调度机制的最小单位,线程的依托于进程而存在,如果没进程,那线程就不复存在

1.3、进程间的通信

进程间总会存在一些交互,如果这个交互在一台计算机上,我们就称这种通信为:IPC=>Inter Process Communication,如果是在不同计算机上的我们称为:RPC=>Remote Procedure Call Protocol,是需要通过网络的远程过程调用协议,例如我们熟悉的Dubbo就是一个RPC框架,HTTP协议也是经常用到RPC上,例如SpringCloud微服务,我们就先来看同一台计算机下的进程通信方式:

  1. 管道:分为匿名管道和命名管道,匿名管道可用于具有亲缘关系的父子进程之间的通信,命名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
    在这里插入图片描述

  2. 消息队列:消息队列是消息的链接表,解决了两种以上的通信方式信号量有限的缺点,对进程给予读写权限分别向队列中写数据与读数据;
    在这里插入图片描述

  3. 共享内存:这个就有点类似JMM内存模型,有一块公共的主内存区域,供给多个线程进行访问读写操作
    在这里插入图片描述

  4. 信号量:主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段

  5. 套接字:主要是用于不同机器之间的进程通信,应用很广泛,例如,我们可以使用不同的机器访问同一个MySQL服务器),这种方式不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,比纯粹基于网络的进程间通信肯定效率更高
    在这里插入图片描述

1.4、线程、进程的上下文切换

就同上述提过=>CPU资源是有限的,那要运行多个程序,就会存在CPU资源调度问题,那如果从一个进程(线程)调度到另一个进程(线程)过程中,执行过的程序或线程产生的数据是如何处理呢,我们先看下图图示:
在这里插入图片描述
CPU正在执行程序A还未执行结束,这时,程序B抢占到CPU的执行权,那这时候,程序B就即将要运行,但是之前程序A产生的数据A,不可能说是程序B加入而被覆盖,这时候,操作系统会将程序A挂起,产生的数据缓存起来,程序B产生的数据正常记录,等待程序B执行完毕后,程序A抢占到CPU的执行权又会将其切换到之前执行过的数据,这个过程我们就称为上下文切换

1.5、线程的创建方式

在java中,创建线程方式,我们可以先看Thread类底层实现的注释中,写到的是两种方式,分别是:继承至Thread类和实现Runable接口,如下图示:
在这里插入图片描述
另外:为什么实现callable接口和线程池不算呢,在个人理解里,callable接口,最终是要把实现了callable接口的对象通过FutureTask包装成Runnable,再交给Thread去执行,所以这个其实可以和实现Runnable接口看成同一类,而线程池方式,更多的是偏池化技术、资源的复用。

当然本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程对象,调用Thread.start启动线程

1.6、线程的启动与终止、生命周期与状态

1.6.1、线程的启动与终止

线程的启动,是通过new Thread()创建线程对象,调用Thread.start启动线程,那在暂停、恢复、终止正在运行的线程时,对应Thread的API分别为:suspend()、resume()、stop(),但是这些API在底层源码中,已经被弃用了,如下图示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

不建议使用的原因主要是:

  • suspend()调用后,通过resume()来恢复的,但在将当前线程挂起后,是不会将持有的资源进行释放,会直接进入睡眠状态,这种情况是很容易发生死锁问题的;
  • stop()是不能保证当前线程是否能正确的释放持有的资源,例如:当前是在写文件,没写完,直接调用stop,会直接把正在写文件的线程关闭,不管文件是否已写完,此时文件就是一个不完整的文件(无结束符等信息)

基于上述存在的缺陷,线程中中断线程的方式优化为:interrupt()
在这里插入图片描述

这个方法是不会直接将要中断的线程停止,而是通过修改中断表示的方式进行告知要中断的线程,要被中断的线程通过方法sInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。
在这里插入图片描述
在这里插入图片描述

1.6.2、线程的生命周期与对应的状态

线程的什么周期主要分为:线程的创建、运行、死亡(终止),伴随每个声明周期的状态为:线程初始状态,也就是创建时的状态,紧接着就是运行状态,这个状态就有两个内部状态,分别为:Running和ready,在运行期间可能会存在阻塞,主要因为当前线程抢占锁的过程,通常是因资源被synchronized内置锁给锁住,而等待分为不设置超时的等待和设置超时时间的等待,最终的终止状态,表示当前线程执行完毕,那我们可以通过下图对这些过程进行清晰的理解:
在这里插入图片描述

1.8、守护线程

守护线程是一种支持型的线程,主要是负责为程序后台调度提供个各种支持服务的,当Java程序中不存在非守护线程后,那守护线程就会退出

1.9、深入理解run()和start()

我们在new Thread类时,其实和我们平时创建普通对象是一致的,而用这个对象调用start()方法时,其实才会去与操作系统进行交互,start会调用native方法=>start0(),而Thread类中的run方法其实就是一个普通方法,在start0执行时,会将其方法进行执行,而且,有下面的图,我们可以看到,start只能被同一个对象调用一次,否则会报IllegalThreadStateException异常
在这里插入图片描述

1.10、线程中的方法

yield() 、sleep()、wait()、notify()、join()
yield()=>释放当前线程持有的CPU执行权,进入就绪状态 、sleep()=>进入休眠,可指定休眠时间,这两个方法被调用后,都不会释放当前线程所持有的锁。
调用wait()=>当前线程进行等待状态,调用方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait方法后面的代码。
调用notify()=>通知等待状态的线程,进行恢复执行,当然也需要先获取到CPU的执行权后才会执行任务,调用方法后,对锁无影响,线程只有在syn同步代码执行完后才会自然而然的释放锁,所以notify()系列方法一般都是syn同步代码的最后一行。
join()=>可以将两个交替执行的线程合并为顺序执行。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B剩下的代码。

二、协程

首先我们先了解下用户线程:严格意义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。其实用户线程也称为协程=>因为最初多数的用户线程是被设计成协同式调度(Cooperative Scheduling) 的,所以它有了一个别名——“协程”(Coroutine) 完整地做调用栈的保护、恢复工作,所以今天也被称为“有栈协程”(Stackfull Coroutine)

三、锁机制—synchronized内置锁

3.1、对象锁

对象锁是用于对象实例方法,或者一个对象实例上的,需要注意的是,对象实例可以有很多个,所以使用对象锁时,需要注意,不同的线程是否锁的是同一个实例对象,如果不算同一个,那加锁是没意义和效果的

3.2、类锁

类锁是用于类的静态方法或者一个类的class对象上的,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。

四、轻量通信/同步机制—volatile

Volatile=>确保了不同线程对同一个变量进行操作时的彼此可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

五、等待/通知机制

我们先看一个例子,例:如果网购一台电脑,商家发了物流,焦急想得到商品的你,是每天都循环的到驿站询问快递到了没,还是等待驿站收到快递后,给你发短信让你去领取?很明显,现实生活中都是后者,那程序也是源于生活,如果两个线程之间存在交互,且是要等待其中一个线程做的任务达到某种效果或是满足什么条件后,另外一个线程才能开始执行,那这时候,我们就引入了线程的等待与通知机制;

那线程中的等待与通知是如何去控制的呢,我们可以先看如下方法:
notify():通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态;
notifyAll():通知所有等待在该对象上的线程;
wait():调用该方法的线程进入 WAITING状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方法后,会释放对象的锁;
wait(long):超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回;
wait (long,int):对于超时时间更细粒度的控制,可以达到纳秒;

在调用wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法、notify()系列方法,进入wait()方法后,当前线程释放锁,在从wait()返回前,线程与其他线程竞争重新获得锁, 执行notify()系列方法的线程退出调用了notifyAll的synchronized代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

等待和通知的标准范式
等待和通知机制需要遵循以下规则:
等待方规则:

  1. 获取对象的锁
  2. 如果条件不满足,那么调用对象的wait()方法,被通知后也需要校验条件是否满足
  3. 如果条件满足,就执行相应逻辑业务代码
synchronizd(对象实例){
	while(条件不满足){
		对象.wait()}
	逻辑业务代码
}

等待方规则:

  1. 获得对象的锁
  2. 改变条件
  3. 通知所有等待在对象上的线程
synchronizd(对象实例){
   改变条件
   对象.notifyAll()
}

就上述快递到站通知收件人去收件的例子,在代码中的实现应该是怎样的呢,如下:

物流信息类

package com.practice.juc;

public class Express {

    public final static String DIST_CITY = "ShangHai"; //目的地
    public final static int TOTAL = 500; // 总路程数
    private int km ;/*快递运输里程数*/
    private String site;/*快递到达地点*/

    public Express() {
    }

    /**
     * 设置里程数与目的地
     * @param km
     * @param site
     */
    public Express(int km, String site) {
        this.km = km;
        this.site = site;
    }

    /**
     * 更改里程数,或目的地
     */
    public void change(){
        if (km < TOTAL){
            km = km +100;
            System.out.println("the Km is "+this.km);
        }
        if(km >= TOTAL){
            site = DIST_CITY;
            System.out.println("the Express is arrived");
        }
    }


    /**
     * 线程等待公里的变化
     */
    public synchronized void waitKm(){
        while(this.km <= TOTAL){
            try {
                wait();
                System.out.println("Map thread["
                        +Thread.currentThread().getId()
                        +"] wake,I will change db");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**线程等待目的地的变化*/
    public synchronized void waitSite(){
        while(!this.site.equals(DIST_CITY)){
            try {
                wait();
                System.out.println("Notice User thread["+Thread.currentThread().getId()
                        +"] wake");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("the site is "+this.site+",I will call user");
    }
}

物流测试类

package com.practice.juc;

public class TestWN {

    /**
     * 初始化目前的里程数和目的地
     */
    private static Express express = new Express(0,"WUHAN");

    /*检查里程数变化的线程,不满足条件,线程一直等待*/
    private static class CheckKm extends Thread{
        @Override
        public void run() {
            express.waitKm();
        }
    }

    /*检查地点变化的线程,不满足条件,线程一直等待*/
    private static class CheckSite extends Thread{
        @Override
        public void run() {
            express.waitSite();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<2;i++){
            new CheckSite().start();
        }
        for(int i=0;i<2;i++){
            new CheckKm().start();
        }
        Thread.sleep(500);

        for(int i=0; i<5; i++){
            synchronized (express){
                express.change();
                express.notify();
            }
            Thread.sleep(500);
        }
    }

}

测试结果图
在这里插入图片描述

六、小结

这一章节,我们主要是认识线程是什么,有什么用,从线程的定义、使用、生命周期和其状态、底层部分源码实现对线程进行探索,以及对线程中等待通知机制、锁机制等方面进行探索和实践

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值