Selenium Web Driver自动化测试(java版)系列上半部分(26) - 线程

这篇文章我们讨论线程,因为它里面的一些方法在自动化测试里也是很常用的。大家或许都听说过进程(process),我之前学java时老师说进程就是正在运行的应用程序。比如你打开浏览器,这就是启动了一个应用程序,操作系统会给它在内存中开辟一块地址空间,它就作为一个进程在里边安安静静地运行。当你关闭这个浏览器时,进程结束,这块地址空间也随之被释放了:

如果浏览器进程运行的时候你又双击了桌面上的一个小视频,那操作系统又会在内存中开辟一块地址空间给它来运行。这两个进程在各自的地址空间中安静工作,互不打扰:

有人说那如果是两个相同浏览器的进程呢?也没问题,操作系统会把它们区别对待,还是两个不同的地址空间。所以,多进程的好处就是我们可以在一台电脑上同时做不同的事,可以一边用word文档写论文一边开着网游,或是一边编着代码一边看着美剧。当然,上班的时候怎么能这样呢?多伤老板的心呐,得体谅他,玩得时候别让他看见:)总之,多线程是个非常有用的功能。但你也能想象,如果进程非常多,势必会对内存造成很大的压力,就那么大一块地方,你不停地让操作系统开辟空间,最后快满了,运行速度就会很慢,过一会儿就带不动了。

所以,一旦发现系统突然变慢了,有时风扇声音突然很大,很可能就是内存不够用了。这时我们可以查看一下内存占用情况。打开任务管理器的进程选项卡,我们可以看到内存的总利用率、所有当前正在运行的应用以及每一个占用内存的情况:

如果有特别大的,而且你觉得没必要开着那该关就关,或许系统一会儿就快了,风扇也不那么吵了,OK,世界清净了。

那什么又是线程(thread)呢?线程是进程的一个实体,从进程中产生并依附于进程,获取某些资源去执行某些任务。不理解这个定义没关系,你可以简单把进程和线程的关系想成是公司和员工的关系,进程是个公司,线程是该公司里的员工。公司里的所有员工都有自己的本职工作,为了完成自己的工作需要获取相关的资源,比如有人需要找客户了解需求,有人需要安装软件写代码,有人需要找广告公司做文案。大家相互配合,协同作战,年底做出了一个产品。同理,一个进程中可能有多个线程,它们在应用程序运行时也是各有分工,去拿完成自己任务需要的资源,最后大家一起完成任务:

一个进程开始运行时,因为它或许会产生很多线程,又因为它们所需要的资源、工作任务、工序不一样,所以不一定每个线程都是在一个进程开始运行时就自动开始运行。一般来说,一个线程的生命周期如下:新建(New) -> 就绪(Runnable) -> 运行(Running) -> 阻塞(Blocked) -> 死亡(Dead)。画张图解释一下:

第一个状态当然就是新建一个线程,线程创建完毕后会自动进入就绪状态等待拿资源做任务。如果需要的资源现在可以拿,那么它就会进入运行状态开始执行;如果需要的资源不能拿,那它就依然会在就绪状态安静等待。有人问怎么资源还有不能拿的时候呢?当然有,上班的时候你突然尿急,跑厕所一看就一个坑,而且还有人,那你还能尿吗?不行,你得等他/她上完了你才能上。同样,你需要一个很重要的文件,正巧你同事正用着呢,你得等一下才行吧,硬夺会闹矛盾。所以,需要的资源正在被别的线程使用时不能强取,需要等待。那如果上厕所的人死活不出来怎么办呢?上厕所的时候喜欢看小说,让外边人等着。这种人不少,特别讨厌,比如说我。这种情况下其它线程难道就一直等着吗?不,也有办法,我们可以写程序逼着当前的线程离开,比如让它中断一会儿。当前线程中断后,这时虽然它什么都不做了,但系统本身并不会空闲,你让当前线程停下来,系统就会马上把它踢出去,让它变成阻塞状态,然后迅速找其它就绪的线程,这样就给了其它线程拿这个资源的机会。阻塞的线程进入线程池中等待。中断时间一过,这个线程会自动退回到就绪状态。注意,是就绪状态,不是运行状态,一个线程解除阻塞后会首先回到就绪状态。等到合适的时机线程调度器才又把它带入了执行状态。这样,一会儿执行,一会儿阻塞,该过程循环往复,通过对线程的调度,让每个线程都有机会运行,都有机会休息,让任务更好更合理地完成。这种线程调度方式也被称作抢占式策略。所以,下次再遇见这种上厕所不出来的人,你也可以找人调度,直接把老板叫来,他肯定出来。最后,系统发现该线程的任务做完了,直接枪毙,线程也就进入了死亡状态。死亡不同于阻塞,休息会儿还能干活,死了就是死了,不存在了,你必须重新创建线程才行。

现在我们按照这个生命周期看一下java文档的线程类Thread:

再强调一遍,多看文档,很多东西我其实就给大家说了个基础,java博大精深,不可能面面俱到,不会用的就一定多参考文档。

