Java并发编程第2讲——线程基础

news2024/12/26 22:32:31

目录

一、线程简介

1.1 什么是线程

1.2 线程的组成

1.3 线程的特点

1.4 Java的main方法

二、线程的创建与启动

2.1 线程的创建

2.1.1 继承Thread类(无返回值)

2.1.2 实现Runnable接口(无返回值)

2.1.3 实现Callable接口(有返回值、可抛异常)

2.1.4 Executors线程池

2.2 start()和run()的区别

三、线程的生命周期

3.1 线程的状态转换

 3.2 转换示意图

四、线程的常用方法

4.1 休眠(sleep)

4.2 放弃时间片(yield)

4.3 加入(join)

4.4 优先级(priority)

4.5 守护线程(daemon)

4.6 线程打断(interrupt)

五、总结


一、线程简介

1.1 什么是线程

这里第一讲已经提到过了,这里就简要说一下吧。

操作系统在运行一个程序时,就会为其创建一个进程。

进程是操作系统中资源分配的基本单位,线程是操作系统中调度执行的基本单位,一个进程包含多个线程,多个线程共享这个进程的资源。

1.2 线程的组成

任何一个线程都具有基本的组成部分:

  • CPU时间片:操作系统会为每个线程分配执行时间。

  • 运行数据:

    • 堆空间:存储线程需要使用的对象,多个线程可以共享堆中的对象。

    • 栈空间:存储线程需要使用的局部变量,每个线程都拥有独立的栈。

  • 线程的逻辑代码

1.3 线程的特点

  • 线程抢占式执行,结果随机。

    • 效率高。

    • 可防止单一线程长时间独占CPU。

  • 在单核CPU中,宏观上同时执行,微观上顺序执行。

1.4 Java的main方法

Java一开始就支持多线程,相较于同一时期的其他语言,这是个明显优势。下面使用JMX来看一下一个普通的main方法包含几个线程:

ps:JMX (Java Management Extensions)是一个为应用程序植入管理功能的框架。

public class JavaMain {
    public static void main(String[] args) {
        //java虚拟机管理线程的接口
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        //两个参数控制分别控制lockedMonitors和lockedSynchronizers的信息获取,这里仅线程和线程的堆栈信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        //打印信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println(threadInfo.getThreadId()+":"+threadInfo.getThreadName());
        }
    }
}

输出内容如下(内容可能不同,大家可以试一下)。

可以看到,一个main方法的运行就包含了多个线程。

二、线程的创建与启动

下面介绍一下线程的创建方式和启动。

2.1 线程的创建

2.1.1 继承Thread类(无返回值)

  1. 创建自定义线程类MyThread,并继承Thread类。
  2. 重写run()方法,在run方法添加要执行的逻辑。
  3. 创建线程对象,调用start()方法。
public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("----自定义线程MyThread------");
    }
    
    public static void main(String[] args) {
       MyThread thread=new MyThread();
       //启动线程
       thread.start();
       System.out.println("=====main线程====");
    }
}

输出结果如下:

2.1.2 实现Runnable接口(无返回值)

  1. 创建自定义线程类MyRunnable,实现Runnable接口,重写run()方法。
  2. 创建线程对象MyRunnable,并把这个对象传递给Thread类对象。
  3. 调用start()启动线程。
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("----自定义线程MyRunnable------");
    }
    public static void main(String[] args) {
        MyRunnable runnable=new MyRunnable();
        Thread thread=new Thread(runnable);
        thread.start();
        System.out.println("=====main线程====");
    }
}

输出结果如下: 

2.1.3 实现Callable接口(有返回值、可抛异常)

  1. 创建自定义MyCallable类,实现Callable接口并重写call()方法。

  2. 创建MyCallable对象,并传入FutureTask类对象,创建Thread类,传入FutureTask对象。

  3. 调用start()方法。

ps:FutureTask类,后续会专门写一篇文章介绍FutureTask和CompletableFuture类。

