快过年了,又该复习线程池了

2024-01-26 09:22:34 浏览数 (1)

前言

又到了复习八股的时间,本文根据一些常见问题进行了一下总结。

一、线程池

1.1 为什么需要线程池?

线程池主要⽤来管理⼦线程,它的优势在于:

  1. 重⽤线程池中的线程,避免频繁创建和销毁线程所带来的内存开销。
  2. 提高响应速度,当任务到达时,任务可以不需要等到线程创建就 能立即执行。
  3. 有效控制线程的最大并发数,避免因线程之间抢占资源⽽导致的阻塞现象。
  4. 提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不 仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进行统⼀的分配,调优和监控。 线程池中是以生产者消费者模式,通过⼀个阻塞队列来实现的,阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

线程池中是以⽣产者消费者模式,通过⼀个阻塞队列来实现的,阻塞队列缓存任务,⼯作线程从阻塞队列中获取任务。

1.2 线程池的种类

FixedThreadPool 固定线程数的线程池

  • 线程数量固定且都是核心线程,核心线程不会被回收,以快速响应外界请求,但是没有超时机制,任务队列也没有大小限制。
  • 新任务使⽤核心线程处理,如果没有空闲的核心线程,则会排队等待执行

CachedThreadPool 按需创建的线程池

  • 线程数量不定,只有非核心线程,最⼤线程数为 Integer.MAX_VALUE,
  • 有新任务时使⽤空闲线程执⾏,没有空闲线程则创建新的线程来 处理,该线程池的每个空闲线程都有超时机制,时⻓为60s,空闲超过60s则会回收空闲线程。
  • 适合执行大量的耗时较少的任务,当所有线程闲置超过60s都会被停止,所以这时几乎不占用系统资源。

SingleThreadExecutor 单线程的线程池

  • 只有⼀个核⼼线程,所有任务在同⼀个线程中按顺序执⾏,所以 不需要处理线程同步的问题。

ScheduledThreadPool 定时和周期性的线程池

核心线程数量固定,非核心线程数量最大为 Integer.MAX_VALUE,当非核⼼线程闲置超过10s会被回收,主要⽤于执行定时任务和具有固定周期的重复任务。

1.3 线程池的构造参数

  1. corePoolSize(核⼼线程数): 默认情况下线程池是空的,只是任务提 交时才会创建线程,如果当前运⾏的线程数少于corePoolSize,则会创 建新线程来处理任务,如果⼤于或者等于corePoolSize,则不再创建。 如果调⽤线程池的preStartAllCoreThread⽅法,线程池会提前创建并启动所有的核心线程来执行等待任务。
  2. maximumPoolSize(线程池允许创建的最⼤线程数): 如果任务队列 满了并且线程数小于maximumPoolSize时,线程池仍然会创建新的线程 来处理任务。
    • CPU密集型 尽量使用较小的线程池,一般CPU核心数 1 因为CPU密集型任务CPU的使用率很高,若开过多的线程,只能增加线程上下文的切换次数,带来额外的开销。
    • IO密集型 方法一:可以使用较大的线程池,一般CPU核心数 * 2,IO密集型CPU使用率不高,可以让CPU等待IO的时候处理别的任务,充分利用cpu时间 。 方法二:线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。 最佳线程数目 = CPU 核心数 *(1 平均等待时间/平均工作时间)
    • 混合型 可以将任务分为CPU密集型和IO密集型,然后分别使用不同的线程池去处理,按情况而定 。
  3. keepAliveTime(⾮核⼼线程闲置的超时时间): 超过这个时间则回 收。另外,如果设置allowCoreThreadTimeOut属性为true时, keepAliveTime也会应⽤到核⼼线程上。线程池keepAliveTime参数是在 内部是怎么实现超时回收的? 通过阻塞队列 workQueue 的 poll ⽅法,当超过 keepAliveTime 的时 候后还⽆法获取新的任务,则返回 null,最后在 runWorker ⽅法中 结束线程整个⽣命。 poll(time):取⾛BlockingQueue⾥排在⾸位的对象,若不能⽴即取 出,则可以等time参数规定的时间,取不到时返回null。
  4. TimeUnit(时间单位): keepAliveTime参数的时间单位。可选的单位有天Days、 ⼩时HOURS、分钟MINUTES、秒SECONDS、毫秒MILLISECONDS等。
  5. workQueue(任务队列): 如果当前线程数⼤于corePoolSzie,则将任 务添加到此任务队列中,该任务队列是BlockingQueue类型的,即阻塞队列。
  • 阻塞队列实现原理
  • BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插⼊数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。
  • 其实阻塞队列实现阻塞同步的方式很简单,使⽤BlockingQueue封装了根据条件阻塞线程的过程,而我们就不⽤关心繁琐的await/signal 操作了。
  1. ThreadFactory(线程⼯⼚): 可以使⽤线程⼯⼚给每个创建出来的线程设置名字,⼀般情况下⽆须设置该参数。
  2. RejectedExecutionHandler(拒绝策略): 当前任务队列和线程池都满了时所采取的应对策略,默认是AbortPolicy,表示⽆法处理新任 务,并抛出RejectedExecutionException异常。此外,拒绝策略还有其它 三种:
  • DiscardPolicy: 直接把当前任务丢弃。
  • DiscardOldestPolicy: 丢弃队列⾥最⽼的⼀个任务,并执行当前的任务。
  • CallerRunsPolicy: ⽤调⽤者所在的线程来处理任务。它提供简单的反馈控制机制,能够减缓新任务的提交速度。