首先是创建线程,根据文档可知,有两种方法可以创建线程,第一种是继承Thread类,第二种是实现Runnable接口。先说第一种,只要一个类继承了Thread类,它就会拥有Thread类中的所有方法,自然就可以当成一个可以控制线程的类。创建一个该类的实例时就是创建了一个线程,而调用继承过来的start()方法就会让该线程进入就绪状态:

再次注意,是就绪状态,不是运行状态,别一看start就是运行状态。任何线程都不是直接就启动的,就好比你不能一进厕所门啥都不看就脱裤子。这里创建了一个叫a的线程实例,准备执行任务。那么执行的任务在哪儿呢?A类继承了Thread类后,除了start(),还有一个重要的方法也继承过来了,叫run()。我们之前介绍过继承方法的重写,这个run()方法可以被子类重写,重写后它里面的内容就是执行的任务。忘了重写怎么回事的回去在复习一下,简单地说重写就是对继承过来的方法重新修改里面的内容。run()里面的内容叫线程执行体,当线程对象调用start()时,该线程进入就绪状态,然后时机一到就进入运行状态,然后开始执行run()方法里面的内容。

暂时它还什么都不做。现在我们让它做点事情,打印出来一句话:

执行一下,打印出来“开始执行任务”。果然,调用start()后执行了run()。所以,看上去你会觉得调用start()其实就是调用run(),start()和run()是同一个方法。其实并不是,调用start()首先让线程进入就绪状态,而进入运行状态后才会调用run(),只不过程序执行太快了,快到你看不到中间的停顿。

既然太快了,那我们就让它跑得慢一点。Thread类中有一个静态方法叫sleep(long millisec),接收一个长整型参数,代表毫秒数。注意,是毫秒,不是秒。既然是静态方法,我们就可以用类名调用。比如Thread.sleep(1000)就代表让当前线程等待1000毫秒,也就是1秒。程序执行到这一步就会停顿1秒钟。别看就1秒钟,已经足够让系统做出反应,因为系统本身不会空闲,你让当前线程停下来,系统就会马上找其它就绪的线程。当然,这个例子目前还是单线程,系统只能用这一个。现在我修改一下刚才的程序,让它从1到10每隔一秒钟打印出来一个数:

sleep()方法会抛异常,注意要用try...catch捕获。还有一句是Thread.currentThread().getName(),currentThread()也是Thread类的静态方法,调用它可以返回当前的进程对象,再调用getName()会返回当前线程的名称。有人问我怎么知道这些的,当然是看文档啦。现在运行一下这个程序,发现和我们想的一样,每隔一秒就会输出一句话,总共打印10次:

这是用继承Thread类来实现线程的方式。第二种是用实现Runnable接口的方式,还用刚刚的例子,注意主类的不同:

A类实现Runnable接口用implements关键字,这点讨论接口的时候已经说过了。然后就是主类的不同,我们不再直接让线程对象调用start()方法,而是用Thread类实例化一个对象,然后把线程对象当参数传递,再用Thread类的对象调用start()。剩下的都一样了,同样还是要对run()方法内容重写。

执行一下会发现效果一样。这两种方式哪个更好呢?当然是第二种。如果我现在再加个类B,并且我让A继承B。这时如果用继承Thread类的方式就有麻烦了,因为之前讨论继承的时候说过,java不允许多继承。你就干着急没法办了。实现Runnable接口至少还能拓展一点:

不过,不管是第一种还是第二种,刚才的例子中我们都只演示了启动一个线程的情况,让一个线程完成了10个任务。这种情况下,即使停顿了一秒,一秒之后也是也是这个线程,而且这一秒钟完全空闲,并没有其它线程抢占资源。这种单线程的情况在实际项目中比较少,大多都是多线程。我现在再启动几个线程,大家看一下效果。创建多线程也很简单,想启动几个线程就创建几个Thread类的类实例。我一下启动四个线程,为了方便演示,我把i的终值设成100:

注意,多线程也一样,调用start()方法后先使所有线程进入就绪状态,由系统来决定谁先跑谁后跑。执行一下:

四个线程各有名称,交替执行。简单说一下执行过程:先看前两行打印结果,Thread-2进入到执行状态,调用run()先打印出“Thread-2正在执行第1个任务”,然后它停顿1秒钟,这时候由于它什么都不做,但系统本身并不会空闲,你让当前线程停下来,系统就会马上把它踢出去,让它变成阻塞状态,然后迅速找其它就绪的线程。于是线程调度器又把Thread-1置成执行状态,开始执行run()方法。这时由于Thread-2还在阻塞中,还没来得及执行i++,i值并没有更新,所以打印出了“Thread-1正在执行第1个任务”。同理,Thread-0和Thread-3也是在其它某个线程阻塞的时候启动的。当Thread-2等了1秒钟后重新进入就绪状态,然后等待时机让线程调度器把它设置成执行状态,它再继续执行run()方法后边的代码,也就是i++。再画个图表示一下:

