多线程编程基础

在简单了解计算机工作原理以及操作系统之后,多线程编程便成为了我们接下来重点需要掌握的知识。由于创建,销毁,调度一个进程的开销是比较大的,线程便应运而生,所以线程也叫做“轻量级进程”,就是在解决并发编程问题的前提下,让创建,销毁,调度的速度更快一些!线程之所以’轻‘,就是因为把申请/释放的资源给省下了。

目录

一.线程和进程的关系

二.线程的启动

三.等待一个线程

四.多线程的意义

一.线程和进程的关系

线程和进程的关系,是进程包含线程。一个进程可以包含一个线程,也可以包含多个线程。只有在第一个线程启动的时候开销是比较大的,后续的线程开销就很小了。同一个进程的多个线程之间,共用了进程的同一份资源,操作系统在实际调度的时候,就是以线程为单位进行调度的。也可以说:线程是操作系统调度执行的基本单位。一个线程也是通过一个PCB来描述的,在之前介绍的PCB中的状态,上下文,优先级等,都是每个线程拥有自己的一份信息,但是同一个进程里的PCB之间, pid是一样的,内存指针,文件描述符表也是一样的。我们通过图来举个例子:

比如需要吃掉100只鸡

多进程:

我设置了两个房间,两套桌子和两个人。这种方式就比较浪费空间,维护的成本也比较高。

多线程:

只设置一个房间,一张桌子,效率相比上面的就快很多了。

此时如果让人更多,吃的速度是否就更快呢?

在增加线程的数量的时候,也不是可以一直提高速度的。比如桌子的空间(CPU核心数量)是有限的 ,线程太多,核心数目有限,不少的开销反而会浪费在线程调度上了。此外还会出现线程安全问题:

在这种多线程的状态下,多个人都想要一块鸡肉,此时就可能会闹矛盾。而在最上面的多进程中就不会出现这种情况,因为鸡肉已经分好了,自己吃自己的。所以多线程中就会发生线程安全问题。

二.线程的启动

那么在Java中如何进行多线程编程呢?关于Java操作多线程,最核心的类是Thread类。在使用Thread类时,是不需要import别的包的,和String,StringBulider,StringBuffer类似,这些类都是在java.lang下的。

 

 首先我们创建出一个Thread类,接下来启动线程:

要注意:start这里的工作是创建新的线程,新的线程负责执行t.run。这个过程调用了操作系统的API,通过操作系统内核创建新线程的PCB,并且把要执行的指令交给这个PCB。当PCB被调度到CPU上执行的时候,就会执行线程中的方法了。

上述是创建出线程并且运行,但是这个线程中并没有任何代码,接下来在此线程中加入代码并且显示出来,如图:

 创建一个MyThread类,并且继承Thread类,重写run方法,同时new对象也改为MyThread。

Thread t=new MyThread();这串代码使用了向下转型。此时我们看运行结果:

 如果我们直接在main方法中打印 hello world,java进程中主要就有一个线程,也就是主线程。而现在的代码就是通过主线程调用t.start,创建出一个新的线程,新的线程去调用run方法。当run方法执行完毕的时候,新的这个线程会自然销毁。

这时候大家可能就会有问题了:main和Thread这两个线程谁先执行谁后执行?

答案是:操作系统在调度线程的时候,采用的是“抢占式执行”的方法,具体哪个线程先执行,哪个线程后执行都是不确定的,这取决于操作系统调度器的具体实现策略。虽然是存在优先级的,但是在应用程序层面上是无法修改的。从代码的角度看到的效果就是随机的。

线程对于初学者比较抽象,我们可以使用jdk自带的工具jconsole来查看当前的java进程中的所有线程。如图:

 双击打开之后显示如下:

 第一行的是jcosole自己,也就是自带的。第二行显示的就是刚刚运行过的程序。第三行是IDEA。打开第二行进程,显示如下:

我们会发现,当前这个进程中的线程不只是一两个,除了里面的main和Thread-0,其余线程都是JVM自带的。点击main线程和Thread线程,显示如下:

 

显示出了线程的状态,调用栈等。利用jconsole工具,可以很好的帮助我们观察多线程工作。

Java中创建线程的写法有很多种。

