Java:多线程概述与创建方式

2023-11-10

Java:多线程概述与创建方式

在之前的学习过程中,已经不止一次地提到了并发啊,线程啊,同步异步的内容,但是出于内容的局部一体,之前总是几笔带过,并附上:以后学习的时候再细说。

那么,现在到了细说的时候,在翻阅并参考了介绍Java并发编程的书之后,突然感觉压力有些大,因为有些概念确实比较抽象。所以之后的内容不定长短,但是每天都会试着输出一些。

进程和线程

一个进程可以拥有多个线程,一个线程必须拥有一个父进程。
进程:当前操作系统正在执行的任务,也是操作系统运行程序的一次执行过程。
线程:是进程的执行单元,是进程中正在执行的子任务。
就好像我们正在使用的QQ,正在放歌的音乐软件,正在打的游戏,就是一个个的进程。我们在QQ进程中执行的各种操作,就是一个个的线程。

每个Java的应用程序运行的时候其实就是个进程,JVM启动之后,会创建一些进行自身常规管理的线程,如垃圾回收和终结管理,和一个运行main函数的主线程

并发与并行

现在大部分的操作系统都是支持多进程并发运行的,就像我们现在正在使用电脑,可以通过任务管理器查查看,会发现有几十个几百个进程在“同时执行”。”同时执行“被打上了引号,显然事实上并不是。

并发:就拿进程来说,在同一个时刻,只能有一条指令执行,但是多个进程可以被快速地轮换执行,CPU的执行速度之快,让人产生这些个进程就是在同时执行。
并行:就是同一时刻,多条进程指令在多个处理器上同时执行

看看下面的图就懂了:

接下来是我对于并发和并行假想场景:
并发场景:假设现在有一台只能一个人玩的电脑,老大和老二兄弟俩都想玩一小会儿,那没办法,得想办法解决啊。打一架吧,谁抢到算谁的。不管是谁抢到,他们一定玩到满足才会罢休,这就是现在操作系统所采用的高效率的抢占式多任务操作策略
并行场景:现在有两台电脑,老大老二都各自玩各自的电脑,不争也不抢。

多线程的优势

线程被称为轻量级进程,大多数情况下,进程中的多线程的执行是抢占式的,就和操作系统的并发多进程一样。

线程拥有自己的堆栈程序计数器局部变量,允许程序控制流的多重分支同时存在于一个线程,共享进程范围内的资源,因此,同一进程中的线程访问相同的变量,并从同一个堆中分配对象,实现良好的数据共享,但是如果处理不当,会为线程安全造成一定的隐患。

多线程相比于多进程的优势:

  • 多个线程之间可以共享内存,而进程之间不可以。
  • 操作系统创建线程的代价比进程小,实现多任务并发效率更高

以下参考自《JAVA并发编程实战》:

  • 一个单线程应用程序一次之能运行在一个处理器上。在双处理器系统中只运行一个应用程序,相当于其中一个处理器空闲,50%的CPU资源没有利用上。随着处理器的增多,单线程的应用程序放弃的CPU资源将会更多。这一点,正好也侧面反映了多线程能够更有效地利用空闲的处理器资源
  • 处理器在某些情况是空闲的,如在等待一个同步IO操作完成的时候。这个时候,暂且不论多处理器,仅仅针对单处理器,多线程的优势也是相当明显的,可以很好地利用处理器空闲的时间运行另外一个线程

线程的创建和启动

先来看看多线程编程中这个相当关键的类,java.lang.Thread,官方文档说了:有两种方式创建线程,就是下面这俩:

继承Thread类

  • 将一个类声明为Thread的子类。
  • 这个子类应该覆盖类Threadrun()方法。

创建线程如下:

/*创建线程*/
//创建一个类继承Thread类
class TDemo extends Thread{
    //线程要执行的任务在run方法中
    @Override
    public void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
        }
    }
}

启动线程如下:

    public static void main(String[] args){
        //创建了TDemo的实例
        TDemo t1 = new TDemo();
        //启动线程,并调用run方法
        t1.start();
        System.out.print("main");
    }
    //输出结果:main01234

