原创

09.Android之多线程


目录介绍

  • 9.0.0.1 Android中还了解哪些方便线程切换的类,AsyncTask相比Handler有什么优点?
  • 9.0.0.2 说一说Android中的多线程?注意,这个问题问的范围很大,需要一步步有条理性地说出自己的理解。
  • 9.0.0.3 线程池的好处、原理、类型?ThreadPoolExecutor的工作策略?
  • 9.0.0.4 IntentService是什么?IntentService的特点?为何不用bindService方式创建IntentService?
  • 9.0.0.5 AsyncTask中使用的线程池大小?使用AsyncTask需要注意什么?
  • 9.0.0.7 HandlerThread是什么?HandlerThread有什么特点?平时那些地方用到了这个?
  • 9.0.0.8 线程池的执行流程是怎样的?线程池有哪些类型,各自用途是什么?线程池的使用技巧有哪些?
  • 9.0.1.0 重入锁是什么?可重入锁的用途是什么?死锁是什么?可能出现死锁的场景有哪些?
  • 9.0.1.1 Java的堆内存是什么? Java中的局部变量、方法定义的参数是否会被线程所共享?Java内存模型的作用?
  • 9.0.1.2 什么是阻塞队列?阻塞队列(BlockingQueue)核心方法?Java中7种阻塞队列的要点有哪些?
  • 9.0.1.3 现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

好消息

  • 博客笔记大汇总【15年10月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计500篇[近100万字],将会陆续发表到网上,转载请注明出处,谢谢!
  • 链接地址:https://github.com/yangchong211/YCBlogs
  • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

9.0.0.1 Android中还了解哪些方便线程切换的类,AsyncTask相比Handler有什么优点?

  • 线程切换的类
    • AsyncTask:底层封装了线程池和Handler,便于执行后台任务以及在子线程中进行UI操作。
    • HandlerThread:一种具有消息循环的线程,其内部可使用Handler。
    • IntentService:是一种异步、会自动停止的服务,内部采用HandlerThread。
  • Handler机制存在的问题
    • 多任务同时执行时不易精确控制线程
  • AsyncTask优势
    • 创建异步任务更简单,直接继承它可方便实现后台异步任务的执行和进度的回调更新UI,而无需编写任务线程和Handler实例就能完成相同的任务。
  • AsyncTask源码分析