1.通过继承Thread,重写run方法,如图:

 2.实现Runnable接口

 3.使用匿名内部类,继承Thread

 4.使用匿名内部类,实现Runnable

 这个写法和写法3本质相同,只不过是把实现Runnable的任务交给匿名内部类的语法

5.使用Lambda表达式,这是最简单也最推荐的写法,如图:

把任务用lambda表达式来描述,直接把lambda传给Thread构造方法。

 接下来是Thread类的几个常见的属性:

其中值得一提的是isDaemon()。代表是否是守护线程。守护线程又叫做后台线程。相对应的叫做前台线程。前台线程会阻止进程结束,也就是说前台线程的工作没做完,进程是完不了的。而后台线程不会阻止进程结束,后台线程工作没做完,进程也是可以结束的。代码中手动创建的线程,默认都是前台线程,包括我们的main方法默认也是前台的。其他的jvm自带的线程都是后台的。我们可以通过手动的使用setDaemon设置后台进程,也就是守护线程。如图:

 设置完之后,此时进程的结束与否就和t无关了。

还有isAlive方法,在调用start之前,t.isAlive就是false;在Thread线程结束后,t.isAlive也是false。如图:

 输出结果如下:

 总结一下就是,如果t的run还没跑,isAlive就是false;如果t的run正在跑,isAlive就是true;如果t的run跑完了,isAlive就是false。

三.线程的终止

上面讲到了线程的启动,下面来讲一下线程的终止。

1.使用标志位来控制线程是否要停止,如图:

对于自定义变量这种方式,尤其是在sleep休眠比较久的时候是不能及时响应的。这个代码之所以可以能够起到修改flag,t线程就结束的效果,完全取决去t线程内部的代码,代码里通过flag控制循环。因此,这里只是告诉让这个线程结束。但是这个线程是否要结束,什么时候结束,都是由线程内部的代码来决定的。

2.使用Thread自带的标志位来进行判定

 这个方法中while中的Thread.currentThread()是Thread类中的静态方法,通过这个方法可以获取到当前线程。哪个线程调用的这个方法,就是得到哪个线程的对象引用,类似于this。后面的isInterrupted()相当于判定一个boolen变量,为true表示被终止,为false表示未被终止。最后的t.interrupt()就是终止t线程。如果线程在sleep中休眠,此时调用interrupt会把t线程唤醒,从sleep中提前返回了,如图:

 我们可以发现,t最开始是正常执行的,当调用到interrupt时触发了异常,过后又正常执行。对此状态我们要知道的是:interrupt会做两件事,第一件事是把线程内部的标志位(boolen)给设置成true;第二件事是,如果线程正在sleep,那么就会触发异常,把sleep立刻唤醒。但是sleep在被唤醒的时候,会把刚才设置的标志位再设置成false,也就是清空标志位。这些操作下来的结果就是,当sleep的异常被catch捕捉之后,循环继续执行。这里会有人问了,为什么sleep要清空标志位?那是因为唤醒之后,线程到底终不终止,到底是立即终止还是稍后终止,把选择权交给程序猿们自己了。

三.等待一个线程

线程是一个随机调度的过程。等待线程,就是在控制两个线程的结束顺序。如图:

 

 在执行完start后,t线程就和main线程并发执行,分头行动。当遇到t.join时,当前的main线程就会来等待t线程执行结束,结束之后,main线程才会从join中恢复回来,继续往下执行。我们看一下运行结果:

四.线程的状态

线程的状态是针对当前的线程调度的情况来描述的。状态是线程的属性。Java对于线程的状态,进行了细化。下面是线程几种常见的状态。

1.NEW   NEW状态创建了Thread对象,但是还没调用start(内核中还没创建对应的PCB)。

2.TERMINATED  表示内核中的PCB已经执行完毕了,但是Thread对象还在。

3.RUNNABLE    表示可运行的,包括正在CPU上执行的,和在就绪队列里,随时可以去CPU上执行的。

4.WAITING

5.TIMED_WAITING

6.BLOCKED

