java基础巩固-宇宙第一AiYWM:为了维持生计,多高(多线程与高并发)_Part3~整起(线程篇之唠唠启动线程的三种基本方式)

首先,应用层面的技术生命周期总是很短暂,那比较不短暂的是什么呢?,先看看自己悟一悟

  • 看一下实现线程的三种主要方式:
    • 使用内核线程(Kernek-Level Thread, KLT)实现:直接由OS内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以看作内核的一个分身,这样OS就可以有能力同时处理多件事情,支持多线程的内核叫做多线程内核
      在这里插入图片描述
    • 使用用户线程实现:一个线程只要不是内核线程就可以认为是用户线程(User Thread, ut),
      在这里插入图片描述
    • 使用用户线程加轻量级进程混合实现
      在这里插入图片描述
      在这里插入图片描述
  • Java线程调度:线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种:
    • 协同式线程调度
      在这里插入图片描述
    • 抢占式线程调度
      在这里插入图片描述

接着回到咱们Java,有比下面三种方法更好的启动或者调用线程的方式,比如线程池,咱们可以从线程池中直接拿。你想呀,如果每个打手各自为战,可能就会出现下面几个缺点:

  • 首先,哪个消费者需要打手来看家护院帮自己闯关练级时,倘如每个人都得自己培养一个打手出来,给打手起名字,给买武器…麻烦成啥了
    • 如果有个打手集团,咱们想要叫啥名字的、长啥样的、带啥武器的以及能干啥的指定打手帮咱们看家护院练级闯关,直接打个电话到打手集团去摇人过来帮咱们顾客解决问题,这不香嘛。这不仅是顾客消费文明进步的一小步,也是打手服务文化进步的一大步呀。
    • 上图再看一下:
      在这里插入图片描述

但是呀,不管是咱们写代码还是面试用,这三个造个线程出来的方法咱们还是过一遍好一点。上菜~

  • 第一种方式,我个人叫他:“从thread类继承,重写run方法”
    • 当创建完thread对象后该线程并没有被启动执行,直到调用了start()方法后才真正启动了线程(此时调用start方法后线程并没有马上执行而是处于就绪状态,也就是这个线程已经获取了除CPU资源外的其他资源,等待获取到CPU资源后这个线程才会真正处于运行状态)
      //咱们自己写的线程类。(因为java规定了Thread默认实现了Runnable接口,你自写类既然实现了人家接口,你就得实现人家接口中的方法)
      public class DemoThread extend Thread{
      	@Override//
      	public void run(){//run() 方在调用 start() 方法后被执行,而且一旦线程启动后 start() 方法后就会立即返回,而不是等到 run() 方法执行完毕后再返回。
      		//......具体逻辑......
      	}
      }
      
      //模拟一下使用(咱们在main()方法中使用一下咱们自己写的线程类)
      public static void main(String[] args){
      	DemoThread demoThread = new DemoThread();
      	//启动线程
      	demoThread.start();//run() 方在调用 start() 方法后被执行,而且一旦线程启动后 start() 方法后就会立即返回,而不是等到 run() 方法执行完毕后再返回。
      }
      
    • Thread 类本质上是实现 Runnable 接口的一个实例,代表一个线程的实例,如下图
      在这里插入图片描述
      老规矩,买一增一
      在这里插入图片描述
      这种方法的好处和坏处如下:
      在这里插入图片描述
    • 第一种方式中 run() 方法究竟是怎么实现的?
      在这里插入图片描述
      • 本质上,实现线程只有一种方式,而要想实现线程执行的内容,却有两种方式,也就是可以通过 实现 Runnable 接口的方式,或是继承 Thread 类重写 run() 方法的方式,把我们想要执行的代码传入,让线程去执行,在此基础上,如果我们还想有更多实现线程的方式,比如线程池和 Timer 定时器,只需要在此基础上进行封装即可
    • 第一种方式:new Thread(…)的优缺点:
      • 优点补充:使用继承的好处就是方便传参,可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递
      • 缺点补充:任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码
  • 第二种方式,我叫他“实现Runnable接口并重写其中的run()方法
    • 从源码中可以看到,第二种方式实际上用的还是人家Thread自身的run方法,只不过此时会判断如果你此时传进来这个target(target的值就是你给Thread传进来Runnable对象)不为空,则调用你自己的run方法;第一种方式就不用说了,他是Thread的子类重写了Thread的run方法,最终执行的是子类中的run方法
      在这里插入图片描述
    • Runnable接口,也是一个函数式接口,所以这个接口里面只有一个方法。而Runnable接口中的run()方法是没有返回值的,所以一般要被重写。
    • 这种方式对上面的补充的缺点有了改善,可以多个线程公用一个demoRunnable,需要的时候可以给DemoRunnable添加参数对多个任务进行区分
      public class DemoRunnable implements Runnable{
      	@Override//step1.自写类中重写run方法
      	public void run(){
      		//......具体的逻辑......
      	}
      }
      
      public static void main(String[] args){
      	DemoRunnable demoRunnable = new DemoRunnable();//step2.自写类实例化后传入new Thread中。因为Thread 类本质上是实现 Runnable 接口的一个实例
      	new Thread(demoRunnable).start;//step3.自写类实例化后引用传入Thread对象后并调用start方法
      	new Thread(demoRunnable).start;//多个线程公用一个demoRunnable,
      }
      