9.0.0.2 说一说Android中的多线程?注意,这个问题问的范围很大,需要一步步有条理性地说出自己的理解。

  • 01.Android中的线程
    • 主线程(有的也成UI线程)
      • 在Android当中, 当应用启动的时候,系统会给应用分配一个进程,顺便一提,大部分应用都是单进程的,不过也可以通过设置来使不同组件运行在不同的进程中, 在创建进程的同时会创建一个线程, 应用的大部分操作都会在这个线程中运行, 所以称为主线程, 同时所有的UI控件相关的操作也要求在这个线程中操作, 所以也称为UI线程.
    • 为何会有子线程
      • 因为所有的UI控件的操作都在UI线程中执行,如果在UI线程中执行耗时操作,例如网络请求等,就会阻塞UI线程,导致系统报ANR(Application Not Response)错误。因此对于耗时操作需要创建工作线程来执行而不能直接在UI线程中执行. 这样就需要在应用中使用多线程, 但是Android提供的UI工具包并不是线程安全的, 也就是说不能直接在工作线程中访问UI控件, 否则会导致不能预测的问题, 因此需要额外的机制来进行线程交互, 主要是让其他线程可以访问UI线程.
  • 02.线程交互之Handler机制
    • 在Android当中, 工作线程主要通过Handler机制来访问UI线程. 当然还有一些封装好的类例如AsyncTask可以使用, 但是本质仍是使用Handler.
    • Handler机制主要由4部分组成, Looper, 消息队列, 消息类和Handler组成, 其中Looper和消息队列是和线程绑定的, 每个线程只会有一个Looper和一个消息队列, 当Looper启动时, 它会无限循环尝试从消息队列中获取消息实例, 如果没有消息则会阻塞等待. 当Handler发送消息时会把消息实例放入消息队列中, Looper从中取得消息实例然后就会调用Handler的相关方法, 因为Looper是线程绑定的, 如果绑定的是UI线程, 那么此时Handler的方法就会在UI线程中得到执行, 线程间就是这样进行交互的.
  • 03.线程切换的类
    • AsyncTask:底层封装了线程池和Handler,便于执行后台任务以及在子线程中进行UI操作。
    • HandlerThread:一种具有消息循环的线程,其内部可使用Handler。
    • IntentService:是一种异步、会自动停止的服务,内部采用HandlerThread。
    • Handler+Thread:这种很常见
  • 04.java中的多线程
    • 而Handler机制的底层实现则是使用java多线程相关的类.
      • java当中主要使用Thread和Executor来实现多线程. Thread用于直接创建线程, 在Android中也可以直接使用这个类, Looper中就包含一个Thread实例. Executor是一个接口, 大部分java中自带的实现都使用了线程池来管理多线程.
  • 05.线程池管理多线程
    • 因为在系统中创建线程是一个比较耗费资源的事, 所以不能频繁创建和释放线程, 因此在效率上考虑通常会使用线程池, 同时也便于线程的管理. Android中的AsyncTask就使用了线程池。
  • 06.Handler和AsyncTask
    • Handler机制存在的问题
      • 多任务同时执行时不易精确控制线程
    • AsyncTask优势
      • 创建异步任务更简单,直接继承它可方便实现后台异步任务的执行和进度的回调更新UI,而无需编写任务线程和Handler实例就能完成相同的任务。
  • 07.线程安全问题
    • 线程安全问题
      • 使用多线程时需要注意的是线程安全的问题, 因为同一进程中的线程可以共享内存, 虽然这种方式效率很高, 但是会导致线程干扰和内存一致性的问题。
    • 锁机制
      • 解决这些问题的主要方法是使用Synchronized关键字来加锁. 基本原理就是线程要对对象进行操作前需要先获取锁, 如果一个线程正在操作某个对象, 那么它就会持有相应的锁, 后来的线程想要操作这个对象, 只能等待前面的线程释放锁之后才有机会获取锁并进行操作.

9.0.0.3 线程池的好处、原理、类型?ThreadPoolExecutor的工作策略?

  • 线程池的分类
    • FixThreadPool:线程数量固定的线程池,所有线程都是核心线程,当线程空闲时不会被回收;能快速响应外界请求。
    • CachedThreadPool:线程数量不定的线程池(最大线程数为Integer.MAX_VALUE),只有非核心线程,空闲线程有超时机制,超时回收;适合于执行大量的耗时较少的任务
    • ScheduledThreadPool:核心线程数量固定,非核心线程数量不定;可进行定时任务和固定周期的任务。
    • SingleThreadExecutor:只有一个核心线程,可确保所有的任务都在同一个线程中按顺序执行;好处是无需处理线程同步问题。
  • 线程池的好处
    • 重用线程池中的线程,避免线程的创建和销毁带来的性能消耗;
    • 有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致阻塞现象;
    • 进行线程管理,提供定时/循环间隔执行等功能
  • 线程池的创建参数
    • corePoolSize核心线程数:一般会在线程中一直存活
    • maximumPoolSize最大线程数:当活动线程数达到这个数值后,后续的任务将会被阻塞
    • keepAliveTime非核心线程超时时间:超过这个时长,闲置的非核心线程就会被回收
    • unit:用于指定keepAliveTime参数的时间单位
    • workQueue任务队列:通过线程池的execute()方法提交的Runnable对象会存储在这个参数中。
    • threadFactory:线程工厂,可创建新线程
    • handler:在线程池无法执行新任务时进行调度
  • ThreadPoolExecutor的工作策略
    • 若程池中的线程数量未达到核心线程数,则会直接启动一个核心线程执行任务。
    • 若线程池中的线程数量已达到或者超过核心线程数量,则任务会被插入到任务列表等待执行。
    • 若任务无法插入到任务列表中,往往由于任务列表已满,此时如果
      • 线程数量未达到线程池最大线程数,则会启动一个非核心线程执行任务;
      • 线程数量已达到线程池规定的最大值,则拒绝执行此任务,ThreadPoolExecutor会调用RejectedExecutionHandler的rejectedExecution方法来通知调用者。

