多线程初阶(一)

news2025/1/16 5:57:53

目录

前言:

认识多线程

创建线程

run方法和start区别

继承Thread类

实现Runnable接口

匿名内部类实现继承Thread类

匿名内部类实现Runnable接口实例

Lambda表达式

中断线程

等待线程

线程休眠

线程状态

线程状态之间切换

代码观察线程的状态

线程安全

线程安全测试

解析

线程安全总结

         小结:


前言:

    多线程在实际开发中会经常使用到,它可以对于硬件资源充分的利用,提升代码的执行效率。这样会对我们开发过程中提供了很大便利。

认识多线程

    上篇文章讲解了并发和并行的区别,目的就是将硬件资源得到充分利用。线程实际在操作系统内核中调度是抢占式调度,随机执行的。每个线程在cpu里执行时,都是以指令的方式去执行一个线程的,一个线程会包含多条指令。

    线程在cpu里执行具体是并发还是并行,我们是不确定的,具体实现是由操作系统内核实现的。当操作系统调度一个线程,会执行多条指令。进行线程切换的时候,这个时间点是不确定的,两个线程的指令会随机组合,有无数种可能。正是由于这种抢占式调度,随机执行的方式,对于代码的执行顺序就会有很大的不确定性,这就带来了线程安全问题。

    解决线程安全问题,我们就需要对于一部分指令,保证其原子性。使另一个线程阻塞等待,通过加锁实现。

创建线程

run方法和start方法区别

    start是启动一个线程,当这个线程的pcb被cpu调度时,这个线程就真实存在了。线程当被调度时执行的代码就是run方法里的代码体,当run方法执行结束时,线程也就结束了,这时候线程的pcb也就释放了,但是线程的引用还在。

继承Thread类

    继承Thread类,实现父类引用子类实例。

class MyThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("aaaa");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadDemo1 {
    public static void main(String[] args) throws InterruptedException {//主线程
        //创建线程
        Thread thread = new MyThread();
        //启动线程
        thread.start();

        while (true) {
            System.out.println("bbbb");
            Thread.sleep(1000);
        }
    }
}

    注意:这里主线程和thread引用的线程里面都有死循环代码,我们可以通过jconsole工具查看线程的一些状态。

 实现Runnable接口

    实现Runable接口,将实例传入Thread的构造方法。这样可以将线程具体要做的事和引用分离开,解耦合。

class MyRunable implements Runnable {
    @Override
    public void run() {
        System.out.println("aaaa");
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        //runable描述了这个线程要干什么
       Runnable runnable = new MyRunable();
       //创建线程
       Thread thread = new Thread(runnable);
       thread.start();
    }
}

匿名内部类实现继承Thread类

public class ThreadDemo3 {
    public static void main(String[] args) {
        //使用匿名内部类
        //匿名内部类为Thread的子类
        //创建了子类的实例,让thread引用
        Thread thread = new Thread() {
            @Override
            public void run() {
                System.out.println("aaaa");
            }
        };
        thread.start();
    }
}

匿名内部类实现Runnable接口实例

public class ThreadDemo4 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("aaaa");
            }
        });
        thread.start();
    }
}

Lambda表达式

    由于Runnable为函数式接口,因此可以使用lanbda表达式。

//lambda表达式实现函数式接口Runable(实例其函数式接口对象)
public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("aaa");
        });
        thread.start();
    }
}

中断线程

    注意:终止线程只是通知说线程该终止了,但具体要不要终止是线程里说了算的。

    可以手动设置标志位,通过另一个线程来改变这个标志位,进一步来决定run方法的结束,控制线程的终止。

public class ThreadDemo7 {
    private static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置标志位,其他线程只要改变标志位,就可以结束run方法,结束本线程
                while (flag) {
                    System.out.println("aaaa");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        thread.start();
        Thread.sleep(3000);
        flag = false;
    }
}

    1)使用自带的isInterrupted方法设置标志位。

    2)interrupt方法控制标志位。

    注意:isInterrupted方法初始值默认为false,可以通过interrupt设置为true。但是如果通过interrupt设置标志位的时候,这个线程处于sleep(),TIMED_WAITING状态时,就会唤醒线程。这时候sleep就会抛出一个异常,并且清空标志位,改回false。接下来要不要中断就看我们代码的结构了。

