01-多线程(概述)
接下来,我们来说一说Java中特有的一个知识技术:多线程。
在说线程这个概念之前呢,需要说一个更显而易见的概念:进程。
何为进程呢?进程就是正在进行中的程序。
进程可以同时开启,其实就是cpu在对它们执行。
不过,虽然它们看起来像是同时在执行,其实,cpu在某一时刻只能执行某一个程序,它只是在做着超快的切换,咻咻咻咻~才导致我们看到的是各个程序在同时执行。
再讲个迅雷的例子。是不是有时候会有多条下载请求呀?一个进程,里面可能会有多条执行路径。什么意思呢?在我们进行迅雷下载的时候呢,大家都知道迅雷可以有多条下载路径,比如100M的文件,迅雷可能会分成五个部分,一个部分一个部分的同时发送请求到服务端去下载数据。我们想一想,如果我们下载是用一个路径,就是先下载第一个数据,然后一个一个下载,一直下载到第100个数据。可是如果是五条路径下载,五个人同时在搬东西,是不是就比一个人快多啦?而且这五个人都是在这一个进程中哦,这其中的每一个人,就叫做线程。线程是进程中的内容,每一个应用程序,里面都至少有一个线程。
线程有一个特点,它是程序中的控制单元,或者叫执行路径。
总结一下:
进程:是一个正在执行中的程序。
每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。
每个程序执行,都需要启动内存空间,而进程其实就是用来标识内存空间的,它用来封装里面的那些控制单元。
线程:就是进程中的一个独立的控制单元。
线程在控制着进程的执行。
一个进程中至少有一个线程。
以Java为例,编译运行的时候,它有两个进程:编译进程和运行进程。
我们重点说一下运行进程。
Java虚拟机启动的时候会有一个进程java.exe。该进程中至少有一个线程来负责Java程序的执行,而且这个线程运行的代码存在于main方法中。该线程称之为主线程。
其实,很多书里面都在说,这个程序在启动执行的时候是单线程程序,因为还没有其他线程存在呢。这样说理解也是对的。
扩展:
但是,如果你深究一下虚拟机的话,其实不是单线程。虚拟机启动的时候,它就是多线程。为什么呢?另外的线程是谁呢?
试想一下,(这个只作为了解喔,因为说这个程序是单线程也没问题哒)程序在运行的时候,是不是要建立对象,调用方法,这个时候是主线程在帮我们做这件事情,堆里面会产生很多对象,如果其中某一个对象不被使用了,它就会被垃圾回收机制回收了。主线程还在继续执行着其他对象中的操作,而这个对象就被干掉了,是不是在同时进行呐?是滴!
这个时候就产生了一个问题,主线程在继续调用方法的同时,另外的不被使用的对象就被垃圾回收机制回收了。所以这个时候虚拟机至少有两个线程,一个是主线程,一个是垃圾回收的线程。
多线程存在的意义是什么呢?
多线程的出现呢,可以让我们程序中的部分产生同时运行的效果。而且在下载东西的时候,多线程下载还可以帮我们提高效率。
02-多线程(创建线程-继承Thread类)
那么,如何在我们的程序中自定一个控制单元,或者说,自定一个线程?
Java中已经为我们提供好了对这类事物的对象体现。
通过对API的查找,我们发现,java的核心包java.lang包中就有一个对象叫做Thread,它就是程序中的执行线程,就是用于描述控制单元这类事物的对象。
我们发现创建新执行线程有两种方法:
连范例都有啦,我们自己来写~
创建线程的第一种方式:继承Thread类。
第一步,继承Thread类:
接下来我们看看Thread中的run方法是怎样的~
第二部,重写run方法:
继承完之后,我们接下来就要创建它的对象啦。注意哦,建立好一个对象,其实就是创建好一个线程。
创建好对象之后就要调用run方法啦。
我们看到范例中有一个start方法:
查一下start方法是干什么的~
OK,写好啦:
现在编译运行:
demo run成功打印了。
总结一下, 创建线程的第一种方式:继承Thread类的步骤为:
1,定义类继承Thread。
2,复写Thread类中的run方法。
3,调用线程的start方法,该方法有两个作用:启动线程,调用run方法。
冷静下来思考一下,但是这跟我们以前调用方法的方式有什么区别呢?怎么看不出来呀?
接下来我们让它多运行一会儿,写了一个循环~
让hello world也参与一下~
编译运行:
我们分析一下它的执行路径:
所以我们在打印结果的时候是交替打印的。
为什么是交替的呢?main线程和d线程它们俩不是同时执行吗?
跟你讲哦,真实情况下是不可能的。
windows本身是一个多任务操作系统,看上去它确实在同时执行,其实真正的情况是,cpu在某一时刻下,只能执行一个程序。为什么看起来是同时执行的呢?因为cpu在这些程序之间做着快速的切换,切换的速度是相当快的,快到你根本感觉不出来它在切换,所以你觉得它在同时执行。
而进程中真正在执行的是线程,所以cpu在切换的是每一个进程中的线程。而一个进程中有多个线程的话,cpu也要做切换呢。(这也是机器中程序开的越多越慢的原因)
我们现在就不说多进程啦,我们就说其中一个进程好啦。
在这个例子中,这一个进程中,就已经有多个线程啦。cpu执行main一会儿,执行d一会儿。那么这种情况,我们形象的把它称之为,多个线程在抢劫cpu资源。这只是一种形象的说法,其实是cpu说了算。只是说“抢”更形象一点儿~
#Java小剧场
cpu:执行main一会儿,执行main一会儿,好了好了,再执行d一会儿,再执行d一会儿,再执行d一会儿,诶,好像都没顾到main了,那我再执行main一会儿好了。。。
#
早期有一些病毒就是通过消耗cpu资源让电脑死机的。它就是一直在抢用cpu资源,从而达到让别人抢不着的效果。
再回到我们的例子中。
到这里main线程就结束啦,那这个进程会结束吗?不会。因为d线程还没有结束,只要d线程还在,这个进程就存在。
现在有了双核、多核处理器,就可以实现同时运行了,这个cpu处理这个,那个cpu处理那个。不过具体是怎么切换的,我们现在暂时控制不了,可以学一下多核编程玩一下~
双核以后谁是瓶颈?内存。
#Java小剧场
小楠老师在做一个一对三的高考冲刺班,她同时带三个孩子,一会给这个辅导,一会给那个辅导,还算忙得过来。这个时候又有一个人报名了,小楠老师忙不过来了,于是就又请了一位老师,这位新老师来带新报名的这个孩子。这样,小楠老师和新老师可以同时带四个孩子。
小楠老师的补习班空间不大,四个孩子就刚好坐满啦。其实新老师也可以再带两个孩子呢,这样小楠老师和新老师就可以同时带六个孩子,达到最大的效率。可是因为补习班太小,不能容纳更多孩子,小楠老师和新老师还是只能带四个孩子。
#
现在双核之后,还有四核、八核,听起来好腻害哦。但是可能会出现cpu想处理数据但没数据处理的情况。(没有地方装数据呀)
再回到刚刚的例子中。我们又多运行了几次,发现每次运行打印的顺序都不同。
因为多个线程都在获取cpu的执行权。cpu执行到谁,谁就运行。
明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)
cpu在做着快速的切换,以达到看上去是同时运行的效果。
我们可以形象的把多线程的运行行为看做在互相抢夺cpu的执行权。
这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长时间,cpu说的算。
当然,也会出现这样的情况,hello world执行完了,才执行demo run,或者demo run执行完了,才想起来执行hello world。这种情况是cpu切换得有点慢。哈哈~迟钝的cpu~
再当然,在没有被特意控制的情况下,不会出现cpu把一个全部执行完再执行另一个的情况。 因为cpu它在优化资源,它得去快速的做着切换,才能实现同时运行的效果,否则会出现一个程序在执行,另一个程序执行不了。
04-多线程(线程练习)
练习:创建两个线程,和主线程交替运行。
运行结果:
这样写会怎样呢:
还是只有一个主线程在执行,另外两个线程没有开启。
主线程先打印one run,再打印two run,最后打印main。
这里我们的循环只有60次,所以主线程运行一段程序的时候,其他程序等待时间不会太长,但是如果是6000、60000次呢?这个其他程序的等待时间就很长啦。
所以,建立多个线程,多个程序就可以“同时”运行~
05-多线程(线程运行状态)
线程的四种状态:
06-多线程(获取线程对象以及名称)
我们来看看关于线程名称的方法,有setName:
getName:
我们用getName方法来获取一下线程名称。
依然用之前的那个例子~
编译运行:
我们发现有Thread-1和Thread-0两个名字。
原来线程都有自己默认的名称。
Thread-编号 该编号从0开始。
我们也可以改变线程的名称,用setName方法就好啦。
不过查一下Thread的构造函数,发现线程初始化的时候就可以有名称啦!
我们调用一下父类的构造方法:
现在在创建它们的时候就给它们起一下名字:
编译运行:
自定义线程名称成功啦~
另外,Thread类还为我们提供了一个方法:currentThread()。
这个方法是静态的,说明这个对象没有访问到对象的特有数据,用类名访问就可以啦。
我们使用这个方法,来获得当前线程的名称:
获取成功~(运行截图略,就和this.getName()运行结果一样)
既然两种方法运行结果一样,它们有什么不同呢?
试一下:
运行:
运行结果是true哦。所以它俩是一样哒。
那用this调用就行了呀,多简单,用currentThread那么长,那么麻烦。
不是的。this调用的方式并不通用,只有在类的对象调用类中方法时才可以用则中方式调用。而currentThread是标准通用方法,无论谁调用都可以用~
我们来复习一下:
static Thread currentThread():获取当前线程对象。
getName():获取线程名称。
设置线程名称:setName()或者构造函数。
还有一个小问题:
Thread-0和Thread-1“同时”运行的时候,用的x是同一个吗?
不是的。
Thread-0建立的时候,内存中会为它分配一块内存空间,其中有一块叫x;Thread-1建立的时候,内存也会为它分配另一块内存空间,其中也有一块叫x。多个线程“同时”运行的时候,注意局部变量是每个内存空间中都有一份哦。
07-多线程(售票的例子)
接下来,我们用一个事例来对第二种方法进行阐述。
需求:简单的卖票程序。
多个窗口同时卖票。
现在4个窗口同时卖票:
运行一下,发现分不清都是哪个窗口卖的:
打印一下线程的名字:
运行,我们发现了一个问题:
1、2、3、4号窗口都卖了1号票。一节车厢就100个座,可是现在卖出了400个座。
问题在于,创建一个对象,里面就有100张票。
我们的解决方式:让4个对象共享100张票。
该用静态啦!
运行:
OK啦。没有重复卖票的啦。
但是,我们一般是不是不定义静态呀?因为它的生命周期太长啦!
那怎么解决呢?
用一个对象来卖100张票:
运行:
是不是也卖完啦~
但是发现这些乱七八糟什么东西呀:
也就是说,线程已经开启了,并且调用start函数,从开启状态进入了运行状态。这个时候又调用了start函数,又进入运行状态,这就是无效的呀。
该怎么解决呢?
快接着看下去~
08-多线程(创建线程-实现Runnable接口)
解决这个问题,我们就需要引入第二种创建线程的方式了。
我们来看一下Runnable接口:
这个接口里面非常的爽呀,就一个方法:
我们来跟着示例代码来写~emmm...不太懂耶。
那慢慢一步一步分析着来~先让Ticket类实现Runnable接口:
主函数中:
我们现在需要想办法让线程调用Ticket中的run,需要让Ticket中的run和Thread创建的对象有关系!
我们需要在创建线程对象时就明确要运行什么代码。
Thread有一个构造方法,比较特殊:
它可以接收Runnable接口类型的对象。
所以这就是我们要实现Runnable接口的原因,因为Thread类认识这个Runnable接口。
所以,我们在new Thread方法的同时,就可以指定run方法所属的对象。
编译运行:
搞定!
这就是创建线程的第二种方式:实现Runnable接口。
步骤:
1,定义类实现Runnable接口。
2,覆盖Runnable接口中的run方法。
将线程要运行的代码存放在该run方法中。
3,通过Thread类建立线程对象。
4,将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
为什么要将Runnable接口的子类对象传递给Thread的构造函数?
因为,自定义的run方法所属的对象是Runnable接口的子类对象。
所以要让线程去指定对象的run方法。就必须明确该run方法所属的对象。
5,调用Thread类的start方法开启线程,并调用Runnable接口子类的run方法。
那么,这两种创建线程的方式,也就是实现方式和继承方式有什么区别呢?
我们先讲一下Runnable接口的由来:
左边的Student类,没有父类,它里面的run方法需要被多线程执行,所以就继承了Thread类。
可是,Student在演变的过程中,抽取出来了一个父类Person,这个时候Student类中的run方法需要被多线程执行,但是因为它已经有父类了,而Java又不支持多继承,所以无法再继承Thread类。
这时,Java提供了一个Runnable接口来解决这个问题,“你可以不叫我爸爸,我也可以帮你执行代码,只要你符合我的规则就行”,将这个功能抽取出来封装到了Runnable接口中。
这就是接口的由来。
因此,实现方式的好处:避免了单继承的局限性,把它作为了一种功能的扩展封装在了接口中。在定义线程时,建议使用实现方式。
而且这个构造方法使用了多态:
因为日后会出现什么样的接口子类是无法预知的,所以它用了多态的形式,你只要符合规则,它都可以用!你只要符合PCI的规则,后期粗现什么样的版本,它都可以帮你运行!
我们发现,Thread类本身也实现了Runnable接口:
Runnable接口的定义,其实就是在确立线程要运行代码所存放的位置。
两种创建线程的方式还有一个区别:
继承Thread:线程代码存放在Thread子类的run方法中。
实现Runnable:线程代码存放在接口子类的run方法中。
它们代码的存放位置不一样。
当然,如果你的类没有父类,用第一种方式也是完全可以的。
09-多线程(多线程的安全问题)
现在只剩最后一张票了,然后4个窗口都拿到了执行资格,在等待执行权,最后执行权比如说给了0号,0号卖出后执行权比如给了1号,这个时候1再继续执行票数就为负了,这显然不符合常理。我们的程序有bug啦!
我们现在模拟一下这个过程,让我们真实的看到这个现象。
怎么做呢?
那怎么让它睡一下呢?
看一下sleep方法:
我们看一下这个方法的特点:
它是静态的,没有访问到对象的特有数据,并且它抛出了异常。
话不多说,写起来:
因为Ticket实现了Runnable接口,而这个接口并未抛出异常,所以Ticket也不能抛出异常,所以只能try哦。
运行一下:
刚刚那个问题就出现啦。
通过分析,发现,打印出0,-1,-2等错票。
多线程的运行出现了安全问题。
安全问题最可怕辣!
多线程中一定要小心安全问题,一旦产生就非常要命。
问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致了共享数据的错误。
解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他过程不可以参与执行。
Java对于多线程的安全问题提供了专业的解决方式。
就是同步代码块。
synchronized(对象)
{
需要被同步的代码
}
写起来~
运行看一下效果:
OK啦!
10-多线程(多线程同步代码块)
在上节课中,这里有个对象:
这个对象如同锁,持有锁的线程可以在同步中执行。
没有持有锁的线程,即使获得了cpu的执行权,也进不去,因为没有获取锁。
火车上的卫生间就是一个经典的的例子。
同步的前提:
1,必须要有两个或者两个以上的线程才需要同步。(就你一个人用这个卫生间,就不需要锁门啦)
2,必须是多个线程使用同一个锁才需要同步。(多个人使用的是同一个卫生间)
必须保证同步中只能有一个线程在运行。
同步的好处:
解决了多线程的安全问题。
不过它也有弊端:虽然解决了问题,但是执行的时候每次都要判断这个锁,所以较为消耗资源。(越安全越麻烦)