1.4 执行任务流程

  1. 启动核心线程执行任务:如果线程池中的线程数量未达到核心线程数(Core Pool Size),线程池会直接启动一个新的核心线程来执行提交的任务。
  2. 任务进入队列:如果线程池中的线程数量已经达到或超过核心线程数,新提交的任务将被插入到线程池的工作队列中等待执行。工作队列通常是一个阻塞队列,用于存放等待执行的任务。
  3. 队列已满时启动非核心线程:如果任务队列已满,无法接受新任务,且当前线程总数还未达到线程池定义的最大线程数(Maximum Pool Size),线程池将创建一个新的非核心线程来执行任务。
  4. 执行拒绝策略:如果当前线程总数已达到最大线程数,线程池将无法处理新提交的任务,此时会执行拒绝策略(RejectedExecutionHandler)。拒绝策略是开发者可配置的,常见的如抛出异常、直接丢弃任务、使提交任务的线程自己执行任务等。

1.5 线程安全

什么是线程安全?

线程安全是指在多线程环境下,当多个线程访问某个类的实例时,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要额外的同步或协调操作,这个类都能表现出正确的行为。具体来说:

  • 原子操作:如果一个类或程序提供的接口能够保证在多线程环境中的单个操作是不可中断的,即这些操作要么完全执行,要么完全不执行,那么这个操作是原子的,从而是线程安全的。
  • 不变性:如果一个对象在被创建后其内部状态和数据不会再发生变化,即使有多个线程同时访问,它也自然是线程安全的。
  • 同步机制:当涉及到对共享数据的读写操作时,如果多个线程对数据的操作存在竞争关系(即至少有一个线程在写),为保证线程安全,通常需要通过同步机制(如synchronized,Locks,原子变量等)来确保只有一个线程能够在同一时间对数据进行操作。
  • 局部变量与线程封闭:局部变量存储在每个线程自己的栈中,因此自然是线程安全的。利用线程封闭的技术,可以确保对象只能被单个线程访问,也是实现线程安全的一种有效方式。

常见线程安全机制

互斥同步锁(悲观锁)

悲观锁是一种在多线程环境下常用的同步策略,与乐观锁相对。悲观锁的核心思想是假设最坏的情况,即总是认为多个线程同时修改同一数据的冲突是很常见的,因此在访问任何共享资源之前都需要先加锁,这样可以确保同时只有一个线程能够访问到共享资源。悲观锁的特点是先加锁再访问数据。

  1. Synchronized **互斥同步锁:**synchronized 关键字在 Java 中用于实现互斥同步锁。它确保同一时刻只有一个线程能够访问被锁定的代码段或对象。 **简易性:**synchronized 是基于 JVM 层面的锁机制,使用方便,不需要显式地创建和释放锁。 **性能提升:**在 JDK 6 及以后,synchronized 的性能得到了显著提升,尤其在锁竞争不激烈的情况下,其性能与 ReentrantLock 相近。

使用方法:

代码语言:javascript复制
//同步方法
//在方法声明上添加 synchronized 关键字,使得整个方法成为同步方法。
public synchronized void syncMethod() {
    // 同步操作
}


//同步代码块
//使用 synchronized 关键字同步一个对象实例。
public void method() {
    synchronized(this) {
        // 同步操作
    }
}

