文章目录
1. 多线程
1.1 了解进程
-
在学习多线程之前需要了解几个重要的概念 。 什么是进程,什么是线程。
-
举个生活中的例子: 工厂 cpu --> 车间(进程)–> 工人(线程)
- 假设一个工厂的能源有限,只能供一个车间工作,这个车间工作的时候,其他车间停止生产。一个车间里有很多工人,他们一起协同完成一个任务。车间里的空间是工人可以共享的,每个人都可以使用车间里的资源。有些资源空间很大,大到可以让 固定数量的 个工人进行使用,有些空间资源很小,小到只能让一个人使用,其他人得等到使用完,才能进入。
- 空间小的资源怎么防止他人进入呢!?
解释:一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫“互斥锁”(Mutual exclusion,缩写 Mutex),
防止多个线程同时读写某一块内存区域。 - 固定数量的工人怎么能确定数量呢!?
解释: 就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做“信号量”(Semaphore)
,用来保证多个线程不会互相冲突。
- 空间小的资源怎么防止他人进入呢!?
- 假设一个工厂的能源有限,只能供一个车间工作,这个车间工作的时候,其他车间停止生产。一个车间里有很多工人,他们一起协同完成一个任务。车间里的空间是工人可以共享的,每个人都可以使用车间里的资源。有些资源空间很大,大到可以让 固定数量的 个工人进行使用,有些空间资源很小,小到只能让一个人使用,其他人得等到使用完,才能进入。
-
什么是进程 ?:是操作系统分配资源的最小单位,进程就表示程序在操作系统中运行,是程序的一个运行实例,一种抽象的概念。。
- 例如:打开一个软件,程序就会运行,此时,就会在任务管理器中增加一个进程。它会占用对应的 内存区域,该区域由CPU进行执行与计算。
- 参考:Windows 中任务管理器(查看相应程序的进程)。
-
多进程,允许多个任务同时运行,多线程,允许单个任务分成不同的部分运行。
- 注意:它们之间有协调机制,确保二者不会冲突,又能保证资源共享。到底是依赖一个进程还是多个进程是由软件开发时决定的。
1.2. 进程的特点
-
并发性:
- 多个进程可以在单个处理器CPU上并发执行, 多个进程之间不会互相影响,空间资源独立。
- 例如:打开多个app,或者 应用程序(google浏览器),它们之间不相互干扰。
-
动态性:
- 进程相当于一次程序的执行过程,具有自己的生命周期和各种不同的状态,它必须由创建而产生、由调度而执行、由撤消而消亡。
-
独立性:
- 进程是系统中独立存在的实体,拥有自己独立的资源,每个进程都拥有自己私有的地址空间, 在没有经过进程本身允许的情况下,其它进程不可以直接访问该进程的地址空间。
2. 进程中的线程
2.1 了解线程
- 线程 是操作系统OS能够进行 运算调度的最小单位,它 必须在进程之中,是进程中的实际运作单位,相当于进程中真正干活的实体。
- 举一个例子:将Cpu比作一个火车站,Cpu的核心就相当于售票人员,线程数就相当于窗口数量,售票人员和窗口数量越多,卖票的能力就越强,相当于电脑可以同时处理多个任务。
- 线程还要基于 硬件的支持,提升的性能处理。
- 一个进程,可以开启一个线程,也可以开启多个线程。
- 注意:但其中肯定有一条 主线程,执行程序,进行具体操作。如:
java中main()方法的入口
, 一个线程就相当于一个“执行流程”,并且按照自己的代码去执行。
- 注意:但其中肯定有一条 主线程,执行程序,进行具体操作。如:
2.2 单线程与多线程
-
单线程,就好像上面工厂中的工人一样,是对代码具体执行的。
- 单线程: 当分配给这个线程一个任务时,只有当这个线程完成了任务,才会对下一个任务有所响应。 执行期间是对其他任务没有任何响应的。
-
单线程与多线程区别:
- 单线程,当执行任务的主线程按照执行流程执行任务,等一个动作完成后,才能执行下一个代码, 主线程在等待期间,用户界面看起来就是“忙碌状态”。
- 多线程, 将原本的任务分成了很多个子任务去执行。这样防止了“阻塞”,一个线程在焖米饭期间,cpu会分给另一个线程时间片,让去执行下一个任务。 也就是说允许单个程序创建多个并行执行的线程来完成各自的任务,提高了程序的执行效率。
2.3 线程的两种调度方式
-
多线程的执行具体有随机性,
cpu
选择线程随机分配的执行顺序。 首先要明确一件事,只有cpu发出指令的时候,线程得到cup授权,才能使用资源执行任务。- 但是注意: 多线程工作,其中并发运行( 指两个或多个事件在同一个时间段内发生),在宏观上看似同时执行,实际上是各个线程轮流获取 CPU 使用权才能执行指令。相当于在电脑中打开了多个应用程序在运行,看上去是在实时运行,实则是需要获取Cpu执行指令才能运行,只是因为Cpu执行很快,感觉上是同时在运行一样。
-
cpu有自己的底层分配机制。
java
抢占式的调度模型(默认)。
-
线程的两种调度模式:分时调度模式 与 抢占式调度模式 。
- 分时调度模型:是指让所有的线程轮流获得CPU的使用权,并且 平均分配 每个线程占用的 Cpu时间片(可以理解为Cpu分给的线程的时间段)。
- 抢占式调度模型:使 优先级高的线程获取更多的运行机会来占用cpu更好的资源, 如果可运行池种的线程优先级相同,则随机选中一个线程去使用,当它失去了CPU的使用权后,再随机选择其他线程获取Cpu 使用权。
- Java使用的为Cpu使用抢占式调度模式在多个线程间进行着高速的 上下文切换,让Cpu的使用率更高。
3. 进程与线程关系
- 多个进程同时执行时,如果一个进程崩溃,一般不会影响其他进程,而同一进程内的多个线程之间,如果一个线程崩溃,很可能使得整个进程崩溃。
3.1 线程的并发与并行
- 并发和并行在多线程技术上是绕不开的词汇。
- 首先,都利用多线程技术。其次,目的都是提高CPU的使用率。无论是单核还是双核。
- 其次,为什么操作系统上可以同时运行多个程序而用户感觉不出来?
- 概念上区别:
- 并发:指在 同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,相对处理器来说,多个程序在同一个时间段内发生。 使得在宏观上具有多个进程同时执行的效果,但在实际上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
- 并行:指在 同一时刻,有多条指令在多个处理器上同时执行,相当于处理器而且,多个程序在同一个时刻发生,一个cpu执行一个线程,不存在抢占资源的情况,看起来,二者都是一起执行的。
4. 多线程的状态
4.1 多线程的生命周期
- 也可以成为线程的生命周期,总共有五种状态:
-
新建状态(New):当线程对象对创建后,即进入了新建状态,
- 如:
Thread t = new MyThread();
- 如:
-
可运行状态(Runnable):也被称为“就绪状态”,当调用线程对象的start()方法(t.start();),线程即进入就绪状态。
- 处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
-
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
- 注意:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
-
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态;
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
- 4.1 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
- 4.2 同步阻塞 :线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- 4.3 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
-
死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
5. 创建多线程方式
5.1 创建方式一:继承Thread类
- 虽然是继承
Thread类
,但是本质上还是实现了Runnable接口
的一个实例,代表一个线程的实例。- 然后需要重写
run()
方法。
- 然后需要重写
- 启动线程的 唯一方法 就是通过Thread类的 start() 实例方法 。
start()
方法是一native方法,
它将通知底层操作系统,.最终由操作系统启动一个新线程,操作系统将执行run()
。- 注意:不要用run()方法去启动线程 如果用run()会有什么效果!?
5.1.1 创建对象
- 创建对象的方式,需要继承该类。
- 构造方法的重载 。
Thread() 分配新的Thread对象 Thread(String name) 分配新的Thread对象 Thread(Runnable target) 分配新的Thread对象 Thread(Runnable target,String name) 分配新的Thread对象
5.1.2 常用方法
- 常用方法如下
方法 | 作用 |
---|---|
static Thread currentThread( ) | 返回对当前正在执行的线程对象的引用 |
long getId() | 返回该线程的标识 |
void setName(String name) | 将此线程的名称更改为等于参数 name |
String getName() | 返回该线程的名称 |
void run() | 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法 |
static void sleep(long millions) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行) |
void start() | 使该线程开始执行:Java虚拟机调用该线程的run() |
5.1.3 测试:多线程创建方式1
run()
方法启动和start()
启动有什么区别!?
public class Test_Thread {
public static void main(String[] args) {
MyThread t = new MyThread();
//t.run(); // 虽然调用run()也能启动但并不是真正的启动方法。
/* 多线程状态:
调用start() 相当于转入了就绪状态(可运行状态),如果被os选中,分配cpu时间片,
才可以运行。
*/
t.start(); // 单个调用只是相当于单线程。
/*
2.模拟多线程调用,通过结果可以验证了,多线程的随机性,是通过cpu分配执行的。
算法: 抢占式调度模型
至于 t t1 t2 谁先执行得看cpu分配时间片。
*/
MyThread t1 = new MyThread("张楚岚"); //通过含参构造设置名称
MyThread t2 = new MyThread();
t2.setName("宝儿姐");// 通过方法设置线程名称
t1.start();
t2.start();
}
}
/**
自定义线程
*/
class MyThread extends Thread{
// 空参构造方法,不用写默认存在
public MyThread() {
super();
}
//含参构造方法
public MyThread(String name) {
super(name);
}
//重写run()方法;
@Override
public void run() {
//写一个循环多次的任务
for (int i = 0; i <5 ; i++) {
System.out.println(getName()+" : "+i);//执行线程+输出结果
}
}
}
-
输出结果: 注意不固定!
张楚岚 : 0 宝儿姐 : 0 Thread-0 : 0 宝儿姐 : 1 张楚岚 : 1 张楚岚 : 2 宝儿姐 : 2 Thread-0 : 1 Thread-0 : 2 Thread-0 : 3 Thread-0 : 4 宝儿姐 : 3 张楚岚 : 3 宝儿姐 : 4 张楚岚 : 4
5.2 创建方式二: 实现Runnable接口
5.2.1 测试:多线程创建方法2
- 创建多线程的方式有两种,哪种更好一些!?
public class Test_Runnable {
public static void main(String[] args) {
//1.创建对象
MyRunnable myRunnable = new MyRunnable();
//没有start()方法怎么启动!?看构造方法找实现类Thread
Thread t = new Thread(myRunnable); //通过Thread含参数构造方法。
t.start(); //通过Thread 嗲用start() 简写 new Thread(myRunnable).start();
//模拟多线程
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable,"钢铁侠"); //通过构造方法设置线程名称
t1.start();
t2.start();
}
}
/*
自定义类实现Runnable接口
*/
class MyRunnable implements Runnable{
//重写run()方法
@Override
public void run() {
//写一个循环的业务
for (int i = 0; i <5 ; i++) {
//线程名称通过谁来获取,Runnable只有一个run()方法!?拿到Thread即可通过Thread静态方法。
System.out.println(Thread.currentThread().getName()+"="+i);
}
}
}
输出结果:结果不固定
Thread-0=0
钢铁侠=0
Thread-1=0
钢铁侠=1
Thread-0=1
钢铁侠=2
Thread-1=1
钢铁侠=3
Thread-0=2
钢铁侠=4
Thread-1=2
Thread-0=3
Thread-1=3
Thread-0=4
Thread-1=4
5.3 简述一下Thread和Runnable区别
-
继承Thread类
- 优点: 创建方式简单,可以直接
new Thread(){重写run()}
,如果需要访问当前线程,getName()即可
,直接获取线程名称。 - 缺点: 只能单继承了,无法在继承其他类。
- 优点: 创建方式简单,可以直接
-
实现Runnable接口 (推荐使用)
- 优点: 自定义的线程类只是实现了Runnable接口,扩展性好,可以在继承其他类, 在这种方式下,多个线程可以共享同一个target对象, 所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码、还有数据分开(解耦),形成清晰的模型,较好地体现了面向对象的思想
- 缺点: 编程稍微复杂,如想访问当前线程,则需使用
Thread.currentThread().getName()
方法