一、线程简介
1.1 进程
本质上是计算机中在执行的任务。
简单理解,一个软件在运行中,就对应一个进程
1.2 线程
进程中执行的某段逻辑或者任务。
是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位
简单理解,应用软件在运行中相互独立,可以同时运行的功能
进程是有界面的,有的时候双击一个进程,并不会出现对应的界面,因为目前的软件都是合成界面的,启动一个程序,启动的不仅仅是该程序的主进程,还有其他很多相关的进程,所以界面有时不是一个进程就可以显示出来的。
服务本质上是没有界面的进程。
1.3 多线程
相互独立同时运行多个线程,则形成了多线程。
多线程有什么用呢?
用个简单的故事说明下:小王应聘去电子厂打螺丝,领导给他分配了一个流水线的工位,工作就是把流水线上处理好的货物搬下来搬到仓库,但是每个货物处理好需要10分钟左右,那么就意味着每个货物处理过程中,小王有10分钟的等待摸鱼时间。老板来一看,这不行呀,干活2分钟,休息10十分钟,相当于大半天都在休息,于是老板就给小王又分配了2条流水线的工作,一会干流水线1,等待时间去做流水线2的工作,等待时间去做流水线3的工作。小王一个人负责3条流水线,工作效率大大提高了。
小王为什么可以同时负责3条流水线,提高工作效率呢?是因为把空闲的摸鱼时间充分利用起来了,这样就可以在3条流水线之间切换,从而提高工作效率。其实我们的代码也和这个故事类似,比如说,有以下代码:
int a = 10;
int b=20;
int c = a+b;
System.out.println(c);
在运行的时候,CPU读到了第一行代码,那么会在内存中创建变量a。创建变量是需要花费时间的(虽然这个时间很短),在创建过程中,CPU是无法继续运行后面代码,只能先等待。假设一行代码等待0.01s,那么4行代码就需要等待0.04s,这种就是典型的单线程程序,就是故事中小王负责一条流水线是一样的逻辑,CPU不会切换到其他代码中去运行,效率会比较低。
多线程程序呢,CPU可以在多个代码块之间切换,把等待时间充分利用起来,这就多线程最大的特点,提高程序运行效率。
总结来说:
什么是多线程?有了多线程,我们就可以让成语同时做多件事情。
多线程的应用场景
软件中的耗时操作:拷贝、迁移大文件;加载大量资源文件(比如打开游戏加载资源);所有聊天软件;所有后台服务器
二、并发和并行
2.1 并发
在同一时刻,有多个指令在单个CPU上交替执行。
例如平时周末在家休息,在电脑上打游戏,还要喝可乐,还要吃零食,于是,右手需要一会按鼠标打游戏,一会拿可乐,一会拿零食,在多个物品上来回交替,这种情况可以看做是并发。
右手就是CPU,鼠标、可乐、零食可以看做3条线程,CPU在3条线程之间进行交替执行。
2.2 并行
在同一时刻,有多个指令在多个CPU上同时执行。
三、多线程的实现方式
3.1 线程的顶级父类:Thread
Thread在底层是实现了Runnable接口
3.2 自定义线程有三种方式
3.2.1 继承Thread类,重写run方法
①继承Thread类,重写run方法,将逻辑写到run方法,通过调用start方法来启动当前线程(如果调用的是run方法,那么就相当于此时是使用的普通类,而不是线程类)
class Example extends Thread {
public void run() {
System.out.println(“Running”);
System.out.println(“Done”);
}
private void xxx() {
System.out.println(“In xxx”);
}
public static void main(String args[]){
Example ttt = new Example();
ttt.start();
ttt.xxx();
}
}
以上代码是否会打印“In xxx”?
会。在主线程上利用start()启动了一个新线程,并不会影响主线程的执行
3.2.2 实现Runnable接口,重写run方法
②实现Runnable接口,重写其中的run方法,然后利用Runnable对象构建一个Thread对象用star()t方法来启动这个线程。
3.2.3 实现Callable接口,重写call()方法
③实现Callable接口,重写其中的call()方法。目前知道有这种方式即可。
3.2.4 三种方式的对比
四、常见的成员方法
sleep和wait有什么区别
sleep:需要指定一个睡眠时间。休眠的时间到了就会自动醒。释放执行权,不会释放锁。该方法被设计在了Thread类上,是一个静态方法。
wait:可以指定等待时间,也可以不指定,需要被唤醒。释放执行权,同时也释放锁。被设计在Object上,是一个普通方法。
4.1 getName()
若果没有给线程设置名字,线程也是有默认名字的,格式:Thread-X(X序号,从0开始)
4.2 currentThread()
当Java虚拟机启动之后,会自动启动很多条线程,其中有一条线程叫做main线程,它的作用就是调用main方法,并执行里面的代码。
以前,没有使用线程之前,我们写的代码,其实都是运行在main线程中的。
4.3 线程的优先级
线程的调度有两种方式
- 抢占式调度: 多个线程,抢夺CPU的执行权,CPU在什么时候,执行哪条线程,执行多长时间都是不确定的。体现了随机性
- 给抢占式调度:所有线程轮流执行,一条线程一次,执行的时间也是差不多的
JAVA中线程的调度采用的是:抢占式调度
线程一共有1-10这10个优先级。理论上,数字越大表示这个线程优先级越高。抢占到资源的几率也就越大。实际上,相邻的两个优先级的差异性并不明显。
当优先级差到5个单位以上的时候才相对有一点明显。
一般在程序中不应该依赖于程序的优先级来决定谁先执行,谁后执行。而应该依赖于在程序中设定的条件,通过一定的代码决定谁先执行谁后执行。
4.4 守护线程
当线程被设置为守护线程的时候,其他的线程就默认是被守护状态。
一旦被守护线程结束,不论守护线程完成与否都会随之结束(不会立即结束,接收到结束命令时,陆续结束)。
Java中的GC本身就是一个守护线程,程序在启动的时候,GC就开始检测。
如果有多个被守护线程,以最后一个被守护线程结束的时间点为准。
应用场景:
比如小王和小李正在聊天,打开了聊天窗口,正在发送一个文件,此时若小王关闭了聊天窗口,则随之传输文件的线程也会结束掉
五、 线程的生命周期
5.1 线程状态
在Java中线程只有6种状态,没有运行状态,为什么会出现这种情况呢?
因为一旦线程抢夺到CPU资源,虚拟机就会把线程交给操作系统去管理,所以就没有运行状态
六、线程安全问题
由于多个线程之间是相互抢占资源,而且抢占过程是发生在线程执行的每一步中。导致出现不合常理的情况—称为多线程并发的安全问题。
同步/异步
同步:是在同一时间段内只允许一个线程对某段代码进行操作。
异步:是在同一时间段内允许多个线程对某段代码进行操作。
异步的效率更高。
6.1 同步代码块
①通过同步代码块(用synchronized修饰的代码块)来解决多线程的并发问题-----需要一个锁对象-----锁对象需要被所有的线程都认识-----可以是:所有线程共享的资源、类的字节码(类名.class)、this(当前对象)。
6.2 同步方法
②通过同步方法(用synchronized修饰的方法)来解决多线程的并发问题
非静态同步方法锁的对象是this。
static静态方法,锁对象就是该方法所在类的Class字节码。
七、锁介绍
7.1 lock锁
7.2 死锁
死锁不是一个知识点,它是一个错误,出现了锁嵌套
- 由于多个线程执行并且线程之间的锁形成嵌套而导致无法继续运行的情况。
- 产生死锁的原因:多线程并发、共享资源比较多、锁嵌套。
- 避免死锁:减少线程数量、减少共享资源、同一锁对象,减少锁嵌套
7.3 生产消费模型
具体可看:生产者消费者模型
八、简单练习
8.1 抢红包