第十七节 多线程

1. 多线程

1.1 了解进程

  1. 在学习多线程之前需要了解几个重要的概念 。 什么是进程,什么是线程。

  2. 举个生活中的例子: 工厂 cpu --> 车间(进程)–> 工人(线程)

    • 假设一个工厂的能源有限,只能供一个车间工作,这个车间工作的时候,其他车间停止生产。一个车间里有很多工人,他们一起协同完成一个任务。车间里的空间是工人可以共享的,每个人都可以使用车间里的资源。有些资源空间很大,大到可以让 固定数量的 个工人进行使用,有些空间资源很小,小到只能让一个人使用,其他人得等到使用完,才能进入。
      • 空间小的资源怎么防止他人进入呢!?
        解释:一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫“互斥锁”(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。
      • 固定数量的工人怎么能确定数量呢!?
        解释: 就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做“信号量”(Semaphore)用来保证多个线程不会互相冲突。
  3. 什么是进程 ?:是操作系统分配资源的最小单位,进程就表示程序在操作系统中运行,是程序的一个运行实例,一种抽象的概念。

    • 例如:打开一个软件,程序就会运行,此时,就会在任务管理器中增加一个进程。它会占用对应的 内存区域,该区域由CPU进行执行与计算
    • 参考:Windows 中任务管理器(查看相应程序的进程)。
  4. 多进程,允许多个任务同时运行,多线程,允许单个任务分成不同的部分运行。

    • 注意:它们之间有协调机制,确保二者不会冲突,又能保证资源共享。到底是依赖一个进程还是多个进程是由软件开发时决定的。

1.2. 进程的特点

  1. 并发性

    • 多个进程可以在单个处理器CPU上并发执行, 多个进程之间不会互相影响,空间资源独立。
    • 例如:打开多个app,或者 应用程序(google浏览器),它们之间不相互干扰。
  2. 动态性

    • 进程相当于一次程序的执行过程,具有自己的生命周期各种不同的状态,它必须由创建而产生、由调度而执行、由撤消而消亡。
  3. 独立性

    • 进程是系统中独立存在的实体,拥有自己独立的资源,每个进程都拥有自己私有的地址空间, 在没有经过进程本身允许的情况下,其它进程不可以直接访问该进程的地址空间。

2. 进程中的线程

2.1 了解线程

  1. 线程 是操作系统OS能够进行 运算调度的最小单位,它 必须在进程之中,是进程中的实际运作单位,相当于进程中真正干活的实体。
    • 举一个例子:将Cpu比作一个火车站,Cpu的核心就相当于售票人员,线程数就相当于窗口数量,售票人员和窗口数量越多,卖票的能力就越强,相当于电脑可以同时处理多个任务。
    • 线程还要基于 硬件的支持,提升的性能处理。
  2. 一个进程,可以开启一个线程,也可以开启多个线程。
    • 注意:但其中肯定有一条 主线程,执行程序,进行具体操作。如:java中main()方法的入口, 一个线程就相当于一个“执行流程”,并且按照自己的代码去执行。

2.2 单线程与多线程

  1. 单线程,就好像上面工厂中的工人一样,是对代码具体执行的。

    • 单线程: 当分配给这个线程一个任务时,只有当这个线程完成了任务,才会对下一个任务有所响应。 执行期间是对其他任务没有任何响应的。
  2. 单线程与多线程区别:

    • 单线程,当执行任务的主线程按照执行流程执行任务,等一个动作完成后,才能执行下一个代码, 主线程在等待期间,用户界面看起来就是“忙碌状态”。
    • 多线程, 将原本的任务分成了很多个子任务去执行。这样防止了“阻塞”,一个线程在焖米饭期间,cpu会分给另一个线程时间片,让去执行下一个任务。 也就是说允许单个程序创建多个并行执行的线程来完成各自的任务,提高了程序的执行效率。
      在这里插入图片描述

2.3 线程的两种调度方式

  1. 多线程的执行具体有随机性, cpu 选择线程随机分配的执行顺序。 首先要明确一件事,只有cpu发出指令的时候,线程得到cup授权,才能使用资源执行任务。

    • 但是注意: 多线程工作,其中并发运行( 指两个或多个事件在同一个时间段内发生),在宏观上看似同时执行,实际上是各个线程轮流获取 CPU 使用权才能执行指令。相当于在电脑中打开了多个应用程序在运行,看上去是在实时运行,实则是需要获取Cpu执行指令才能运行,只是因为Cpu执行很快,感觉上是同时在运行一样。
  2. cpu有自己的底层分配机制。

    • java 抢占式的调度模型(默认)
      在这里插入图片描述
  3. 线程的两种调度模式:分时调度模式 与 抢占式调度模式 。

    • 分时调度模型:是指让所有的线程轮流获得CPU的使用权,并且 平均分配 每个线程占用的 Cpu时间片可以理解为Cpu分给的线程的时间段)。
    • 抢占式调度模型:使 优先级高的线程获取更多的运行机会来占用cpu更好的资源, 如果可运行池种的线程优先级相同,则随机选中一个线程去使用,当它失去了CPU的使用权后,再随机选择其他线程获取Cpu 使用权。
    • Java使用的为Cpu使用抢占式调度模式在多个线程间进行着高速的 上下文切换,让Cpu的使用率更高。

