一.线程
概念:
进程是程序运行资源分配的最小单位。
线程是CPU调度的最小单位,必须依赖于进程而存在。
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。
线程生命周期:
在Java JDK类库中的Thread类里,定义了7种状态。
New:新建状态
Ready:就绪状态
Running:运行状态
Terminated:终止状态
Waiting:等待状态
TimedWaiting:超时等待状态
Blocked:阻塞状态
线程实现
实现方式
继承Thread覆盖run()方法。
实现Runnable接口,实现run()方法。
实现Callable接口,实现call()方法。
之间的区别:
Thread类与Runnable接口区别:
Thread:每个线程都独立,不共享资源。
Runnable:存在线程共享概念。
Runnable和Callable的区别:
对比 | Runnable | Callable |
---|---|---|
方法返回值 | 没有返回值 | 有返回值 |
异常 | 没办法处理受检异常 | 可以处理受检异常 |
在Thread类中使用 | 可以 | 不可以 |
在ExecutorService中使用 | 可以 | 可以 |
Java 中用到的线程调度算法
分时调度模型和抢占式调度模型
分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。
Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。
与线程同步以及线程调度相关的方法
(1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
(3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
(4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
sleep() 和 wait() 有区别
两者都可以暂停线程的执行
- 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
- 是否释放锁:sleep() 不释放锁;wait() 释放锁。
- 用途不同:wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
- 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
注意:
wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用
通过“共享变量”实现线程间数据共享
Java 如何实现多线程之间的通讯和协作?
可以通过中断 和 共享变量的方式实现线程间的通讯和协作。
比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。
Java中线程通信协作的最常见的两种方式:
一.syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
二.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
线程间直接的数据交换:
三.通过管道进行线程间通信:1)字节流;2)字符流
什么是线程同步和线程互斥,有哪几种实现方式?
线程间的同步方法
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
实现线程同步的方法
- 同步代码方法:sychronized 关键字修饰的方法
- 同步代码块:sychronized 关键字修饰的代码块
- 使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
- 使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义
什么叫线程安全?servlet 是线程安全吗?
线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。
SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。
Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。
怎么保证多线程的运行安全
- 方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
- 方法二:使用自动锁 synchronized。
- 方法三:使用手动锁 Lock。
二.线程池
生产者消费者模式
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,所以便有了生产者和消费者模式。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
这个阻塞队列就是用来给生产者和消费者解耦的。纵观大多数设计模式,都会找一个第三者出来进行解耦,如工厂模式的第三者是工厂类,模板模式的第三者是模板类。在学习一些设计模式的过程中,如果先找到这个模式的第三者,能帮助我们快速熟悉一个设计模式。
Executors
让我们再看看Executors提供的那几个工厂方法。
newSingleThreadExecutor
创建一个单线程的线程池。
这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。
此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
new ThreadPoolExecutor(1, 1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>())
newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
newCachedThreadPool
创建一个可缓存的线程池。
如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。
此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
corePoolSize,Integer.MAX_VALUE, 0,DelayedWorkQueue
new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
ThreadPoolExecutor
Executors 各个方法的弊端:
- newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
- newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
submit和execute区别
方法名 | 任务接口 | 是否有返回值 | 向外层调用者抛出异常 |
---|---|---|---|
submit | Callable和Runnable | 有(Future) | 能抛出异常,通过Future.get捕获抛出的异常 |
execute | Runnable | 无 | 无法抛出异常 |
参数介绍
ThreadPoolExecutor 3 个最重要的参数:
corePoolSize
:核心线程数,线程数定义了最小可以同时运行的线程数量。maximumPoolSize
:线程池中允许存在的工作线程的最大数量(必须大于等于核心线程数,否则抛出异常)workQueue
:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。
ThreadPoolExecutor
其他常见参数:
- keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
- unit :keepAliveTime 参数的时间单位。
- threadFactory:为线程池提供创建新线程的线程工厂
- handler :线程池任务队列超过 maxinumPoolSize 之后的拒绝策略
4种拒绝策略
- ThreadPoolExecutor.AbortPolicy()
当新任务被线程池拒绝时,会抛出RejectedExecutionException
- ThreadPoolExecutor.CallerRunsPolicy
当任务被线程池拒绝时,线程池会将被拒绝的任务添加到线程池中正在运行的线程中运行,由此线程去处理该任务
- ThreadPoolExecutor.DiscardPolicy()
丢弃任务,但是不抛弃异常。当缓冲队列和线程池满时,其它任务将被丢弃。
- ThreadPoolExecutor.DiscardOldestPolicy()
当新任务被线程池拒绝时,首先会丢弃缓冲队列最前面的任务,然后将被拒绝的任务添加到末尾。
线程池执行过程
运行状态解释:
workerCount(工作线程数量)
runState(运行状态)
状态 | 解释 | |
---|---|---|
RUNNING | -1 << COUNT_BITS | 运行态,可处理新任务并执行队列中的任务 |
SHUTDOW | 0 << COUNT_BITS | 关闭态,不接受新任务,但处理队列中的任务 |
STOP | 1 << COUNT_BITS | 停止态,不接受新任务,不处理队列中任务,且打断运行中任务 |
TIDYING | 2 << COUNT_BITS | 整理态,所有任务已经结束,workerCount = 0 ,将执行terminated()方法 |
TERMINATED | 3 << COUNT_BITS | 结束态,terminated() 方法已完成 |
三.ThreadLocal: 线程局部变量
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。
ThreadLocal内存泄漏分析与解决方案
ThreadLocal造成内存泄漏的原因?
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法
ThreadLocal内存泄漏解决方案?
- 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
- 在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。