JavaEE之多线程

一.认识线程

1.多进程实现并发编程的不足之处:

引入多个进程的核心:实现并发编程(c++的CGI技术就是通过多进程的方式实现的网站后端开发)。因为现在是一个多核cpu的时代,并发编程就是刚需。多进程实现并发编程,效果理想。但很多进程进行编程的模式也有缺点:就是进程太重量,效率不高(创建一个进程,消耗的时间很多,销毁一个进程,调度一个进程,也都需要消耗时间小号空间等,也就是都消耗在申请资源上)。

进程是资源分配的基本单位,它的分配主要是通过一定的数据结构。以分配内存为例:操作系统内部有一定的数据结构,会把空闲的内存分块管理,当申请内存时,系统就会从这样的数据结构中找到大小合适的空闲内存,返回给对应的进程。此处通过数据结构虽然可以一定程度上提高效率,但当管理的空间变多,就还会是费时费力的操作。

也就是说,如果需要频繁的创建销毁进程,这个开销就不可以忽视了。

2.引入线程

进程包含线程

由于上述缺点,我们引入了线程(也叫轻量级进程)。它不可独立存在,依附于进程。进程包含一个或多个线程,也就是说,一个进程至少有一个线程,负责执行代码完成工作,也可以根据需要,创建更多线程,从而实现并发编程。

进程与线程的关系,就好比剧组与演员的关系,剧组是一个进程,演员是多个线程

线程的结构

我们在讲进程时说的进程的调度,其实都是基于“一个进程只有一个线程”,实际上,一个进程可以有多个线程,可独立进行调度。每个进程都有自己独立的pid,内存指针,文件描述符表,状态,优先级,上下文,记账信息等。

3.线程是调度执行的基本单位

上述线程的结构决定了现成的特点:

1.每个线程都可以独立去cpu上面执行调度

2.同一个进程的多个线程之间,公用同一份内存空间和文件资源。也就是说,创建新的线程时,不用重新申请资源,而是直接复用之前分配给进程的资源,这就省去了资源分配到开销,所以创建效率更高,更轻量。

4.进程与线程的区别

1.进程包含线程。

2.进程和线程都是用来实现并发编程场景的,但线程比进场更轻量更高效

3.同一个进程的线程之间,共用同一份资源(内存+硬盘)(这表现在:后面写代码的时候,可以直接访问同一个变量,就能实现线程间的通信),省去了申请资源的开销。

4.进程和进程之间有独立性,一个进程挂了,不会影响其他进程。但同一个进程的线程和线程之间可能会相互影响(线程安全问题+线程出现异常)

5.进程是资源分配的基本单位,线程是调度执行的基本单位。

5.线程也不能无限增多

起初,增加线程的数目可以提高进程的效率。但无线增加,会使调度开销变大,就降低了进程的效率。

而且,当线程增多时,可能会引起冲突,这就是线程不安全问题

还有,如果一个线程出现异常,就会抛出异常,如果不及时解决,就会中断程序进行,也就是终端进程,这就导致其他线程消亡了。

二.多线程编程

线程是操作系统的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些API供用户使用(比如Linux的pthread库)。Java标准库中的Thread类可以视为是对操作系统提供的API进行了进一步的封装和抽象

1.法一:继承Thread类,重写run方法

run方法是一个自定义线程的入口方法。每个线程都是一个独立的执行流,可独立执行一系列逻辑。那么一个线程跑起来是从哪里开始执行?从它的入口方法。运行Java程序,就是跑一个Java进程,Java进程里面至少有一个线程,也称为主线程,这个主线程的入口方法就是main方法。

这是我们自定义的一个线程,要想让它跑起来,就先得创建线程:

run是一个线程的入口,所以我们调用了run方法

当加上循环时,会不会俩个语句都打印呢?

执行发现,竟然没有都打印,继续让代码执行,我们打开jconsole.exe来看看是否是俩个进程都执行:

发现只有主线程在执行,也就是说,调用了t.run之后,自定义线程没有真正执行。这是为什么呢?不是说一个java程序就是一个进程吗?哪个进程里面的线程不都是同时执行吗?