3. 进程与线程关系

  1. 多个进程同时执行时,如果一个进程崩溃,一般不会影响其他进程,而同一进程内的多个线程之间,如果一个线程崩溃,很可能使得整个进程崩溃。

在这里插入图片描述

3.1 线程的并发与并行

  1. 并发和并行在多线程技术上是绕不开的词汇。
    • 首先,都利用多线程技术。其次,目的都是提高CPU的使用率。无论是单核还是双核
    • 其次,为什么操作系统上可以同时运行多个程序而用户感觉不出来?
  1. 概念上区别:
    • 并发:指在 同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,相对处理器来说,多个程序在同一个时间段内发生。 使得在宏观上具有多个进程同时执行的效果,但在实际上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
    • 并行:指在 同一时刻,有多条指令在多个处理器上同时执行,相当于处理器而且,多个程序在同一个时刻发生,一个cpu执行一个线程,不存在抢占资源的情况,看起来,二者都是一起执行的。

在这里插入图片描述

4. 多线程的状态

4.1 多线程的生命周期

1. 随机性

  • 也可以成为线程的生命周期,总共有五种状态:
  1. 新建状态(New):当线程对象对创建后,即进入了新建状态,

    • 如:Thread t = new MyThread();
  2. 可运行状态(Runnable):也被称为“就绪状态”,当调用线程对象的start()方法(t.start();),线程即进入就绪状态。

    • 处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行
  3. 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。

    • 注意:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
  4. 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态;

    根据阻塞产生的原因不同,阻塞状态又可以分为三种:

    • 4.1 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
    • 4.2 同步阻塞 :线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
    • 4.3 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

5. 创建多线程方式

5.1 创建方式一:继承Thread类

  1. 虽然是继承Thread类,但是本质上还是实现了Runnable接口的一个实例,代表一个线程的实例。
    • 然后需要重写 run() 方法。
  2. 启动线程的 唯一方法 就是通过Thread类的 start() 实例方法 。
    • start()方法是一native方法,它将通知底层操作系统,.最终由操作系统启动一个新线程,操作系统将执行run()
    • 注意:不要用run()方法去启动线程 如果用run()会有什么效果!?

5.1.1 创建对象

  1. 创建对象的方式,需要继承该类。
    • 构造方法的重载 。
    	Thread() 
    		分配新的Thread对象
    	Thread(String name) 
    			分配新的Thread对象
    	Thread(Runnable target) 
    			分配新的Thread对象
    	Thread(Runnable target,String name) 
    			分配新的Thread对象
    

5.1.2 常用方法

  1. 常用方法如下
方法作用
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

  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区别

  1. 继承Thread类

    • 优点: 创建方式简单,可以直接new Thread(){重写run()},如果需要访问当前线程,getName()即可,直接获取线程名称。
    • 缺点: 只能单继承了,无法在继承其他类。
  2. 实现Runnable接口 (推荐使用)

    • 优点: 自定义的线程类只是实现了Runnable接口,扩展性好,可以在继承其他类, 在这种方式下,多个线程可以共享同一个target对象, 所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码、还有数据分开(解耦),形成清晰的模型,较好地体现了面向对象的思想
    • 缺点: 编程稍微复杂,如想访问当前线程,则需使用Thread.currentThread().getName()方法
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吴琼老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值