1.进程
一、进程
1.1进程的定义:
进程指的是正在运行的程序,它代表了程序多占用的内存区域
1.2进程的特点:
1)独立性
进程是系统中独立存在的实体,拥有自己独立的资源,每个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间
2)动态性
进程和程序的区别在于:程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,程序加入了时间的概念后,才称为进程;具有自己的生命周期和各种不同的状态,这些概念是程序不能具备的
3)并发性
多个进程可以在单个处理器CPU上并发执行,多个进程之间不会互相影响
1.3并发与并行的区别
HA(High Availability)高可用:指在高并发的情景中,尽可能的保证程序的可用性,减少系统不能提供服务的时间
二、线程
2.1线程的定义:
- 线程是操作系统(OS)能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.
- 一个进程可以开启多个线程,其中有一个主线程来调用本进程中的其他线程
- 我们看到的进程的切换,切换的也是不同进程的主线程
- 多线程扩展了多进程的概念,使同一个进程可以同时并发处理多个任务
2.2进程与线程的关系
- 一个操作系统中可以有单个进程也可以有多个进程,而一个进程中可以包含一个线程(单线程程序),也可以包含多个线程(多线程程序)--图1
- 每个线程在共享同一个进程中的内存的同时,又有自己独立的内存空间,所以想使用线程技术,得先有进程,进程的创建是OS操作系统来创建的,一般都是C或者C++完成--图2
2.3多线程的三个特性
1)随机性
- 线程的随机性指的是同一时刻,只有一个程序在执行;
- 宏观层面上,所有的进程/线程看似同时运行,但是微观层面上,同一时刻,一个CPU只能处理一件事.切换的速度甚至是纳秒级别的,非常快
2)CPU分时调度
CPU分配给各个线程的一个时间段,称作它的时间片
如果就绪队列中的线程被OS选中,会被执行,当时间片用完会被挂起,或者阻塞/结束
注意:我们不可以控制OS,OS有自己的调度规则【FCFS SJS】
- FCFS(First Come First Service 先来先服务算法)
- SJS(Short Job Service短服务算法)
3)线程的状态
分为三态和五态模型:
- 创建状态:线程的创建比较复杂,需要先申请PCB,然后为该线程运行分配必须的资源,并将该线程转为就绪状态插入到就绪队列中
- 终止状态:等待OS进行善后处理,最后将PCB清零,并将PCB返回给系统
- 就绪 → 执行:准备为就绪线程分配CPU即可变为执行状态"
- 执行 → 就绪:正在执行的线程由于时间片用完被剥夺CPU暂停执行,就变为就绪状态
- 执行 → 阻塞:由于发生人为或者某问题,使正在执行的线程受阻,无法执行,则由执行变为阻塞,(例如线程正在访问临界资源,而资源正在被其他线程访问)
- 反之,如果获得了之前需要的资源,则由阻塞变为就绪状态,等待分配CPU再次执行
PCB(Process Control Block线程控制块:存储线程相关信息的模块):为了保证参与并发执行的每个线程都能独立运行,OS配置了特有的数据结构PCB来描述线程的基本情况和活动过程,进而控制和管理线程
2.4线程状态与代码对照
线程生命周期,主要有五种状态:
- 新建状态(New) : 当线程对象创建后就进入了新建状态.如:Thread t = new MyThread();
- 就绪状态(Runnable):当调用线程对象的start()方法,线程即为进入就绪状态.处于就绪(可运行)状态的线程,只是说明线程已经做好准备,随时等待CPU调度执行,并不是执行了t.start()此线程立即就会执行
- 运行状态(Running):当CPU调度了处于就绪状态的线程时,此线程才是真正的执行,即进入到运行状态。就绪状态是进入运行状态的唯一入口,也就是线程想要进入运行状态状态执行,先得处于就绪状态
- 阻塞状态(Blocked):处于运状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入就绪状态才有机会被CPU选中再次执行.根据阻塞状态产生的原因不同,阻塞状态又可以细分成三种:(1)等待阻塞:运行状态中的线程执行wait()方法,本线程进入到等待阻塞状态(2)同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态(3)其他阻塞:调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态.当sleep()状态超时.join()等待线程终止或者超时或者I/O处理完毕时线程重新转入就绪状态
- 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期
面试题: sleep 和wait 的区别
2.多线程代码的两种创建方式
多线程Thread常用方法 | |
构造方法 | |
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() |
2.1继承 Thread类
步骤:
1)自定义线程类 extends Thread
2)重写run()方法
3)方法中自定义功能+getName()方法
4)创建对象,每new一个对象代表一个线程(new对象对应的就是线程的新建状态)
5)通过start()方法实现多线程的抢占效果
6)new一个自定义名字的线程,需要手动添加无参构造和String类型的含参构造方法
TIPS:run()放着的是业务,但是想以多线程方式启动的话,需要调用start()
练习:继承Thread类创建多线程对象
package cn.tedu.thread;
/*本类练习多线程Thread创建方式1*/
public class TestThread1 {
public static void main(String[] args) {
//5.创建多线程对象
MyThread t = new MyThread();/*new对象对应的就是线程的新建状态*/
MyThread t1 = new MyThread();/*new对象对应的就是线程的新建状态*/
MyThread t2 = new MyThread();/*每new对象一次对应的就是线程的新建状态(就是一个新线程)*/
MyThread t3 = new MyThread("划水的鸭子");/*new一个自定义名字的线程*/
// t.run();
// t1.run();
// t2.run();//只是普通的调用方法
t.start();
t1.start();
t2.start();//使用start()才会有多线程抢占效果
t3.start();
}
}
//1.自定义多线程类
/*方式一:extends Thread继承父类*/
class MyThread extends Thread{
//6.1创建一个子类的无参构造,防止被覆盖
public MyThread() {//无参构造默认存在,如果有含参构造,要手动添加无参构造
super();//默认存在super()
}
//6.2创建一个子类的含参构造,传入线程,具体业务是父类构造方法
public MyThread(String name) {
super(name);
}
//2.手动打出run,自动添加一个重写的run()方法,写自己的业务
@Override//注解:子类调用父类方法
public void run() {
//3.super表示父类的引用,但我们不用调用父类功能,不需要父类方法就注释掉,
//super.run();
//4.自定义功能
//需求:循环输出10次当前正在执行的线程的名称
for (int i = 1;i<=10;i++){
//5.getName()获取当前正在执行的线程的名称
System.out.println(i+"="+getName());
}
}
}
2.2Runnable接口实现类对象绑定创建多线程
注意:一、Runnable接口中没有getName()方法,那么如何获取线程名称呢?
- 方案:可以从之前的Thread下的方法入手
- currentThread(),可以被类名直接调用【静态】,获取当前正在执行的线程
- 之后再进一步获取这个线程的名称getName()
二、接口和线程建立关系--绑定
练习:Runnable接口实现类对象绑定创建多线程
package cn.tedu.map;
/*本类测试多线程创建方式2:多个线程可以干同一件事*/
public class TestThread2 {
public static void main(String[] args) {
//2.创建线程对象进行测试
//2.1创建子接口实现类对象
MyRunnable target = new MyRunnable();
//2.2接口和线程建立关系--绑定
Thread t1 = new Thread(target,"jack");//接口线程可以直接定义名字
Thread t2 = new Thread(target,"rose");
Thread t3 = new Thread(target,"泰坦尼克");
t1.start();
t2.start();
t3.start();
}
}
//1自定义多线程类
/*方式二:实现Runnable接口完成*/
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i <=10; i++) {
/*注意问题:Runnable接口中没有getName()方法,那么如何获取线程名称呢?
* 方案:可以从之前的Thread下的方法入手
* currentThread(),可以被类名直接调用【静态】,获取当前正在执行的线程
* 之后再进一步获取这个线程的名称getName()*/
System.out.println(i+Thread.currentThread().getName());
}
}
}
2.3多线程编程的两种实现方式区别:
1.继承的方式:extends Thread
优点:写法简单
缺点:后续无法再继承其他类【java中的类是的单继承】
2.实现的方式:implements Runnable
优点:多实现,多继承,更加灵活且解耦
缺点: 编程稍微复杂,如想访问当前线程,则需使用Thread.currentThread()方法
2.4多线程创建过程需要注意的点:
1)new--对应的就是线程的新建状态
2)run()--这是一个必须重写/实现的方法,本方法里放着的是业务
3)start()--调用此方法,才会使线程以多线程的方式启动【会把本线程加入到就绪队】,start()在执行时,底层会自动调用run()完成业务,抢占效果完成
4)问题:调用start()时,线程会立即执行吗?
答:不会立即执行,只是加入到了就绪队列,等等OS选中
3.多线程案例
练习1:使用继承Thread类创建方式编程售票案例
package cn.tedu.tickets;
/*需求:设计多线程编程模型,4个售票窗口共计售卖100张票
* 本类通过继承Thread类的方式完成多线程编程案例*/
public class TestThread {
public static void main(String[] args) {
//5.创建线程对象
TicketsThread t1 = new TicketsThread();
TicketsThread t2 = new TicketsThread();
TicketsThread t3 = new TicketsThread();
TicketsThread t4= new TicketsThread();
//6.以多线程的方式启动--将线程加入到就绪队列
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//1.自定义线程售票业务类
class TicketsThread extends Thread{
//3.定义一个变量用来保存票数
//7.将票数设置为静态,被所有对象共享,否则有几个线程就卖几百张票(重卖现象)
//int tickets = 100;//
static int tickets = 100;/*静态资源属于类资源,被全局共享,只有一份(只第一时间加载一次)*///超卖现象
//2.完成业务,重写父类Thread的run()
@Override
public void run() {
//4.在死循环结构中完成售票业务--售完为止
while(true){
try {
//8.休眠--让程序休眠10ms--人为提升问题发生的概率
/*我们可以人为让程序休眠10毫秒,为了增加线程状态切换的概率
* 但会产生问题1:重卖现象,同一张票卖给了两个人
* 产生问题2:超卖现象,票数出现了0 -1 -2,超出了实际售卖的票数*/
Thread.sleep(10);//InterruptedException中断的异常,需要try-catch捕获
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印下当前正在售票的线程名称,并且票数-1
System.out.println(getName()+"="+tickets--);//获取当前执行的线程名称+票数每次少一张
//一定要设置循环出口!!!判断,没票了就停止售票
if (tickets<=0) break;
}
}
}
练习2:使用实现Runnable类创建多线程方式编程售票案例
package cn.tedu.tickets;
/*需求:设计多线程编程模型,4个售票共计售卖100张票
* 本类通过实现Runnable接口的方式完成多线程编程案例*/
public class TestRunnable {
public static void main(String[] args) {
//5.创建实现类(目标类)对象
TicketsRunnable target = new TicketsRunnable();//因为接口只创建一个对象,所以只调用一次100张票
//6.以多线程的方式启动线程
/*若想以多线程的方式启动实现类业务,需要借助Thread
*触发的是Thread的含参构造,将目标业务对象target与Thread作绑定 */
Thread t1 = new Thread(target);//100张票给到4个售票员
Thread t2 = new Thread(target);
Thread t3 = new Thread(target);
Thread t4 = new Thread(target);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
//1.自定义多线程实现类
class TicketsRunnable implements Runnable{
//3.创建变量保存票数
int tickets = 100;
//2.添加未实现的方法run(),run()里是要实现的业务
@Override
public void run() {
//4.创建循环结构,实现卖票的功能
while(true){
try {
//7.休眠--让程序休眠10ms--人为提升问题发生的概率
/*我们可以人为让程序休眠10毫秒,为了增加线程状态切换的概率
* 但会产生问题1:重卖现象,同一张票卖给了两个人
* 产生问题2:超卖现象,票数出现了0 -1 -2,超出了实际售卖的票数*/
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//4.1想获取当前正在售票的线程名,并且票数-1
System.out.println(Thread.currentThread().getName()+"="+tickets--);
//4.2注意死循环需要设置出口!!!当没票时,就停止售票
if (tickets<=0) break;//if只有一条语句,大括号可以省略
}
}
}
4.多线程安全隐患
4.1多线程安全隐患怎么产生的?
答:线程的随机性+访问延迟造成
4.2以后如何判断程序是否有线程安全问题?
答:在多线程程序中+有共享数据+多条语句操作共享数据
4.3总结:
1)单线程程序不会出现多线程抢占资源的情况
2)如果没有共享数据,互不干涉,也不会出现数据的安全问题
3)多条语句操作了共享数据,而多条语句的执行是需要时间的,存在延迟,所以这个时间差导致了数据的安全问题