public class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println("--------自定义Callable线程-----------");
        return "hello world";
    }
    public static void main(String[] args) 
            throws ExecutionException, InterruptedException {
        MyCallable callable=new MyCallable();
        FutureTask<String> task=new FutureTask<>(callable);
        new Thread(task).start();
        System.out.println("---------main线程----------");
        //获取返回值
        String s = task.get();
        System.out.println("s = " + s);
    }
}

输出结果如下: 

2.1.4 Executors线程池

  1. 创建Executors类对象

  2. 创建实现Runnable或Callable接口的任务

  3. 将任务提交给线程池执行

public class MyExecutors implements Runnable{
    @Override
    public void run() {
        System.out.println("-------线程池------");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        //创建线程池对象
        ExecutorService executor=Executors.newFixedThreadPool(5);
        //提交任务给线程池执行
        executor.submit(new MyExecutors());
        //关闭线程池
        executor.shutdown();
        System.out.println("=========main线程========");
    }
}

 输出结果如下:

2.2 start()和run()的区别

简单概括就是:start()方法会启动一个新线程并调用run()方法;而run()方法是直接调用线程的逻辑,并不会启动新线程。

下面用代码举例说明一下:

首先看一下start()方法

public class StartAndRun {
    public static void main(String[] args) {
        //创建一个线程(匿名内部类写法)
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("Runnable-----"+i);
                }
            }
        });
        //start()
        thread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("Main----"+i);
        }
    }
}

输出结果如下:

如图,Runnable和Main线程是交错穿插执行的,说明start()开启了一个新线程。

下面再看看run()方法:

public class StartAndRun {
    public static void main(String[] args) {
        //创建一个线程
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("Runnable-----"+i);
                }
            }
        });
        thread.run();

        for (int i = 0; i < 5; i++) {
            System.out.println("Main----"+i);
        }
    }
}

输出结果如下:

 从上可以看出,Runnable和Main线程是顺序执行,所以run()方法并不会启动新的线程。

三、线程的生命周期

3.1 线程的状态转换

Java中线程的状态分为6种:

  1. 初始(New):新创建了一个线程,还没调用start()方法

  2. 运行(Runnable):Java线程中将就绪(Ready)运行中(Running)统称为“运行”。就绪(Ready):线程对象创建后,其他线程(比如Main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中并分配CPU使用权。运行中(Running):就绪(Ready)的线程获得了CPU时间片,开始执行代码

  3. 阻塞(Blocking):表示线程阻塞于锁(后续会讲到),线程无法继续执行。在该状态下,可以通过满足条件回到就绪状态,然后再次竞争执行

  4. 等待(Waiting):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

  5. 超时等待(Time_Waiting):该状态不同于Waiting,可以在制定的时间后自行返回。

  6. 终止(Terminated):表示该线程已经执行完毕。(终止是最终状态,无法再转换为其他状态)。

 3.2 转换示意图

四、线程的常用方法

4.1 休眠(sleep)

  • public static native void sleep(long millis) throws InterruptedException;

当前线程休眠millis毫秒后,继续执行。

public class Sleep {
    public static void main(String[] args) {
        SleepThread sleepThread=new SleepThread();
        sleepThread.start();
    }
    static class SleepThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()+"...."+i);
                //休眠
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

 输出结果如下:

4.2 放弃时间片(yield)

  • public static native void yield();

当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。

public class Yield {
    public static void main(String[] args) {
        YieldThread yieldThread = new YieldThread();
        YieldThread yieldThread1 = new YieldThread();
        yieldThread.start();
        yieldThread1.start();
    }
    static class YieldThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "...." + i);
                //临时放弃cpu,让给优先级和它相同或比它优先级高的线程
                Thread.yield();
            }
        }
    }
}

输出结果如下:

由输出结果可以看出,启动两个线程执行上述代码,所得到的结果更接近于交替执行。

4.3 加入(join)

  • public final void join() throws InterruptedException

允许其他线程加入到当前线程中。当某个线程调用该方法时,加入并阻塞当前线程,直到加入的线程执行完毕,当前线程才继续执行。

