Java 多线程编程学习笔记(7月16号)

2023-11-08

作者信息

作者:黄钰朝
邮箱:kobe524348@gmail.com
日期:2019年7月16日

前言

今天的学习内容是多线程编程的基础知识,包括对线程和进程,并发和并行,多线程和多进程的基本理解,创建线程的三种方式的原理和区别,线程的生命周期,线程的属性:守护线程,线程优先级,线程的三个方法:sleep,yield,join,线程池的使用。

一.什么是线程?

1.1 进程和线程

线程是建立在进程的基础上的,因此要了解线程首先需要知道进程。

进程是指一个运行中的程序,是程序运行的实例,是正在执行的一串指令,也就是说,程序并不是直接被运行的,程序只是一些指令,数据的集合,真正在运行的时候,是被操作系统加载进入内存,以进程的形式在计算机中被CPU执行,一个程序通常会产生多个进程,可以由多个用户同时使用。每个进程都需要占用一定的系统资源,比如CPU资源,内存资源等等,操作系统会给每一个进程分配一定的内存空间,并且分配一个进程id(PID),每个进程独自占有这些资源,不能共享。因此进程是资源分配的基本单位

线程是属于进程的,线程是一串正在被执行的指令,一个程序的进程往往要同时完成不同的任务,而进程就是这些一个个的任务,不同的线程共享父进程的资源,但是为了不相互干扰,每个线程也有自己私有的资源,比如独立的堆栈,程序计数器和局部变量,一个进程至少拥有一个线程(一个程序启动当然需要执行一定的指令),通常被系统启动的线程称为主线程,一个线程可以被启动,挂起,停止等等操作,因此线程是运行和调度的基本单位

1.2 并发和并行

  • 一个CPU在同一时间只能执行一个线程,因此,只有一个CPU核心时,只能是把时间“分片”,轮流执行不同的线程,从而达到同时进行的效果,实际在时间上是”不同步“的,这就是"并发"

  • 并行是多个处理器同时执行多条线程,也就是把多个线程分配给不同的处理器同时执行,在时间上是真正“同时”的

多线程和多进程都可以实现并发,但是进程拥有自己的独立资源,有独立的内存空间,多个进程间不能共享数据。

而线程虽然也有自己独立的堆栈,程序计数器和局部变量,但是多个线程共享父进程的共享变量和部分环境,因此使用多线程进行并发编程更加方便,但是也因为存在共享的资源,尤其是数据,并发时因为实际上多个线程在时间上是不同步的,因此也更容易出现问题,这就是线程安全问题

1.3 上下文切换(Context Switch)

上下文切换也就是环境切换。包括进程的上下文切换和线程的上下文切换。

CPU从一个线程切换到另外一个线程执行,需要先保存当前的线程的状态和数据,然后载入另外一个进程的状态和数据进行执行,这个过程就是上下文切换,需要消耗一定的系统资源,这便是多线程带来的额外开销。

二.创建线程的三种方式

2.1 概述

线程在Java中的抽线是Thread类,启动线程其实只需要调用Thread类对象的start方法。如下:

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

上面的代码已经启动了一个线程,但是因为没有给这个线程添加执行体代码,因此这个线程一开始执行就结束了。其实我打开看了一下源码,start方法其实只是做了一些检查和准备工作,而且是被当前线程执行的,新启动的线程实际执行的是Thread对象的run方法。而里面的run方法是这样的:

/**
     * If this thread was constructed using a separate
     * <code>Runnable</code> run object, then that
     * <code>Runnable</code> object's <code>run</code> method is called;
     * otherwise, this method does nothing and returns.
     * <p>
     * Subclasses of <code>Thread</code> should override this method.
     *
     * @see     #start()
     * @see     #stop()
     * @see     #Thread(ThreadGroup, Runnable, String)
     */   
public void run() {
        if (target != null) {
            target.run();
        }
}

从注释和源代码可以看出来,如果我们像上面那样启动线程,那么它会先看看target是否存在,如果存在,那么执行target的run方法,如果为null,那么啥也不做。而且,注释提示,子类应该重写run方法。看一下target变量:

private Runnable target;
可以看到target其实是一个实现了Runnable接口的类的对象(并且其实这个target是通过Thread的有参构造方法传进来的)

