5.继承Thread类和实现Runnable接口两种方式对比.
6.9.2案例2-判断线程sleep过程中是否处于活动状态.
7.0 interrupt&interrupted&isinterrupted方法的区别.
一、相关概念
1.1程序.
-
程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象(下载的软件里面的各种系统文件)。
-
1.2进程.
-
进程(process):
-
程序的一次执行过程,或是正在内存中运行的应用程序。如:运行的Eclipse、QQ音乐等。
-
每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创建、运行到消亡的过程(生命周期)。
-
进程到运行不仅仅需要CPU,还需要很多其它资源,比如:内存、显卡、磁盘等。
-
程序是静态的,进程是动态的。
-
进程是操作系统调度和分配资源的最小单位(亦是系统运行程序的基本单位。一定要和线程区分开),系统在运行时会为每个进程分配不同的内存区域和时间片。操作系统调度器:拆分CPU为一段段时间片的运行片,轮流分配给不同的程序。
-
现代的操作系统,大都是支持多进程的,支持同时运行多个程序。比如:一边打游戏,一边使用QQ聊天,一边使用录屏软件,同时还开着画图板,dos窗口等软件。
-
不同进程之间是不共享运行时内存的,但是进程和进程之间是可以进行数据交互和通信,成本很高。如:在京东里面通过微信付款、在淘宝里面通过支付宝付款。
-
1.3线程.
-
线程(thread):
-
进程可进一步细化为线程,是程序内部的一条执行路径。一个进程中至少有一个线程。
-
一个进程同一时间若并行执行多个线程,就是支持多线程的。
-
-
-
线程作为CPU调度和分配的最小单位(一定要和进程区分开)。有句话说CPU只能看到线程,可以这么理解,假设我是CPU,我闭着眼,操作系统调度器将一个进程分配给我之后,我拿到进程睁开眼,我看到的是什么?我看到的是进程中的很多线程,那么我现在能调度和分配的是什么?进程?不行,因为我看不到其他进程,何来调度分配,只能调度我看到的那些线程,如果我是4核的话,把线程ABCD分配到核心1234,其他的线程依然要等待分配,至于等待多久,如何分配,暂不在本文讨论范围。于是线程是CPU调度和分配的基本单位。
-
一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
-
下图中,红框的蓝色区域为线程独享,黄色区域为线程共享。
-
-
1.4查看进程和线程.
-
电脑底部任务栏,右键----->打开任务管理器,可以查看当前任务的进程。
-
1. 每个运行的应用程序都是一个进程:
2. 一个应用程序的多次运行,就是多个进程:
3. 一个进程中包含多个线程:
1.5线程调度.
-
分时调度:所有线程轮流使用 CPU 的使用权,并且平均分配每个线程占用 CPU 的时间。
-
抢占式调度:让优先级高的线程以较大的概率优先使用 CPU。如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
-
1.6多线程程序的优点.
-
提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
-
提高计算机系统CPU的利用率。
-
改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
-
1.7单核CPU和多核CPU.
-
单核CPU,在一个时间单元内,只能执行一个线程的任务。例如,可以把CPU看成是医院的医生诊室,在一定时间内只能给一个病人诊断治疗。所以单核CPU就是,代码经过前面一系列的前导操作(类似于医院挂号,比如有10个窗口挂号),然后到cpu处执行时发现,就只有一个CPU(对应一个医生),大家排队执行。这时候想要提升系统性能,只有两个办法,要么提升CPU性能(让医生看病快点),要么多加几个CPU(多整几个医生),即为多核的CPU。
-
问题:多核的效率是单核的倍数吗?譬如4核A53的cpu,性能是单核A53的4倍吗?理论上是,但是实际不可能,至少有两方面的损耗。
-
一个是多个核心的其他共用资源限制。譬如,4核CPU对应的内存、cache、寄存器并没有同步扩充4倍。这就好像医院一样,1个医生换4个医生,但是做B超检查的还是一台机器,性能瓶颈就从医生转到B超检查了。
-
另一个是多核CPU之间的协调管理损耗。譬如多个核心同时运行两个相关的任务,需要考虑任务同步,这也需要消耗额外性能。好比公司工作,一个人的时候至少不用开会浪费时间,自己跟自己商量就行了。两个人就要开会同步工作,协调分配,所以工作效率绝对不可能达到2倍。
-
-
参考资料:多核CPU和多个CPU的区别:https://www.zhihu.com/question/20998226。
-
1.8并行与并发.
1.8.1并行.
-
并行(parallel):指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻,有多条指令在多个CPU上同时执行。比如:多个人同时做不同的事。
-
比如,A、B、C三个指令同时执行:
-
1.8.2并发.
-
并发(concurrency):只是一种现象,指两个或多个事件在同一个时间段内发生。即在一段时间内,有多条指令在单个CPU上快速轮换、交替执行,使得在宏观上具有多个进程同时执行的效果。
-
比如,A、B、C三个指令在同一时间段内快速切换执行:
-
解释一:
多线程并发只是表面和感觉上的并发,并不是实质上的并发。一个线程要运行,它必须占有CPU,而我们目前用的计算机大多都是单CPU的,所以一次最多只能有一个线程获取CPU并运行。
多线程的实质是“最大限度地利用CPU资源”,当某一个线程的处理不需要占用CPU而只需要和I/O等资源打交道时,让其他线程有机会获得CPU资源。这有点类似于“统筹方法”,例如让你打扫房子和烧水,要在最短的时间内做好这两件事,你一定会想到先把水烧上,然后在等水烧开的空闲时间中去打扫房子,而不是先打扫好了再去烧水,也不是先烧好了再去打扫,这个例子里面,你是那个唯一的CPU,而烧水和打扫就是两个线程。
虽然CPU只有一个,但是它在多个线程之间频繁切换,当切换的频率高到一定程度时,我们就感觉所有的线程在同时运行,于是感觉这多个线程是并发的。因此,并发并不是真的指多个线程同时运行,它仅仅只是形容一种现象。
解释二:
在操作系统中,启动了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单核 CPU 系统中,每一时刻只能有一个程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。宏观并行,微观其实是串行(排队)。 而在多核 CPU 系统中,则这些可以并发执行的程序便可以分配到多个CPU上,实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序才可以真正的同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。
1.8.3总结.
-
并发只是一种程序在运行时的现象效果(宏观并行,微观串行),和串行、并行没有可比性。串行即排队,并行才是真正的多个程序同时执行。
-
单CPU中进程只能是并发,多CPU计算机中进程可以并行也可以并发。
-
单CPU单核中线程只能并发,单CPU多核中线程可以并行也可以并发。
-
无论是并发还是并行,使用者来看,看到的是多进程,多线程。
-
1.9线程分类.
-
JAVA中的线程主要分为两类:用户线程(User Thread)和守护线程(Daemon Thread)。
-
用户线程:AVA语言中无论是线程还是线程池,默认都是用户线程,用户线程也被称为普通线程。
-
main线程也是用户线程,又叫主线程,是由JVM创建的。
-
用户线程创建的子线程默认是用户线程,可以设置为守护线程:setDaemon(true)。比如在main()方法(主线程)里面创建的线程就是子线程。
-
-
守护线程:也被称之为后台线程、服务线程或精灵线程。
-
守护线程是为用户线程服务的,当线程中的用户线程都执行结束后,守护线程也会跟随结束。
-
守护线程具有自动结束生命周期的特性,而非守护线程则不具备该特性。
-
守护线程的优先级和用户线程优先级一致;
-
守护线程的子线程也是守护线程。
-
垃圾回收线程就是典型的守护线程,随主线程结束而结束。
-
-
-
设置用户线程为守护线程方法:setDaemon(true) ,该方法必须在 start()方法之前设置, 如果设置在 start() 之后,程序执行会报错,守护线程也不会生效。
-
判断线程是否是守护线程方法:isDaemon();
-
JVM必须确保所有的用户线程执行结束之后才能退出;如果只还有守护线程在运行,JVM不用等待守护线程执行结束,会直接退出。验证代码如下:
-
二、创建线程的方式.
2.1概述.
-
Java语言的JVM允许程序运行多个线程,使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
-
Thread类的特性:
-
每个线程都是通过某个特定Thread对象的run()方法来完成操作的,因此把run()方法体称为线程执行体。
-
通过该Thread对象的start()方法来启动这个线程,而非直接调用run(),要想实现多线程,必须在主线程中创建新的线程对象。
-
2.2创建线程的几种方式.
-
继承Thread类;
-
实现Runnable接口;
-
实现Callable接口(JDK1.5出现的);
-
使用线程池(JDK1.5出现的)。
3.继承Thread类方式.
3.1使用步骤.
-
定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务;
-
创建Thread子类的实例,即创建了线程对象;
-
调用线程对象的start()方法来启动该线程。
3.2代码演示.
-
创建 Thread类的子类:
-
测试类:
-
线程运行效果图:
3.3变形写法.
-
说明:在有时候我们只是临时使用一次线程,此时就没必要专门定义线程类,浪费资源。那么我们就可以通过以下两种写法实现多线程。
3.3.1在同一个文件里面创建Thread子类.
3.3.2匿名内部类.
补充lambda表达式写法。
3.4注意事项.
-
子线程之间的运行时随机的,到底运行谁,由CPU分配时间片决定。
-
不是main线程执行结束后才执行子线程。程序是先从main方法开始执行的(即从main线程开始执行),如果main方法里执行了其他线程的start方法,其它线程就会进入就绪状态,这时就看CPU分配时间片给谁,谁就执行。
-
如果自己手动通过线程对象调用run()方法,那么就只是调用一个普通方法,没有启动多线程模式。
-
run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
-
想要启动多线程,必须调用start方法。
-
一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出异常“IllegalThreadStateException”。
4.实现Runable接口方式.
-
Java有单继承的限制,当我们无法继承Thread类时,那么该如何做呢?在核心类库中提供了Runnable接口,我们可以实现Runnable接口,重写run()方法,然后再通过Thread类的对象代理启动和执行我们的线程体run()方法。
4.1使用步骤.
-
定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
-
创建Runnable实现类的实例,并以此实例作为Thread的target参数(线程要执行的内容)来创建Thread对象,该Thread对象才是真正的线程对象。
-
调用线程对象的start()方法,启动线程。调用Runnable接口实现类的run方法。
4.2代码演示.
-
定义Runnable接口实现类.
-
测试:
4.3变形写法.
4.3.1在同一个文件里面创建Runnable接口实现类.
4.3.2匿名内部类方式.
-
写法一:未使用Lambda表达式。
-
写法二:使用Lambda表达式。
4.4注意事项.
-
通过实现Runnable接口,使得该类有了多线程类的特征。所有的分线程要执行的代码都在run方法里面。
-
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
-
实际上,所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
-
说明:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。可以理解为 Runnable对象就是一个数据体,里面包含了线程要执行的内容。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。
5.继承Thread类和实现Runnable接口两种方式对比.
-
Thread类实际上也是实现了Runnable接口的类。
-
继承Thread方式适合线程之间处理各自任务(不共享资源)情况。
-
实现Runnable接口比继承Thread类所具有的优势:
-
避免了单继承的局限性;
-
多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源;
-
增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
-
6.相关方法.
6.1设置线程名.
-
不设置线程名,默认是:Thread-0、Thread-1、Thread-2....这样命名。可以在线程的 run方法里面获取当前线程名测试 :Thread.currentThread().getName();
-
手动设置线程名:
-
继承Thread类方式:
-
通过创建的线程对象调用:setName("线程名");方法进行设置;
-
在线程类里面定义带参构造函数,然后调用父类构造函数。如:
-
-
-
-
实现Runnable接口方式:
-
6.2设置线程休眠- .
-
Thread.sleep():是Thread类的一个静态方法。作用:使当前线程休眠,进入阻塞状态(暂停执行),简单理解就是“线程暂停方法”。该方法可接收长整型毫秒和整型纳秒,可读性差(因为需要根据毫秒数或者纳秒数换算成其它时间单位)。
-
TimeUnit.SECONDS.sleep():是 concurrent 包下的一个类,提供了可读性更好的线程暂停操作。里面提供了很多枚举类,如:
枚举 | 单位 |
DAYS | 天 |
HOURS | 小时 |
MINUTES | 分钟 |
SECONDS | 秒 |
MILLISECONDS | 毫秒(千分之一秒) |
MICROSECONDS | 微秒(一百万分之一秒(就是毫秒/1000)) |
NANOSECONDS | 毫微秒(十亿分之一秒(就是微秒/1000)) |
-
使用方式如下:
-
除了枚举之外可以进行时间转换:
6.3设置&获取线程优先级.
-
getPriority() :返回线程优先级;
-
setPriority(int newPriority) :改变线程的优先级,范围在[1,10]之间。
6.4join方法.
-
join方法的作用是:在当前 线程执行的某个阶段,加入并执行其它线程。
-
案例:
6.5yield方法.
-
yield方法的作用是当前正在运行的线程放弃CPU资源,重新回到就绪状态.
-
案例:
6.6stop方法.
-
stop():已过时,不建议使用。强行杀死执行,直接进入死亡状态。run()即刻停止,可能会导致一些清理性的工作得不到完成,如文件,数据库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题。比如转账转了一半,突然停止。
-
案例演示:
-
运行结果:
-
当线程2运行到第10次的时候就会暴力停止线程1的运行。但是从结果上发现,当线程2运行完第10次后,线程1又运行了一次,然后才被停止的,这是因为当线程2执行完第10次的输出语句后,还没来得及执行暴力停止线程1的代码,CPU马上给线程1分配时间片,线程1运行,然后CPU再给线程2分配时间片,线程2运行,马上暴力停止线程1.
-
这样会导致线程1在停止的时候没有来得及处理后事,不安全。就好比,你在工厂里面生成产品,别人突然叫你停止生产,连给你关闭设备的时间都没有留。正确的应该是先提前通知你要停止生产了,给你时间关闭设备。
6.7suspend&resume方法.
-
suspend() & resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则非常容易发生死锁。suspend()调用会导致线程暂停,但不会释放任何锁资源,导致其它线程都无法访问被它占用的锁,直到调用resume()。已过时,不建议使用。取而代之的是wait()、notify()、notifyAll().
6.8wait¬ify¬ifyAll方法.
-
wait():线程等待,可以传入参数指定等待时间。线程等待的时候会释放当前占用的锁,这也是和suspend方法最大的区别。
-
notify:唤醒当前线程;
-
notifyAll:唤醒所有线程。
6.9isAlive方法.
-
isAlive方法是判断线程是不是处于活动状态。
-
注,如果线程处于阻塞状态也属于活动状态。
6.9.1案例1-判断线程死亡之后是否还处于活动状态.
-
案例如下:
-
一开始线程处于活动状态,当2秒过后线程已经运行结束,就处于未活动状态(死亡了)。
6.9.2案例2-判断线程sleep过程中是否处于活动状态.
-
一开始线程肯定处于活动状态,当线程运行一次,然后休眠10秒钟(sleep),再休眠未结束期间线程仍处于活动状态。
6.9.3案例3-判断线程wait是否处于活动状态.
-
线程运行到第五秒的时候处于等待状态(wait),但仍是活动状态。
6.9.4案例4-判断线程被join后是否处于活动状态.
-
一开始线2程肯定处于活动状态,当线程1加入执行的时候,线程2处于阻塞状态(join),但仍然是活动状态。
7.0 interrupt&interrupted&isinterrupted方法的区别.
7.0.1interrupt方法.
-
interrupt 方法是打断活动状态的线程。
-
详解:要中断一个Java线程,可调用线程类(Thread)对象的实例方法:interrupte();然而interrupte()方法并不会立即执行中断操作;具体而言,这个方法只会给线程设置一个为true的中断标志(中断标志只是一个布尔类型的变量),而设置之后,则根据线程当前的状态进行不同的后续操作。
-
情况1:如果线程的当前状态处于阻塞状态,那么在将中断标志设置为true后,还会有如下三种情况之一的操作:
-
如果是wait、sleep以及jion三个方法引起的阻塞,那么会将线程的中断标志重新设置为false,并抛出一个InterruptedException;
-
如果是java.nio.channels.InterruptibleChannel进行的io操作引起的阻塞,则会对线程抛出一个ClosedByInterruptedException;(待验证);
-
如果是轮询(java.nio.channels.Selectors)引起的线程阻塞,则立即返回,不会抛出异常。(待验证)。
-
-
情况2:如果线程的当前状态处于非阻塞状态,那么仅仅是线程的中断标志被修改为true,而在此基础上,一旦进入阻塞状态,则按照阻塞状态的情况来进行处理;例如,一个线程在运行状态中,其中断标志被设置为true,则此后,一旦线程调用了wait、jion、sleep方法中的一种,立马抛出一个InterruptedException,且中断标志被清除,重新设置为false。
-
-
通过上面的分析,我们可以总结,调用线程类的interrupted方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常。因此,通过interrupted方法真正实现线程的中断原理是:开发人员根据中断标志的具体值,来决定如何退出线程。
-
补充:interrupte方法的调用,该方法可在需要中断的线程本身中调用,也可在其他线程中调用需要中断的线程对象的该方法。
-
案例演示1-打断非阻塞状态线程,然后在该线程内部通过判断打断标记停止线程,并处理后事和获取打断标记.
-
执行效果如下:
-
线程被打断后打断标记变为 true,这就是打断非休眠状态下的线程效果。
-
案例演示2-打断阻塞状态线程,并获取打断过后的打断 .
-
运行效果如下:
-
从结果上可以发现,线程在休眠前和休眠被断后的打断标记都是false,这是因为 interrupt方法在打断休眠状态的线程后又会把线程的打断标记重置为false。
7.0.2interrupted方法.
-
interrupted()方法是 Thread类里面的静态方法,判断当前执行线程是否停止,会获取先的打断标记,获取完之后又会重置该线程的打断标记为:false。
-
举例:当一条线程被打断后,如果执行两次interrupted()方法获取线程的打断标记,第一次输出的是 true,第二次输出的则是 false。
7.0.isinterrupted方法.
-
是对象方法,判断该对象是否已经停止,会获取线程的打断标记,但不会重置线程的打断标记。
-
该方法和interrupted()方法的区别就是该方法不会重置线程的打断标记为false,而interrupted()方法则会重置为false。
7.线程的生命周期.
7.1JDK1.5之前.
-
新建: 刚new出来在内存中
-
就绪: start表示该线程有资格抢CPU!
-
运行: 抢到了CPU,会执行run方法
-
阻塞: 调用了sleep方法或其他阻塞的方法
-
死亡: 执行完run方法
7.2JDK1.5开始.
-
6个阶段
8.例子
-
设计两条线程,分别打印1-100之间的偶数和奇数.
-
例子2
-
声明一个匿名内部类继承Thread类,重写run方法,实现打印[1,100]之间的偶数,要求每隔1秒打印1个偶数。
-
声明一个匿名内部类继承Thread类,重写run方法,实现打印[1,100]之间的奇数,
-
当奇数线程打印到5时,让奇数线程暂停一下,再继续。
-
当奇数线程打印到5时,让奇数线程停下来,让偶数线程执行完再打印。
-
-