进程
- 程序由指令和数据组成,但是这些指令要运行,数据要读写,就必须将指令加载到cpu,数据加载至内存。在指令运行过程中还需要用到磁盘,网络等设备,进程就是用来加载指令管理内存管理IO的
- 当一个指令被运行,从磁盘加载这个程序的代码到内存,这时候就开启了一个进程
- 进程就可以视为程序的一个实例,大部分程序都可以运行多个实例进程(例如记事本,浏览器等),部分只可以运行一个实例进程(例如360安全卫士)
线程
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
- Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器
二者对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享进程间通信较为复杂
- 同一台计算机的进程通信称为 IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
线程的创建
直接创建 Thread ,重写 run() 方法
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("创建线程");
}
};
t.start();
实现 Runnable 接口创建线程
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("创建线程");
}
};
Thread t2 = new Thread(r, "t2");
t2.start();
以上两种创建线程方式存在问题:
没有参数
没有返回值
没办法抛出异常
- 实现 Callable 接口创建线程(可接收返回值)
```java
FutureTask futureTask = new FutureTask<>(new Callable
() { @Override public Integer call() throws Exception {
} });log.debug("多线程任务");
Thread.sleep(100);
return 100;
// 主线程阻塞,同步等待 task 执行完毕的结果 new Thread(futureTask,”我的名字”).start(); System.out.println(futureTask.get());
- 通常使用 FutureTask 来封装 Callable 和 Runnable,可对执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞main方法线程直到任务返回结果。
<a name="ZrKyw"></a>
# 线程的状态
<a name="mr4Wz"></a>
## 五种状态
![](https://gitee.com/gu_chun_bo/picture/raw/master/image/20200307093417-638644.png#crop=0&crop=0&crop=1&crop=1&id=ixBRq&originHeight=312&originWidth=539&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
1. 初始状态,仅仅是在语言层面上创建了线程对象,即`Thead thread = new Thead();`,还未与操作系统线程关联
1. 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
1. 运行状态,指线程获取了CPU时间片,正在运行
1. 当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换
4. 阻塞状态
1. 如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
1. 等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
1. 与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
5. 终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
<a name="CUxxX"></a>
## 六种状态
![](https://gitee.com/gu_chun_bo/picture/raw/master/image/20200307093352-614933.png#crop=0&crop=0&crop=1&crop=1&id=ScpRI&originHeight=400&originWidth=566&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
1. NEW 跟五种状态里的初始状态是一个意思
1. RUNNABLE 是当调用了 `start()` 方法之后的状态,注意,Java API 层面的 `RUNNABLE` 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
1. `BLOCKED` , `WAITING` , `TIMED_WAITING` 都是 Java API 层面对【阻塞状态】的细分<br />`sleep --> TIMED_WAITING`<br />`join --> WAITING`<br />`拿不到锁 --> BLOCKED`
1. TREMINATED 线程代码运行结束时的状态
<a name="btICo"></a>
# 线程间的通信
<a name="woTgn"></a>
## sleep、yield、join、wait 与 interrupt
- **sleep**
1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
1. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出 `InterruptedException`异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
1. 睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
1. 建议用 TimeUnit 的 `sleep()` 代替 Thread 的 `sleep()`来获得更好的可读性
```java
TimeUnit.HOURS.sleep(1); // 睡眠1h
TimeUnit.SECONDS.sleep(1); // 睡眠1s
Thread.sleep(1000); // 睡眠1000ms
- yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)
- join
在主线程中调用t1.join,则主线程会等待t1线程执行完之后再继续执行
t1.start();
t1.join(); // 主线程等待t1线程运行结束
- interrupt
sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态
打断sleep,wait,join的线程,会清空打断标记(把isInterrupted
重制为false)
- 对比
- sleep,join,yield,interrupted是Thread类中的方法
- wait / notify 是object中的方法
- 打断sleep,wait,join的线程,会清空打断标记(把
isInterrupted
重制为false)
sleep 不释放锁、释放cpu
join 释放锁、抢占cpu
yield 不释放锁、释放cpu
wait 释放锁、释放cpu
synchronized 与 wait / notify
- 必须配合
synchronized
关键字(使用重量级锁)才能使用 wait/notify 进行控制 - wait / notify 是 Object 对象中的方法,只有使用对象级别的锁才能使用 wait/notify 进行控制
static final Object room = new Object(); // 对象锁
static boolean hasCigarette = false;
// xiaonan线程锁住room,等待cigarette
new Thread(() -> {
synchronized (room) {
System.out.println("xiaonan: hasCigarette ? " + hasCigarette);
if (!hasCigarette) {
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("xiaonan: hasCigarette ? " + hasCigarette);
if (hasCigarette) {
System.out.println("xiaonan: begins working");
}
}
}, "xiaonan").start();
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
System.out.println("delivers the takeout");
// notify()只能从waitset里面随机选择一个线程唤醒
room.notify();
}
}, "deliver").start();
保护性暂停模式
即 Guarded Suspension,用在一个线程等待另一个线程的执行结果,要点:
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject(一对一)
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
- JDK 中,join 的实现、Future 的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
// 获取结果
public Object get() {
synchronized (this) {
// 当response未被赋值时,调用get()方法将会使该线程处于等待状态
while (response == null) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return this.response;
}
}
// 产生结果
public void complete(Object obj) {
synchronized (this) {
this.response = obj;
// response获取到结果后,通知所有等待线程
notifyAll();
}
}
生产者-消费者模式
要点
- 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
- JDK 中各种阻塞队列,采用的就是这种模式
“异步”的意思就是生产者产生消息之后消息没有被立刻消费,而“同步模式”中,消息在产生之后被立刻消费了。
public class MessageQueue {
private int capacity;
private LinkedList<Message> queue;
public MessageQueue(int capacity) {
this.capacity = capacity;
this.queue = new LinkedList<Message>();
}
// 产生消息
public void put(Message message) {
synchronized (this.queue) {
while (queue.size() == capacity) {
try {
System.out.println("生产者:队列已满,进入等待");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(message);
System.out.println("生产者:放入消息");
queue.notifyAll();
}
}
// 消费消息
public Message take() {
synchronized (this.queue) {
while (queue.isEmpty()) {
try {
System.out.println("消费者:队列为空,进入等待");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Message message = queue.removeFirst();
System.out.println("消费者:取出消息");
queue.notifyAll();
return message;
}
}
}
LockSupport 与 park / unpark
- park / unpark 是 LockSupport 中提供的方法
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter, _cond和 _mutex
- 打个比喻线程就像一个旅人,Parker 就像他随身携带的背包,条件变量 _ cond就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)
- 调用 park 就是要看需不需要停下来歇息
- 如果备用干粮耗尽,那么钻进帐篷歇息
- 如果备用干粮充足,那么不需停留,继续前进
- 调用 unpark,就好比令干粮充足
- 如果这时线程还在帐篷,就唤醒让他继续前进
- 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
- 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
// 2s后再调用park()暂停线程
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.park();
}, "t1");
t1.start();
// 1s后先调用unpark()恢复线程
Thread.sleep(1000);
LockSupport.unpark(t1);
}
与 Object 的 wait & notify 相比:
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
- park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
ReentrantLock 与 await / signal
- ReentrantLock中提供了 await / signal 方法
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比:synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤
醒
使用流程:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被
signal()
唤醒(或打断、或超时)取重新竞争 lock 锁,执行唤醒的线程必须先获得锁 竞争 lock 锁成功后,从 await 后继续执行
public class Test01 { static ReentrantLock lock = new ReentrantLock(); static Condition waitCigaretteQueue = lock.newCondition(); static Condition waitTakeoutQueue = lock.newCondition(); static volatile boolean hasCigarette = false; static volatile boolean hasTakeout = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { try { lock.lock(); while (!hasCigarette) { System.out.println("等烟"); // 如果没有烟,则在等烟队列中进行等待 try { waitCigaretteQueue.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("等到了烟,开始干活"); } finally { lock.unlock(); } }).start(); } }
线程池
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
其实Java线程池的实现原理很简单,说白了就是一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。
线程池的实现
首先实现一个简单的阻塞队列
/** * 任务阻塞队列,用于协调任务的生产者和消费者 * @param <T> */ public class BlockQueue<T> { // 队列长度 private int capacity; // 任务队列 private Deque<T> queue = new ArrayDeque<>(); // 锁 private ReentrantLock lock = new ReentrantLock(); // 生产者等待区 private Condition fullWaitSet = lock.newCondition(); // 消费者等待区 private Condition emptyWaitSet = lock.newCondition(); // 构造方法 public BlockQueue(int capacity) { this.capacity = capacity; } // 阻塞获取(持续等待,不会超时) public T take() { lock.lock(); try{ // 队列为空则进行等待 while (queue.isEmpty()) { try { emptyWaitSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } // 获取任务并通知生产者 T t = queue.removeFirst(); fullWaitSet.signal(); return t; } finally { lock.unlock(); } } // 带超时的阻塞获取 public T poll(long timeout, TimeUnit unit) { // 队列为空则进行等待 // 超时则不再等待 // 获取任务并通知生产者 } // 阻塞添加 public void put(T element) { // 队列已满则进入等待 // 添加任务并通知消费者 } }
在此基础上实现线程池,内部依赖一个阻塞队列
/** * 线程池 */ public class ThreadPool { // 任务队列 private BlockQueue<Runnable> taskQueue; // 线程集合(worker作为线程的包装类) private HashSet<Worker> workers = new HashSet<>(); // 核心线程数 private int coreSize; // 获取任务的超时时间 private long timeout; private TimeUnit timeUnit; // 构造方法 public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapacity) { this.coreSize = coreSize; this.timeout = timeout; this.timeUnit = timeUnit; this.taskQueue = new BlockQueue<>(queueCapacity); } /** * 通过线程池执行线程的run方法 * @param task 线程对象 */ public void execute(Runnable task) { // 当任务数没有超过 coreSize 时,直接交给 worker 对象执行 // 如果任务数超过 coreSize 时,加入任务队列暂存 synchronized (workers) { if (workers.size() < coreSize) { // 加入线程集合并立即执行 Worker worker = new Worker(task); // 包装为worker System.out.println("新增Worker对象:" + worker + ",执行任务:" + task); workers.add(worker); worker.start(); } else { // 加入任务队列暂存 System.out.println("Worker对象已达到核心数,加入"+task+"到任务队列"); taskQueue.put(task); } } } /** * 内部类worker,用来封装Thread */ class Worker extends Thread{ // 待执行任务,可以从构造方法添加,也可以从任务队列中获取 private Runnable task; public Worker(Runnable task) { this.task = task; } @Override public void run() { // 可以从构造方法添加或任务队列中获取task // while (task != null || (task = taskQueue.take()) != null) { // 一直运行 while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) { // 有等待时间 try{ System.out.println("执行任务: " + task); task.run(); } catch (Exception e) { e.printStackTrace(); } finally { // 执行完毕后task赋值为null task = null; } } // 运行完毕后将当前对象从workers中移除 synchronized (workers) { System.out.println("执行结束,从集合中移除worker:" + this); workers.remove(this); } } } }
工作原理
构造方法
public ThreadPoolExecutor(
int corePoolSize, // Set集合中的核心线程数
int maximumPoolSize, // 线程池中允许的最大线程数(Set + 阻塞队列中线程总数)
long keepAliveTime, // Set中的空闲线程最大存活时间
TimeUnit unit, // keepAliveTime的单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
RejectedExecutionHandler handler // 阻塞队列已满后的拒绝策略
)
- Java中提供的拒绝策略
- AbortPolicy: 直接抛出异常,默认策略;
- CallerRunsPolicy: 用调用者所在的线程来执行任务;
- DiscardOldestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务;
- DiscardPolicy: 直接丢弃任务;
- 也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务
三种实现类型
newFixedThreadPool
- 这个是Executors类提供的工厂方法来创建线程池,Executors 是Executor 框架的工具类
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// Executors.newFixedThreadPool(2)
通过源码可以看到 new ThreadPoolExecutor(xxx)方法其实是是调用了之前说的完整参数的构造方法,使用了默认的线程工厂和拒绝策略
特点
- 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
- 阻塞队列是无界的,可以放任意数量的任务
- 适用于任务量已知,相对耗时的任务
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
特点
- 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着
- 全部都是救急线程(60s 后可以回收)
- 救急线程可以无限创建
- 队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)
- 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线
程。 适合任务数比较密集,但每个任务执行时间较短的情况
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
使用场景:
- 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
- 区别:
- 和自己创建单线程执行任务的区别:自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
- Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改。FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
- 和Executors.newFixedThreadPool(1) 初始时为1时的区别:Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改,对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
实际应用
不推荐使用 Executors 返回线程池对象
- FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
- CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
- 实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。
- 我们应该显示地给我们的线程池命名,这样有助于我们定位问题。
推荐使用方式
- spring配置线程池方式:自定义线程工厂bean需要实现ThreadFactory
- 引入:com.google.guava包
配置线程池需要考虑因素
从任务的优先级,任务的执行时间长短,任务的性质(CPU密集/ IO密集),任务的依赖关系这四个角度来分析。并且近可能地使用有界的工作队列。
性质不同的任务可用使用不同规模的线程池分开处理:
- CPU密集型: 尽可能少的线程,Ncpu+1
- IO密集型: 尽可能多的线程, Ncpu*2,比如数据库连接池
- 混合型: CPU密集型的任务与IO密集型任务的执行时间差别较小,拆分为两个线程池;否则没有必要拆分。
ThreadLocal
在JDK8中 ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例对象本身(对象的引用做为key),value才是真正要存储的值Object。
- set方法:
A. 首先获取当前线程,并根据当前线程获取一个Map
B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
C. 如果Map为空,则给该线程创建 Map,并设置初始值
- get方法:
A. 首先获取当前线程, 根据当前线程获取一个Map
B. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到D
C. 如果e不为null,则返回e.value,否则转到D
D. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
public class ThreadLocalTest {
public static void main(String[] args) {
/**
* 类加载过程中创建了ThreadLocal对象,将001放入了类加载线程的ThreadLocalMap中;
* t1线程执行set()方法时首先创建了属于t1线程的ThreadLocalMap,再以ThreadLocal对象为key,放入value=002;
* 同理,t2线程也需要先创建属于自己线程的ThreadLocalMap;
* 三次放入均以类加载时创建的ThreadLocal对象为key,存入各自线程的Map中,value值互不干扰
*/
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
TL.set("t1线程放入了002");
System.out.println(TL.get());
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
TL.set("t2线程放入了003");
System.out.println(TL.get());
}
});
t1.run();
t2.run();
/**
* 类加载线程放入了001
* t1线程放入了002
* t2线程放入了003
*/
}
}
public class TL {
private static ThreadLocal<String> tl = new ThreadLocal<>();
static {
tl.set("类加载线程放入了001");
System.out.println(tl.get());
}
public static String get() {
return tl.get();
}
public static void set(String message) {
tl.set(message);
}
}
ThreadLocalMap
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
- 在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。
另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
static class Entry extends WeakReference<ThreadLocal<?>> { Object value; // 构造方法 Entry<ThreadLocal<?> k, Object v> { super(k); value = v; } }
ThreadLocal和内存泄漏
有些程序员在使用ThreadLocal的过程中会发现有内存泄漏的情况发生,就猜测这个内存泄漏跟Entry中使用了弱引用的key有关系。这个理解其实是不对的。
(1) 内存泄漏相关概念
Memory overflow:内存溢出,没有足够的内存提供申请者使用。
Memory leak: 内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。
(2) 弱引用相关概念
Java中的引用有4种类型: 强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用:
强引用(“Strong” Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。
- 避免内存泄漏:
1、使用完ThreadLocal,调用remove方法删除对应的Entry
2、使用完ThreadLocal,当前线程也随之结束
- 使用弱引用的原因:
在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。
这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
避免哈希冲突
- 通过斐波纳切散列法计算哈希值 ```java private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } ```
通过 哈希值 & (数组长度-1)计算元素下标
ThreadLocalMap使用线性探测法来解决哈希冲突的
A. 首先还是根据key计算出索引 i,然后查找i位置上的Entry,
B. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,
C. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry,
D. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。
最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz 是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理(回收弱引用)。