public class ThreadDemo7 {
public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //可以获得当前线程的引用
                //isInterrupted()线程是否中断,默认为false
                while (!Thread.currentThread().isInterrupted()) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        //也可等待一会再中断线程
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException ex) {
                            ex.printStackTrace();
                        }
                        //跑完异常可以主动中断线程
                        break;
                    }
                    System.out.println("aaaaa");
                }
            }
        });
        thread.start();
        Thread.sleep(3000);
        //当设置另一个线程标志位时(会设置为true),如果这个线程正处于休眠状态,sleep就会抛异常,并且清空标志位(改回标志位为false)
        //sleep清空标志位的原因:当触发sleep唤醒线程后,这个线程的状态就交给我们自己了吗,类似于代码“暂停”的做法,都会清空标志位(wait,join)
        thread.interrupt();//设置标志位,告诉线程该中断了
    }
}

等待线程

    由于线程的抢占式执行,随机调度。不可以确定线程的执行顺序,但是我们可以通过一个线程等待一个线程(阻塞),来控制线程的结束时间。java里使用join方法。

    当线程在就绪队列里,这个线程就可以被操作系统内核调度。线程一旦阻塞就会进入阻塞队列,直到阻塞结束时,才会被调入就绪队列。

public class ThreadDemo8 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 3; i++) {
                    System.out.println("aaa");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        thread.start();
        Thread.sleep(5000);
        //线程等待(阻塞)
        //当主线程走到join时就会阻塞,直到thread线程执行结束,才会执行主线程(thread线程肯定比main线程先结束)
        //可设置参数为最大阻塞时间
        thread.join();
        System.out.println("bbb");
    }
}

    注意:当主线程执行到join就会阻塞,进入阻塞队列。直到thread线程执行结束,主线程才会进入就绪队列,就可以被操作系统内核调度。

线程休眠

    线程休眠会由就绪队列换到阻塞队列。当指定的休眠时间结束,就可以由阻塞队列回到就绪队列。这样才可以被调度,所以休眠时间会大于等于我们指定的时间。java里使用sleep()方法,参数以毫秒位单位。

    注意:上述代码里使用的sleep均为线程休眠。

线程状态

    1)NEW:有线程对象,但没有启动线程。

    2)RUNNABLE:1)线程正在cpu上执行 2)线程处于就绪队列,随时可以被调度。

    3)TERMINATED:线程结束,pcb已经释放,但是线程对象还在。

    线程阻塞时状态:

    4)TIMED_WAITING:线程阻塞,处于sleep,wait,join等。

    5)BLOCKED:等待锁产生的阻塞。

    6)WAITING:等待其他线程来通知。

线程状态之间切换

 注意:

    线程状态之间切换主线是由NEW ---> RUNNABLE ---> TERMINATED。阻塞时的一些状态都是线程已经执行起来了,在这个基础上的一些不同方式的阻塞。线程在等待锁产生的阻塞就是BLOCKED。线程遇到sleep,join,wait等方式阻塞是TIMED_WAITING状态。当线程需要其他线程来通知时是WAITING状态。

代码观察线程的状态

public class ThreadDemo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 3; i++) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("aaa");
                }
            }
        });
        //NEW
        //有线程对象,但是没有启动线程
        System.out.println(thread.getState());
        thread.start();//一个线程只能start一次
        //RUNNABLE
        //可执行的,1.就绪队列 2.正在cpu上执行的
        for(int i = 0; i < 10; i++) {
            System.out.println(thread.getState());
        }
        //TIMED_WAITING
        //当线程处于sleep时,这个时间获取线程状态
        //BLOCKED
        //等待锁产生的最阻塞
        //WAIT
        //等待其他人来通知
        //(这三种状态都是在描述不同的阻塞状态)
        Thread.sleep(8000);
        //TERMINATED
        //线程执行结束,pcb已经释放,但对象还在
        System.out.println(thread.getState());
    }
}

线程安全

线程安全测试

class Cumsum {
    public int a = 0;
    public void add() {
        a++;
    }
}
public class ThreadDemo15 {
    public static void main(String[] args) {
        Cumsum cumsum = new Cumsum();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 5000; i++) {
                     cumsum.add();
                }

            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 5000; i++) {
                    cumsum.add();
                }
            }
        });

        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(cumsum.a);

    }
}

    注意:这里有两个线程t1和t2,分别针对a进行累加5000次,结果很显然不是10000。为什么呢?

解析

    当两个线程都启动时,它们是并发执行的。线程是抢占式调度,随机执行的。执行一次a++需要有三条指令:1.首先将内存中数据读到cpu内存中(load) 2.将寄存器中数据加一(add) 3.将寄存器中数据写回内存(save)。

    由于这种抢占式调度,随机执行。这三条指令可以有无数种组合,只要第一个线程load完,没有save,这个期间第二个线程再去读内存中的数据,就会造成脏读问题(例如MySQL中的脏读),那么最终两个线程执行一次循环只会累加一次。如果在这个期间第二个线程执行了多次这三条指令,那么最终第一个线程执行一次循环,第二个线程执行多次循环也只累加一次。

    只有当第一个线程save完,第二个线程再去读内存中的数据,然后save。每个线程在读数据时保证在上一个线程save之后,这样数据就是正确的,这样的作法其实就是保证了这三条指令的原子性,可以通过加锁实现指令的原子性。

