Java多线程笔记

Java线程

Java线程和操作系统线程的区别

现在的 Java 线程的本质其实就是操作系统的线程。

线程模型是用户线程和内核线程之间的关联方式,常见的线程模型有这三种:

  • 一对一(一个用户线程对应一个内核线程)
  • 多对一(多个用户线程映射到一个内核线程)
  • 多对多(多个用户线程映射到多个内核线程)
    在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个 Java 线程对应一个系统内核线程。
说说线程的生命周期和状态?

在这里插入图片描述

常用方法介绍

Thread.sleep()
sleep() 方法是 Thread 类中的静态方法,用于使当前线程进入指定时间的休眠状态。在休眠期间,线程不会释放它所持有的任何锁。

Thread.join()
join() 方法通常用于在一个线程中等待另一个线程执行完成,然后再继续执行后续的操作。

Object.wait()
使当前线程进入等待状态,并释放对象的锁,直到其他线程调用该对象的 notify() 或 notifyAll() 方法来唤醒它。通常与 synchronized 关键字一起使用,用于实现线程间的协调。

Object.notify()/Object.notifyAll()
notify() 方法用于唤醒因调用该对象的 wait() 方法而处于等待状态的单个线程。而 notifyAll() 方法则会唤醒所有因调用该对象的 wait() 方法而处于等待状态的线程。

public class ProducerConsumerExample {
    private static final int CAPACITY = 5;
    private static List<Integer> buffer = new ArrayList<>();
    private static Object lock = new Object();

    public static void main(String[] args) {
        Thread producer = new Thread(new Producer());
        Thread consumer = new Thread(new Consumer());

        producer.start();
        consumer.start();
    }

