1.线程的生命周期?线程有几种状态
- 线程通常有5种状态:创建、就绪、运行、阻塞和死亡状态
- 阻塞的情况又分为三种:
- 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入等待池中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或者notifyAll()方法才能被唤醒,wait()是Object类中的方法
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中
- 其他阻塞:运行的线程执行sleep()或者join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep()时Thread类的方法
- 新建状态(New):新创建了一个线程对象
- 就绪状态(Runable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待CPU的使用权
- 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行,直到线程进入就绪状态,才有机会转到运行状态
- 死亡状态(Dead):线程执行完了或者因异常退出run()方法,该线程结束生命周期
2.sleep()和wait()的异同
-
相同点:
- 一旦执行方法,都可以使线程进入阻塞状态
- 两个方法都需要捕获异常
-
不同点:
-
两个方法的声明位置不同:sleep()是Thread类中的静态本地方法;wait()是Object的本地方法
-
调用的要求不同:sleep()可以在任何需要的场景下调用;wait()必须在同步代码块或者同步方法中调用
-
关于是否释放同步监视器:如果两个方法都使用在同步代码块或者同步方法中,sleep()不会释放锁,而是会把锁带着进入冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁,也就是说无法执行程序;wait()会释放锁,并加入等待队列中
-
sleep()不需要被唤醒(休眠之后退出阻塞);但wait()需要(不指定时间需要被别人打断)
-
唤醒条件不同:sleep需要等到休眠超时或者调用了interrupt()方法;wait需要其他线程调用对象的notify()或者notifyAll()方法
注意:设置了超时时间的wait方法一旦过了超时时间,并不需要其他线程执行notify也能自动解除阻塞,但是如果没设置超时时间的wait方法必须等待其他线程执行notify
-
sleep()一般用于当前线程的休眠;而wait()则多用于多线程之间的通信
-
3.join()和yield()
- yield():线程让步
- 调用yield()方法会让当前线程交出CPU资源,让CPU去执行其他的线程。但是,yield()不能控制具体的交出CPU的时间。需要注意的是,yield()方法只能让拥有相同优先级的线程有获取 CPU 执行时间的机会
- 调用yield()方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新得到 CPU 的执行;
- 它同样不会释放锁
- join():
- 当前线程调用其他线程的join方法,会阻塞当前线程,直到其他线程执行完毕,才会进入就绪状态。
- 低优先级的线程也可以获得执行
4.线程的创建
创建线程的四种方式
-
继承 Thread 类,重写 run() 方法;
-
实现 Runnable 接口,实现 run() 方法,并将 Runnable 实现类的实例作为 Thread 构造函数的参数 target;
-
实现 Callable 接口,实现 call() 方法,然后通过 FutureTask 包装器来创建 Thread 线程;
-
通过 ThreadPoolExecutor 创建线程池,并从线程池中获取线程用于执行任务
四种方式的异同:
- 第①②种方式无法获取线程的执行结果,因为通过重写的 run() 方法的返回值是void
- 第③种方式可以获取线程的执行结果,因为通过 Callable 接口的 call() 方法的返回值是 Object,可以将返回的结果可以放在 Object 对象中
- 第④种方式对于两种情况都支持,具体取决于任务的类型,有返回值的任务必须实现 Callable 接口,无返回值的任务必须实现 Runnable 接口
实现 Runnable 接口比继承 Thread 类所具有的优势主要有:
- 可以避免 JAVA 中单继承的限制;
- 线程池只能放入实现 Runable 或 Callable类线程,不能直接放入继承 Thread 的类
- 代码可以被多个线程共享,代码和数据独立,适合多个相同的程序代码的线程去处理同一个资源的情况
5.对守护线程的理解
守护线程:为所有非守护线程提供服务的线程,任何一个守护线程都是整个JVM中所有非守护线程的保姆
守护线程依赖整个进程而运行,所有的其他线程结束时,它也就没有执行的必要了,程序结束了,守护线程也就直接中断
注意:由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它,因为它会在任何时候甚至在一个操作的中间发生中断
守护线程的作用是什么?
-
举例,GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不再生产垃圾,垃圾回收器也就无事可做,所有当垃圾回收线程是JVM上仅存的线程时,垃圾回收线程会自动离开,它始终在低级别的状态中运行,用于监控和管理系统中可回收的资源
-
应用场景:
- 来为其他线程提供服务支持的情况
- 或者任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程;反之,如果一个正在执行某个操作的线程必须要正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户现场。通常都是些关键的事务,比方说:数据库的录入或更新,这些操作都是不能中断的
thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个Illegalthreadstateexception异常,不能把正在运行的常规线程设置为守护线程
在Daemon线程中产生的新线程也是Daemon的
6.程序、进程、线程
- 程序:是为了完成特定任务、用某种语言编写的一组指令的合集。即一段静态的代码,静态对象
- 进程:是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。—生命周期
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位:系统在运行时会为每个进程分配不懂得内存区域
- 线程:进程可以进一步细分为线程,是一个程序内部得一条执行路径
- 若是一个进程同一时间并行执行多个线程,就是支持多线程得
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间—>>>它们从同一堆中分配对象,可以
访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资
源可能就会带来安全的隐患。
7.并发、并行、串行
- 并发:允许多个个任务彼此干扰。同一时间点,只有一个任务运行,交替执行(任务可以执行一半)
- 串行:在时间上不可能发生重叠,前一个任务没搞定,下一个任务只能等待(强调任务完成)
- 并行:在时间上是重叠的,多个任务在同一时刻互不干扰的同时执行
8.并发的三大特性
- 原子性
- 可见性
- 有序性
9.线程池的优势
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁的消耗
- 提高响应速度。当任务到达时,任务可以不需要等线程创建就能立即使用
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
10.线程池的7大参数
- corePoolSize:线程池中的常驻核心线程数
- 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
- 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
- maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于或等于1
- keepAliveTime:多余的空闲线程的存活时间
- 当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
- 默认情况下:只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize
- unit:keepAliveTime的单位
- workQueue:任务队列,被提交但尚未被执行的任务
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可
- handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)
11.线程池的工作原理
- 在创建线程池后,等待提交过来的任务请求
- 当调用execute()方法添加一个请求任务时,线程池会做如下判断
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程大于或者等于corePoolSize,那么将这个任务放入队列
- 如果队列满了且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务(而不是先去运行队列中的任务)
- 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
- 当一个线程完成任务是,它会从队列中取下一个任务来执行
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就会被停掉,所以线程池中的所有任务完成之后它最终会收缩到corePoolSize的大小
12.线程池的4种拒绝策略
都实现了RejectedExecutionHandler接口
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止程序正常运行
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果任务允许丢失,这是最好的一种方案
- DiscardOldestPolicy:抛弃队列中等待时间最久的任务,然后把当前任务加入队列种尝试再次提交当前任务
- CallerRunsPolicy:既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者。例如,时main调用的,则用main线程执行该任务
13.ThreadLocal
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
- ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景
14.ThreadLocal与Thread,ThreadLocalMap之间的关系
-
ThreadLocalMap其实是Thread线程的一个属性,而ThreadLocal是维护ThreadLocalMap这个属性的一个工具类
-
每一个Thread线程内部都有一个Map(ThreadLocalMap)
-
Map里面存储了ThreadLocal对象(key)和线程的变量副本(value)
-
Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值
-
对于不同的线程,每次获取副本值时,别的线程并不能获取当前线程的副本中,从而形成了副本的隔离,互不干扰
15.ThreadLocal与Synchronized的区别
ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问
但是ThreadLocal与synchronized有本质的区别
- Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离
- Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享
16.ThreadLocal的内存泄露
内存泄露跟OOM(内存溢出)不同:内存泄露是指程序种已动态分配的堆由于程序的某种原因未释放或者无法释放,造成内存资源的浪费,导致程序运行的速度变慢甚至崩溃等一系列严重的后果
- 因为ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉
- 所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value
- 所以在不需要或者使用结束,需要将ThreadLocal内的变量删除(remove)或替换
- 否则它的生命周期将会与线程共存。而通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致