创建TDemo的实例对象不等于启动了该实例所对应的线程,启动需要调用线程对象的start()方法。

start()和run()

  • new创建了TDemo的实例,只是创建了一个线程,此时它处于新建状态,有JVM分配内存,并初始化成员变量的值,是个配置的过程。

  • 线程对象调用start()方法之后,线程就会处于就绪状态,JVM会为其创建方法调用栈和程序计数器,表示这个线程可以执行,但真正啥时候开始执行取决于JVM中线程调度器的调度

  • 之后才进入运行状态,执行run()方法中的方法体。

我们试着把start()方法换成run()方法看看结果:01234main


我们通过输出结果可以看到,调用start()方法,系统会把run()方法当成线程执行体处理,主线程和我们创建的线程将并发执行。但如果单纯调用run()方法,系统会把线程对象当成一个普通的对象,run()方法也只是普通对象方法的一部分,是主线程的一部分

实现Runnable接口

这是Runnable接口的内容,@FunctionalInterface注解表示函数式接口,和Java8新特性lambda表达式相关,之后再做学习总结。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
  • 创建线程的另一种方法是声明一个实现Runnable接口的类。
  • 然后,该类实现run方法。然后可以分配类的实例,
  • 在创建线程时作为参数传递,并启动它。
//实现Runnable接口
class RDemo implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
        }
    }
}

//创建并启动线程
Thread t = new Thread(new RDemo());
t.start();

调用public Thread(Runnable target)构造器,将Runnble接口类型对象传入作为参数,构建线程对象。
当然还可以用匿名内部类的形式:

    //匿名内部类创建并启动线程
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.print(i);
            }
        }
    }).start();

实现Callable接口

这是Callable接口的内容:

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

除了上面两种方法之外,从书上看到还有一种Java5新增的方法,利用Callable接口,官方文档是这样描述的:

  • Callable接口类似于Runnable,需要实现接口中的call()方法。但是,Runnable不返回结果,也不能抛出已检查的异常。
  • Runnable接口提供run()方法支持用户定义线程的执行体,而Callable中提供call()方法。
    • 拥有返回值
    • 允许抛出异常
  • 通过泛型我们可以知道,Callable接口中的形参类型需要和call方法返回值类型相同:

光有Callable接口还不行,毕竟隔了5年才出来,为了尽量避免修改之前的代码,适应当前环境,Java5还新增了配套的Future接口:

public interface Future<V> {

    //试图取消Callable中任务的执行,如果任务已经完成、已经被取消、或因其他原因无法被取消,返回false。
    boolean cancel(boolean mayInterruptIfRunning);

    //如果此任务在正常完成之前被取消,则返回true
    boolean isCancelled();

    //如果此任务已完成(正常的终止、异常或取消),则返回true
    boolean isDone();

    //如果需要,则等待计算完成,然后检索其结果。
    V get() throws InterruptedException, ExecutionException;
    
    //如果需要,将等待最多给定的时间以完成计算,然后检索其结果。
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

通过继承关系可以发现,RunnableFuture接口同时继承了RunnableFuture接口,意味着实现RunnableFuture接口的类既是Runnable的是实现类,又是Future的实现类。FutureTask就是充当这样的角色,它的实例可以作为target传入Thread的构造器中。

通过查看源码,可以发现FutureTask内部维护了一个Callable的对象,可以通过下面的这个构造器初始化Callable对象。

    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
  • 用匿名内部类的方式,将实现call()方法的Callable实现类对象作为参数传递给FutureTask的构造器中,构建一个FutureTask类的对象。
    FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            int i = 0;
            while(i<10){
                System.out.println(Thread.currentThread().getName());
                i++;
            }
            return i;
        }
    });

  • 之后可以通过Thread类构造器:public Thread(Runnable target, String name)将task对象作为参数创建新线程并启动。name参数是可以自定义线程的名字。