别着急,thread类中还有一个start方法,我们来试一试:

这回,我们不仅找到啦main,而且还找到了Thread-0,这就说明,调用了start方法,线程才真正创建,否则,MyThread兑现只是一个普通的对象,run方法也只是一个普通的方法。

run与start的区别:

start和run都是thread的成员

而run只是描述了线程的入口,在主线程中如果单纯调用t.run,就不会真正创建线程,而只有一个主线程在工作。但start就不一样了,调用了start,才能真正调用系统的API,在系统内核中创建线程,一旦创建了线程,线程就会自动调用run方法,去执行内部的代码。

sleep方法:

如何让线程变慢?可以用到Thread里面的sleep静态方法

它会抛出受查异常,所以注意处理异常,如下:

注意,调用了sleep方法后,在main方法中可以throws向上抛出,那为什么在run方法中不能添加异常到方法标签呢?因为这个run方法是重写自父类的方法,父类方法没有填加异常到方法标签,重写的时候自然也不能啦!

2.法二:实现Runnable,重写run方法

thread类实现了runnable接口,所以我们创建自定义线程的时候也可以实现runnable接口

注意,MyRunnable这个类就表示待会儿创建的线程可以运行,然后再实例化Thread对象时,讲MyRunnable对象传入进构造方法即可:

这是我们使用的构造方法,上面的英文解释了形参target:当线程启动时,调用这个target的run方法。如果这个target为null,那么创建的线程thread就什么都不做。

法二相比于法一的优点:

1.Java不支持多继承,如果继承了Thread,就不能再继承其他类;而使用了Runnable就可以再继承其他类了

2.解耦合!!!

创建一个线程,需要俩个关键操作:一个是明确线程要执行的任务,另一个是调用系统api来创建线程。

而任务本身,不一定和线程的概念强相关,这个任务只是单纯的执行一段代码,它是使用单线成还是多线程还是其他方式,都与任务无关。所以就可以把任务单独从线程中提取出来,然后就可以随时把代码改写成用其他方式执行(现在像用单线程来执行这个任务,肯议会的需求是用多线程执行)。

3.法三:继承Thread,重写run,但使用匿名类

4.法四:实现Runnable,但是用匿名内部类

5.法五:直接创建Runnable对象,后面重写run

6.法六:用lambda表达式

具体怎么使用Lambda表达式请看文章http://t.csdnimg.cn/m2bqG

三.Thread类及其常见属性和方法

1.常用属性及其获得

ID:线程的身份标识   getID()

名称:getName()

讲到这个,就不得不说一说Thread中的构造方法:其中常用的就有可以传入线程名字的方法

状态:getState()

优先级:getPriority()

是否为后台线程:isDaemon()

注意,线程分为后台线程(也叫守护线程)和前台线程。一个java进程中,若前台线程没有结束,那么这个进程就一定没有结束,但后台线程没有结束不会影响整个线程的结束。如下举例:

有俩个线程,线程1先休息5秒然后再打印,线程2直接打印,当没有调用isDaemon时,这俩个线程都会执行:

但当调用了setDaemon将daemon改成true时(表示该线程被改成了后台线程),会有如下结果:

由于线程1要休眠5秒,在这期间主线程和线程2已经结束,它们都默认是前台线程,所有前台线程结束后,不管后台线程是否结束,进程都要结束

所以说,创建线程默认是前台线程,只有通过setDaemo主动将daemon改成true,才能编程后台线程

是否存活:isAlive()

Thread对象的生命周期,要比系统内核中的线程的生命周期更长一些,也就是说,Thread对象还在,但内核线程已被销毁。什么时候线程就没了?就是回调方法执行完毕后

看下面的代码,t线程只休眠2秒,而主线程休眠3秒,当主线程休眠完毕后,t线程一定已经执行完毕,所以会有如下结果

注意,true和“执行开始”这俩条日志谁先打印可不一定,因为线程是并发执行的,调度顺序无法确定,但大概率是先打印true,因为线程对象的创建也要消耗时间

是否被中断:isInterrupted()

2.启动一个线程

