文章目录
前言:多线程相关概念理解
大家应该都听过多线程这个概念,那到底是什么呢?下面辨析一些多线程里的概念,让大家对多线程理解更深一点,我也算是整理我的知识。
1.并发与并行
-
并发:两个或多个事件在用一个时间段内发生
-
并行:两个或多个事件在同一时刻发生(同时发生)
在我们的计算机中,如果你的CPU是多核的,这个核的数量就是处理任务的线程的数量,比如你是双核的,那你的计算机便能并行处理两个任务。如果你只有单核,但又有多个任务或者双核有两个以上的任务,这时候怎么办呢?这时候就是并发处理任务了。
并发处理时,我们的CPU会在多个线程间反复横跳,一下子执行A线程,一下子执行B线程。由于CPU操作的时间是毫秒甚至是纳秒级别的,所以对我们来说可以忽略不记,可以理解把他们当作是同时发生的,但对于某一个时间点,都只有一个线程在工作。
2.进程与线程
关于多线程,对于线程与进程也是要理解一下:
- 程序:是指令和数据的的有序集合,其自身没有任何执行的概念,是一个静态的概念。
- 进程:表示程序执行一次的过程,是一个动态的概念,也是系统分配资源的单位。
- 线程:一个进程里有许多个线程,但至少也要有一个线程,不然进程便失去意义了。线程是CPU执行和调用的单位。
3.线程调度
在并发执行的时候,有多个线程要去抢夺CPU的执行权,那谁决定它们的执行权呢?线程调度器决定。它会随机选择一个线程去执行,当然前提是线程的优先级都一样,下面我们会讲到有一些更高优先级的线程(VIP线程)它们会先被线程调度器调用。
4.多线程
在我们学习多线程之前,我们使用的一直是单线程。我们都知道,程序是从main方法开始执行的,我们一直使用的单线程也就是main线程,也叫主线程。多线程也就是同时开启多个线程去并发的工作。
比如我们观看一个视频,它就是多线程同时工作的。声音、图形、字幕都是不同的线程,它们并发执行组成一部完整影片。
从上图可以得知,如果我们没有使用线程类,那就是主线程一条直线执行完;如果我们有了一个线程类,但是我们是直接调用它的run方法,这样也仍然是单线程作业;只有我们调用它的start方法去开启线程,才能实现多线程操作。run()
和start()
下面会讲到。
有一点需要注意:在开启多线程的时候,都是从主线程作为入口先执行主线程的。比如下面的代码,在开启多线程之前,一定先执行主线程里的东西。
public class A {
public static void main(String[] args) {
System.out.println("无论如何爷先执行");
new Thread(new Receive(5555)).start();
System.out.println("这里就不一定先执行了,看线程调度大哥的心情");
}
}
开启多线程的三种方式
1.继承Thread类
- 自定义线程类继承Thread
- 重写run方法,编写线程逻辑体
- 创建线程对象,调用start()方法开启线程
2.实现Runnable接口
- 自定义类实现Runnable接口
- 实现run()方法, 编写线程逻辑体
- 创建线程对象,把自定义类作为参数传入,调用start()方法开启线程
这时候你可能就有疑问了?咦为啥还要有这种方式呢,第一种不够吗?确实不够,使用实现Runnable接口来开启线程优势是很明显的:
- 避免单继承的局限:一个类只能继承一个类(一个人只能有一个亲爹),类继承了Thread类就不能继承其他的类,实现了Runnable接口,还可以继承其他的类,实现其他的接口。可以是线程逻辑更加灵活。
- 增强了程序的扩展性,降低了程序的耦合性(解耦):将线程的逻辑和线程的操作分开了,Thread类中还有许多用来操作线程的方法,这里将执行逻辑与操作方法分离开,体现了面向对象的思想。
- 适合多个相同程序代码的线程去处理同一个资源:比如有多个线程它们的执行逻辑都是一样的,如果使用runnable接口就不必写多个重复的线程类了。
其实还有一点,大家可能都没有发现,就是这种实现runnable接口的方式其实是一种静态代理的方式。Thread是中介对象,Runnable接口是真实对象。
启动采用静态代理的模式:
-
创建真实角色
-
创建代理对象 (Thread 持有 真实角色 引用)
-
代理对象.start 启动
3.实现Callable接口
- 自定义线程类实现callable接口,需要定义返回值类型
- 重写call方法,定义线程逻辑,需要抛出异常
- 创建自定义类对象(目标对象)
- 创建线程池,产生线程,这种方式不能直接new出一个线程
- 利用线程池的sumit方法,将目标对象作为参数传入,返回一个Future,这个类后面会讲解到
- 利用Futrue的get方法获取线程执行结果
- 关闭线程池
你可能又有疑问了,第二种又不够了?确实不够,因为第三种的好处是显而易见的,但是我们日常使用中较多还是使用第二种,第三种的使用还是较少的,我们它创建过程就可以看出第三种的优势:
- 拥有返回值
- 能够抛出异常
Thread类
通过第二种方式创建线程的时候,我们已经知道实现runnable接口其实是一种静态代理模式,Thread类是代理对象。那它到底能做什么呢?
我们可以把它和Runnable按代理模式的角度来理解:Runnable接口是真实角色(被代理的对象),里面规定了任务的逻辑,这里也就是线程的逻辑;Thread是它的代理对象,所以它也实现了Runnable接口,拥有编写线程逻辑的功能,同时它里面也定义了许多方法用来操作线程,实现了对Runnable的增强。
总结一下:Runnable实现线程逻辑,Thread可以理解成工具类,提供给我们操作线程的方法。
1.知识引入:线程的状态
线程重新建到死亡一共有上图的几种状态,了解它们对我们下面学习Thread的方法有很大好处。实际上,Thread的方法就是让线程的状态改变。
-
新生状态(NEW)
在程序中用构造方法(new操作符)创建一个新线程时,如new Thread()
,该线程就是创建状态,此时它已经有了相应的内存空间和其它资源,但是还没有开始执行。 -
就绪状态(READY)
新建线程对象后,调用该线程的
start()
方法就可以启动线程。当线程启动时,线程进入就绪状态(runnable)。由于还没有分配CPU,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件。当系统挑选一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态。
系统挑选的动作称之为“CPU调度"。一旦获得CPU线程就进入运行状态并自动调用自己的
run方法
。 -
运行状态(RUNNING)
当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的
run()方法
。run()方法定义了该线程的操作和功能。运行状态中的线程执行自己的run方法中代码。直到调用其他方法或者发生阻塞而终止。
-
死亡状态(TERMINATED)
线程调用 stop()方法、destory()方法或 run()方法执行结束后,线程即处于死亡状态。处于死亡状态的线程不具有继续运行的能力,即一个线程死亡后不能再重写调用
start()
方法。
上面那些状态可以只是有个理解就好,下面的阻塞状态和等待超时等待状态则是大家必须掌握的,这是一些很重要的线程状态。
如果你要将线程状态分的大一点的话,等待状态和超时等待状态都会归为阻塞状态,这里把它分的小一点。网上很多博客将这些归结在一起,统称为阻塞状态,我们看他们博客时要懂得更具语境判断是哪种状态。
下图的阻塞状态就是将等待状态和超时等待状态也包括进去了的。
-
阻塞状态(BLOCKED)
阻塞状态特指一些线程在争取锁的过程中失败,然后进入的一种状态。当线程被阻塞时,JVM会把它们放在锁池队列中。锁池队列中的线程会等待锁的释放,然后它们会再次去竞争这个锁,获得锁的则会进入就绪状态,等待线程调度的翻牌。
对于锁池的加深理解:
- 当前线程想调用对象A的同步方法时,发现对象A的锁被别的线程占有,此时当前线程进入锁池状态。简言之,锁池里面放的都是想争夺对象锁的线程。
- 当一个线程1被另外一个线程2唤醒时,1线程进入锁池状态,去争夺对象锁。
- 锁池是在同步的环境下才有的概念,一个对象对应一个锁池。
-
等待状态(WAITING)
等待状态也叫无限等待状态,当一个线程在执行过程中调用了
wait()
方法,它便会进入等待状态,进入JVM中的等待队列中(waitting queue),这种等待状态是无限时间的,直到它被notify()
或者notifyAll()
唤醒或者它被打断进入死亡状态.被唤醒的线程重新进入就绪状态,等待线程调度而不是直接进入运行状态。此时线程会释放掉锁。
如果有用同步方法或者用锁锁住,这时线程就没有马上进入就绪状态,而是会进入锁池等待获取锁。
对于等待队列的加深理解:
- 等待队列中存放的线程是被同一个锁阻塞的线程,不是所有锁阻塞的线程都在同一个等待队列里。
- 一定要注意是同一个锁阻塞才会出现在同一个队列里!!!
- 可以理解为每一个锁配有一个专门的等待队列,用来放被它阻塞的线程,不同锁的等待队列里面的线程不一样。
-
超时等待状态(TIME_WAITING)
这个状态可以看出是上一个状态的补充,它不仅能用唤醒方法唤醒,也能等超过规定的等待时间后,自己进入就绪状态。
它也同等待状态一样,醒来都是进入就绪状态,且在沉睡过程释放锁。如果有用同步方法或者用锁锁住,就会进入锁池。
对于等待和超时等待状态,要注意一些方法,比如join()
、sleep()
,它们有些特殊,它们虽然也是会进入等待和超时状态,但它们不会进入等待队列。下面会讲解到。对于线程的状态,这里有一篇博客,大家也可以去看下:
https://blog.csdn.net/xingjing1226/article/details/81977129?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522159540187219195264564454%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=159540187219195264564454&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v3~pc_rank_v2-1-81977129.first_rank_ecpm_v3_pc_rank_v2&utm_term=%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%AD%89%E5%BE%85%E7%8A%B6%E6%80%81&spm=1018.2118.3001.4187
2.普通方法:不改变线程状态
-
void run()
首先是run方法,Runnable接口里面就只有一个run方法,表示线程的逻辑。Thread代理Runnable,所以也实现了这个方法,我们使用第一种方式创建线程的时候就是重写了这个方法,所以两种创建线程的方式都是重写了
run()
方法。 -
void start()
对于
start()
方法,它是开启线程的方法,让一个线程进入就绪状态。线程在运行也就是进入运行状态时,会自动调用run()
方法。 -
String getName()
与void setName(String name)
这两个方法分别代表获取和设置线程的名字,当然我们也可以在new线程的时候使用它的构造方法传入它的名字。
-
Thread.State getState()
获取当前线程的状态,返回值是一个枚举变量
-
static Thread currentThread()
获取当前线程,是一个静态方法。