9.0.0.4 IntentService是什么?IntentService的特点?为何不用bindService方式创建IntentService?

  • IntentService是什么?
    • 是一种特殊Service
    • 为抽象类,必须创建子类才可以使用IntentService
    • 可用于执行后台耗时的任务,任务执行后会自动停止
    • 具有高优先级(服务的原因),优先级比单纯的线程高很多,适合高优先级的后台任务,且不容易被系统杀死。
    • 封装了HandlerThread和Handler
  • IntentService的特点
    • 和普通线程和普通Service相比,它不同于线程,IntentService是服务,优先级比线程高,更不容易被系统杀死,因此较适合执行一些高优先级的后台任务;不同于普通Service,IntentService可自动创建子线程来执行任务,且任务执行完毕后自动退出。
  • 为何不用bindService方式创建IntentService
    • IntentService的工作原理是,在IntentService的onCreate()里会创建一个HandlerThread,并利用其内部的Looper实例化一个ServiceHandler对象;而这个ServiceHandler用于处理消息的handleMessage()方法会去调用IntentService的onHandleIntent(),这也是为什么可在该方法中处理后台任务的逻辑;当有Intent任务请求时会把Intent封装到Message,然后ServiceHandler会把消息发送出,而发送消息是在onStartCommand()完成的,只能通过startService()才可走该生命周期方法,因此不能通过bindService创建IntentService。
    • 这里可以回顾一下service两种启动方式的生命周期:
    • context.startService() ->onCreate()- >onStartCommand()->Service running--调用context.stopService() ->onDestroy()
    • context.bindService()->onCreate()->onBind()->Service running--调用>onUnbind() -> onDestroy()
  • 关于IntentService源码分析

9.0.0.5 AsyncTask中使用的线程池大小?使用AsyncTask需要注意什么?AsyncTask的4个核心方法的作用?

  • AsyncTask中使用的线程池大小
    • 在AsyncTask内部实现有两个线程池:
    • SerialExecutor:用于任务的排队,默认是串行的线程池,在3.0以前核心线程数为5、线程池大小为128,而3.0以后变为同一时间只能处理一个任务
    • THREAD_POOL_EXECUTOR:用于真正执行任务。
  • 使用AsyncTask需要注意什么
    • 不要直接调用onPreExecute()、doInBackground()、onProgressUpdate()、onPostExecute()和onCancelled()方法
    • 一个异步对象只能调用一次execute()方法
  • AsyncTask的4个核心方法的作用?
    • 具有4个核心方法:onPreExecute()、doInBackground(Params… params)、onProgresUpdate(Progress… value)、onPostExecute(Result result)
    • onPreExecute(): 主线程中,任务前的准备工作(对UI进行一些标记等)
    • doInBackground():线程池中执行,在onPreExecute后执行耗时操作。过程中可以调用publishProgress更新进度
    • onProgresUpdate: 主线程;在doInBackground中调用publishProgress后,会调用该方法,会在UI上更新进度。
    • onPostExecute: 主线程中,任务执行完成后的收尾工作,result参数就是doInBackground最后返回的值
  • AysncTask的缺点
    • 生命周期:Activity中的AsyncTask不会随Acitivity的销毁而销毁。AsyncTask会一直运行到doInBackground()方法执行完毕,然后会执行onPostExecute()方法。如果Acitivity销毁时,没有执行onCancelled(),AysncTask在结束后操作UI时出现崩溃
    • 内存泄漏:如果AsyncTask被声明为Acitivity的非静态的内部类,会拥有Activity的引用。在Acitivity已经被销毁后,AsyncTask后台线程仍在执行,则会导致Acitivity无法被回收,造成内存泄漏。
    • 结果丢失: 屏幕旋转或Acitivity在后台被系统杀掉等情况下,Acitivity会重新创建。之前运行的AsyncTask持有的Acitivity引用会失效,导致更新UI的操作无效。
    • 并行还是串行:1.6之前,AsyncTask是串行的;在1.6至2.3版本,AsyncTask是并行的;在3.0及以上版本中,AsyncTask支持串行和并行(默认串行)-execute()方法就是串行执行,executeOnExecutor(Executor)就是并发执行