new Thread(task,"name").start();

  • 最后可以通过task对象调用get()方法得到call()方法的返回值,需要注意处理抛出的异常。
    try {
        System.out.println(task.get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }

创建方式的区别

继承类Thread和**实现接口(Runnable或Callable)**这两种方式的区别?

  • 前者需要定义子类继承Thread类,可以直接通过创建子类对象作为线程对象,而后者创建的Runnable对象只是线程对象的target。
  • 同样的,获取当前对象的方法也不同,前者可以直接使用this获取当前对象的引用。后者则需要调用Thread的静态方法currentThread()
    下面是两个获取当前线程名的示例:
//继承Thread
System.out.print(this.getName()+i);
//实现Runnable接口
System.out.print(Thread.currentThread().getName()+i);

  • 前者线程类每创建一个线程都需要创建一个对象,对象之间不能共享实例变量。而后者通过接口的实现类创建的多个线程可以共享同一个Runnable类型的target,也就是这个线程类的实例变量。

  • 前者定义线程类需要继承Thread,而Java只支持单继承,支持接口多实现,显然在灵活性方面,后者优于前者。


本文作为个人学习笔记,仍停留在比较浅显的层面,还需要大量的实践去感悟并发编程的奥义。

参考资料:《JAVA并发编程实战》、《疯狂Java讲义》、《JAVA多线程设计模式》

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

Java:多线程概述与创建方式 的相关文章

随机推荐

  • 以太坊2.0主网即将上线 一轮前所未有的超级牛市来了

    8月4日 以太坊2 0第0阶段信标链的最终版测试网Medalla上线 多个客户端同时运行测试 至少有16384个验证者参与了本次测试 在测试过程中 虽然出现了Nimbus 和 Lodestar 节点因无法处理测试网的负载量而卡住 导致Med
  • logback配置两套日志路径

    日志推送工作根据公司日志管理规范及国家攻防演练需要 请协助对所有应用重要场景的日志进行收集 需求如下 1 涉及场景 用户登录场景 密码修改场景 密码找回场景 注册场景 2 日志字段需求及格式 filebeat对接日志格式 登录 修改 注册
  • 消息队列MQ-面试题

    一个用消息队列 的人 不知道为啥用 MQ 这就有点尴尬 1 什么是消息队列 可以看作是一个存放消息的容器 当我们需要使用消息的时候可以取出消息供自己使用 消息队列是分布式系统中重要的组件 使用消息队列主要是为了通过异步处理提高系统性能和削峰
  • vue3 中应用 element plus,pinia 实现简易购物车

    使用pinia 进行全局的状态管理 降低组件之间的耦合性 一 项目需要的资源 1 element plus官方地址 https element plus gitee io zh CN guide quickstart html 2 pini
  • matlab卡农,又用Mathematica写了一段卡农

    你能在这里听到它 用数学软件演奏音乐早已不是什么新鲜事 很早就有人用Matlab弹奏卡农或是最炫民族风 最近我知道还有人用无理数生成一段音乐 很有趣 而我选择用Mathematica弹奏卡农是因为卡农这种谱曲方式很奇妙 体现在程序上也会是简
  • 内网如何下载docker镜像

    内网无法联网 因此不能直接运行docker pull命令下载docker镜像 可按下述方法获取docker镜像 步骤 1 在可以联网的机子上执行docker pull命令下载镜像 如 sudo docker pull freewil bit
  • 2015年1月13日星期二(11-1深度缓存和可见性简介)

    新的一年 开始新的篇章 终于进入第11章了 必须首要搞这个 争取今年搞完 我也相信 会对3D学习不是浪费时间 而是内力提升 在以前 用画家算法进行多边形排序 即根据渲染列表中的每个多边形的平均 最小或最大值进行排序 然后 再从后到前的顺序绘
  • oracle修改open_cursors,oracle的open_cursors问题探究

    今天老魏遇到了一个这样的问题 在google之后 发现讲open cursors参数调大一些 就可以避免这样的问题 具体操作方法 可以自行百度 google 老魏在学习的时候 对这个游标的概念有点傻傻分不清 于是花费了一些时间 想把这个问题
  • Kali系统MSF模块暴力破解MySQL弱口令漏洞

    一 实验环境 1 攻击方 攻击环境使用KALI系统 使用虚拟机搭建 的Metasploit模块 msfconsole是metasploit中的一个工具 它集成了很多漏洞的利用的脚本 并且使用起来很简单的网络安全工具 这里要特别强调 被攻击的
  • leetcode小白随笔记(二)

    auto类型说明符 用它就能让编译器替我们去分析表达所属的类型 string对象上的操作 初始化的方式 string对象 上的操作 标准库类型vector 定义和初始化vector对象 迭代器介绍 标准容器迭代器的运算符 使用迭代器 不使用
  • 中科大DIA复习内容

    DIA复习 第2章 图像数字化 可分离和正交图像变换 图像变换 成像过程 视觉过程 成像变换 成像亮度 视觉系统 采样和量化 空间分辨率越低 区域边界出现方块 图像幅度分辨率越低 图像出现虚假轮廓 量化的比特数越多 灰度级越多 连通悖论 考
  • 进入到文件系统,使用mount命令挂载到Linux不成功。出现 failed: No such device

    如图 原因 nfs模块未配置加载到内核中 解决办法 加入nfs模块到内核中 重新配置 配置如下 File systems gt Network File Systems gt lt gt NFS client support NFS cli
  • STM32RS485通信

    硬件图 具体看各自的板载资源图 本章所要实现的功能是 通过操作 KEY UP 键 STM32F1 的串口 2 将 PC 机发 送过来的数据原封不动的返回给 PC 机串口 同时 DS0 指示灯不断闪烁 提示系 统正常运行 程序框架如下 1 初
  • js几种加密/解密方法

    1 斯坦福大学的js加密库 简称SJCL 关于1 是斯坦福大学计算机安全实验室创立的项目 旨在创建一个安全 快速 短小精悍 易使用 跨浏览器的js加密库 如果有更复杂的安全需求 可以查阅API 文档地址是 http bitwiseshift
  • C++多态

    个人简介 作者简介 大家好 我是菀枯 支持我 点赞 收藏 留言 格言 不要在低谷沉沦自己 不要在高峰上放弃努力 前言 之前我们已经将面向对象三大特性中的封装和继承讲了 接下来剩下最后一个环节了 那就是 多态 多态概念 通俗来说 就是去做相同
  • 04-----无法执行二进制文件: 可执行文件格式错误

    这个问题一般是gcc的命令参数导致的 例如我在cmake添加如下参数 set CMAKE CXX FLAGS DEBUG ENV CXXFLAGS O0 g ggdb3 Wall std c 11 fPIC 生成的可执行文件会很小 在生成的
  • 二:PWM配置

    1 PWM其实是一个定时器 配置这些寄存器要注意 需要打开特殊功能寄存器的开关 1 程序 P SW2 0x80 最高位EAXFR置1 允许访问XFR寄存器 P SW2的B7位要置1 才能使用特殊功能寄存器哦 2 打开想要使用PWM模块的时钟
  • 给我一个爬取天气数据的Python程序

    这是一个使用 Python 爬取天气数据的简单示例 import requests def get weather data city 向天气 API 发起请求 url https api openweathermap org data 2
  • 主充/pd charger 调试精品文章转载

    PD充电调试问题解析 一 之前一直对充电的调试有了解 这次经过一个案子 对最新的充电技术和方案有所了解 现记录一下 充电框图 几个关键的概念 1 PD的输出电流 电压 他的对象是 充电IC PD Out gt gt Charge IN 2
  • Java:多线程概述与创建方式

    文章目录 Java 多线程概述与创建方式 进程和线程 并发与并行 多线程的优势 线程的创建和启动 继承Thread类 start 和run 实现Runnable接口 实现Callable接口 创建方式的区别 Java 多线程概述与创建方式