因此,为线程添加执行体代码有两种基础的方式:

  • 实现Runnable接口
  • 继承Thread类

后面为了能够接收线程执行的返回值,并且能够声明抛出异常,所以又多弄了个FutureTask和Callable接口来添加执行体代码。

2.2 继承Thread类

这个感觉不用说了,把你想执行的代码写到重写的run方法中,然后用这个子类的对象来启动线程就行了

//继承Thread类
public class MyThread extend Thread{
     @Override
     public void run(){
          do something...
     }
}

然后:

MyThread myThread = new Thread();
myThread.start();

2.3 实现Runnable接口

知道了上面的原理之后,贼简单:

//实现Runnable接口
public class MyRunnableClass implements Runnable{
     public void run(){
          do something...
     }
}

然后:

//把Runnable对象传进去 
Thread thread = new Thread(new MyRunnableClass());
thread.start();

完事了。

2.4 实现Callable接口

Callable接口其实也是从Runnable接口发展来的,底层还是Runnable接口,因为Callable接口中的call方法是被FutureTask的run方法调的,而FutureTask其实RunnableFuture的实现类,而RunnableFuture又是继承自Runnable接口的。

说白了,FutureTask的对象其实也就是target对象,只是它在run方法中调用了Callable接口实现类的对象中的call方法,所以最终执行的是call方法,但是为什么要加一个FutureTask对象做中介?目的就是在中介FutureTask的run方法中实现接收返回值,接收call方法抛出的异常等等扩展的功能,也就是说,Callable通过FutureTask的辅助,实现了Runnable接口的增强。

也正因为有了一层中介,Callable的对象要使用FutureTask再包装一层再传给Thread对象。

因为时间有限,我就不自己写示例代码了,直接摘了菜鸟教程的一段代码,需要注意的是,call方法重写时的返回值类型,需要通过泛型传递给FutureTask对象,比如这段代码中的Integer类型返回值。

public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args)  
    {  
        CallableThreadTest ctt = new CallableThreadTest();  
        FutureTask<Integer> ft = new FutureTask<>(ctt);  
        for(int i = 0;i < 100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);  
            if(i==20)  
            {  
                new Thread(ft,"有返回值的线程").start();  
            }  
        }  
        try  
        {  
            System.out.println("子线程的返回值:"+ft.get());  
        } catch (InterruptedException e)  
        {  
            e.printStackTrace();  
        } catch (ExecutionException e)  
        {  
            e.printStackTrace();  
        }  
  
    }
    @Override  
    public Integer call() throws Exception  
    {  
        int i = 0;  
        for(;i<100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" "+i);  
        }  
        return i;  
    }  
}

2.5 三种方法的比较

  • 直接继承Thread类的方式编写时最简单,但是因为是继承方式,不可再继承其他类。
  • 实现Runnable接口或者Callable接口的方式相对复杂,但是还可以继承其他类。

三.线程的生命周期

3.1 概述

线程并不是一创建就开始执行,也不是一直在执行。线程在生命周期的不同时期具有不同状态。

3.2 新建

新建状态就是刚刚创建(刚刚被new出来),还没有被执行,也还没有”等待执行“。

3.3 就绪

线程创建之后,调用start方法,就进入”等待执行“的状态了,就是”就绪“,也就是可以执行了,但是还不是开始执行。什么时候才开始,取决于JVM线程调度器的调度,相当于开始"排队"等待被执行了。

3.4 运行

也就是线程中的代码被执行的过程,但是这个状态不是一直保持,会有中断。

3.5 阻塞

阻塞也就是运行状态中断了,停止执行,并且释放出所占有的资源,进入的等待状态,其实不会被执行,也不是“等待执行”,它需要再次进入就绪状态,才能被接着执行。

线程可以主动进入阻塞状态,比如调用sleep方法,也可以被动进入,比如由于阻塞式I/O的方法没有返回,该线程被阻塞。

3.6 死亡

顾名思义,就是线程结束了。包括:

  • 执行体代码执行完成,正常结束
  • 抛出异常,异常结束
  • 调用stop方法停止运行

死亡的线程无法再次启动。

四.线程的属性

4.1 概述

线程拥有一些属性,包括:守护线程,线程优先级,线程组和处理未捕获异常的处理器。用来指示JVM如何对待这个线程。