9.0.0.7 HandlerThread是什么?HandlerThread有什么特点?

  • HandlerThread是什么?
    • Thread中如果要使用Handler需要创建Looper等操作,比较繁琐。
    • HandlerThread就是内部创建了Looper的Thread.
    • 内部具有队列,任务会串行处理。(如果一个任务执行时间过长,会阻塞后续任务)
    • 执行任务:外界需要通过Hanlder的消息方式来通知HandlerThread来执行一个具体任务
    • 退出:HandlerThread的run方法是无限循环执行的,需要通过HandlerThread的quit或quitSafely方法来终止线程的执行
    • 使用场景:HandlerThread典型使用场景是IntentService
  • HandlerThread有什么特点
    • HandlerThread是一个线程类,它继承自Thread。与普通Thread不同,HandlerThread具有消息循环的效果,这是因为它内部HandlerThread.run()方法中有Looper,能通过Looper.prepare()来创建消息队列,并通过Looper.loop()来开启消息循环。

9.0.0.8 线程池的执行流程是怎样的?线程池有哪些类型,各自用途是什么?线程池的使用技巧有哪些?

  • 线程池的执行流程是怎样的?
    • 大概的流程图如下
      • image
    • 文字描述如下
      • ①如果在线程池中的线程数量没有达到核心的线程数量,这时候就回启动一个核心线程来执行任务。
      • ②如果线程池中的线程数量已经超过核心线程数,这时候任务就会被插入到任务队列中排队等待执行。
      • ③由于任务队列已满,无法将任务插入到任务队列中。这个时候如果线程池中的线程数量没有达到线程池所设定的最大值,那么这时候就会立即启动一个非核心线程来执行任务。
      • ④如果线程池中的数量达到了所规定的最大值,那么就会拒绝执行此任务,这时候就会调用RejectedExecutionHandler中的rejectedExecution方法来通知调用者。博客
  • newFixedThreadPool
    • 通过Executors中的newFixedThreadPool方法来创建,该线程池是一种线程数量固定的线程池。
      ExecutorService service = Executors.newFixedThreadPool(4);
      
    • 在这个线程池中 所容纳最大的线程数就是我们设置的核心线程数。
      • 如果线程池的线程处于空闲状态的话,它们并不会被回收,除非是这个线程池被关闭。如果所有的线程都处于活动状态的话,新任务就会处于等待状态,直到有线程空闲出来。
      • 由于newFixedThreadPool只有核心线程,并且这些线程都不会被回收,也就是它能够更快速的响应外界请求
    • 从下面的newFixedThreadPool方法的实现可以看出,newFixedThreadPool只有核心线程,并且不存在超时机制,采用LinkedBlockingQueue,所以对于任务队列的大小也是没有限制的。
      public static ExecutorService newFixedThreadPool(int nThreads) {
      	return new ThreadPoolExecutor(nThreads, nThreads,
      		0L, TimeUnit.MILLISECONDS,
      		new LinkedBlockingQueue<Runnable>());
      }
      
  • newCachedThreadPool
    • 通过Executors中的newCachedThreadPool方法来创建。博客
      public static ExecutorService newCachedThreadPool() {
      	return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
      		60L, TimeUnit.SECONDS,
      		new SynchronousQueue<Runnable>());
      }
      
    • 通过s上面的newCachedThreadPool方法在这里我们可以看出它的 核心线程数为0, 线程池的最大线程数Integer.MAX_VALUE。而Integer.MAX_VALUE是一个很大的数,也差不多可以说 这个线程池中的最大线程数可以任意大。
      • 当线程池中的线程都处于活动状态的时候,线程池就会创建一个新的线程来处理任务。该线程池中的线程超时时长为60秒,所以当线程处于闲置状态超过60秒的时候便会被回收。
      • 这也就意味着若是整个线程池的线程都处于闲置状态超过60秒以后,在newCachedThreadPool线程池中是不存在任何线程的,所以这时候它几乎不占用任何的系统资源。
      • 对于newCachedThreadPool他的任务队列采用的是SynchronousQueue,上面说到在SynchronousQueue内部没有任何容量的阻塞队列。SynchronousQueue内部相当于一个空集合,我们无法将一个任务插入到SynchronousQueue中。所以说在线程池中如果现有线程无法接收任务,将会创建新的线程来执行任务。
  • newScheduledThreadPool
    • 通过Executors中的newScheduledThreadPool方法来创建。
      public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
          return new ScheduledThreadPoolExecutor(corePoolSize);
      }
      public ScheduledThreadPoolExecutor(int corePoolSize) {
          super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
                new DelayedWorkQueue());
      }
      
    • 它的核心线程数是固定的,对于非核心线程几乎可以说是没有限制的,并且当非核心线程处于限制状态的时候就会立即被回收。
      • 创建一个可定时执行或周期执行任务的线程池:博客
      ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
      service.schedule(new Runnable() {
      	public void run() {
      		System.out.println(Thread.currentThread().getName()+"延迟三秒执行");
      	}
      }, 3, TimeUnit.SECONDS);
      service.scheduleAtFixedRate(new Runnable() {
      	public void run() {
      		System.out.println(Thread.currentThread().getName()+"延迟三秒后每隔2秒执行");
      	}
      }, 3, 2, TimeUnit.SECONDS);
      
      • 输出结果:

        pool-1-thread-2延迟三秒后每隔2秒执行
        pool-1-thread-1延迟三秒执行
        pool-1-thread-1延迟三秒后每隔2秒执行
        pool-1-thread-2延迟三秒后每隔2秒执行
        pool-1-thread-2延迟三秒后每隔2秒执行

  • newSingleThreadExecutor
    • 通过Executors中的newSingleThreadExecutor方法来创建,在这个线程池中只有一个核心线程,对于任务队列没有大小限制,也就意味着这一个任务处于活动状态时,其他任务都会在任务队列中排队等候依次执行
    • newSingleThreadExecutor将所有的外界任务统一到一个线程中支持,所以在这个任务执行之间我们不需要处理线程同步的问题。博客
      public static ExecutorService newSingleThreadExecutor() {
      	return new FinalizableDelegatedExecutorService
      	(new ThreadPoolExecutor(1, 1,
      		0L, TimeUnit.MILLISECONDS,
      		new LinkedBlockingQueue<Runnable>()));
      }
      
  • 线程池的使用技巧
    • 需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为CPU密集型任务、IO密集型任务和混合型任务。(N代表CPU个数) | 任务类别 | 说明 | | ------ | ----------- | | CPU密集型任务 | 线程池中线程个数应尽量少,如配置N+1个线程的线程池。| | IO密集型任务 | 由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率,如2*N。| | 混合型任务 | 可以拆分为CPU密集型任务和IO密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。 |