4,5,6这三种状态都是阻塞状态,分别代表不同原因的阻塞。

 程序运行过程中,就会有如下状态的转换,如图:

 其中,对于TERMINATE状态,代表着PCB已经被销毁了,t线程就无法被再次使用了,但是可以调用对象的一些方法属性,只是没法通过多线程来做一些事情了。为什么要这么做?编程圈子里约定俗成的规则是:对于一个变量/一个对象,建议只有一个用途。对于t线程对象,如果TERMINATED之后还有重新启用的机会,程序猿就不好判定当前这里的t到底是有效的还是无效的。如果明确TERMINATED就是终结,没有重新start的机会了,此时程序猿们就可以心安理得的放弃t,同时后续任何代码中使用t来进行和线程有关的操作都可以视为是不科学的操作了。

接下来通过代码来演示一下:

 打印获取到的线程的状态:

 此处之所以能看到RUNNABLE,主要就是因为当前的线程run里面没写任何sleep之类的方法。我们对代码稍作修改,如图:

 运行结果如下:

通过这里的循环获取,就能看到交替状态了。但是对于当面获取到的状态到底是啥,完全取决于系统里的调度操作,也就是获取状态的这一瞬间,线程t到底处于什么样的状态(正在执行还是正在sleep)。

当前只演示这四种状态,剩下的WAITING和BLOCKED在后面的文章再提及。

四.多线程的意义

讲了这么多,那么多线程的意义到底是什么?我们通过代码来感受一下单个线程和多个线程之间执行速度的差别,只使用串行执行时,如图:

 此处使用时间戳来对串行执行计时,多次运行代码,得到的结果如下:

 

 三次执行的结果时间误差在70ms以内可以忽略不计,得到的运行时间大概为5100多毫秒。

然后,我们使用多线程编程进行上述一样的操作,代码如图:

 

此时运行代码,得到的结果如图:

 

 再快也不可能是0ms,这显然是不对的。原因是什么呢?因为在代码运行的时候,不仅仅只有t1和t2两个线程在工作,还用调用方法的main线程也在工作。看似是2个线程,实际上是三个线程。t1,t2,main是并发执行的关系,main线程在执行完两次start之后就会立即执行结束计时。那么如何解决这种问题呢?很简单,既然main线程最先停止执行,那么在main线程停止之前让它阻塞等待另外两个线程结束再结束就可以了,也就是在中间加上t1.join和t2.join。如图:

 加上这两串代码之后,程序正常运行,运行多次,得到的结果如图所示:

 从此处可以看到,比起上面使用串行执行,时间缩短的确实很明显。多线程可以更充分的利用到多核心CPU的资源,所以运行效率更高。像刚刚我们写的这些代码示例,就相当于一个最简单的跑分程序,执行时间越短,认为你的电脑硬件配置越好。我们日常使用的一些程序经常会看到“程序未响应”的提示,这是因为程序进行了一些耗时的IO操作,阻塞了界面的响应,这种情况下使用多线程也是可以有效改善的(一个线程负责IO,另一个线程负责响应用户的操作)。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java多线程编程是指在Java语言中使用多个线程来同时执行多个任务,以提高程序的并发性能和响应速度。Java多线程编程PDF是一本介绍Java多线程编程的PDF文档,其中包含了Java多线程编程的基本概念、原理、技术和实践经验。该PDF文档可以帮助读者快速了解Java多线程编程的相关知识,并提供实用的编程示例和案例分析,有助于读者掌握Java多线程编程的核心技术和方法。 在Java多线程编程PDF中,读者可以学习到如何创建和启动线程、线程的状态和生命周期、线程间的通信与同步、线程池的使用、并发容器等相关内容。同时,该PDF文档还介绍了Java中的并发包(concurrent package)的使用和实现原理,以及多线程编程中的常见问题和解决方案。 通过学习Java多线程编程PDF,读者可以深入了解Java多线程编程的理论和实践,掌握多线程编程的核心知识和技能,提高自己的并发编程能力,为开发高性能、高并发的Java应用程序打下坚实的基础。同时,对于已经掌握多线程编程知识的读者来说,该PDF文档也能够帮助他们进一步巩固和扩展自己的多线程编程技能,提升自己的编程水平和竞争力。 总之,Java多线程编程PDF是一本全面介绍Java多线程编程的优秀文档,对于Java程序员来说具有很高的参考价值,可以帮助他们在多线程编程领域取得更好的成就。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晚报大街-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值