4.2 守护线程(daemon属性)

守护线程其实就是后台线程,也叫“精灵线程”,把一个线程设置为守护线程是为了让该线程为其他线程服务,并且不要阻碍JVM的关闭

普通线程在停止前JVM不会自动关闭,而普通线程全部死亡时,JVM就会自动关闭,而不会理会是否还有守护线程在运行。

具体的设置方法为:setDaemon(boolean isDaemon);

注意:这个方法必须在start方法之前调用,否则会报错IllegalThreadStateException

4.3 线程优先级(priority属性)

线程优先级越高,被执行的机会越多。

线程优先级的范围是1·10,Thread类中有三个静态常量:

  • MAX_PRIORITY = 10
  • MIN_PRIORITY =1
  • NORM_PRIORITY = 5

PS:后面两个属性太啰嗦了,不想写了,用的时候再说

五.线程的方法

5.1 概述

Thread类提供了一些方法来方便我们对类进行控制。

5.2 sleep方法

让类停止运行一段时间(进入阻塞状态),参数为停止的毫秒数

5.3 yield方法

这个方法是用来线程让步,不会阻塞该线程,会进入就绪状态让JVM线程调度器重新调度,也就是让优先级高的线程执行,如果该线程刚好就是优先级最高的,那么继续执行。

5.4 join方法

让当前线程等待被调用了jion方法的线程执行完毕,再继续执行。

六.使用线程池

关于线程池的知识,这篇博客:深入理解 Java 线程池:ThreadPoolExecutor 讲得很明白了,我还是等做了项目实践之后,再来写心得吧,这一块暂且留空。

七.总结

之前训练营和考核时期,没有系统地学过多线程编程,基本都是遇到问题再去查,现查现用,今天比较完整地学习了多线程的基础知识,还专门去看了源码一探究竟,感觉对线程的理解比以前更加深刻了,但是多线程的知识远远不止这些,接下来的打算是明天学习线程安全和同步机制的知识,后天去学更深入的同步器和一些Java并发集合类,多线程的实践任务已经发下来了,主要还是要多在项目中实践。

小结今日收获:

  • 进程是资源分配的基本单位,线程是执行和调度的基本单位
  • 并发是轮流执行,并行是真正的同步执行
  • 多线程和多进程都可以实现并发,但是多线程更容易编程,线程切换开销小,弊端是存在线程安全问题
  • 启动线程的实质都是执行run方法中的执行体
  • 线程的生命周期包括:创建,就绪,运行,阻塞,死亡
  • JVM的关闭不会理会守护线程的状态
  • 三个线程方法:sleep,yield,join

参考资料

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java 多线程编程学习笔记(7月16号) 的相关文章