//或者同步一个类的类对象来实现类级别的锁定。
public void staticMethod() {
    synchronized(MyClass.class) {
        // 同步操作
    }
}
  1. ReentrantLock **API 层面锁:**ReentrantLock 是 Java 并发包 java.util.concurrent.locks 中提供的一种互斥同步锁。

使用方法

代码语言:javascript复制
import java.util.concurrent.locks.ReentrantLock;

public class MyObject {
    private final ReentrantLock lock = new ReentrantLock();

    public void method() {
        lock.lock(); // 获取锁
        try {
            // 同步操作
        } finally {
            lock.unlock(); // 释放锁
        }
    }
}


//ReentrantLock 可以设置为公平锁,使等待时间最长的线程优先获得锁。
private final ReentrantLock fairLock = new ReentrantLock(true);

public void fairMethod() {
    fairLock.lock();
    try {
        // 同步操作
    } finally {
        fairLock.unlock();
    }
}
synchronizedReentrantLock区别
  1. 底层实现 synchronized:是 JVM 层面的锁,是 Java 的关键字,它是基于监视器对象(Monitor)实现的,涉及到 monitorenter 和 monitorexit 两个指令。 ReentrantLock:是从 JDK 1.5 开始提供的 API 层面的锁,属于 java.util.concurrent.locks.Lock 接口的实现类。
  2. 实现原理 synchronized:它的实现涉及到锁的升级过程,包括无锁状态、偏向锁、轻量级锁(自旋锁)和重量级锁(操作系统层面的锁)。 ReentrantLock:通过 CAS(Compare-And-Swap)操作实现原子性保证,并利用 volatile 变量确保可见性,从而实现锁的功能。
  3. 手动释放 synchronized:不需要用户手动释放锁,当 synchronized 代码块或方法执行完毕后,JVM 会自动释放锁。 ReentrantLock:需要用户手动释放锁,如果未释放,可能会导致死锁。
  4. 可中断性 synchronized:不可中断,除非执行完毕或方法抛出异常。 ReentrantLock:提供了中断等待锁的能力,可以通过 tryLock(long timeout, TimeUnit unit) 实现超时等待,或者使用 lockInterruptibly() 在响应中断。
  5. 公平性 synchronized:是非公平锁,它不保证等待锁的线程获得锁的顺序。 ReentrantLock:可以是公平锁也可以是非公平锁,通过传入布尔值到构造函数来选择。公平锁会按照等待队列中的顺序分配锁,但性能相对较低。
非阻塞同步锁(乐观锁)

乐观锁是一种在多线程环境下进行同步的机制,与悲观锁相对。它基于这样一种假设:在大多数时间里,数据通常不会发生冲突,因此不需要加锁。相反,它会在数据更新时进行冲突检测,从而提高系统的并发能力。Java 中的乐观锁主要通过以下两种方式实现:

  1. **版本号机制 **版本号管理:通常是在数据表中增加一个版本字段(如 version),用于记录数据的版本。 操作流程:读取数据时,同时读取版本号。更新时,比较当前版本号与数据库中存储的版本号,只有当两者一致时,才执行更新,并将版本号加一。如果版本号不一致,表明数据在读取后已被其他线程修改,更新操作将被拒绝。
  2. CAS(Compare And Swap) 核心思想:CAS 操作包含三个参数:内存位置 V(需要读写的变量)、预期原值 A 和新值 B。当且仅当位置 V 的值等于 A 时,才将 V 的值更新为 B,否则不进行操作。CAS 保证了比较和替换是作为一个原子操作执行的。 应用实例:在 java.util.concurrent.atomic 包下的原子变量类,如 AtomicInteger,就是应用了 CAS 算法实现的乐观锁。 自旋操作:通常 CAS 操作会伴随自旋,即在更新失败时重试,直到更新成功或者达到一定的重试限制。
自旋锁

自旋锁是一种锁机制,它在等待获取锁的过程中保持线程在“忙等”(busy-waiting)状态。这意味着线程不会立即进入阻塞状态,而是在一个循环中反复检查锁是否可用。

  1. 实现方式
  • 锁占用时间短:如果预期锁被占用的时间非常短,使用自旋锁可以避免线程从用户态切换到内核态,减少上下文切换的开销。
  • 自旋等待时间:自旋锁需要设定一个合理的等待时间或自旋次数,以防止长时间占用 CPU。

二、拓展

Callable、Future、FutureTash详解

Callable

