1.作用
ThreadLocal是一种线程隔离机制
, 使得每个线程都可以拥有自己独立的变量副本,从而避免了多线程环境下的线程安全问题。
public class Demo {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
static void print(String str){
System.out.println(str + ":" + threadLocal.get());
}
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("abc");
print("thread1 variable");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("def");
print("thread2 variable");
}
});
thread1.start();
thread2.start();
}
}
2.内部结构和原理
最初的设计
每个ThreadLocal是自己维护一个ThreadLocalMap, key是当前线程, value是要存储的局部变量, 这样就可以达到各个线程的局部变量隔离的效果
JDK8的设计
每个Thread维护一个ThreadLocalMap, 这个Map的key是ThreadLocal本身, value是要存储的变量. 具体的流程如下
这样设计的优点:
3.ThreadLocal与synchronized对比
虽然ThreadLocal与synchronized都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用时间换空间的方法, 只提供了一份变量, 让不同的线程排队访问 | ThreadLocal采用空间换时间的方式, 为每一个线程提供了一份变量的副本, 从而实现同时访问, 互不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间数据相互隔离 |
4.源码分析
1.内存泄漏和内存溢出的区别
内存溢出 | 内存泄漏 | |
---|---|---|
定义 | 内存溢出指的是程序在运行过程中申请的内存超过了系统或者进程所能提供的内存大小(结果) | 内存泄漏指的是程序中已经不再需要的内存未被释放,造成系统内存的浪费(起因) |
原因 | 通常是由于程序中存在大量的内存申请,而且没有及时释放,导致系统的可用内存被耗尽 | 内存泄漏通常是由于程序中存在指针或引用,指向了不再使用的内存块,但程序却没有释放这些内存 |
表现 | 当内存溢出发生时,程序通常会崩溃,并且系统可能会报告无法分配内存的错误 | 内存泄漏不会导致程序立即崩溃,但随着时间的推移,系统可用内存会逐渐减少,最终可能导致系统变慢或者崩溃 |
总体来说,内存溢出是由于申请的内存过多,超出了系统限制,而内存泄漏是因为未能及时释放已经不再使用的内存。
解决内存溢出和内存泄漏的方法通常包括合理管理内存的申请和释放过程,使用合适的数据结构,以及利用内存管理工具进行检测和优化。
需要说明一点: 虽然内存泄漏可能会导致内存溢出,但内存溢出也可能是由于其他原因,例如程序中存在大量的内存申请,但这些内存并没有被泄漏,而是在程序执行期间一直保持被占用状态,最终导致系统内存耗尽。
2.强引用和弱引用的区别
Object obj = new Object(); // 强引用
WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 弱引用
总结:
3.哪些情况下, ThreadLocal会导致内存泄漏?
3.1 长时间存活的线程
public class MyRunnable implements Runnable {
private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();
@Override
public void run() {
MyObject obj = new MyObject();
myThreadLocal.set(obj);
// 执行任务...
// 如果线程一直存活,myThreadLocal 将一直持有对 obj 的引用,即使任务执行完毕。
}
}
在这个例子中,即使任务执行完毕,ThreadLocal 对象仍然持有对 MyObject 的引用,而线程的生命周期可能会很长,导致 MyObject 无法被垃圾回收,从而引发内存泄漏。
为了避免这种情况,需要在不再需要 ThreadLocal 存储的对象时,显式调用 remove() 方法来清理 ThreadLocal。这样可以确保 ThreadLocal 对象中的弱引用被正确清理,从而防止内存泄漏。例如:
public class MyRunnable implements Runnable {
private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();
@Override
public void run() {
try {
MyObject obj = new MyObject();
myThreadLocal.set(obj);
// 执行任务...
} finally {
// 清理 ThreadLocal,防止内存泄漏
myThreadLocal.remove();
}
}
}
3.2 使用线程池
如果在使用线程池的情况下,ThreadLocal被设置在某个任务中,而这个任务在线程池中执行完成后线程被放回线程池而不是销毁,那么ThreadLocal可能在下一次任务执行时仍然持有对上次设置的对象的引用。
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(() -> {
MyObject obj = new MyObject();
myThreadLocal.set(obj);
// 执行任务...
// 线程被放回线程池,但 ThreadLocal 可能仍然持有对 obj 的引用。
});
为了避免这类问题,确保在ThreadLocal不再需要时,调用remove()方法清理它所持有的对象引用。这通常在任务执行结束时或者线程即将被销毁时执行。例如:
public class MyRunnable implements Runnable {
private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();
@Override
public void run() {
try {
MyObject obj = new MyObject();
myThreadLocal.set(obj);
// 执行任务...
} finally {
// 清理 ThreadLocal,防止内存泄漏
myThreadLocal.remove();
}
}
}
死锁是指多个线程(两个或以上)在执行过程中互相等待对方释放资源, 无法继续执行下去.
在一个典型的死锁情况中, 每个进程都在等待某个资源, 但同时拥有另一个资源, 由于每个进程都不愿意先释放自己已经占有的资源, 所以形成了相互等待的状况
1.死锁的4个必要条件
i++
操作本身并不是线程安全的
i++
实际上是一个复合操作,包括读取 i 的当前值、将其加一、然后将结果写回 i。其中涉及到读取、修改、写入三个步骤。如果两个线程同时执行这段代码,可能会导致竞态条件,使得最终的结果不是期望的增加2,而可能是增加1或者其他值。
解决方式一: 使用同步(synchronization)
synchronized (lock) {
i++;
}
解决方式二: 使用原子类(Atomic Classes)
AtomicInteger i = new AtomicInteger(0);
i.incrementAndGet();
1.synchronized的三种用法
public synchronized void increase() {
}
public static synchronized void increase() {
}
public Object synMethod(Object a1) {
synchronized(a1) {
// 操作
}
}
2.synchronized用于静态方法与普通方法有区别吗?
public class MyClass {
public synchronized void instanceMethod() {
// 实例方法的同步代码块
}
}
实例方法中, 锁住的是当前实例对象(this)
, 对于MyClass类的不同实例, 它们的实力方法是独立的, 可以同时执行
public class MyClass {
public static synchronized void staticMethod() {
// 静态方法的同步代码块
}
}
静态方法中, 锁住的是整个类的Class对象
, 对于MyClass类的所有实例,同一时间只能有一个线程执行该静态方法。
SimpleDateFormat类在多线程环境下是线程不安全的。
SimpleDateFormat内部维护了一个Calendar实例,而Calendar是线程不安全的
Calendar 的实现并没有在设计上考虑到多线程并发访问的情况。因此,多个线程可能同时修改 Calendar 内部的状态,而不受到足够的同步或锁的保护,从而导致线程安全问题。
解决方式一: 使用局部变量
在每个线程中创建一个独立的 SimpleDateFormat 实例,而不是共享一个实例。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
解决方式二: 使用线程安全的替代方案
如果你需要在多线程环境中进行日期格式化和解析操作,可以考虑使用 java.time.format.DateTimeFormatter,它是 java.time 包中的类,设计为线程安全的。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
1.优点
线程池通过提供一种有效的线程管理和调度机制,帮助提高应用程序的性能和可维护性,尤其在处理大量并发任务时,线程池是一种强大而有效的工具。
2.缺点
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
当我们核心线程数已经到达最大值、阻塞队列也已经放满了所有的任务、而且我们工作线程个数已经达到最大线程数, 此时如果还有新任务, 就只能走拒绝策略了
1.作用
2.用法
在线程池中, BlockingQueue主要通过以下两个参数进行配置
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个LinkedBlockingQueue作为任务队列
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(10);
// 创建一个ThreadPoolExecutor,使用LinkedBlockingQueue作为任务队列
ExecutorService executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
1, // keepAliveTime
TimeUnit.SECONDS,
queue);
// 提交任务到线程池
for (int i = 0; i < 15; i++) {
executor.submit(() -> {
try {
Thread.sleep(1000);
System.out.println("Task completed by: " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
}
}
打印结果:
Task completed by: pool-1-thread-5
Task completed by: pool-1-thread-1
Task completed by: pool-1-thread-2
Task completed by: pool-1-thread-4
Task completed by: pool-1-thread-3
Task completed by: pool-1-thread-1
Task completed by: pool-1-thread-4
Task completed by: pool-1-thread-2
Task completed by: pool-1-thread-5
Task completed by: pool-1-thread-3
Task completed by: pool-1-thread-2
Task completed by: pool-1-thread-4
Task completed by: pool-1-thread-5
Task completed by: pool-1-thread-1
Task completed by: pool-1-thread-3
在这个例子中,LinkedBlockingQueue 作为任务队列,可以存储最多 10 个等待执行的任务。线程池的核心线程数为 5,最大线程数为 10,因此在任务队列未满时,新任务将放入队列等待。如果队列已满,新任务将创建新线程执行,但不会超过最大线程数。
因为开启了15个线程, 而核心线程数+阻塞队列容量正好为15个, 所以不会创建新的线程
1.ArrayBlockingQueue
基于数组实现的有界队列
。
固定容量,一旦创建就不能更改。
需要指定容量,适用于任务数量固定的情况。
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
使用 ArrayBlockingQueue 有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到 corePoolSize 时,则会将新的任务加入到等待队列中。若等待队列已满,即超过 ArrayBlockingQueue 初始化的容量,则继续创建线程,直到线程数量达到 maximumPoolSize 设置的最大线程数量,若大于 maximumPoolSize,则执行拒绝策略。在这种情况下,线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在 corePoolSize 以下,反之当任务队列已满时,则会以 maximumPoolSize 为最大线程数上限
2.LinkedBlockingQueue
基于链表实现的有界或无界队列
。
可以选择是否指定容量,如果不指定容量则默认是 Integer.MAX_VALUE
适用于任务数量不固定的情况
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
无界任务队列,线程池的任务队列可以无限制的添加新的任务,而线程池创建的最大线程数量就是你 corePoolSize 设置的数量,也就是说在这种情况下 maximumPoolSize 这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池的线程数达到 corePoolSize 后,就不会再增加了;若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。
3.SynchronousQueue
一个不存储元素的队列
每个插入操作必须等待另一个线程的对应移除操作,反之亦然。
主要用于直接传递任务的场景,一个线程产生任务,另一个线程消费任务
4.PriorityBlockingQueue
基于优先级堆的无界队列。
元素需要实现Comparable接口或者在构造方法中提供Comparator
5.DelayedWorkQueue
一个支持延时获取元素的无界队列,用于实现定时任务。
元素需要实现 Delayed 接口
FixedThreadPool | SingleThreadExecutor | ScheduledThreadPool | CachedThreadPool | |
---|---|---|---|---|
名称 | 固定大小线程池 | 单线程线程池 | 定时任务线程池 | 缓存线程池 |
特点 | 固定线程数量的线程池,适用于负载较重的服务器 | 只有一个工作线程的线程池,确保所有任务按顺序执行 | 支持定时及周期性任务执行的线程池 | 线程数量根据需求动态调整,线程空闲一定时间后被回收 |
1.FixedThreadPool
创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
public class NewFixedThreadPoolTest {
public static void main(String[] args) {
System.out.println("主线程启动");
// 1.创建1个有2个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
}
};
// 2.线程池执行任务(添加4个任务,每次执行2个任务,得执行两次)
threadPool.submit(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
System.out.println("主线程结束");
}
}
上述代码:创建了一个有2个线程的线程池,但一次给它分配了4个任务,每次只能执行2个任务,所以,得执行两次。
该线程池重用固定数量的线程在共享的无界队列中运行。 在任何时候,最多 nThreads 线程将是活动的处理任务。如果在所有线程都处于活动状态时提交了其他任务,它们将在队列中等待,直到有线程可用。 所以,它会一次执行 2 个任务(2 个活跃的线程),另外 2 个任务在工作队列中等待着。
submit() 方法和 execute() 方法都是执行任务的方法。它们的区别是:submit() 方法有返回值,而 execute() 方法没有返回值。
2.CachedThreadPool
创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
适用场景:快速处理大量耗时较短的任务,如 Netty 的 NIO 接受请求时,可使用 CachedThreadPool。
public class NewCachedThreadPool {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
}
3.SingleThreadExecutor
创建单个线程数的线程池,它可以保证先进先出的执行顺序。
4.SingleThreadScheduledExecutor
创建一个单线程的可以执行延迟任务的线程池。
public class SingleThreadScheduledExecutorTest {
public static void main(String[] args) {
ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
System.out.println("添加任务,时间:" + new Date());
threadPool.schedule(() -> {
System.out.println("任务被执行,时间:" + new Date());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
}, 2, TimeUnit.SECONDS);
}
}
ThreadFactory是一个接口, 用于创建新线程的工厂, 它允许你自定义线程的创建过程, 例如设置线程的名称、优先级、守护状态等…
import java.util.concurrent.*;
public class CustomThreadFactoryExample {
public static void main(String[] args) {
// 创建一个自定义的ThreadFactory
ThreadFactory customThreadFactory = new CustomThreadFactory("CustomThread");
// 使用自定义的ThreadFactory创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(5, customThreadFactory);
// 提交一些任务
for (int i = 0; i < 10; i++) {
executorService.submit(() -> System.out.println(Thread.currentThread().getName()));
}
// 关闭线程池
executorService.shutdown();
}
}
// 自定义的ThreadFactory实现
class CustomThreadFactory implements ThreadFactory {
private final String threadNamePrefix;
public CustomThreadFactory(String threadNamePrefix) {
this.threadNamePrefix = threadNamePrefix;
}
@Override
public Thread newThread(Runnable r) {
// 创建新线程并设置线程名称
Thread thread = new Thread(r, threadNamePrefix + "-" + System.nanoTime());
// 设置为后台线程(可选)
thread.setDaemon(false);
// 设置线程优先级(可选)
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
}
一、AbortPolicy
这是默认的拒绝策略,当队列满时直接抛出
RejectedExecutionException异常,阻止系统继续运行。
ExecutorService executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
new LinkedBlockingQueue<>(capacity),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
二、CallerRunsPolicy
新任务会被直接在提交任务的线程中运行。这样做可以避免任务被拒绝,但会影响任务提交的线程的性能。
ExecutorService executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
new LinkedBlockingQueue<>(capacity),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
三、DiscardPolicy
新任务被直接丢弃,不做任何处理。
ExecutorService executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
new LinkedBlockingQueue<>(capacity),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
四、DiscardOldestPolicy
尝试将最旧的未处理任务从队列中删除,然后重新尝试执行任务
ExecutorService executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
new LinkedBlockingQueue<>(capacity),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
验证主线程无法捕获线程池中异常
public static void main(String[] args) {
try {
System.out.println("主线程开始");
// 创建线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// 执行线程方法
executor.execute(()->{
System.out.println("子线程运行开始");
int i = 1 / 0;
System.out.println("子线程运行结束");
});
executor.shutdown();
System.out.println("主线程结束");
} catch (Exception e) {
System.out.println("异常信息:" + e.getMessage());
}
}
在上面的代码中, 异常无法被捕获的原因是因为异常发生在子线程中,而主线程并不直接捕获这个异常。
execute方法提交任务后,异常会被线程池中的线程捕获并处理,但是这个异常处理是在线程内部进行的,不会传递到主线程中。
我们发现不论是使用execute方法或者submit方法提交任务, 都没有办法在主线程中捕获到异常, 有没有解决方式?
1.使用submit提交任务, 再通过阻塞方法等待任务完成, 如果这个过程中发生了异常, 会在主线程中被捕获
你可以通过Future对象的get方法来获取异常,但需要注意的是,如果任务执行过程中发生了异常,调用get方法会抛出ExecutionException,你需要在主线程中处理这个异常。
try {
System.out.println("主线程开始");
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
System.out.println("运行开始");
int i = 1 / 0;
System.out.println("运行结束");
});
executor.shutdown();
// 获取任务执行的结果,这里会阻塞直到任务完成
future.get();
System.out.println("主线程结束");
} catch (Exception e) {
System.out.println("捕获到异常:" + e.getMessage());
}
上述的方法使用submit提交任务, 那如果使用execute提交任务, 可以捕获线程池中的异常吗?
submit和execute方法的区别
submit和execute都可以提交任务, 两者有一些关键的区别
Callable的返回值类型是在实现接口时指定的, 例如下面这个例子
public class GetStrService implements Callable<String> {
private int i;
public GetStrService(int i) {
this.i = i;
}
@Override
public String call() throws Exception {
int t=(int) (Math.random()*(10-1)+1);
System.out.println("第"+i+"个任务开始啦:"+Thread.currentThread().getName()+"准备延时"+t+"秒");
Thread.sleep(t*1000);
return "第"+i+"个GetStrService任务使用的线程:"+Thread.currentThread().getName();
}
}
2.自定义ThreadPoolExecutor作为线程池
// 自定义线程池
class MyThreadPoolExecutor extends ThreadPoolExecutor {
public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
System.out.println("捕获到异常。异常信息为:" + t.getMessage());
System.out.println("异常栈信息为:");
t.printStackTrace();
}
}
public static void main(String[] args) {
System.out.println("主线程开始");
// 创建线程池
ExecutorService executor = new MyThreadPoolExecutor(5,50, 3, TimeUnit.SECONDS, new LinkedBlockingQueue<>(20));
// 执行线程方法
executor.execute(()->{
System.out.println("子线程运行开始");
int i = 1 / 0;
System.out.println("子线程运行结束");
});
executor.shutdown();
System.out.println("主线程结束");
}