老规矩,买一赠一,上菜:
在这里插入图片描述

  • 第三种方式是实现Callable接口,通过有返回值的 Callable 创建线程(使用FutureTask方式)【Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例
    在这里插入图片描述
    public class DemoCallable implements Callable<Integer>{
    	@Override//step1.自己写的类里面重写call()
    	public Integer call(){
    		return 123;
    	}
    }
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    	//创建异步任务.//step2.将自写类实例化后将引用传入FutureTask中,再实例化FutureTask
    	FutureTask<Integer> futureTask = new FutureTask<Integer>(new DemoCallable());//使用创建的FutureTask对象作为任务创建了了一个线程并启动线程
    	//step3.再把FutureTask引用传入new Thread中,并启动线程
    	new Thread(futureTask).start();
    
    	try{
    		//等待任务执行完毕返回结果
    		String result = futureTask.get();
    		//打印呀或者干啥随你
    	} catch(ExceptionException e) {
    		e.printStackTrace();
    	}
    }
    
    //另外一种方式:
    class CallableTask implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            return new Random().nextInt();
        }
    }
    
    //创建线程池
    ExecutorService service = Executors.newFixedThreadPool(10);
    
    //提交任务【submit()方法把任务放到线程池中,并由线程池创建线程,不管用什么方法,最终都是靠线程来执行的】,并用 Future提交返回结果
    Future<Integer> future = service.submit(new CallableTask());
    
    • 无论是 Callable 还是 FutureTask,它们首先和 Runnable 一样,都是一个任务,是需要被执行的,而不是说它们本身就是线程。它们可以放到线程池中执行。
    • Future.get()用于异步结果的获取。它是阻塞的,背后原理是什么呢?
      • FutureTask的类结构图:FutureTask实现了RunnableFuture接口,RunnableFuture继承了Runnable和Future【Future 表示一个任务的生命周期】这两个接口
        在这里插入图片描述
        在这里插入图片描述
        • FutureTask的两个构造方法:FutureTask 常用来封装 Callable 和 Runnable,可以作为一个任务提交到线程池中执行。除了作为一个独立的类之外,也提供了一些功能性函数供我们创建自定义 task 类使用。
          在这里插入图片描述
        • FutureTask 线程安全由 CAS 来保证。
          在这里插入图片描述
        • Future提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。
          在这里插入图片描述
      • FutureTask 就是Runnable和Future的结合体,我们可以把Runnable看作生产者, Future 看作消费者。而FutureTask 是被这两者共享的,生产者Runnable运行run方法计算结果,消费者Future 通过get方法获取结果
        • 生产者消费者模式,如果生产者数据还没准备的时候,消费者会被阻塞。当生产者数据准备好了以后会唤醒消费者继续执行。
        • FutureTask内部维护了任务状态state
          在这里插入图片描述
        • 生产者run方法:
          在这里插入图片描述
        • 消费者的get方法
          在这里插入图片描述