//线程安全测试
class Cumsum {
    public int a = 0;
    synchronized public void add() {
        a++;
    }
}
public class ThreadDemo15 {
    public static void main(String[] args) {
        Cumsum cumsum = new Cumsum();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 5000; i++) {
                     cumsum.add();
                }

            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 5000; i++) {
                    cumsum.add();
                }
            }
        });

        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(cumsum.a);

    }
}

    注意:加锁之后,数据就正确了。下篇详细介绍。

线程安全总结:

    1)根本原因:抢占式执行,随机调度。

    2)代码结构,多个线程修改一个变量产生线程安全问题。多个线程修改多个变量,多个线程读同一个变量,一个线程修改一个变量都不会产生线程安全问题。

    3)原子性,保证一些指令不可拆分(加锁)。

    4)内存可见性问题(后面介绍)。

    5)指令重排序(编译器优化出bug)。

小结:

    多线程的学习我们需要理解线程之间的关系,理清它们执行的逻辑,分析代码。这样会对我们学习有很大帮助。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/48623.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

多点DMALL × Apache Kyuubi:构建统一SQL Proxy探索实践

伴随着国家产业升级的推进和云原生技术成熟&#xff0c;多点 DMALL 大数据技术也经历了从存算一体到存算分离的架构调整变迁。本文将从引入 Kyuubi 实现统一 SQL Proxy 的角度讲述这一探索实践的历程。 多点 DMALL 成立于2015年&#xff0c;提供一站式全渠道数字零售解决方案 D…

STL的常用算法-查找 (20221130)

STL的常用算法 概述&#xff1a; 算法主要是由头文件<algorithm> <functional> <numeric> 组成。 <algorithm>是所有STL头文件中最大的一个&#xff0c;涉及比较、交换、查找、遍历等等&#xff1b; <functional>定义了一些模板类&#xff0…

使用记账软件记录生活收支明细,如何防止收支不被他人修改

坚持记账是每个人都必须要做的事情&#xff0c;日常生活中的生活开支都是一笔笔的支出&#xff0c;一个月挣来的工资&#xff0c;在不知不觉之中就花完了&#xff0c;可以使用——晨曦记账本记录生活明细&#xff0c;为了防止被他人修改&#xff0c;该如何操作呢&#xff1f;一…

Allegro调丝印规范操作指导

Allegro调丝印规范操作指导 Allegro和其它PCB设计软件一样,丝印的排布也是类似的,具体规范介绍如下 以下图为例 打开Setup-Design Parameter 选择text 设置丝印字体的参数 设置需要丝印的字体,比如3号字体,参数如下 然后点击OK Edit-Change需要调整丝印的字体

Linux-Hadoop部署

部署Hadoop一、Hadoop部署模式1、独立模式2、伪分布式模式3、完全分布式模式二、Hadoop集群规划1、集群拓扑2、角色分配三、JDK安装与配置1、下载JDK压缩包2、上传到master虚拟机3、在master虚拟机上安装配置JDK4、将JDK分发到slave1和slave2虚拟机5、将环境配置文件分发到slav…

图的初识·存储结构

邻接矩阵存储结构 用矩阵表示表示图中各个顶点之间的邻接关系和权值。如图G(V,E)G(V,E)G(V,E)&#xff0c;其中有N个结点&#xff0c;使用NxNNxNNxN的矩阵表示。 不带权值的图 Gij{1,无向图(vi,vj)或有向图的<vi,vj>是图中的边0,无向图的(vi,vj)或有向图的<vi,vj>…

猕猴桃的红色果肉受到特定的激活-抑制系统的控制

文章信息 题目&#xff1a;The red flesh of kiwifruit is differentially controlled by specific activation–repression systems 刊名&#xff1a;New Phytologist 作者&#xff1a;Wen-qiu Wang&#xff0c;Andrew C. Allan,Xue-ren Yin et al 单位&#xff1a;Zhejia…

猿如意开发工具|Sublime Text(4126)

文章目录 一、猿如意是什么&#xff1f; 二、如何使用猿如意下载安装Sublime Text 三、总结 一、猿如意是什么&#xff1f; 猿如意是一款面向开发者的辅助开发工具箱&#xff0c;包含了效率工具、开发工具下载&#xff0c;教程文档&#xff0c;代码片段搜索&#xff0c;全网搜…

