美文网首页多线程
Java基础day11笔记:多线程概述|创建线程的两个方法|线程

Java基础day11笔记:多线程概述|创建线程的两个方法|线程

作者: 楠楠喜欢泡枸杞 | 来源:发表于2018-12-12 23:12 被阅读0次

        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,必须是多个线程使用同一个锁才需要同步。(多个人使用的是同一个卫生间)

            必须保证同步中只能有一个线程在运行。

            同步的好处:

            解决了多线程的安全问题。

            不过它也有弊端:虽然解决了问题,但是执行的时候每次都要判断这个锁,所以较为消耗资源。(越安全越麻烦)

    相关文章

      网友评论

        本文标题:Java基础day11笔记:多线程概述|创建线程的两个方法|线程

        本文链接:https://www.haomeiwen.com/subject/xusuhqtx.html