胡&敏:怎么除了有个run()方法,还有个call()方法,你得给我解释一下:
author:其实就是个方法嘛,(之前咱们知道要是实现接口或者说继承抽象方法时,必须重写其中的抽象方法),我就拿call()方法举例吧。咱们就看看人家call()方法的源码,看看人家方法使用时有啥特性就行,该对应的就给人家对应上就行了呗。你看这里的call()方法的接口的泛型和方法的返回值类型一致
在这里插入图片描述
咱们的使用场景就像下面这样:
在这里插入图片描述
Notes:call()方法还有个特异功能,可以缓存结果提高程序执行效率,上图:
在这里插入图片描述

🕴创建线程的三种方式稍微对比总结一下:

  • 使用继承Thread类的方式创建多线程
    • 如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
    • 缺点是上面图里面的【线程类已经继承了Thread类,所以不能再继承其他父类。】
  • 采用实现Runnable&Callable接口的方式创建多线程
    • 优势:
      • 线程类只是实现了Runnable接口(如果任务不需要返回结果或抛出异常推荐使用 Runnable接口,这样代码看起来会更加简洁)或Callable接口(Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Callable 接口可以返回结果或抛出检查异常),还可以继承其他类,非常适合多个相同线程来处理同一份资源的情况
        • 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
    • 缺点
      • 就是如果要访问当前线程,则必须使用Thread.currentThread()方法。
    • 总结:
      • 两种创建线程方式本质上是一样的,它们的不同点仅仅在于实现线程运行内容的不同。运行内容主要来自于两个地方,要么来自于 target,要么来自于重写的 run() 方法,在此基础上我们进行拓展,可以这样描述:本质上,实现线程只有一种方式,而要想实现线程执行的内容,却有两种方式,也就是可以通过 实现 Runnable 接口的方式,或是继承 Thread 类重写 run() 方法的方式,把我们想要执行的代码传入,让线程去执行,在此基础上,如果我们还想有更多实现线程的方式,比如线程池和 Timer 定时器,只需要在此基础上进行封装即可
      • 实现 Runnable 接口比继承 Thread 类实现线程要好:
        • 从代码的架构考虑,实际上,Runnable 里只有一个 run() 方法,它定义了需要执行的内容,在这种情况下,实现了 Runnable 与 Thread 类的解耦,Thread 类负责线程启动和属性设置等内容,权责分明
        • 第二点就是在某些情况下可以提高性能,使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程,执行完任务后线程走到生命周期的尽头被销毁,如果还想执行这个任务,就必须再新建一个继承了 Thread 类的类,如果此时执行的内容比较少,比如只是在 run() 方法里简单打印一行文字,那么它所带来的开销并不大,相比于整个线程从开始创建到执行完毕被销毁,这一系列的操作比 run() 方法打印文字本身带来的开销要大得多,相当于捡了芝麻丢了西瓜,得不偿失。如果我们 使用实现 Runnable 接口的方式,就可以把任务直接传入线程池,使用一些固定的线程来完成任务,不需要每次新建销毁线程,大大降低了性能开销
        • 第三点好处在于 Java 语言不支持双继承,如果我们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其他的类,这样一来,如果未来这个类需要继承其他类实现一些功能上的拓展,它就没有办法做到了,相当于限制了代码未来的可拓展性。
    • Runnable和Callable的区别
      • Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()
      • Callable的任务执行后可返回值,而Runnable的任务是不能返回值的
      • Call方法可以抛出异常,run方法不可以。(Runnable 接口不会返回结果或抛出检查异常
      • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
      • 工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。
        Executors.callable(Runnable task)
        Executors.callable(Runnable task,Object resule))
        
    • Callable 和 Future 的区别:
      • Callable 用于产生结果
      • Future 用于获取结果
        • 如果是对多个任务多次自由串行、或并行组合,涉及多个线程之间同步阻塞获取结果,Future 代码实现会比较繁琐,需要我们手动处理各个交叉点,很容易出错。

胡&敏:中中中,大概按照你这三道程序和图片,以后也能自己用了。不过人家老是说启动线程启动线程,这到底是什么意思。
author:上图
在这里插入图片描述
在这里插入图片描述

后面的几集的内容大概就是这些:
在这里插入图片描述
明天同一时间,再见,拜拜~

巨人的肩膀:
Java并发编程之美
深入理解Java虚拟机

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值