public class Join {
    public static void main(String[] args) {
        JoinThread joinThread=new JoinThread();
        joinThread.start();
        //加入线程
        try {
            //阻塞主线程
            joinThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程:-----"+i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    static class JoinThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()+"...."+i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

输出结果如下:

4.4 优先级(priority)

  • pubic final void setPriority(int newPriority)

改变该线程的优先级,线程优先级为1-10,默认为5,10为最高优先级,1为最低优先级,优先级越高,表示获取CPU机会越多。

ps:线程是抢占式执行的,并不会严格遵守优先级的设置。

public class Priority {
    public static void main(String[] args) {
        PriorityThread priority=new PriorityThread("线程1");
        PriorityThread priority1=new PriorityThread("线程2");
        PriorityThread priority2=new PriorityThread("线程3");
        priority.setPriority(1);
        priority1.setPriority(10);
        priority.start();
        priority1.start();
        priority2.start();
    }
    static class PriorityThread extends Thread{
        public PriorityThread(String name){
            super(name);
        }
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                System.out.println(Thread.currentThread().getName()+"-----"+i);
            }
        }
    }
}

输出的内容太多了,这里就不贴图片了。循环次数少没效果,我把循环次数加到了1w甚至10w,执行了很多次才有结果:线程1最后执行完的几率最大,因为它的优先级是1,属于最低优先级。所以,别妄想着通过设置线程优先级来控制程序的执行顺序了😂。

4.5 守护线程(daemon)