    static class Producer implements Runnable {
        public void run() {
            int value = 0;
            while (true) {
                synchronized (lock) {
                    while (buffer.size() == CAPACITY) {
                        try {
                            System.out.println("Buffer is full, waiting for consumer to consume...");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("Producing: " + value);
                    buffer.add(value++);
                    lock.notifyAll();
                }
            }
        }
    }

    static class Consumer implements Runnable {
        public void run() {
            while (true) {
                synchronized (lock) {
                    while (buffer.isEmpty()) {
                        try {
                            System.out.println("Buffer is empty, waiting for producer to produce...");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    int consumedValue = buffer.remove(0);
                    System.out.println("Consuming: " + consumedValue);
                    lock.notifyAll();
                }
            }
        }
    }
}
为什么 wait() 方法不定义在 Thread 中?

wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。类似的问题:为什么 sleep() 方法定义在 Thread 中?因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。可以直接调用 Thread 类的 run 方法吗?

可以直接调用 Thread 类的 run 方法吗?

调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。会把 run() 方法当成一个 main 线程下的普通方法去执行。

Volatile关键字

Volatile是什么

volatile关键字可以保证变量的可见性,但不能保证变量的原子性。synchronized 关键字两者都能保证。
volatile关键字还具有禁止指令重排序的功能。
在这里插入图片描述

在这里插入图片描述
Volatile如何使用
使用volatile保证变量可见性的例子

public class VolatileExample {
    private volatile boolean isRunning = true;

    public void stop() {
        isRunning = false;
    }

    public void start() {
        while (isRunning) {
            // 执行一些操作
            System.out.println("Thread is running...");
        }
        System.out.println("Thread stopped.");
    }

    public static void main(String[] args) {
        VolatileExample example = new VolatileExample();
        
        Thread t1 = new Thread(() -> example.start());
        t1.start();
        
        try {
            Thread.sleep(1000); // 主线程休眠1秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        example.stop(); // 停止线程
        
        // 主线程等待t1线程结束
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Main thread exits.");
    }
}

如果isRunning 变量没有使用 volatile 关键字修饰,这个例子可能会出现问题,因为在多线程环境下,一个线程对共享变量的修改可能不会被其他线程立即感知到。
具体地说,当 main 线程调用 example.stop() 方法时,它会修改 isRunning 变量的值为 false,但是由于没有使用 volatile 关键字修饰,另一个线程 t1 可能不会立即看到这个修改,导致 t1 线程无法及时结束。

禁止指令重排序的例子
双重校验锁实现对象单例(线程安全):

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

synchronized关键字

synchronized是什么

synchronized解决的是多个线程之间访问资源的互斥性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

如何使用

synchronized 关键字的使用方式主要有下面 3 种:

  1. 修饰实例方法(锁当前对象实例)
    给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。
synchronized void method() {
    //业务代码
}
  1. 修饰静态方法 (锁当前类)
    给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
    这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
synchronized static void method() {
   //业务代码
}
  1. 修饰代码块 (锁指定对象/类)
    对括号里指定的对象/类加锁:synchronized(object) 表示进入同步代码库前要获得 给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁
synchronized(this) {
    //业务代码
}
底层原理

ReentrantLock

ReentrantLock 是什么

ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。

public class ReentrantLock implements Lock, java.io.Serializable {}

ReentrantLock 里面有一个内部类 Sync,Sync 继承 AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类。

在这里插入图片描述
ReentrantLock 默认使用非公平锁,也可以通过构造器来显式的指定使用公平锁。

// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

从上面的内容可以看出, ReentrantLock 的底层就是由 AQS 来实现的。关于 AQS 的相关内容推荐阅读 AQS 详解 这篇文章。

ReentrantLock 如何使用
public class ReentrantLockExample {
    private static ReentrantLock lock = new ReentrantLock();
    private static int count = 0;

    public static void main(String[] args) {
        // 创建并启动多个线程
        Thread thread1 = new Thread(new Worker());
        Thread thread2 = new Thread(new Worker());
        thread1.start();
        thread2.start();
    }

    static class Worker implements Runnable {
        @Override
        public void run() {
            // 加锁
            lock.lock();
            try {
                // 在临界区内对共享资源进行操作
                for (int i = 0; i < 5; i++) {
                    System.out.println("Thread " + Thread.currentThread().getId() + " is incrementing count to " + ++count);
                    try {
                        Thread.sleep(100); // 模拟一些处理时间
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                // 释放锁
                lock.unlock();
            }
        }
    }
}

1. java 实现多线程的三种方式

在 Java 中,有三种主要的方式来实现多线程:

  1. 继承 Thread 类
  2. 实现 Runnable 接口
  3. 使用 Callable 和 FutureTask

下面我们将分别介绍这三种方式的实现方法。

1. 继承 Thread 类

通过继承 Thread 类并重写其 run() 方法来实现多线程。以下是一个简单的示例:

class MyThread extends Thread {
	public void run() {
		System.out.println("This is a thread using inheritance");
	}
}
public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}
2. 实现 Runnable 接口

通过实现Runnable接口,并将其作为参数传递给Thread类的构造函数来实现多线程。以下是一个示例:

class MyRunnable implements Runnable {
	public void run(){
		System.out.println("This is a thread using Runnable interface");
	}
}

public class Main(){
	public static void main(String[] args){
		MyRunnable myRunnable = new MyRunnable();
		Thread thread = new Thread(myRunnable);
		thread.start();
	}
}
3.使用Callable 和 FutureTask

在 Java 中,Callable 接口的实现类不能直接作为 Thread 的参数,因为 Thread 类的构造方法需要的是一个 Runnable 对象而不是 Callable 对象。
为了将 Callable 的实现类作为 Thread 的参数,可以借助 FutureTask 类来实现这一功能。FutureTask 类实现了 Runnable 接口,同时可以包装一个 Callable 对象,并且在 run() 方法中执行 Callable 的 call() 方法。
在这里插入图片描述

Callable 接口允许线程返回结果,并且可以结合FutureTask来使用。以下是一个
示例:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<String> {
	public String call() {
		return "This is a thread using Callable and FutureTask";
	}
}
public class Main() {
	public static void main(String[] args) throws InterruptedException, ExecutionException{
		MyCallable myCallable = new MyCallable();
		FutureTask<String> futureTask = new FutureTask<>(myCallable);
		Thread thread = new Thread(futureTask);
		thread.start();
		System.out.println(futureTask.get());
	}
}

每种方式都有其独特的优势和适用场景:

继承Thread类:
这种方式是最简单的多线程实现方式,通过继承Thread类并重写run方法来定义线程执行的任务。

  • 优势:简单直观,适用于定义简单的线程任务。
  • 缺点:Java是单继承的语言,继承了Thread类就无法继承其他类,限制了代码的灵活性。

实现Runnable接口:
实现Runnable接口的类可以作为线程的任务传递给Thread对象来创建线程。

  • 优势:避免了单继承的限制,使得类可以继承其他类或实现其他接口;提供了更好的代码组织和结构。
  • 缺点:需要手动创建Thread对象并将实现了Runnable接口的对象传递给Thread对象。

实现Callable接口:
Callable接口与Runnable接口类似,但它可以返回执行结果,并且允许抛出受检查的异常。

  • 优势:支持返回结果和抛出异常,适用于需要获取线程执行结果的场景。
  • 缺点:相对于Runnable,使用起来略显复杂,需要结合FutureTask和ExecutorService来使用。
Callable接口允许抛出受检查的异常的理解
什么是受检查异常

Checked Exception 即受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。比如下面这段 IO 操作的代码:
在这里插入图片描述

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有:IO 相关的异常、ClassNotFoundException、SQLException…。

import java.util.concurrent.Callable;

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 在call方法中可能会抛出受检查异常
        try {
            // 可能抛出异常的代码
            return performSomeTask();
        } catch (Exception e) {
            // 捕获异常并处理
            throw new Exception("Task execution failed", e);
        }
    }

    private Integer performSomeTask() throws InterruptedException {
        // 模拟任务执行
        Thread.sleep(1000);
        return 42;
    }

    public static void main(String[] args) {
        MyCallable callable = new MyCallable();
        try {
            Integer result = callable.call();
            System.out.println("Result: " + result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在上面的例子中,call 方法声明了可能抛出异常 Exception,并在内部捕获并重新抛出异常,以确保异常被适当处理。

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        try {
            // 可能抛出异常的代码
            performSomeTask();
        } catch (InterruptedException e) {
            // 捕获InterruptedException异常
            e.printStackTrace();
            Thread.currentThread().interrupt(); // 重新设置中断状态
        }
    }

    private void performSomeTask() throws InterruptedException {
        // 模拟任务执行
        Thread.sleep(1000);
    }

    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread completed");
    }
}

在上述 MyRunnable 类中,run 方法内部捕获了可能抛出的 InterruptedException,并在方法内部处理。因为 run 方法无法声明抛出 InterruptedException,所以需要在方法内部捕获和处理这个异常。

2. Future是什么

在 Java 中,Future 类只是一个泛型接口,位于 java.util.concurrent 包下,其中定义了 5 个方法,主要包括下面这 4 个功能:

  • 取消任务;
  • 判断任务是否被取消;
  • 判断任务是否已经执行完成;
  • 获取任务执行结果。
// V 代表了Future执行的任务返回值的类型
public interface Future<V> {
   // 取消任务执行
   // 成功取消返回 true,否则返回 false
   boolean cancel(boolean mayInterruptIfRunning);
   // 判断任务是否被取消
   boolean isCancelled();
   // 判断任务是否已经执行完成
   boolean isDone();
   // 获取任务执行结果
   V get() throws InterruptedException, ExecutionException;
   // 指定时间内没有返回计算结果就抛出 TimeOutException 异常
   V get(long timeout, TimeUnit unit)

       throws InterruptedException, ExecutionException, TimeoutExceptio

}

简单理解就是:我有一个任务,提交给了 Future 来处理。任务执行期间我自己可以去做任何想做的事情。并且,在这期间我还可以取消任务以及获取任务的执行状态。一段时间之后,我就可以 Future 那里直接取出任务执行结果。

Callable 和 Future 有什么关系?

我们可以通过 FutureTask 来理解 Callable 和 Future 之间的关系。FutureTask 提供了 Future 接口的基本实现,常用来封装 Callable 和 Runnable,具有取消任务、查看任务是否执行完成以及获取任务执行结果的方法。ExecutorService.submit() 方法返回的其实就是 Future 的实现类 FutureTask 。

<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);

FutureTask 不光实现了 Future接口,还实现了Runnable 接口,因此可以作为任务直接被线程执行。
在这里插入图片描述
FutureTask 有两个构造函数,可传入 Callable 或者 Runnable 对象。实际上,传入 Runnable 对象也会在方法内部转换为Callable 对象。

public FutureTask(Callable<V> callable) {
	if(callable==null){
		throw new NullPointerException();
	}
	this.callable = callable;
	this.state = NEW;
}
public FutureTask(Runnable runnable,V result){
	// 通过适配器RunnableAdapter 来将Runnable对象runnable转换成callable对象
	this.callable = Excutors.callable(runnable,result);
	this.state = NEW;
}

FutureTask相当于对Callable 进行了封装,管理着任务执行的情况,存储了 Callable 的 call 方法的任务执行结果。

CompletableFuture类有什么用?

Future 在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 get() 方法为阻塞调用。

Java 8 才被引入CompletableFuture 类可以解决Future 的这些缺陷。CompletableFuture 除了提供了更为好用和强大的 Future 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。
下面我们来简单看看 CompletableFuture 类的定义。

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
}

可以看到,CompletableFuture 同时实现了 Future 和 CompletionStage 接口。
在这里插入图片描述
CompletionStage 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。CompletionStage 接口中的方法比较多,CompletableFuture 的函数式能力就是这个接口赋予的。从这个接口的方法参数你就可以发现其大量使用了 Java8 引入的函数式编程。
在这里插入图片描述
下面是一个使用CompletableFuture类的简单例子:

import java.util.concurrent.CompletableFuture;

public class Main {
    public static void main(String[] args) {
        // 创建一个CompletableFuture对象,并指定异步计算任务
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 模拟一个耗时任务
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 返回计算结果
            return "Task completed successfully";
        });

        // 主线程可以在任务执行的同时做其他事情
        System.out.println("Main thread is doing something...");

        // 注册回调函数,当任务完成时执行该回调函数
        future.thenAccept(result -> {
            // 输出任务的结果
            System.out.println("Task result: " + result);
        });

        // 主线程可以继续做其他事情,不必等待任务完成

        // 阻塞主线程,以等待任务完成
        future.join();
    }
}

在这个例子中,我们使用CompletableFuture.supplyAsync()方法创建了一个CompletableFuture对象,并指定了一个异步计算任务,该任务会在一个新的线程中执行。主线程可以在任务执行的同时做其他事情,然后使用thenAccept()方法注册了一个回调函数,当任务完成时会执行该回调函数,并在回调函数中输出任务的结果。最后,我们通过join()方法阻塞主线程,以等待任务的完成。CompletableFuture提供了丰富的方法来处理异步计算的结果和执行过程,可以更加灵活地控制异步任务的执行流程。

结合线程使用CompletableFuture可以实现更复杂的异步操作,例如在多个任务之间进行协调、组合多个异步操作的结果等。下面是一个简单的示例,演示了如何使用线程和CompletableFuture结合实现并行异步任务:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        // 创建一个线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 异步任务1
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            // 模拟一个耗时任务
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Result of Task 1";
        }, executorService);

        // 异步任务2
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            // 模拟一个耗时任务
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Result of Task 2";
        }, executorService);

        // 组合两个异步任务的结果
        CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {
            // 模拟对两个任务结果的处理
            return result1 + " and " + result2;
        });

        // 注册回调函数,当组合任务完成时执行该回调函数
        combinedFuture.thenAccept(result -> {
            System.out.println("Combined result: " + result);
        });

        // 主线程可以继续做其他事情,不必等待任务完成

        // 关闭线程池
        executorService.shutdown();
    }
}