关键就是start方法,前面已经提到了,start方法内部会调用系统的API,从而在系统内核中创建线程

而run方法,只是单纯的描述了一个线程要干什么,要执行啥内容,它会在start创建好线程之后自动被调用

所以说start和run的本质区别是是否在系统内核中创建一个线程。

3.打断(终止)一个线程

要想终断一个线程,就要想办法让run方法尽快执行完毕

法一:手动创建一个标志位,作为run方法结束的条件

要明确,往往一个线程迟迟不结束,主要是有while循环,我们要想办法让循环结束,代码如下:

主线程休眠5秒后,将isQuit改成true,然后创建的线程就结束了

但有一个问题,当前,上述代码是使用了一个成员变量作为标志位,那我可不可以用局部变量作为标志位呢?答案是不可以!!!

上面我们使用的是lambd表达式,涉及到了变量的捕获,它只能捕获被final修饰的变量或者未被进行修改的变量!!这在lambda表达式一文中有详细解释。如下:用了局部变量后,由于待会要进行修改,所以会报错:

法二:使用Thread类中现成的标志位

上述使用手动创建的标志位的方法不够完善,首先,我们得自己创建标志,其次,当新县城还在休眠而主线程已经把标志改为true时,新线程无法立刻终止,而是得等休眠完指定事件后再终止,只让终止处理不够及时,所以有了下面的方法:

我们一个一个来解释:首先调用Thread.currentThread()可以获取到当前正被调度的线程,也就是t,再调用isInterrupted判断是否被终止,如果没被终止,就执行while,在while中,每休眠1秒就打印一次。然后是主线程,在休眠了5秒后,调用interrupt()手动终断线程。这里就可以看出与自定义标志位的区别啦,用了interrupt后,即使进程正在sleep,也能立马终止,这是因为sleep有可能会抛出InterruptedException这样的异常(就是说,正常情况下,sleep会一直持续到休眠够定义的时间,但当遇到interrupt将线程设定为被打断状态后,他就会抛异常,从而终止线程,结果如下:

欸?为什么已经抛出异常了,但还是继续执行了线程?

这是因为sleep抛出异常之后,会再次把标志位清除,导致线程又继续执行起来。为啥这么设定?就是为了让程序员自己能自己决定接下来要干什么,或者说如何处理。

接下来又三种处理方式:

1.假装没看见,让线程继续执行

2.加上一个break,让线程立即结束

3.做一些其他工作,完成后再结束

也就是再在break之前加一些其他代码,执行完再结束

4.等待一个线程

调用join方法,让一个线程,等待另一个线程完全执行结束后,再继续执行,代码如下:

在t线程中进行5次循环,相当于执行了5秒多,调用了join之后,主线程会在过了五秒之后再向后执行,如下:

总结一下:若线程t正在执行,调用了join的哪个线程就会触发阻塞;若线程t已经执行完了,那么调用了join的线程就直接返回了,不会涉及阻塞。

当未设置时,join默认是死等,但我们实际还可以自定义一个等待时间,那么超过这个时间就不会继续等待了,如下:

第二种就时更精细一点

5.获取当前线程的引用

就是我们用到的Thread.currentThread()

打印出来的名字就是main

四.线程的状态

线程的状态是一个枚举类型Thread.State,(State是一个枚举类,它是定义在Thread类里面的)我们可以如下来观察线程的所有状态:

1.NEW

表示thread对象已经拥有,但start方法还没屌用(就是说,已经有了对象,但还没在系统内核中创建线程)

这时打印出来的就是NEW

2.RUNNABLE

就绪状态,线程已经在cpu上执行或者线程正在排队等待cpu的调度

这时打印出来的就是RUNNABLE

3.WAITING

阻塞状态,由于wait这种不固定时间的方式产生的阻塞状态(之后会进行讲解)

4.TIMED-WAITING

阻塞状态:由于sleep这种固定时间的方式造成的阻塞状态

5.BLOCKED

阻塞状态:由于竞争导致的阻塞状态(之后会进行讲解)

6.TERMINATED

表示Thread对象还在,但内核中的线程已被销毁

这时打印的就是TERMINATED

  • 25
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值