【Pandas数据处理100例】(九十九):Pandas使用at_time()筛选出特定时间点的数据行

前言 大家好,我是阿光。 本专栏整理了《Pandas数据分析处理》,内包含了各种常见的数据处理,以及Pandas内置函数的使用方法,帮助我们快速便捷的处理表格数据。 正在更新中~ ✨ 🚨 我的项目环境: 平台:Windows10语言环境:python3.7编译器:PyCharmPandas版本:1.3.5N…

如何建立一套完善的销售管理体系?

怎样创建两套健全的产品销售体系&#xff1f;用工程建设观念管理工作产品销售项目组&#xff0c;创建健全的产品销售体系&#xff01; 逐步形成精确的最终目标虽说重要&#xff0c;但缺乏有效率的方式来破冰&#xff0c;最终目标可能将仅是这份无用。 篮球赛事球手的最终目标…

IOC 的底层原理和Bean管理XML方式、xml注入集合属性

目录 什么是IOC IOC底层管理 工厂模式 IOC 的过程 IOC 接口 IOC 操作Bean 原理 Bean 管理操作有两种方式 1. 基于xml 配置方式创建对象 2. 基于xml方式注入属性 第二种使用有参数构造注入 p 名称空间注入 ICO操作Bean管理&#xff08;xml 注入其他类型属性&#xff…

微信小程序的 websocket 以及 微信开发者工具测试 ws 协议没有数据的 离奇解决方案 记录

微信小程序的 websocket 在本地web能够使用ws协议去链接websocket&#xff0c;但是小程序不能使用。一、WSS 协议与 WS 协议二、业务场景记录 : 使用 ws 协议的 websocekt 做测试,但是在 h5中可以拿到实时数据,在微信开发者工具中以及真机调试中拿不到模拟数据的问题1. 首先在 …

2022安洵杯babybf

babybf 赛后分析了下&#xff0c;发现是一道很有意思的题目 Brainfuck是一种极小化的计算机语言&#xff0c;它是由Urban Mller在1993年创建的。由于fuck在英语中是脏话&#xff0c;这种语言有时被称为brainf*ck或brainf**k&#xff0c;甚至被简称为BF。 其实本题是一个c语言实…

三、【redux】异步action

文章目录1、不成熟的异步修改1.1、CODE1.1.1、count_action.js1.1.1、count_reducer.js1.2、异常2、异步action代码修正2.1、store.js2.2、count_action.js3、小总结action分两类&#xff1a; 同步&#xff1a;指action的值是Object类型的一般对象异步&#xff1a;指action的值…

在C#中使用Halcon开发视觉检测程序

本文的初衷是希望帮助那些有其它平台视觉算法开发经验的人能快速转入Halcon平台下&#xff0c;通过文中的示例开发者能快速了解一个Halcon项目开发的基本步骤&#xff0c;让开发者能把精力完全集中到算法的开发上面。 首先&#xff0c;你需要安装Halcon&#xff0c;HALCON 18.…

D. X-Magic Pair(辗转相除)

Problem - 1612D - Codeforces 给你一对整数&#xff08;a,b&#xff09;和一个整数x。 你可以用两种不同的方式改变这对整数。 设置&#xff08;分配&#xff09;a:|a-b|。 设置&#xff08;分配&#xff09;b:|a-b|。 其中|a-b|是a和b之间的绝对差值。 如果只用给定的操作就…

【Linux】shell命令行简单解释器

回顾一下&#xff0c;我们前面学习了进程创建&#xff0c;进程终止&#xff0c;进程等待&#xff0c;进程替换&#xff0c;通过这些内容我们可以来进行实现简单的shell命令行解释器&#xff01;&#xff01;&#xff01;下面我们直接来看一看如何去实现shell命令行解释器&#…

结合编辑器和PDFXplorer工具查看PDF文件结构

首先用编辑器打开PDF,可以看到如下结构 1.寻找文件结构入口 /Root k-value形式&#xff0c;/Root 98 0 R/ k是Root value是98 0 R 98 0 R 代表被引用&#xff0c;指向的对象是98 0 obj 用PDFExploer工具打开看&#xff0c;如下&#xff1a; 可以看出用工具打开的树形结构跟上…

[附源码]计算机毕业设计springboot快转二手品牌包在线交易系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

YonBuilder开发之后端函数

在前几期的文章中我们已经介绍过如何在YonBuilder中使用前端函数实现数据过滤功能。相对应于前端函数&#xff0c;在YonBuilder中还可以使用后端函数实现对于程序的扩展。通过前端函数实现的更多的是与页面交互相关的功能&#xff0c;而后端函数主要是用于预制按钮功能的扩展开…