9.0.1.0 重入锁是什么?可重入锁的用途是什么?死锁是什么?可能出现死锁的场景有哪些?

  • 重入锁是什么?
    • 重入锁ReentrantLock在Java SE 5.0引入
    • 该锁支持一个线程对资源的重复加锁
    • 一个线程在锁住锁对象后,其他任何线程都无法进入Lock语句
      val mLock = ReentrantLock()
      mLock.lock()
      try {
        //需要同步的操作
      }finally {
        mLock.unlock() //finally中进行解锁,避免死锁问题
      }
      
  • 可重入锁的用途是什么?
    • 阻塞队列就是使用ReentrantLock实现的。
  • 死锁是什么?
    • 死锁是指两个或者两个以上线程/进程进行资源竞争时出现了阻塞现象,如果没有外力帮助,它们都将无法继续工作下去,此时系统处于死锁状态。
  • 可能出现死锁的场景有哪些?
    • 可重入锁ReentrantLock在mLock.lock()后如果出现异常,但是没有在try-catch的finally中没有执行mLock.unLock()就会导致死锁。
    • notify比notifyAll更容易出现死锁

9.0.1.1 Java的堆内存是什么? Java中的局部变量、方法定义的参数是否会被线程所共享?Java内存模型的作用?

  • Java的堆内存是什么?
    • 堆内存用于存储对象实例
    • 堆内存被所有线程所共享: 会存在内存可见性问题
  • Java中的局部变量、方法定义的参数是否会被线程所共享?
    • 局部变量、方法定义的参数则不会在线程间共享:不存在内存可见性问题,也不会受到内存模型影响
  • Java内存模型的作用?
    • Java内存模型控制线程之间的通信,决定了一个线程对共享内存的写入何时对另一个线程可见。
    • 定义了线程和主存之间的抽象关系:
      • 线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存,本地内存中存储了该该线程贡献变量的副本,
      • 本地内存是Java内存模型中的抽象概念,实际上并不存在,覆盖了缓存、写缓存区、寄存器等区域。
  • 线程间共享变量的通信需要满足的步骤?
    • A、B线程之间数据通信,需要满足两个步骤:
        1. 第一步线程A将本地更新过的共享数据刷新到主存中;
        1. 第二步线程B到主存中读取已经刷新的最新共享数据
  • 可见性是什么?
    • 可见性是指线程修改的状态能否立即对另一个线程可见
    • volatile修饰的变量,在发生变化后会立即更新到主存,已保证共享数据的可见性

