1.1 概念:
进程是一个正在运行的程序,代表该程序所占用的内存区域。
1.2 工艺特点:
独立
进程是系统中的一个独立实体。 它可以拥有自己独立的资源。 每个进程都有自己的私有地址空间。 用户进程未经进程本身的许可,不能直接访问其他进程的地址空间。 。
动态的
进程和程序之间的区别在于,程序只是一组静态指令,而进程是系统中活动的一组指令。 程序中加入了时间的概念后,就称为进程,有自己的生命周期和各种状态。 ,这些概念在程序中是没有的。
并发性
多个进程可以在单个处理器CPU上并发执行,而不会互相影响。
1.3 并行性和并发性:
HA(High)高可用:是指在高并发场景下尽可能保证程序的可用性,减少系统无法提供服务的时间。
2 线程
2.1 线程的概念
线程是操作系统OS能够进行计算调度的最小单位。 它包含在流程中,是流程中的实际操作单元。
一个进程可以启动多个线程,其中包括一个主线程来调用进程中的其他线程。
我们看到的进程切换也是不同进程的主线程。
多线程扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务
2.2 进程和线程的关系
一个操作系统中可以有多个进程。 一个进程可以包含一个线程(单线程程序)或多个线程(多线程程序)。
同一进程中各个线程在共享内存的同时,也拥有自己独立的内存空间。
因此,想要使用线程技术,首先必须要有进程。 该进程由OS操作系统创建,通常用C或C++完成。
3 多线程特性
3.1 随机性
线程的随机性是指同一时间只有一个程序在执行。
从宏观上看,我们感觉这些程序是同时运行的,但实际上,微观时间是因为CPU在高效切换,使得各个程序看起来像是在同时运行。 也就是说,在宏观层面上,所有的进程/线程看起来都在同时运行,但在微观层面上,一个CPU同时只能处理一件事。 切换速度甚至可以达到纳秒级,速度非常快。
3.2 CPU分时调度
时间片,即CPU分配给每个线程的时间段,称为它的时间片,即允许线程运行的时间。 如果时间片用完后线程仍在执行,CPU就会被剥夺,分配给另一个线程,挂起当前线程。 如果线程在时间片用完之前阻塞或结束,CPU会立即切换,避免浪费CPU资源。 当再次切换到之前挂起的线程时,场景将恢复并继续执行。
注意:我们无法控制操作系统选择执行哪些线程。 OS底层有自己的规则,比如:
1.FCFS(先来先服务算法)
2.SJS(Short Job短服务算法)
相关视频推荐
++多进程、多线程、线程使用场景分析
Linux内核中红黑树的三种场景(红黑树证明、进程管理CFS、内存管理)
90 分钟掌握协程、线程和进程。 专访某大厂| 如何让线程池最高效
++后端服务器开发架构师免费学习地址
【文章福利】:小编整理了一些我个人认为比较好的学习书籍和视频资料,分享在群档里。 如果有需要的话,可以自己添加! ~点击加入(需自取)
3.3 线程状态
由于线程状态比较复杂,所以我们从易到难,首先学习线程的三种基本状态及其转变,简称“三态模型”:
就绪(可运行)状态:线程已准备好运行,只要获得CPU就可以立即执行。
执行(运行)状态:线程已获得CPU,其程序正在运行。
阻塞状态:正在运行的线程由于某些事件(I/O请求等)而暂时无法执行的状态,即线程执行被阻塞。
就绪→执行:分配CPU给就绪线程,成为执行状态”
执行→就绪:执行线程因时间片用完而被剥夺CPU暂停执行,变为就绪状态。
执行→阻塞:由于事件的发生,执行线程被阻塞而无法执行,则执行变为阻塞。
(例如,一个线程正在访问某个关键资源,而该资源正在被其他线程访问)
反之,如果获得了之前需要的资源,阻塞状态就会转变为就绪状态,等待CPU被分配来再次执行。
我们还可以添加两个状态:
创建状态:线程创建比较复杂。 需要先申请PCB,然后分配线程运行所需的资源,并将线程转换为就绪状态插入就绪队列。
终止状态:等待OS处理善后,最终清除PCB并将PCB返回给系统
PCB(Block):为了保证参与并发执行的每个线程都能独立运行,操作系统配置了一个独特的数据结构PCB来描述线程的基本情况和活动过程,进而对线程进行控制和管理。
3.4 线程状态及代码对比
线程生命周期主要有五种状态:
1.新建状态(New):当线程对象创建后,就进入新建状态。 例如:t=new();
2.就绪状态():当调用线程对象的start()方法时,线程进入就绪状态。
线程处于就绪(可运行)状态仅意味着该线程已就绪并随时等待CPU调度执行。 并不意味着执行t.start()后线程会立即执行
3、运行状态():当CPU调度一个处于就绪状态的线程时,这个线程才真正被执行,即进入运行状态。
就绪状态是进入运行状态的唯一入口。 也就是说,一个线程想要进入运行状态执行,首先必须处于就绪状态。
4.阻塞状态():由于某种原因,处于运行状态的线程暂时放弃了CPU的使用权,停止执行。 此时就进入阻塞状态。 直到进入就绪状态,它才会有机会再次被CPU选择执行。
根据阻塞状态产生的原因不同,阻塞状态可以细分为三种类型:
等待阻塞:运行状态的线程执行wait()方法,该线程进入等待阻塞状态。
同步阻塞:当线程获取同步锁失败(因为该锁被其他线程占用)时,就会进入同步阻塞状态。
其他阻塞:当调用线程的sleep()或join()或者发出I/O请求时,线程将进入阻塞状态。 当sleep()状态超时时,join()等待线程终止或超时或I/O处理完成。 当线程返回就绪状态时
5、死亡状态(Dead):线程执行完毕或因异常退出run()方法,线程结束生命周期。
4 多线程代码创建方法一:继承
4.1 概述
类本质上是一个实现接口并代表线程实例的实例。
启动线程的唯一方法是通过类的实例方法start()
start()方法是一个通知底层操作系统的方法。 最终操作系统会启动一个新的线程,操作系统会执行run()
这样实现的多线程非常简单。 通过直接使用自己的类并重写 run() 方法,您可以自动启动一个新线程并执行您自己定义的 run() 方法。
模拟启动多个线程并在每个线程上调用 run() 方法。
4.2 常用方法
施工方法
Thread() 分配新的Thread对象
Thread(String name) 分
配新的Thread对象
Thread(Runnable target)
分配新的Thread对象
Thread(Runnable
target,String name)
分配新的Thread对象
普通法
static Thread currentThread( )
返回对当前正在执行的线程对象的引用
long getId()
返回该线程的标识
String getName()
返回该线程的名称
void run()
如果该线程是使用独立的
Runnable 运行对象构造的
,则调用该 Runnable 对象的 run 方法
static void sleep(long millions)
在指定的毫秒数内让当
前正在执行的线程休眠(暂停执行)
void start()
使该线程开始执行:Java
虚拟机调用该线程的run()
4.3 测试多线程创建方法1
想法一:继承
1. 自定义一个类
2.重写run()中的业务
3.创建线程对象
4. 调用start()
注意:可以调用父类的带参数的构造函数(名称)
给自定义线程对象起个名字,调用方法:super(name)
5 多线程代码创建方法二:实现接口
5.1 概述
如果你的类已经属于另一个类,你就不能继承它。 在这种情况下,您可以实现一个接口。
5.2 常用方法
当 void run() 使用实现该接口的对象创建线程时,启动线程将导致在独立执行的线程中调用该对象的 run() 方法。
5.3 练习2:测试多线程创建方法2
想法 2:实施
1. 自定义一个类。 实现接口中未实现的run()。
3.打印线程名称:.().()
4.创建目标业务对象——接口实现类的对象——包含我们的业务
5、创建线程对象——t1=new();
目的:为了与实现类建立关系,原因是使用start()
6、通过线程对象调用start(),将线程对象添加到就绪队列中。
5.4 两种实现方法的比较
继承类
优点:写起来简单,如果需要访问当前线程,不需要使用.()方法,可以直接使用this来获取当前线程
缺点:自定义线程类已经继承了该类,所以后面不能继承其他类。
实现接口
优点:定制的线程类只实现接口或者接口,以后可以继承其他类。 这样多个线程就可以共享同一个对象,所以非常适合多个相同的线程处理同一个资源。 ,使得CPU、代码、数据能够分离(解耦),形成清晰的模型,更好的体现了面向对象的思想。
缺点:编程稍微复杂。 如果要访问当前线程,需要使用.()方法。
6 门票销售案例
需求:设计4个售票窗口,共售票100张。使用多线程编程设计并编写代码
6.1 选项 1:继承
6.2 选项 2:实施
6.3 问题
1、每次创建线程对象时,都会生成一个值为100的变量。 如果该对象创建 4 次,将生成 400 个票据。 如果不符合要求,如何解决? 每个对象之间是否可以共享变量以确保尽可能多的对象可以出售这 100 张票?
解决方案:使用静态修改
2、超卖发生,0、-1、-2。
3. 发生转售,同一张票被卖给多人。
4、多线程安全问题是如何产生的? 常见的情况是由于线程的随机性+访问延迟。
5、以后如何判断程序是否存在线程安全问题?
多线程程序中 + 有共享数据 + 多条语句对共享数据进行操作
同步锁——线程安全问题的解决方案
7 同步锁
7.1 前言
前面学习多线程编程后,我们遇到了线程安全相关的问题,比如多线程打票场景中的超卖/超卖。
我们如何判断一个程序是否可能存在线程安全问题主要取决于以下三个条件:
多线程程序中 + 有共享数据 + 多条语句对共享数据进行操作
多线程场景和共享数据的条件不能改变(就像用4个窗口卖100张票,这是一门生意)
所以思路可以从第3点“多语句操作共享数据”开始。 由于问题是在这多个语句操作数据的过程中出现的
然后我们可以将所有可能引起问题的代码包装起来,一次只让一个线程执行。
7.2 同步和异步
那么如何“把所有可能引起问题的代码包裹起来”呢? 我们可以利用关键字来达到同步的效果
也就是说,当多个对象对共享数据进行操作时,可以使用同步锁来解决线程安全问题,将加锁的代码进行同步。
接下来我们介绍一下同步和异步的概念:
同步:体现排队的效果。 同时只有一个线程可以独占占用资源,其他没有权限的线程会排队。
缺点是效率会降低,但安全有保证。
异步:体现多线程抢占资源的效果。 线程之间不会互相等待并互相抢占资源。
缺点是存在安全风险,效率较高。
7.3 同步关键字
7.3.1 写入方法
synchronized (锁对象){
需要同步的代码(也就是可能出
现问题的操作共享数据的多条语句);
}
7.3.2 先决条件
使用同步效果有两个前提条件:
前提1:同步需要两个或多个线程(单线程不需要考虑多线程安全问题)
前提2:多个线程必须使用同一个锁(我加锁后其他人也能看到这个锁,否则我的锁无法锁住其他人,就会失去加锁效果)
7.3.3 特点
1.关键字可以用来修饰方法,称为同步方法,使用的锁对象是this
2、关键字可以用来修改一个代码块,这个代码块称为同步代码块。 使用的锁对象可以是任意的。
3、同步的缺点是会降低程序的执行效率,但为了保证线程的安全,必须牺牲一些性能。
4.但是为了性能,需要控制锁定的范围。 例如,我们不需要锁定整个商场,只需锁定试衣间即可。
为什么代码块的锁对象可以是任意同一个对象,而方法却使用this?
因为代码块可以保证同一时间只有一个线程进入
但同步方法不能保证同一时刻只有一个线程可以调用,所以使用该类来引用对象this来保证同步。
7.4.1 练习 - 转换票务案例
package cn.tedu.tickets;
/*本类用于改造多线程售
票案例,解决数据安全问题*/
public class TestRunnableV2 {
public static void
main(String[] args) {
//5.创建目标业务类对象
TicketR2 target
= new TicketR2();
//6.创建线程对象
Thread t1 = new
Thread(target);
Thread t2 =
new Thread(target);
Thread t3 =
new Thread(target);
Thread t4 =
new Thread(target);
//7.以多线程的方式运行
t1.start();
t2.start();
t3.start();
t4.start();
}
}
/*1.多线程中出现数据安全问题的
原因:多线程程序+共享数据+
多条语句操作共享数据*/
/*2.同步锁:相当于给容易出现问
题的代码加了一把锁,包裹了所有
可能会出现数据安全问题的代码
* 加锁之后,就有了同步(排队)的效
果,但是加锁的话,需要考虑:
* 锁的范围:不能太大,太大,
干啥都得排队,也不能太小,太
小,锁不住,还是会有安全隐患*/
//1.创建自定义多线程类
class TicketR2 implements Runnable {
//3.定义成员变量,保存票数
int tickets = 100;
//创建锁对象
Object o = new Object();
//2.实现接口中未实现的
方法,run()中放着的是我们的业务
@Override
public void run() {
//4.通过循环结构完成业务
while (true) {
/*3.同步代码
块:synchronized
(锁对象){会出现安全隐患的所有代码}
* 同步代码块在同
一时刻,同一资源只会被一个线程独享*/
/*这种写法不对,相当于
每个线程进来的时候都会new一个锁
对象,线程间使用的并不是同一把锁*/
//synchronized (new Object()){
//修改同步代码块的锁
对象为成员变量o,因为锁对象必须唯一
synchronized (o) {
//同步代码块解决的是重卖的问题
//如果票数>0就卖票
if (tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//4.1打印当前正在售票的线程名以及票数-1
System.out.println
(Thread.currentThread
().getName() + "=" + tickets--);
}
//4.2退出死循环--没票的时候就结束
if (tickets <= 0) break;
}
}
}
}
7.4.2 练习 - 转换票务案例
package cn.tedu.tickets;
/*本类用于改造多线程售票
案例,解决数据安全问题*/
public class TestThreadV2 {
public static void
main(String[] args) {
//5.创建多个线程对
象并以多线程的方式运行
TickectT2 t1 =
ew TickectT2();
TickectT2 t2 =
new TickectT2();
TickectT2 t3 =
new TickectT2();
TickectT2 t4
= new TickectT2();
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//1.自定义多线程类
class TickectT2 extends Thread {
//3.新增成员变量用来保存票数
static int tickets = 100;
//static Object o = new Object();
//2.添加重写的run()来完成业务
@Override
public void run() {
//3.创建循环结构用来卖票
while (true) {
//Ctrl+Alt+L调整代码缩进
//7.添加同步代码块,解决数据安全问题
//synchronized (new Object()) {
/*static的Object的对象o这种写法也可以*/
//synchronized (o) {
/*我们每通过class关键字创建一个
类,就会在工作空间中生成一
个唯一对应的类名.class字节码文件
* 这个类名.class对应的对象我
们称之为这个类的字节码对象
* 字节码对象极其重要,是反射技
术的基石,字节码对象中包含了
当前类所有的关键信息
* 所以,用这样一个唯一且明
确的对象作为同步代码块
的锁对象,再合适不过了*/
synchronized
(TickectT2.class) {/*比较标准的写法*/
if(tickets > 0){
//6.添加线程休眠,暴露问题
try {
Thread.sleep(10);//让线
程休眠,增加线程状态切换的频率
} catch (InterruptedException e) {
e.printStackTrace();
}
//4.1打印当前正在售票的线程名与票数-1
System.out.println(getName()
+ "=" + tickets--);
}
//4.2给程序设置一个出
口,没有票的时候就停止卖票
if (tickets <= 0) break;
}
}
}
注意:如果是继承的话,锁对象最好使用“类名.class”。 否则,在创建自定义线程类的多个对象时,无法保证锁的唯一性。
7.5 之前遇到的同步例子
JDK1.0
补充,性能比较低(需要排队和同步),安全性高
JDK1.5
去掉,性能更高(无排队,异步),有安全风险
8 其他创建线程的方法
8.1/
:用于存储线程的池。 创建新线程/启动线程/关闭线程的任务都是由池来管理的。
(任务对象) 将任务扔到线程池中
协助创建线程池的工具类
(int) 最多包含 n 个线程的线程池
() 足够的线程使任务不必等待
tor() 线程池,只有一个线程
8.2 练习:创建线程的其他方法
public class TestRunnableV2 {
public static void main(String[] args)
//5.创建接口实现类对象作为
目标对象(目标对象就是要做的业务)
SaleTicketsV2 target
= new SaleTicketsV2();
//6.将目标对象与Thread线程对象绑定
// Thread t1 = new Thread(target);
// hread t2 = new Thread(target);
// Thread t3 = new Thread(target);
// Thread t4 = new Thread(target);
//7.以多线程的方式启动线程--会将
线程由新建状态变为就绪状态
**1.如果只创建了一个线程对象,是单
线程场景,不会出现数据问题*/
// t1.start();
// t2.start();
// t3.start();
// t4.start();
/**7.线程池ExecutorService:用来存
储线程的池子,把新建线程/启动线程/关
闭线程的任务都交给池来管理*/
/**8.Executors用来辅助创建
线程池对象,newFixedThreadPool
()创建具有参数个数的线程数的线程池*/
ExecutorService pool =
Executors.newFixedThreadPool(5);
for(int i = 0;i<5;i++) {
/**9.excute()让线程池
中的线程来执行任务,每
次调用都会启动一个线程*/
pool.execute(target);
本方法的参数就是执行
的业务,也就是实现类的目标对象
}
}