随机推荐

  • MiniGPT-4本地部署的实战方案

    大家好 我是herosunly 985院校硕士毕业 现担任算法研究员一职 热衷于机器学习算法研究与应用 曾获得阿里云天池比赛第一名 CCF比赛第二名 科大讯飞比赛第三名 拥有多项发明专利 对机器学习和深度学习拥有自己独到的见解 曾经辅导过若
  • Ubuntu22下OpenCV4.6.0+contrib模块编译安装

    目录 第一章 Ubuntu22下OpenCV4 6 0 contrib模块编译安装 第二章 ubuntu22下C kdevelop环境搭建 OpenCV示例 第三章 C 下OPENCV驱动调用海康GigE工业相机 文章目录 目录 Ubunt
  • K8S常用资源认识

    文章目录 一 Namespace 二 Pod 三 Label 四 Deployment 五 Service 一 Namespace namespace是kubernetes系统中的一种非常重要的资源 它的主要作用是用来实现多套环境的资源隔离
  • 基于栈与基于寄存器的指令集架构

    用C的语法来写这么一个语句 C代码 a b c 如果把它变成这种形式 add a b c 那看起来就更像机器指令了 对吧 这种就是所谓 三地址指令 3 address instruction 一般形式为 op dest src1 src2
  • python 模块和包

    文章目录 前言 模块 什么是模块 导入模块 import 导入模块 from 模块名 import 功能 from 模块名 import as定义别名 制作模块 模块的定位顺序 all 包 导入包 import 包名 模块 导入包 from
  • 打开Ubuntu18.04出现启动紫屏卡死不弹登录框问题

    1 进入grub高级模式 重启虚拟机 按esc进入 按 进入Ubuntu高级选项 2 选择recovery mode 3 选择root shell会话 输入root密码 4 编辑 etc gdm3 custom conf文件 将 Wayla
  • 5-快速排序

    假设有下面这样一排数据需要排序 3 7 6 1 2 9 1 2 6 排开常用的冒泡和选择排序 今天我们来试一下一种新的方法 快速排序 快速排序的思路如下 首先我们要先从这组数据中选出一个任意值X 然后以X为分界 比X小的数据排到X的左半边
  • Shiro之@RequiresPermissions注解原理详解

    前言 shiro为我们提供了几个权限注解 如下图 这几个注解原理都类似 这里我们讲解 RequiresPermissions的原理 铺垫 第一 首先要清楚 RequiresPermissions的基本用法 就是在Controller的方法里
  • python爬虫超时重试_python爬虫多次请求超时的几种重试方法(6种)

    第一种方法 headers Dict url https www baidu com try proxies None response requests get url headers headers verify False proxi
  • C#中的Dictionary简介

    简介 在C 中 Dictionary提供快速的基于键值的元素查找 当你有很多元素的时候可以使用它 它包含在System Collections Generic名空间中 在使用前 你必须声明它的键类型和值类型 详细说明 必须包含名空间Syst
  • tensorflow:使用全连接(full-connection)网络实现mnist

    tensorflow1 8 python3 6 4 coding utf 8 import tensorflow as tf import numpy as np from tensorflow examples tutorials mni
  • 【pytorch目标检测】FPN网络结构

    语义一般指的是图像每个像素点的类别归属 语义信息可以理解为与类别划分有关的信息 对网络前端通过非线性变换 对图像内容中纹理 几何颜色等信息表达 这种表达会使网络后端对类别归属做出正确的预测 低级语义信息 对浅层特征的表达 如颜色 几何 纹理
  • vue中的异步请求Axios(个人学习笔记五)

    目录 友情提醒 第一章 传统的jQuery方式获取数据 1 1 后端controller层代码 1 2 传统的jQuery获取数据 1 3 使用vue对象和jQuery获取异步数据 第二章 使用Axios获取数据 2 1 axios简介 2
  • 数据库技术基础--基本概念

    说在前面 本系列文章专注于软考备考复习内容梳理 文章内容是对教材中知识点和考点的提炼 备考过程中可以有针对的进行复习 减少阅读量 有的放矢 导航目录 一 数据库与数据库管理系统 1 数据库 2 硬件 3 软件 4 人员 二 DBMS的功能
  • Android开发 - 掌握ConstraintLayout(六)链条(Chains)

    本文我们介绍链条 Chains 使用它可以将多个View连接起来 互相约束 可以创建横向的链条 也可以创建纵向的链条 我们以横向的链条举例 我们先创建三个按钮 我们选中三个按钮后在上面点右键创建链条 创建后我们发现这三个View平均分布地排
  • Flutter中神奇的Builder组件

    经常遇到of这样的方法 却怎么都不可用 google之后找到了正确答案 原答案地址 https stackoverflow com questions 52502498 get current tab of defaulttabcontro
  • WPS以及它的两种方式PIN与PBC的理解

    WPS Wi Fi Protected Setup PBC Push ButtonConfiguration 这个是不需要密码的方式 PIN Personal Information Number 这个是要用密码的方式通常用在电子设备的互通
  • vue2 使用 Sortable 库进行拖拽操作

    一 vue 项目使用 文档地址 https www itxst com sortablejs neuinffi html 1 安装依赖 npm i S vuedraggable 2 vue 文件引入组件 import draggable f
  • Verilog抽象和语言功能

    Verilog模型的不同级别抽象 1 系统级 system level 2 算法级 algorithm level 3 RTL级 register transfer level 以上三种属于行为描述 只有 3 才与逻辑电路有明确的对应关系
  • Java 多线程编程学习笔记(7月16号)

    文章目录 作者信息 前言 一 什么是线程 1 1 进程和线程 1 2 并发和并行 1 3 上下文切换 Context Switch 二 创建线程的三种方式 2 1 概述 2 2 继承Thread类 2 3 实现Runnable接口 2 4