有人可能会问第四行和第五行为什么没打印出哪个线程正在执行第2个任务呢?是因为四个线程是同时在执行状态的,当Thread-0打印出“Thread-0正在执行第1个任务”时其它线程也执行了i++,并且还执行了两次,正巧Thread-1这时打印,所以就打印出了“Thread-1正在执行第3个任务”这句话:

由于线程之间彼此独立,每一个都在独立执行,你可以把它们简单理解成每一个线程都有一个run()方法,只不过它们共享一个i值而已。这点有点像我们以前讨论过的静态变量:

以上这个过程就叫做多线程的并发。几个线程同时执行,停停走走,进进出出(此处不能邪恶)。但是,这种并发看起来很乱,很没规律。就好比你把100块钱存卡里,你,你老婆,你爸你妈四个人谁都可以取。想得虽好,但按照上面的并发方法就麻烦了。假如你们四个都取了1块钱后发现里边竟然还剩99块,你当然开心了。但如果你们都只取了1块钱发现里边就剩90块钱了,你哭不哭?有本事就别哭,占得了便宜就得忍得了委屈。好吧,我承认我不是这样的人,我只能改程序,把它改成更合理的样子。

我的目标是一个线程执行一个任务,不让几个线程重复执行的现象发生,正如不让几个人取相同的一块钱。也就是说,当一个线程在执行Thread.sleep(1000)等待时,不能有其它线程同时执行i++,要让这个线程从打印、等待直到i++整个过程都不受其它线程干扰。这个过程可以用一把锁来控制一样,虽然四个线程都在跑,但被锁住的代码每次只能有一个线程拿到钥匙进去执行,其它的线程只能等待。等该线程干完活离开会打开锁,然后下一个线程又会进去锁住。周而复始:

这把锁就是一个叫做synchronized(object)的方法。synchronized是同步的意思,方法体里的东西一步到位不受干扰嘛,不难理解。它接受一个参数,叫object,又叫同步锁,其实就是Object类的一个对象。我们知道,在java中所有的类都继承自Object类,所以这里的object可以是任意的对象,在本例中你可以写t1/t2/t3/t4,不过通常都用this关键字,代表当前对象嘛。现在我用synchronized(this)把需要同步的几步包起来:

当然,如果不需要同步的代码就不用包起来。再执行一下看一下结果:

你会发现有时候会出现同一个线程连续执行的情况,因为线程也有优先级,根据策略不同而表现不同。不过不管怎么样,这次这100个任务每一个都被且只被一个线程执行,目标达到,我们的锁确实管用。这把锁有两个状态,0和1。上锁时状态为1,此时别的线程无法进入同步代码段。待执行任务的线程结束后,锁打开,状态变为0,其它的某个线程只有看到0才会进入同步代码段。

但是,咱们的代码还有个小问题,你把执行结果拉到最后,会发现最后i值到103才结束:

按照循环不是i到100就该跳出循环了吗?怎么出103了呢?这是因为虽然当Thread-2执行完毕,打印出“Thread-2正在执行第100个任务”,然后把i值变成了101,做完这些离开了同步代码段,完成使命进入死亡状态。可此时其它的线程还没死呢,还在循环体内,这时Thread-3进入同步代码段,打印出“Thread-3正在执行第101个任务”,又把i变成了102。同理,Thread-0和Thread-1会把i最后变为104,只不过最后一次执行打印操作的时候显示的是103,其实程序最后结束后i应该是104:

咱们完善一下程序,在最前面加一个if判断,如果i值一旦超过100马上跳出循环:

这回就行了,几个线程在调度器的管理下循环往复,把任务执行完毕。

加锁虽然能解决不少事,但如果不慎就会造成死锁,比如看下图:

两个线程都需要借助对方正在占据的资源才能解锁,僵在这里无法动弹,这种情况下就叫死锁。死锁发生的原因有很多,其中一个就是写代码时出现错误。实际测试产品时也有死锁造成的bug,让人很恼火。

线程就介绍到这儿。其实线程类中还有不少方法,比如yield(),interrupted()等等,大家可以抽空看文档查资料多试试。对于写自动化测试代码来说,sleep()这个方法用的情况很多,主要就是利用它可以等待一秒钟的这个特性,让网页和程序做到同步。具体讨论的时候会细说,大家留意一下。

这篇文章的源代码是ThreadControlThreadControlRunnable

本篇知识点及注意事项:
1. 线程是进程的一个实体,从进程中产生并依附于进程,获取某些资源去执行某些任务。一个进程中可能有多个线程,它们在应用程序运行时也是各有分工,去拿完成自己任务需要的资源,最后大家一起完成任务。
2. 一个线程的生命周期有新建(New) -> 就绪(Runnable) -> 运行(Running) -> 阻塞(Blocked) -> 死亡(Dead)几个状态。
3. 阻塞时线程在线程池里等待,过后先是就绪,然后才可能运行。
4. 线程并发时指的是多个线程一起参与的执行,各个线程通过线程调度器在就绪、运行、阻塞状态之间来回切换。
5. 用synchronized(object)方法可以给某段代码上锁,上锁后的代码段一个时间段只能被一个线程执行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容