一.认识线程
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