Callable 是一个接口,类似于 Runnable 接口。与 Runnable 不同的是,Callable 允许任务返回值,并且可以抛出异常。 返回值:Callable 使用泛型来指定任务执行后的返回类型。 异常处理:Callable 允许在任务中抛出受检查的异常。

代码语言:javascript复制
Callable<Integer> task = () -> {
    // 执行任务,返回结果
    return 123;
};
Future

Future 用于表示异步计算的结果。它提供了检查计算是否完成的方法,以及等待计算完成并检索其结果的方法。 **结果检索:**可以通过 get 方法获取 Callable 返回的结果,此方法会阻塞直到结果可用。 **状态检查:**提供了方法来检查任务是否完成(如 isDone)或取消(如 cancel)。

代码语言:javascript复制
// 使用 Callable 和 Future
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(() -> {
    // 一些耗时的操作
    return 123;
});
Integer result = future.get(); // 获取结果
FutureTask

FutureTaskFuture 的一个具体实现,它同时实现了 FutureRunnable 接口。这意味着它可以直接提交给 Thread 对象执行。

  • 任务封装FutureTask 可以包装 CallableRunnable 对象。对于 Runnable,它会忽略计算的结果。
  • 多功能性:由于 FutureTask 实现了 Runnable,它既可以作为任务执行,也可以用来获取执行结果。
代码语言:javascript复制
// 使用 FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(() -> 123);
Thread thread = new Thread(futureTask);
thread.start();
Integer result = futureTask.get(); // 获取结果
三者之间的关系:

CallableRunnable封装的异步运算任务。 Future用来保存Callable异步运算的结果 FutureTask封装Future的实体类。

Callable与Runnbale的区别
  • Callable定义的方法是call,而Runnable定义的方法是run
  • call方法有返回值,而run方法是没有返回值的。
  • call方法可以抛出异常,而run方法不能抛出异常。
使用场景
  • Callable 和 Future:当需要执行异步任务并且要获取返回结果时使用。比如执行一些耗时的计算任务。
  • FutureTask:在需要 Future 的特性,同时要将任务提交给 Thread 对象执行时使用。它提供了一种方便的机制来启动、管理异步任务。

volatile的适用场景

状态标志

当一个变量被多个线程共享,并且这个变量用于指示某种状态(如线程是否运行),可以将其声明为 volatile。例如,一个线程控制开关:

代码语言:javascript复制
volatile boolean keepRunning = true;

public void run() {
    while (keepRunning) {
        // 执行任务
    }
}
实现线程安全的单例模式

在双重检查锁定(Double-Checked Locking)单例模式中,volatile 可以防止实例化对象时的指令重排序,确保其他线程看到的对象是完全初始化过的:

代码语言:javascript复制
public class Singleton {
    private volatile static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
独立观察

用于定期更新的变量,这些变量的最新值需要被其他线程可见。比如,在一个线程中定期更新数据,在其他线程中读取这些数据。

代码语言:javascript复制
public class EnvironmentMonitor {
    public volatile int currentTemperature;

    public void updateTemperature() {
        // 更新温度值
        this.currentTemperature = readFromSensor();
    }

    // 其他线程可以安全读取 currentTemperature
}
"volatile bean” 模式

volatile bean" 模式是一种在 Java 并发编程中使用 volatile 关键字的模式,用于确保 JavaBean 中的属性在多线程环境下能够安全地进行读写操作。

代码语言:javascript复制
public class Person {
    private volatile String first;
    private volatile String last;
    private volatile int age;

    public String getFirst() { return first; }

    public String getLast() { return last; }

    public int getAge() { return age; }

    public void setFirst(String first) {
        this.first = first;
    }