9.0.1.2 什么是阻塞队列?阻塞队列(BlockingQueue)核心方法?Java中7种阻塞队列的要点有哪些?

  • 什么是阻塞队列
    • 阻塞队列常应用于生产者-消费者模型
    • 阻塞队列需要满足1:队列中没有数据时,消费者端的所有线程全部自动阻塞(挂起)
    • 阻塞队列需要满足2:队列中填满数据时,生产者端的所有线程都自动阻塞
  • 阻塞队列(BlockingQueue)核心方法?
    • 放入数据:
      • offer(object),可以存入,返回true;不可以存入,返回false。该方法不会阻塞当前线程
      • put(Object), 阻塞队列有空间,则存入;没有空间,当前线程阻塞,直至阻塞队列有空间存放数据。
    • 获取数据:
      • poll(time):从阻塞队列中将首位对象取出。若不能取出,再等待time的时间。取不到就返回null
      • take():取走队列中首位数据。若队列为空,则当前线程阻塞,直到队列中有数据,并且返回该数据。
      • drainTo(): 一次性取走所有可用数据。无需多次加锁操作,提高效率。
  • Java中7种阻塞队列的要点有哪些?
    • image
  • 阻塞队列ArrayBlockingQueue实现原理
    • 内部维护一个Object类型的数组
    • lock所采用的可重入锁(ReentrantLock)

9.0.1.3 现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

  • 考察对Join是否熟悉:父线程会等待子线程运行结束
  • 代码如下所示
    /**
     * 现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
     */
    private void test2(){
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d("线程执行","Thread1");
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d("线程执行","Thread2");
            }
        });
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d("线程执行","Thread3");
            }
        });
    
        t1.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        t2.start();
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        t3.start();
        try {
            t3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
  • join方法的作用?
    • Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。

关于其他内容介绍

01.关于博客汇总链接

02.关于我的博客

  • 我的个人站点:www.yczbj.org, www.ycbjie.cn
  • github:https://github.com/yangchong211
  • 知乎:https://www.zhihu.com/people/yczbj/activities
  • 简书:http://www.jianshu.com/u/b7b2c6ed9284
  • csdn:http://my.csdn.net/m0_37700275
  • 喜马拉雅听书:http://www.ximalaya.com/zhubo/71989305/
  • 开源中国:https://my.oschina.net/zbj1618/blog
  • 泡在网上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1
  • 邮箱:yangchong211@163.com
  • 阿里云博客:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
  • segmentfault头条:https://segmentfault.com/u/xiangjianyu/articles
  • 掘金:https://juejin.im/user/5939433efe88c2006afa0c6e

评论

留言