在这个例子中,我们创建了一个固定大小为2的线程池,然后分别定义了两个异步任务future1和future2,它们会在不同的线程中执行。接着,使用thenCombine()方法组合了这两个异步任务的结果,并在组合任务完成时注册了一个回调函数。最后,关闭了线程池。

通过结合线程和CompletableFuture,我们可以更加灵活地实现并行异步任务的执行和结果处理。

ThreadLocal

ThreadLocal 是什么

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量就可以使用ThreadLocal。

ThreadLocal 怎么用
import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable{

     // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        formatter.set(new SimpleDateFormat());

        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }

}

执行结果

Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 2 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = yy-M-d ah:mm
Thread Name= 4 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 6 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 7 formatter = yy-M-d ah:mm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mm
ThreadLocal 底层原理

从 Thread类源代码入手。

public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

从上面Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法。
ThreadLocal类的set()方法

public void set(T value) {
    //获取当前请求的线程
    Thread t = Thread.currentThread();
    //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 将需要存储的值放入到这个哈希表中
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

通过上面这些内容,我们足以通过猜测得出结论:最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。
每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

ThreadLocal 的内存泄漏问题:

在这里插入图片描述
这里假设将 ThreadLocal 定义为方法中的局部变量,那么当线程进入该方法的时候,就会将 ThreadLocal 的引用给加载到线程的栈 Stack 中

如上图所示,在线程栈 Stack 中,有两个变量,ThreadLocalRef 和 CurrentThreadRef,分别指向了声明的局部变量 ThreadLocal ,以及当前执行的线程

而 ThreadLocalMap 中的 key 是弱引用,当线程执行完该方法之后,Stack 线程栈中的 ThreadLocalRef 变量就会被弹出栈,因此 ThreadLocal 变量的强引用消失了,那么 ThreadLocal 变量只有 Entry 中的 key 对他引用,并且还是弱引用,因此这个 ThreadLocal 变量会被回收掉,导致 Entry 中的 key 为 null,而 value 还指向了对 Object 的强引用,因此 value 还一直存在 ThreadLocalMap 变量中,由于 ThreadLocal 被回收了,无法通过 key 去访问到这个 value,导致这个 value 一直无法被回收,ThreadLocalMap 变量的生命周期是和当前线程的生命周期一样长的,只有在当前线程运行结束之后才会清除掉 value,因此会导致这个 value 一直停留在内存中,导致内存泄漏

当然 JDK 的开发者想到了这个问题,在使用 set get remove 的时候,会对 key 为 null 的 value 进行清理,使得程序的稳定性提升。

当然,我们要保持良好的编程习惯,在线程对于 ThreadLocal 变量使用的代码块中,在代码块的末尾调用 remove 将 value 的空间释放,防止内存泄露。

ThearLocal 内存泄漏的根源是:

由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏

ThreadLocal 正确的使用方法:

每次使用完 ThreadLocal 都调用它的 remove() 方法清除数据
将 ThreadLocal 变量定义成 private static final,这样就一直存在 ThreadLocal 的强引用,也能保证任何时候都能通过 ThreadLocal 的弱引用访问到 Entry 的 value 值,进而清除掉

那么 ThreadLocal 为什么要将 key 设计为弱引用呢?

允许垃圾回收器回收未使用的 ThreadLocal 实例,当一个 ThreadLocal 实例不再被任何地方强引用时,它就变得不可达了。如果ThreadLocal它是强引用,那么即使线程中已经没有对这个 ThreadLocal 实例的操作,它也会始终存在于 ThreadLocalMap 中,导致无法被垃圾回收,从而造成内存泄漏。
使用弱引用后,当 ThreadLocal 实例没有其他强引用时,它可以被垃圾回收,这样就不会长时间占用内存。

那么 ThreadLocal 为什么要不将 value 设计为弱引用呢?
保证数据的可访问性,由于 ThreadLocal 的 key 被设计成弱引用,这意味着 ThreadLocal 实例可能会被回收。但是,只要线程还活着并且线程中的 ThreadLocalMap 条目还存在,我们就希望能够访问到相应的数据(value)。如果 value 也是弱引用,那么一旦垃圾回收触发,value 也可能会被回收掉,这就违背了 ThreadLocal 的设计初衷——为每个线程提供独立的、持久的变量副本。

ThreadLocal 是 Java 中用于实现线程本地存储的类。每个使用 ThreadLocal 变量的线程都有自己独立初始化的副本,这样可以避免多个线程间的共享和竞争,从而提供线程隔离。

ThreadLocal 脏数据问题?

  1. 未清理的旧数据:
    如果一个线程使用完 ThreadLocal 变量后没有显式地清理(调用 remove 方法),那么该线程在之后的运行过程中可能会继续使用到之前的值,而这些值可能已经不再适用当前上下文,造成“脏数据”。
  2. 线程复用导致的脏数据:
    在使用线程池时,线程会被复用。如果一个线程在上一次任务中使用了 ThreadLocal 变量且没有清理,那么在下一次新任务中,该线程可能会带着旧任务的数据,导致“脏数据”问题。

如何避免 ThreadLocal 的脏数据问题

  1. 使用完毕后手动清理:
    在任务结束或者线程不再需要使用 ThreadLocal 变量时,明确调用 ThreadLocal 对象的 remove 方法,确保清除该线程副本中的数据。例如:
ThreadLocal<MyObject> threadLocal = new ThreadLocal<>();

try {
    MyObject obj = threadLocal.get();
    // 使用obj做一些操作
} finally {
    threadLocal.remove();
}
  1. 线程池管理和清理:
    在线程池中使用 ThreadLocal 时,更要格外小心,确保在任务完成后对 ThreadLocal 变量进行清理,以防止下一个任务复用线程时带来脏数据。

总结一下:

那么这里总结一下,将 ThreadLocal 定义为局部变量,会导致方法执行完之后 ThreadLocal 被回收,而 value 没有被回收,导致无法通过 key 访问到这个 value,导致内存泄漏

如果规范使用,将 ThreadLocal 定义为 private static final,那么这个 ThreadLocal 不会被回收,可以随时通过这个 ThreadLocal 去访问到 value,随时可以手动回收,因此不会内存泄漏,但是会导致脏数据

所以在 ThreadLocal 的内存泄漏问题主要是针对将 ThreadLocal 定义为局部变量的时候,如果不手动 remove 可能会导致 ThreadLocalMap 中的 Entry 对象无法回收,一直占用内存导致内存泄漏,直到当前 Thread 结束之后才会被回收

这里再说一下 ThreadLocal 的使用规范就是:将 ThreadLocal 变量定义为 private static final,并且在使用完,记得通过 try finall 来 remove 掉,避免出现脏数据

3. 线程池

线程池常用方法

Java 中的线程池主要由 java.util.concurrent.ExecutorService 接口定义,常用的方法包括:

  1. submit(Runnable task): 提交一个可运行的任务,并返回一个表示该任务的未来结果的 Future 对象。
  2. submit(Callable task): 提交一个可调用的任务,并返回一个表示该任务的未来结果的 Future 对象。
  3. shutdown(): 优雅地关闭线程池,即不再接受新的任务,等待已提交的任务执行完成后关闭。
  4. shutdownNow(): 立即关闭线程池,尝试取消所有未完成的任务,并且不再执行已提交但尚未开始的任务。
  5. awaitTermination(long timeout, TimeUnit unit): 阻塞当前线程,直到线程池中所有任务都执行完成,或者等待超时。
  6. isShutdown(): 判断线程池是否已经调用了 shutdown() 方法。
  7. isTerminated(): 判断线程池是否已经调用了 shutdown() 方法,并且所有任务都已经执行完成。
  8. execute(Runnable command): 提交一个可运行的任务,但没有返回值。

除了 ExecutorService 接口定义的方法外,ThreadPoolExecutor 类还提供了一些额外的方法用于获取线程池的状态、调整线程池的参数等

ExecutorService和ThreadPoolExecutor的关系

ThreadPoolExecutor 是 ExecutorService 接口的一个具体实现类。换句话说,ThreadPoolExecutor 实现了 ExecutorService 接口,因此可以将 ThreadPoolExecutor 实例视为 ExecutorService 接口的一种具体实现。

ExecutorService 接口定义了一组操作线程池的方法,如提交任务、关闭线程池等。而 ThreadPoolExecutor 则是其中的一个实现,它提供了一个可配置的线程池,可以设置核心线程数、最大线程数、线程空闲时间等参数。

因此,可以通过以下方式来理解它们之间的关系:

  • ExecutorService 定义了线程池应该具备的行为和功能
  • ThreadPoolExecutor 是按照 ExecutorService 接口的规范实现的一个具体线程池,提供了对线程池的实际控制和管理。

通过使用 ThreadPoolExecutor,我们可以直接操作线程池的各种参数,如核心线程数、最大线程数、线程工厂等,来满足不同的应用场景的需求。

3.1 线程池中线程异常后,销毁还是复用?

先说结论,需要分两种情况:

  • 使用execute()提交任务:当任务通过execute()提交到线程池并在执行过程中抛出异常时,如果这个异常没有在任务内被捕获,那么该异常会导致当前线程终止,并且异常会被打印到控制台或日志文件中。线程池会检测到这种线程终止,并创建一个新线程来替换它,从而保持配置的线程数不变。
  • 使用submit()提交任务:对于通过submit()提交的任务,如果在任务执行中发生异常,这个异常不会直接打印出来。相反,异常会被封装在由submit()返回的Future对象中。当调用Future.get()方法时,可以捕获到一个ExecutionException。在这种情况下,线程不会因为异常而终止,它会继续存在于线程池中,准备执行后续的任务。

简单来说:使用execute()时,未捕获异常导致线程终止,线程池创建新线程替代;使用submit()时,异常被封装在Future中,线程继续复用。
这种设计允许submit()提供更灵活的错误处理机制,因为它允许调用者决定如何处理异常,而execute()则适用于那些不需要关注执行结果的场景。

案例:多线程加速PDF转换

1. 改进前

未使用多线程的转换方式,pdf文件将使用单线程的方式从第一页到最后一页进行转换。对于比较大的pdf文件。转换速度较慢,需要6分钟左右。

  public List<File> pdf2Image(File pdfFile, int dpi) throws IOException {
    List<File> outFileList = Lists.newArrayList();
    // try-with-resource
    try (PDDocument pdDocument = PDDocument.load(pdfFile)) {
      PDFRenderer pdfRenderer = new PDFRenderer(pdDocument);
      int pages = pdDocument.getNumberOfPages();
      for (int i = 0; i < pages; i++) {
        File dstFile = new File(FilenameUtils.getBaseName(pdfFile.getName()) + "_" + i + ".jpg");
        BufferedImage image = pdfRenderer.renderImageWithDPI(i, dpi);
        ImageIO.write(image, "jpg", dstFile);
        outFileList.add(dstFile);
      }
    }
    return outFileList;
  }
2.改进后

多线程实现PDF转换为图片。每个线程负责转换pdf文件的某几页。最终将所有线程转换的结果进行汇总,得到完整被转换的pdf文件。

  public List<File> pdfTurnImage(File pdfFile, int dpi){

    List<File> fileImageList = null;
    try(PDDocument pdDocument = PDDocument.load(pdfFile)) {
      ExecutorService executorService =
              new ThreadPoolExecutor(5, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(50));
      int pages = pdDocument.getNumberOfPages();
      log.info("pdf文件共有"+ pages +"页文件");
      fileImageList = new ArrayList<>(pages);
      for(int i=0;i<pages;i++) {
        fileImageList.add(null);
      }
      List<Future<Boolean>> futureTaskList =new ArrayList<>();
      for(int i=0; i<pages; i++) {
        log.info("读取转换第"+ (i+1) +"页文件");
        Pdf2ImageTask pdf2ImageTask = new Pdf2ImageTask(fileImageList, i, i, pdfFile, dpi);
        futureTaskList.add(executorService.submit(pdf2ImageTask));
      }

      for (Future<Boolean> future : futureTaskList) {
        if(future.get()) {
          future.cancel(true);
        }
      }
      executorService.shutdown();
    } catch (Exception e) {
      log.error("pdf转图片失败,异常e:"+e.getMessage());
      throw new YunkeExceptionWithRetData(ResultCodeEnum.PROCESS_ERROR, "pdf转图片失败");
    }

    return fileImageList;
  }
import java.awt.image.BufferedImage;
import java.io.File;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.Callable;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;

import javax.imageio.ImageIO;

@Slf4j
public class Pdf2ImageTask implements Callable<Boolean> {

    /**
     * 图片文件列表
     */
    private List<File> imageList;
    /**
     * 图片存放集合位置
     */
    private int listPosition;
    /**
     * 页码
     */
    private int pageNumbers;

    /**
     * pdf文件
     */
    private File pdfFile;

    /**
     * pdf转图片的dpi
     */
    private Integer dpi;

    public Pdf2ImageTask(List<File> imageList, int pageNumbers, int listPosition,File pdfFile,Integer dpi) {
        this.imageList = imageList;
        this.listPosition = listPosition;
        this.pageNumbers = pageNumbers;
        this.pdfFile = pdfFile;
        this.dpi = dpi;
    }

    @Override
    public Boolean call() {
        try(PDDocument pdDocument = PDDocument.load(pdfFile)) {
            PDFRenderer renderer = new PDFRenderer(pdDocument);
            log.info( LocalDateTime.now()+"线程开始执行,转换第"+pageNumbers+"页文件");
            BufferedImage image = renderer.renderImageWithDPI(pageNumbers, dpi);
            File dstFile = new File(FilenameUtils.getBaseName(pdfFile.getName()) + "_" + listPosition + ".jpg");
            ImageIO.write(image, "jpg", dstFile);
            imageList.set(listPosition, dstFile);
            log.info( LocalDateTime.now()+"线程执行结束,转换第"+pageNumbers+"页文件");
            return true;
        } catch (Exception e) {
            log.error( LocalDateTime.now()+"线程执行异常结束"+e.getMessage(),e);
        }
        return false;
    }
}

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值