    public void setLast(String last) {
        this.last = last;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
开销较低的“读-写锁”策略

这个模式是针对那些读操作远远多于写操作的场景。它通过结合使用内部锁(synchronized)和 volatile 变量来减少公共代码路径的开销。

  • 使用 synchronized 实现写操作:保证写操作(如增量操作)是原子的。
  • 使用 volatile 实现读操作:确保读操作能够看到最新的结果。
代码语言:javascript复制
public class SafeCounter {
    private volatile int count = 0;

    public synchronized void increment() {
        count  ;
    }

    public int getCount() {
        return count;
    }
}

synchronized和volatile区别

  1. 内存可见性和同步
  • volatile: 确保变量对所有线程的可见性。当一个线程修改了一个 volatile 变量时,该变化会立即被更新到主内存中,其他线程读取时直接从主存中获取,确保能够看到最新的值。
  • synchronized: 除了保证内存可见性外(通过在释放锁之前将变更刷新到主内存),还提供互斥性,确保同一时刻只有一个线程能执行同步代码块或方法。
  1. 应用范围
  • volatile: 只能作用于变量级别。
  • synchronized: 可以作用于变量、方法以及整个类级别。
  1. 线程阻塞
  • volatile: 不会造成线程阻塞。线程访问 volatile 变量不会被挂起。
  • synchronized: 可能会导致线程阻塞。当多个线程争夺 synchronized 锁时,未获得锁的线程会被挂起。
  1. 原子性和操作完整性
  • volatile: 仅保证变量修改的可见性,不保证复合操作的原子性。
  • synchronized: 既保证可见性,也保证原子性。当线程进入同步代码块或方法时,其他线程必须等待,从而保证了操作的完整性和一致性。
  1. 编译器优化和指令重排
  • volatile: 标记的变量不会被编译器优化,能防止指令重排序。
  • synchronized: 标记的代码块在优化时会考虑同步的需求,但 JVM 会尝试优化锁的获取和释放过程,比如通过锁消除和锁粗化。

死锁

产生
  • 互斥条件:资源不能被多个线程或进程共享,只能由一个线程或进程独占。
  • 占有且等待:一个线程或进程至少已经持有一个资源,同时又在等待获取另一个由其他线程或进程占有的资源。
  • 不可抢占:已经分配给线程或进程的资源不能被强制抢占,只能由占有它的线程或进程主动释放。
  • 循环等待:存在一个等待循环,其中每个线程或进程都在等待另一个线程或进程所持有的资源。
代码语言:javascript复制
//假设有两个线程(线程 A 和线程 B)和两个资源(资源 1 和资源 2):
//线程 A 持有资源 1,并等待资源 2。
//同时,线程 B 持有资源 2,并等待资源 1。

public class DeadlockDemo {
    public static void main(String[] args) {
        final Object resource1 = new Object();
        final Object resource2 = new Object();

        // 线程1尝试锁定资源1然后资源2
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("线程1锁定了资源1");
                try {
                    Thread.sleep(100); // 确保线程2有时间锁定资源2
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程1尝试锁定资源2");
                synchronized (resource2) {
                    System.out.println("线程1锁定了资源2");
                }
            }
        });

        // 线程2尝试锁定资源2然后资源1
        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("线程2锁定了资源2");
                System.out.println("线程2尝试锁定资源1");
                synchronized (resource1) {
                    System.out.println("线程2锁定了资源1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

//结果
线程1锁定了资源1
线程2锁定了资源2
线程2尝试锁定资源1
线程1尝试锁定资源2
解决
  1. 顺序加锁 确保所有线程获取多个锁的顺序一致。如果每个线程都按相同的顺序获取锁,就不会发生循环等待的情况,从而避免了死锁。
  2. 使用锁超时 使用带有超时的尝试锁定机制,例如 tryLock() 方法。这样,当一个线程无法获取所有必需的锁时,它可以放弃已有的锁并重试,这也可以减少死锁的可能性。
  3. 减少锁的使用 重新设计代码,减少锁的使用,或者尽量使用更高级的并发控制工具,如 java.util.concurrent 包中的 Lock 接口。
  4. 锁分解 如果可能,将大的锁分解成几个小锁,每个锁保护资源的一个部分。这样可以减少不同线程之间的竞争。
  5. 死锁检测 实现死锁检测逻辑,当检测到死锁时,主动释放一些锁,打破死锁状态。虽然这是一种更复杂的策略,但在某些复杂的应用中可能是必要的。
代码语言:javascript复制
public class DeadlockDemo {
    public static void main(String[] args) {
        final Object resource1 = new Object();
        final Object resource2 = new Object();

        // 线程1和线程2都按照相同的顺序获取锁(先resource1,后resource2)
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("线程1锁定了资源1");
                try {
                    Thread.sleep(100); // 确保线程2有时间运行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程1尝试锁定资源2");
                synchronized (resource2) {
                    System.out.println("线程1锁定了资源2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("线程2锁定了资源1");
                System.out.println("线程2尝试锁定资源2");
                synchronized (resource2) {
                    System.out.println("线程2锁定了资源2");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

//结果
线程1锁定了资源1
线程1尝试锁定资源2
线程1锁定了资源2
线程2锁定了资源1
线程2尝试锁定资源2
线程2锁定了资源2

Java 中的阻塞队列

  1. ArrayBlockingQueue(数组实现的有界阻塞队列) 特点:基于数组的有界阻塞队列,按先进先出(FIFO)原则排序元素。可以选择公平性(即按线程等待的先后顺序访问队列)或非公平性,默认是非公平的。 创建示例:
代码语言:javascript复制
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000, true);

用途:适用于需要固定大小的队列场景。

  1. LinkedBlockingQueue(链表实现的阻塞队列) 特点:基于链表的可选边界(有界/无界)阻塞队列。默认情况下,其大小为 Integer.MAX_VALUE。对生产者和消费者使用两个不同的锁,提高并发性能。 用途:适用于吞吐量较高的场景,如 I/O 密集型任务。
  2. PriorityBlockingQueue(支持优先级的无界阻塞队列) 特点:无界的并发队列,但支持优先级排序。元素按照自然顺序或指定的比较器排序。 用途:适用于需要按照特定规则(如优先级)处理元素的场景。
  3. DelayQueue(延迟队列) 特点:一个无界阻塞队列,内部使用 PriorityQueue 实现。队列中的元素必须实现 Delayed 接口,只有在特定延迟时间到了之后才能从队列中取出。 用途:适用于缓存系统和定时任务。
  4. SynchronousQueue(不存储元素的阻塞队列) 特点:一种不存储任何元素的阻塞队列。每个 put 操作必须等待一个 take 操作,反之亦然。 用途:适合于传递性场景,如任务调度。
  5. LinkedTransferQueue(链表实现的无界 TransferQueue) 特点:一种由链表结构组成的无界 TransferQueue。除了常见的队列操作外,还提供 tryTransfer 和 transfer 方法,用于直接将元素传输给消费者。 用途:适用于高并发场景,支持更高的并发数据结构。
  6. LinkedBlockingDeque(链表实现的双向阻塞队列) 特点:一个由链表结构组成的双向阻塞队列,允许从两端插入和移除元素。 用途:适用于双端操作的场景,如“工作窃取”模式。

CAS

概念

CAS(Compare-And-Swap,比较并交换)是一种用于实现多线程同步的技术,被广泛应用于编写无锁的并发算法。CAS 是一种乐观锁机制,它假设多个线程之间不会发生冲突,先进行操作,如果发现有冲突再进行相应的处理,这与悲观锁机制(如使用 synchronized)形成对比。

基本原理
  • 操作原子性:CAS 操作涉及三个操作数 —— 内存位置(V,即变量的内存地址)、预期原值(E)和新值(N)。它执行以下步骤:
    1. 首先比较内存位置的当前值与预期原值,如果相等,说明自上次读取以来没有其他线程修改过该变量,那么就将此内存位置的值更新为新值。
    2. 如果内存位置的当前值与预期原值不相等,说明已经有其他线程修改了该变量,那么不做任何操作。
  • 操作结果:CAS 操作返回一个布尔值,表示是否成功执行了交换。
乐观锁机制

CAS 是一种典型的乐观锁技术。乐观锁总是假设最好的情况,它不会去锁定任何资源,而是先进行操作,如果失败了再重试或者回滚。

锁自旋

在使用 CAS 时,如果操作未能成功(可能由于其他线程的干扰),通常会采取自旋的方式重试,即不断重复比较并交换的过程,直到成功为止。这种自旋策略避免了线程阻塞和唤醒的开销,但如果长时间无法成功,也可能导致CPU资源的浪费。

应用

在 Java 中,CAS 操作是通过 sun.misc.Unsafe 类中的方法实现的,而在高级API中,java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)就是使用了 CAS 操作,提供了无锁的线程安全编程方式。

ABA
问题的描述

假设存在两个线程:线程 one 和线程 two

  • 线程 one 从内存位置 V 中取出值 A。
  • 线程 two 也从内存中取出值 A,并执行一些操作,将该值改为 B,然后又将其改回 A。
  • 线程 one 执行 CAS操作,检查位置 V 发现值仍然是 A,于是认为没有发生变化,继续执行操作。

尽管线程 oneCAS 操作成功,但是在整个过程中,位置 V 的值确实发生了变化,这可能会导致线程 one 基于错误的假设进行操作。

解决 ABA 问题

ABA 问题的一个常见解决方案是引入版本号机制,这是一种乐观锁的实现方式:

  • 版本号机制: 在执行 CAS 操作的同时,每个值都关联一个版本号。每次数据变化时,版本号也会增加。
  • 操作流程: 进行 CAS 操作时,不仅比较值,还比较版本号。即使值相同,但版本号不同,也不会执行 CAS 操作。
  • 优点: 通过版本号的不断增加,可以确保即使数据值恢复为原始值,也能检测到其变化过程,避免了 ABA 问题。

线程相关的基本方法

  1. start()
    • 描述启动一个新线程。在新线程中执行 run() 方法中定义的代码。
    • 用法:new Thread(runnable).start();
  2. run()
    • 描述: 定义线程执行的操作。通常在 Thread的子类中重写此方法或者通过实现 Runnable接口提供**run()**方法的实现。
    • 用法:start() 方法调用,不应直接调用 run()
  3. sleep(long millis)
    • 描述: 使当前正在执行的线程暂停执行指定的时间(毫秒),不释放任何锁。
    • 用法:Thread.sleep(1000);
  4. join()
    • 描述: 等待该线程终止。
    • 用法:thread.join(); 会使当前执行的线程等待 thread线程终止后再继续执行。
  5. wait()
    • 描述:导致当前线程等待,直到另一个线程调用此对象的 notify() 方法或 notifyAll() 方法。
    • 用法object.wait();
  6. interrupt()
    • 描述: 中断线程。并不是强制线程停止,而是设置线程的中断标志位。
    • 用法:thread.interrupt();
  7. isInterrupted()
    • 描述: 检测线程是否被中断。
    • 用法:thread.isInterrupted();
  8. static interrupted()
    • 描述: 检测当前线程是否被中断,并清除当前线程的中断状态。
    • 用法:Thread.interrupted();
  9. yield()
    • 描述: 使当前线程让出 CPU 执行权,给其他线程执行机会。并不是阻塞线程,只是使线程重新回到可执行状态。
    • 用法:Thread.yield();
  10. setPriority(int newPriority)
    • 描述: 改变线程的优先级。
    • 用法:thread.setPriority(Thread.MAX_PRIORITY);
  11. setName(String name)
    • 描述: 设置线程的名称。
    • 用法:thread.setName("MyThread");
  12. getName()
    • 描述: 返回线程的名称。
    • 用法:String name = thread.getName();
  13. setDaemon(boolean on)
    • 描述: 将线程标记为守护线程或用户线程。
    • 用法:thread.setDaemon(true);
  14. isDaemon()
    • 描述: 检查线程是否为守护线程。
    • 用法:thread.isDaemon();
  15. activeCount()
    • 描述: 返回当前线程的线程组中活动线程的数量。
    • 用法:Thread.activeCount();
  16. enumerate(Thread[] tarray)
    • 描述: 枚举线程组中的所有线程到提供的数组中。
    • 用法:Thread.enumerate(threadArray);
  17. currentThread()
    • 描述: 返回当前正在执行的线程对象的引用。
    • 用法:Thread current = Thread.currentThread();

JAVA中的锁

  1. 内部锁(Intrinsic Locks)/监视器锁(Monitor Locks)
  • 描述: 通过synchronized关键字实现,每个 Java 对象都可以作为一个同步锁。
  • 用途: 用于方法或代码块,保证同一时刻只有一个线程可以执行该方法或代码块中的代码。
  1. 重入锁(Reentrant Lock)
  • 描述:java.util.concurrent.locks.ReentrantLock,一种显式锁,功能比内部锁更丰富。
  • 特点: 支持尝试非阻塞获取锁、可中断的锁获取操作、公平锁等。
  1. 读写锁(Read-Write Lock)
  • 描述:java.util.concurrent.locks.ReadWriteLock 接口及其实现类 ReentrantReadWriteLock
  • 用途: 允许多个线程同时读取共享资源,但写操作是排他的。
  1. 乐观锁(Optimistic Locking)
  • 描述: 基于冲突检测的锁机制,不会阻塞线程。在 Java 中,通过原子变量(如 java.util.concurrent.atomic 包中的类)实现。
  • 特点: 利用CAS(Compare-And-Swap) 操作实现。
  1. 自旋锁(Spin Lock)
  • 描述: 不是Java 标准库中的锁,但可以通过循环CAS操作实现。
  • 特点: 线程在获取锁失败时不会立即阻塞,而是循环检查直到获取锁。
  1. 分段锁(Segmented Lock)
  • 描述: 将数据分成几段,每段数据有自己的锁。
  • 用途:ConcurrentHashMap 中使用,提高并发访问的效率。
  1. 条件锁(Condition Lock)
  • 描述:java.util.concurrent.locks.Condition 接口,与 ReentrantLock 配合使用。
  • 特点:允许线程在某些条件下被挂起(condition.await())和被唤醒(condition.signal())
  1. 公平锁/非公平锁
  • 描述:ReentrantLockArrayBlockingQueue 等数据结构中可以设置公平性。
  • 特点: 公平锁按等待锁的顺序分配锁,非公平锁则随机分配,可能造成优先级反转或饥饿。
  1. 重量级锁(Mutex Lock)
  • 特点:synchronized 关键字在遇到竞争时使用的锁。当线程尝试获取一个已被其他线程持有的锁时,该线程会被阻塞。
  • 实现: 依赖于操作系统的互斥量(Mutex),涉及用户态到内核态的切换,这个过程成本较高。
  • 场景: 适用于线程竞争激烈的情况。
  1. 轻量级锁
  • 特点: 在没有线程竞争的情况下使用的锁。通过对象标记字段中的锁标志位和线程栈帧中的锁记录(Lock Record)来实现同步。
  • 过程: 当锁是偏向锁时,如果另一个线程尝试获取锁,偏向模式就会结束,锁会膨胀为轻量级锁。轻量级锁通过循环 CAS 操作尝试获取锁。
  • 场景: 适用于线程交替执行同步块的情况。
  1. 偏向锁
  • 特点: 是一种极度优化的锁类型,主要用于减少在无竞争情况下的同步开销。
  • 实现: 偏向锁会在对象头中存储获取它的线程 ID,一旦线程获取了偏向锁,后续的同步操作不再需要任何同步操作。
  • 场景: 适用于只有一个线程访问同步块的情况。
  1. 锁的升级过程
  • 升级路径: 随着竞争的增加,锁可以从偏向锁升级到轻量级锁,再升级为重量级锁。
  • 单向性: 锁的升级是单向的,即只能从低到高升级,不会降级。

一个线程两次调用 start 会出现什么情况?

在 Java 中,当一个线程对象的 start() 方法被调用两次时,会抛出 IllegalThreadStateException 异常。这是因为一旦线程开始执行,其状态就发生了改变,根据线程的生命周期,它不能重新回到新建(New)状态,而 start() 方法只能在线程处于新建状态时被调用一次。

ThreadLocal

基本原理
  • 线程局部变量:ThreadLocal 创建的是线程的局部变量,每个线程都可以通过它来存储自己的数据。
  • 数据隔离: 不同线程间的这些局部变量互不影响,每个线程只能访问自己的局部变量。
基本用法
  • 创建: 通过 new ThreadLocal<>() 创建一个 ThreadLocal 对象。
  • 设置值: 使用 set(T value) 方法来为当前线程设置一个值。
  • 获取值: 使用 get() 方法来获取当前线程存储的值。
  • 移除值: 使用 remove() 方法来移除当前线程的值。
代码语言:javascript复制
public class Example {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            threadLocal.set(1);
            System.out.println("Thread1: "   threadLocal.get());
        });

        Thread thread2 = new Thread(() -> {
            threadLocal.set(2);
            System.out.println("Thread2: "   threadLocal.get());
        });

        thread1.start();
        thread2.start();
    }
}

execute() 和 submit()

返回值
  • execute(): 此方法没有返回值。它接受一个 Runnable 对象作为参数,并异步执行它。
  • submit(): 此方法返回一个 Future 对象。它可以接受 RunnableCallable 对象作为参数。Callable 是类似于 Runnable 的接口,不同之处在于它可以返回一个结果并且能抛出一个异常。
异常处理
  • execute(): 如果在任务执行过程中抛出异常,这些异常将被线程池的 UncaughtExceptionHandler 处理,如果没有设置处理器,则默认情况下异常会被忽略。
  • submit(): 提交的任务在执行过程中抛出的异常会被捕获并存储在返回的 Future 对象中。调用 Future.get() 时,可以通过 ExecutionException 来获取这些异常。
任务类型
  • execute(): 仅接受 **Runnable **类型的任务。
  • submit(): 可以接受 RunnableCallable 类型的任务,后者允许有返回值。
用途
  • execute(): 更适合于那些不需要关心任务结果的场景。
  • submit(): 适用于需要处理任务结果或异常的场景。
补充

再深一点就是FutureTask的实现原理可以自己去看看

线程池监控

Java线程池实现原理及其在美团业务中的实践

三、结尾

线程池面试题多如牛毛,我只记了一些常见的,希望能帮到大家。

0 人点赞