  • public final void setDaemon(boolean on)

如果参数为true,则标记该线程为守护线程。

在JAVA中线程有两类:用户线程(前台线程)守护线程(后台线程)。

守护可以理解为守护用户线程。如果程序中所有用户线程都执行完毕了,守护线程会自动结束。

例如:垃圾回收线程属于守护线程。

下面创建一个线程循环40次,并设置为守护线程,主线程循环30次。

public class Deamon {
    public static void main(String[] args) {
        DaemonThread daemonThread=new DaemonThread();
        //设置为守护线程
        daemonThread.setDaemon(true);
        daemonThread.start();
        for (int i = 0; i < 30; i++) {
            System.out.println("主线程========="+i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    static class DaemonThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 40; i++) {
                System.out.println(Thread.currentThread().getName()+"----"+i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

输出结果如下:

我们可以看到,虽然子线程循环40次,但执行到30就结束了,原因就是子线程是守护线程,随着主线程的结束而结束。

4.6 线程打断(interrupt)

  • public void interrupt()

当线程处于阻塞状态(例如调用了sleep()、wait()、join()等方法)时,如果线程接收到中断的信号,会抛出一个异常,开发中可以捕获该异常并选择继续执行或者终止。

ps:中断只是发出一个信号,并不会停止线程执行。

public class Interrupt {
    public static void main(String[] args) throws Exception{
        InterruptThread interruptThread=new InterruptThread();
        interruptThread.start();
        System.out.println("20秒内输入任意字符结束子线程。。");
        System.in.read();
        interruptThread.interrupt();
        System.out.println("主线程结束。。。。");
    }
    static class InterruptThread extends Thread{
        @Override
        public void run() {
            System.out.println("子线程开始执行。。。");
            try {
                //先睡20秒
                Thread.sleep(20000);
                System.out.println("子线程自然醒了。。。。");
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("子线程被打醒了。。。");
            }
            System.out.println("子线程结束了。。。");
        }
    }
}

输出结果如下:

五、总结

本篇文章首先介绍了线程的基本概念、组成和特点,又使用JMX看了一下main方法的启动包含了几个线程。接着介绍了4种创建线程的方式,所以又介绍了run()和start()的区别(因为面试中有被问到过)。第三部分介绍了线程的6个状态以及它们之间的转换,并画了示意图。第四部分介绍了线程的sleep()、yield()、join()、setPriority()、setDaemon()等方法。

怎么感觉面试的时候或多或少都被面试官问到过,死去的记忆又开始攻击我了。😅

End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。 

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

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

相关文章

【Luogu】 P4331 [BalticOI 2004] Sequence 数字序列

题目链接 点击打开链接 题目解法 首先做一个重要的转化&#xff1a;把 b i b_i bi​ 单调上升变为 b i b_i bi​ 单调不降 如何转化&#xff1f;将 a i − i a_i-i ai​−i 变成新的 a i a_i ai​&#xff0c;将 b i − i b_i-i bi​−i 变新的 b i b_i bi​&#xff…

练习时长两年半的双机热备

1.双机热备技术产生的背景 传统的组网方式如下左图所示&#xff0c;内部用户和外部用户的交互报文全部通过防火墙A。如果防火墙A出现故障&#xff0c;内部网络中所有以防火墙A作为默认网关的主机与外部网络之间的通讯将中断&#xff0c;通讯可靠性无法保证。防火墙作为安全设备…

金蝶云星空与旺店通·企业版对接集成物料查询连通创建货品档案(cp_KW货品同步)

金蝶云星空与旺店通企业版对接集成物料查询连通创建货品档案(cp_KW货品同步) 接入系统&#xff1a;金蝶云星空 金蝶K/3Cloud结合当今先进管理理论和数十万家国内客户最佳应用实践&#xff0c;面向事业部制、多地点、多工厂等运营协同与管控型企业及集团公司&#xff0c;提供一个…

前端实现导出excel表格(合并表头)

需求&#xff1a;勾选行导出为excel表格&#xff08;合并表头 &#xff09; 一、安装插件 npm install --save file-saver xlsx运行项目报如下警告的话 运行npm install xlsx0.16.0 --save 来降低版本号&#xff08;最初我安装的版本号是0.18.16的版本&#xff09;再次运行项目…

VS构建项目报错信息及解决办法01

报错信息及解决1&#xff1a; 报错信息详情&#xff1a;1>MSVCRT.lib(exe_winmain.obj) : error LNK2019: 无法解析的外部符号 _WinMain16&#xff0c;该符号在函数 "int cdecl scrt_common_main_seh(void)" (?__scrt_common_main_sehYAHXZ) 中被引用 原因&…

SAP RFC介绍(sRFC/aRFC/tRFC/qRFC/pRFC)

异步RFC&#xff1a; aRFC后缀&#xff1a; STARTING NEW TASK CALL FUNCTION - STARTING NEW TASK / RECEIVE / WAIT UNTIL tRFC 后缀&#xff1a; IN BACKGROUND TASK. CALL FUNCTION - IN BACKGROUND TASK qRFC 是tRFC的一个扩展。它允许你将多个tRFC调用序列化为一个…

RocketMQ集群4.9.2升级4.9.6版本

本文主要记录生产环境短暂停机升级RocketMQ版本的过程 一、整体思路 1.将生产环境MQ4.9.2集群同步到测试环境&#xff0c;并启动&#xff0c;确保正常运行。 2.参照4.9.2配置4.9.6集群 3.停掉4.9.2集群&#xff0c;启动4.9.6集群&#xff0c;测试确保正常运行。 4.停掉4.9.6集…

Python Web开发技巧VII

目录 装饰器inject_serializer 装饰器atomic rebase git 清理add的数据 查看git的当前工作目录 makemigrations文件名称 action(detailTrue, methods["GET"]) 如何只取序列化器的一个字段进行返回 Response和JsonResponse有什么区别 序列化器填表和单字段如…

理解Android中不同的Context

作者&#xff1a;两日的blog Context是什么&#xff0c;有什么用 在Android开发中&#xff0c;Context是一个抽象类&#xff0c;它是Android应用程序环境的一部分。它提供了访问应用程序资源和执行各种操作的接口。可以说&#xff0c;Context是Android应用程序与系统环境进行交…

LoadRunner使用教程

1. LoadRunner简介 LoadRunner是一款广泛使用的性能测试工具 可以对各种应用程序进行性能测试&#xff0c;包括Web应用程序、移动应用程序、企业级应用程序等。它提供了一个综合的性能测试解决方案&#xff0c;包括测试计划设计、脚本录制、测试执行、结果分析和报告生成等功…

三、函数-5.流程函数

一、常见函数 【对比】 二、示例 1、if 和 ifnull -- if(value, t, f) 如果value为true&#xff0c;则返回t&#xff0c;否则返回f ok select if(true, ok, error);-- ifnull(value1, value2) 如果value1不为空&#xff0c;返回value1&#xff0c;否则返回value2&#…

MFC表格控件CListCtrl的改造及用法

1、目的 简单描述MFC的表格控件使用方法。Qt适用习惯了以后MFC用的比较别扭&#xff0c;因此记录一下以备后续复制代码使用。由于MFC原生的CListCtrl比较局限&#xff0c;比如无法改变表格的背景色、文字颜色等设定&#xff0c;因此先对CListCtrl类进行重写&#xff0c;以便满足…

哪些报表工具更适合中国企业?看完本文就知道了

企业级报表工具是指能够处理大量数据、支持多种数据源连接、具有强大的数据分析和可视化功能的工具。进入大数据时代&#xff0c;企业数据量剧增、分析需求精细化且要求高效率、高灵活自主性&#xff0c;一般都采用BI报表工具来做智能化、可视化数据分析&#xff0c;推动企业的…

Neo4j数据库中导入CSV示例数据

本文简要介绍Neo4j数据库以及如何从CSV文件中导入示例数据&#xff0c;方便我们快速学习测试图数据库。首先介绍简单数据模型以及基本图查询概念&#xff0c;然后通过LOAD CSV命令导入数据&#xff0c;生成节点和关系。 环境准备 读者可以快速安装Neo4j Desktop&#xff0c;启…

Mysql中(@i:=@i+1)的介绍

i:i1 表达式 生成伪列实现自增序列 语法&#xff1a; select (i:i1) as ,t.* from table_name t,(select i:0) as j (i:i1)代表定义一个变量&#xff0c;每次叠加 1&#xff1b; (select i:0) as j 代表建立一个临时表&#xff0c;j是随便取的表名&#xff0c;但别名一定…

python和c++哪个更值得学,python和c++学哪个简单

大家好&#xff0c;本文将围绕python和c哪个更值得学展开说明&#xff0c;python和c学哪个简单是一个很多人都想弄明白的事情&#xff0c;想搞清楚c和python哪个好学需要先了解以下几个事情。 1、想学编程&#xff0c;选择Python 还是Java或者C&#xff1f; 首先&#xff0c;我…

MySQL索引失效原因及解决方案

MySQL索引失效原因及解决方案 在使用MySQL数据库时&#xff0c;索引是一种重要的性能优化工具。然而&#xff0c;有时候我们可能会遇到索引失效的情况。本文将介绍几种常见的MySQL索引失效原因以及相应的解决方案&#xff0c;并提供SQL语句的错误示例和正确示例。 1. 字符串字…

HarmonyOS学习路之方舟开发框架—学习ArkTS语言(状态管理 二)

Prop装饰器&#xff1a;父子单向同步 Prop装饰的变量可以和父组件建立单向的同步关系。Prop装饰的变量是可变的&#xff0c;但是变化不会同步回其父组件。 概述 Prop装饰的变量和父组件建立单向的同步关系&#xff1a; Prop变量允许在本地修改&#xff0c;但修改后的变化不会…

tinkerCAD案例:11.制作齿轮

tinkerCAD案例&#xff1a;11.制作齿轮 制作齿轮 Add a cylinder to be the main part of the gear. 添加一个圆柱体作为齿轮的主要部分。 说明 Click and drag a cylinder onto the Workplane. 单击圆柱体并将其拖动到工作平面上。 Change the cylinder dimensions to 35mm …

四、约束-1.概述

一、概念 约束是作用于表中字段上的规则&#xff0c;用于限制存储在表中的数据。 二、目的 保证数据库中数据的正确、有效性和完整性。 三、分类