并发

多线程

使用多线程要注意哪些问题?

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,在Java中使用了atomic包(这个包提供了一些支持原子操作的类,这些类可以在多线程环境下保证操作的原子性)和synchronized关键字来确保原子性;
  • 可见性:一个线程对主内存的修改可以及时地被其他线程看到,在Java中使用了synchronizedvolatile这两个关键字确保可见性;
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,在Java中使用了happens-before原则来确保有序性。

保证数据的一致性

  • 事务管理:数据库事务
  • 锁机制::使用锁来实现对共享资源的互斥访问。使用 synchronized 关键字、ReentrantLock 或其他锁机制来控制并发访问
  • 版本控制:乐观锁

线程的创建方式

1.继承Thread类

用户自定义类继承java.lang.Thread类,重写其run()方法,run()方法中定义了线程执行的具体任务。创建该类的实例后,通过调用start()方法启动线程。

2.实现Runnable接口

如果一个类已经继承了其他类,就不能再继承Thread类,此时可以实现java.lang.Runnable接口。实现Runnable接口需要重写run()方法,然后将此Runnable对象作为参数传递给Thread类的构造器,创建Thread对象后调用其start()方法启动线程。

1
2
3
4
5
6
7
8
9
10
class MyRunnable implements Runnable{
@Override
public void run(){
//线程执行的代码
}
}
public static void main(string[]args){
Thread t new Thread(new MyRunnable());
t.start();
}

3.实现Callable接口与FutureTask

java.util.concurrent.Callable接口类似于Runnable,但Callable的call()方法可以有返回值并且可以抛出异常。要执行Callable任务,需将它包装进一个FutureTask,因为Thread类的构造器只接受Runnable参数,而FutureTask实现了Runnable接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ImplementsCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("3......");
return "zhuZi";
}

public static void main(String[] args) throws Exception {
ImplementsCallable callable = new ImplementsCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
System.out.println(futureTask.get());// 获取线程执行结果
}
}

4.使用线程池(Executor框架)

1
2
3
4
5
6
7
8
9
10
11
12
13
class Task implements Runnable {
@Override
public void run(){
//线程执行的代码
}
}
public static void main(string[] args){
ExecutorService executor=Executors.newFixedThreadPool(10);//创建固定大小的线程池
for (int i= 0; i < 100; i+){
executor.submit(newTask();/提交任务到线程池执行
}
executor.shutdown();/关闭线程池
}

如何停止一个线程的运行?

在Java中,安全停止线程的推荐方法是通过协作式终止,而非强制停止(如已废弃的Thread.stop())。以下是具体实现方式及步骤:

为什么弃用stop:

  1. 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
  2. 调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。

1. 使用标志位终止线程

通过设置一个线程可见的标志位,让线程在运行时定期检查该标志位,并在满足条件时主动退出。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ServerThread extends Thread {
//volatile修饰符用来保证其它线程读取的总是该变量的最新的值
public volatile boolean exit = false;

@Override
public void run() {
ServerSocket serverSocket = new ServerSocket(8080);
while(!exit){
for(print)
...
}
}

public static void main(String[] args) {
ServerThread t = new ServerThread();
t.start();
...
t.exit = true; //修改标志位,退出线程
}
}

  • 必须使用 interrupt():当线程可能处于阻塞状态(如 sleep()、wait())时,需调用 interrupt() 确保即时响应。

  • 可省略 interrupt():若线程无任何阻塞操作,仅通过循环检查 stopRequested 即可立即退出。


2. 使用 interrupt() 方法中断线程

通过调用线程的interrupt()方法,结合对中断状态的检查,终止线程运行。尤其适用于处理阻塞操作(如sleep()wait()join())(阻塞期间,线程无法响应外部的中断请求)。JVM会抛出 InterruptedException,并自动清除中断状态(即 isInterrupted() 返回 false)。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class InterruptibleThread extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
// 模拟阻塞操作(如 I/O、sleep 等)
System.out.println("Thread is running...");
Thread.sleep(1000);
} catch (InterruptedException e) {
// 捕获中断异常后,需重新设置中断标志
Thread.currentThread().interrupt();
break;
}
}
System.out.println("Thread stopped.");
}
}

// 使用示例
public static void main(String[] args) throws InterruptedException {
InterruptibleThread thread = new InterruptibleThread();
thread.start();
Thread.sleep(3000);//主线程
thread.interrupt(); // 中断线程
}

3. 结合标志位和 interrupt()

综合使用标志位和中断机制,确保线程在各种场景下安全终止:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SafeStoppableThread extends Thread {
private volatile boolean stopRequested = false;

@Override
public void run() {
while (!stopRequested && !isInterrupted()) {
try {
// 执行任务逻辑
System.out.println("Thread is running...");
Thread.sleep(1000);
} catch (InterruptedException e) {
// 响应中断,退出循环
Thread.currentThread().interrupt();
break;
}
}
System.out.println("Thread stopped.");
}

public void requestStop() {
stopRequested = true;
interrupt(); // 中断可能的阻塞操作
}
}

总结

方法 适用场景 优点 缺点
标志位 简单循环任务 实现简单 无法中断阻塞操作
interrupt() 需要处理阻塞操作 支持中断阻塞 需手动处理中断状态
标志位+interrupt() 综合场景(推荐) 兼顾阻塞和非阻塞操作 代码稍复杂

Java线程的状态

1743922132753.png

  • BLOCKED是锁竞争失败后被被动触发的状态,WAITING是人为的主动触发的状态
  • BLCKED的唤醒时自动触发的,而WAITING状态是必须要通过特定的方法来主动唤醒

sleep 和 wait的区别

  • 所属分类的不同:sleep是Thread类的静态方法,可以在任何地方直接通过Thread. sleep()调 用,无需依赖对象实例。////wait是object类的实例方法,这意味着必须通过对象实例来调用。

  • 锁释放的情况:Thread.sleep()在调用时,线程会暂停执行指定的时间,但不会释放持有的对象锁。也就是说,在sleep 期间,其他线程无法获得该线程持有的锁。Object.wait():调用该方法时,线程会释放持有的对象锁,进入等待状态,直到其他线程调用相同对象的**notify()或notifyAll()**方法唤醒它

  • 使用条件:sleep可在任意位置调用,无需事先获取锁。wait必须在同步块或同步方法内调用(即线程需持有该对象的锁),否则抛出IllegalMonitorStateException。

  • 唤醒机制:sleep休眠时间结束后**,线程自动恢复到就绪状态**,等待CPU调度。wait需要其他线程调用相同对象的 notify()或 notifyAll()方法才能被唤醒。 notify()会随机唤醒一个在该对象上等待的线程,而notifyAll()会唤醒所有在该对象上等待的线程。

并发安全

juc包下你常用的类?

线程池相关:

  • ThreadPoolExecutor(推荐):最核心的线程池类,用于创建和管理线程池。通过它可以灵活地配置线程池的参数,如核心线程数、最大线程数、任务队列等,以满足不同的并发处理需求。
  • Executors:线程池工厂类,提供了一系列静态方法来创建不同类型的线程池,如newFixedThreadPool(创建固定线程数的线程池)、newCachedThreadPool(创建可缓存线程池)、newSingleThreadExecutor(创建单线程线程池)等,方便开发者快速创建线程池。

并发集合类:

  • ConcurrentHashMap:线程安全的哈希映射表,用于在多线程环境下高效地存储和访问键值对。它采用了分段锁等技术,允许多个线程同时访问不同的段,提高了并发性能,在高并发场景下比传统的Hashtable性能更好。
  • CopyOnWriteArrayList:线程安全的列表,在对列表进行修改操作时,会创建一个新的底层数组,将修改操作应用到新数组上,而读操作仍然可以在旧数组上进行,从而实现了读写分离,提高了并发读的性能,适用于读多写少的场景。

同步工具类:

  • CountDownLatch:允许一个或多个线程等待其他一组线程完成操作后再继续执行。它通过一个计数器来实现,计数器初始化为线程的数量,每个线程完成任务后调用countDown方法将计数器减一,当计数器为零时,等待的线程可以继续执行。常用于多个线程完成各自任务后,再进行汇总或下一步操作的场景。
  • CyclicBarrier:让一组线程互相等待,直到所有线程都到达某个屏障点后,再一起继续执行。与CountDownLatch不同的是,CyclicBarrier可以重复使用,当所有线程都通过屏障后,计数器会重置,可以再次用于下一轮的等待。适用于多个线程需要协同工作,在某个阶段完成后再一起进入下一个阶段的场景。
  • Semaphore:信号量,用于控制同时访问某个资源的线程数量。它维护了一个许可计数器,线程在访问资源前需要获取许可,如果有可用许可,则获取成功并将许可计数器减一,否则线程需要等待,直到有其他线程释放许可。常用于控制对有限资源的访问,如数据库连接池、线程池中的线程数量等。

原子类:

  • AtomicInteger:原子整数类,提供了对整数类型的原子操作,如自增、自减、比较并交换等。通过硬件级别的原子指令来保证操作的原子性和线程安全性,避免了使用锁带来的性能开销,在多线程环境下对整数进行计数、状态标记等操作非常方便。
  • AtomicReference:原子引用类,用于对对象引用进行原子操作。可以保证在多线程环境下,对对象的更新操作是原子性的,即要么全部成功,要么全部失败,不会出现数据不一致的情况。常用于实现无锁数据结构或需要对对象进行原子更新的场景。

线程池

线程池中参数

1743924370525.png

  • 1.如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
  • 2.如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
  • 3.如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
  • 4.如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用RejectedExecutionHandler.rejectedExecution()方法。

Executors 返回线程池对象的弊端如下

  • SingleThreadExecutor 和 FixedThreadPool (固定)一样,使用的都是容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue(无界队列)作为线程池的工作队列。一直往队列里面放都会OOM (X,X)or(1,1)

  • CachedThreadPool 。SynchronousQueue 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,CachedThreadPool 的最大线程数是 Integer.MAX_VALUE ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。(0,max)

  • ScheduledThreadPool 和 SingleThreadScheduledExecutor:使用的无界的延迟阻塞队列DelayedWorkQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。

**ThreadPoolExecutor**拒绝策略:

  • ThreadPoolExecutor.AbortPolicy抛出RejectedExecutionException异常来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy不处理新任务,直接丢弃掉
  • ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。

怎么保证多线程安全?

  • synchronized关键字:可以使用synchronized关键字来同步代码块或方法,确保同一时刻只有一个线程可以访问这些代码。对象锁是通过synchronized关键字锁定对象的监视器(monitor)来实现的。
  • volatile关键字:volatile关键字用于变量,确保所有线程看到的是该变量的最新值,而不是可能存储在本地寄存器中的副本。
1
public volatile int sharedVariable;
  • Lock接口和ReentrantLock类:java.util.concurrent.locks.Lock接口提供了比synchronized更强大的锁定机制,ReentrantLock是一个实现该接口的例子,提供了更灵活的锁管理和更高的性能。
1
2
3
4
5
6
7
8
9
10
private final ReentrantLock lock = new ReentrantLock();

public void someMethod() {
lock.lock();
try {
/* ... */
} finally {
lock.unlock();
}
}
  • 原子类:Java并发库(java.util.concurrent.atomic)提供了原子类,如AtomicIntegerAtomicLong等,这些类提供了原子操作,可以用于更新基本类型的变量而无需额外的同步。

示例:

1
2
AtomicInteger counter = new AtomicInteger(0);
int newValue = counter.incrementAndGet();
  • 线程局部变量:ThreadLocal类可以为每个线程提供独立的变量副本,这样每个线程都拥有自己的变量,消除了竞争条件。
1
2
3
ThreadLocal<Integer> threadLocalVar = new ThreadLocal<>();
threadLocalVar.set(10);
int value = threadLocalVar.get();
  • 并发集合:使用java.util.concurrent包中的线程安全集合,如ConcurrentHashMapConcurrentLinkedQueue等,这些集合内部已经实现了线程安全的逻辑。
  • JUC工具类: 使用java.util.concurrent包中的一些工具类可以用于控制线程间的同步和协作。例如:SemaphoreCyclicBarrier等。

Java中有哪些常用的锁,在什么场景下使用?

Java中的锁是用于管理多线程并发访问共享资源的关键机制。锁可以确保在任意给定时间内只有一个线程可以访问特定的资源,从而避免数据竞争和不一致性。Java提供了多种锁机制,可以分为以下几类:

  • 内置锁(synchronized):Java中的synchronized关键字是内置锁机制的基础,可以用于方法或代码块。当一个线程进入synchronized代码块或方法时,它会获取关联对象的锁;其中,syncronized加锁时有无锁、偏向锁、轻量级锁和重量级锁几个级别。偏向锁用于当一个线程进入同步块时,如果没有任何其他线程竞争,就会使用偏向锁,以减少锁的开销。当偏向锁被其他线程竞争时,会升级为轻量级锁。重量级锁则涉及操作系统级的互斥锁。

    1746600272536.png

  • ReentrantLockjava.util.concurrent.locks.ReentrantLock是一个显式的锁类,提供了比synchronized更高级的功能,如可中断的锁等待、定时锁等待、公平锁选项等。ReentrantLock使用lock()unlock()方法来获取和释放锁。其中,公平锁按照线程请求锁的顺序来分配锁,保证了锁分配的公平性,但可能增加锁的等待时间。非公平锁不保证锁分配的顺序,可以减少锁的竞争,提高性能,但可能造成某些线程的饥饿。

  • 读写锁(ReadWriteLock)java.util.concurrent.locks.ReadWriteLock接口定义了一种锁,允许多个读取者同时访问共享资源,但只允许一个写入者。读写锁通常用于读取远多于写入的情况,以提高并发性。

  • 乐观锁和悲观锁:悲观锁(Pessimistic Locking)通常指在访问数据前就锁定资源,假设最坏的情况,即数据很可能被其他线程修改。synchronizedReentrantLock都是悲观锁的例子。乐观锁(Optimistic Locking)通常不锁定资源,而是在更新数据时检查数据是否已被其他线程修改。乐观锁常使用版本号或时间戳来实现。

  • 自旋锁:自旋锁是一种锁机制,线程在等待锁时会持续循环检查锁是否可用,而不是放弃CPU并阻塞。通常可以使用CAS来实现。这在锁等待时间很短的情况下可以提高性能,但过度自旋会浪费CPU资源。

Reentrantlock工作原理

ReentrantLock 的底层实现主要依赖于 AbstractQueuedSynchronizer(AQS)这个抽象类。AQS 是一个提供了基本同步机制的框架,其中包括了队列、状态值等。

ReentrantLock 在 AQS 的基础上通过内部类 Sync 来实现具体的锁操作。不同的 Sync 子类实现了公平锁和非公平锁的不同逻辑:

  • 可中断性: ReentrantLock 实现了可中断性,这意味着线程在等待锁的过程中,可以被其他线程中断而提前结束等待。在底层,ReentrantLock 使用了与 LockSupport.park() 和 LockSupport.unpark() 相关的机制来实现可中断性。
  • 设置超时时间: ReentrantLock 支持在尝试获取锁时设置超时时间,即等待一定时间后如果还未获得锁,则放弃锁的获取。这是通过内部的 tryAcquireNanos 方法来实现的。
  • 公平锁和非公平锁: 在直接创建 ReentrantLock 对象时,默认情况下是非公平锁。公平锁是按照线程等待的顺序来获取锁,而非公平锁则允许多个线程在同一时刻竞争锁,不考虑它们申请锁的顺序。公平锁可以通过在创建 ReentrantLock 时传入 true 来设置,例如:
1
ReentrantLock fairLock = new ReentrantLock(true);
  • 多个条件变量: ReentrantLock 支持多个条件变量,每个条件变量可以与一个 ReentrantLock 关联。这使得线程可以更灵活地进行等待和唤醒操作,而不仅仅是基于对象监视器的 wait() 和 notify()。多个条件变量的实现依赖于 Condition 接口,例如:
1
2
3
4
5
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 使用下面方法进行等待和唤醒
condition.await();
condition.signal();
  • 可重入性: ReentrantLock 支持可重入性,即同一个线程可以多次获得同一把锁,而不会造成死锁。这是通过内部的 holdCount 计数来实现的。当一个线程多次获取锁时,holdCount 递增,释放锁时递减,只有当 holdCount 为零时,其他线程才有机会获取锁。

综上,synchronized适用于简单同步需求和不需要额外锁功能的场景,而ReentrantLock适用于需要更高级锁功能、性能优化或复杂同步逻辑的情况。

AQS

AQS 解决了开发者在实现同步器时的复杂性问题。它提供了一个通用框架,用于实现各种同步器,例如 可重入锁ReentrantLock)、信号量Semaphore)和 倒计时器CountDownLatch)。通过封装底层的线程同步机制,AQS 将复杂的线程管理逻辑隐藏起来,使开发者只需专注于具体的同步逻辑。

简单来说,AQS 是一个抽象类,为同步器提供了通用的 执行框架。它定义了 资源获取和释放的通用流程,而具体的资源获取逻辑则由具体同步器通过重写模板方法来实现。 因此,可以将 AQS 看作是同步器的 基础“底座”,而同步器则是基于 AQS 实现的 具体“应用”


主要改进点有以下两方面:

  1. 自旋 + 阻塞

    : CLH 锁使用纯自旋方式等待锁的释放,但大量的自旋操作会占用过多的 CPU 资源。AQS 引入了

    自旋 + 阻塞

    的混合机制:

    • 如果线程获取锁失败,会先短暂自旋尝试获取锁;
    • 如果仍然失败,则线程会进入阻塞状态,等待被唤醒,从而减少 CPU 的浪费。
  2. 单向队列改为双向队列:CLH 锁使用单向队列,节点只知道前驱节点的状态,而当某个节点释放锁时,需要通过队列唤醒后续节点。AQS 将队列改为 双向队列,新增了 next 指针,使得节点不仅知道前驱节点,也可以直接唤醒后继节点,从而简化了队列操作,提高了唤醒效率。


synchronized

特性

  • 原子性:synchronized保证语句块内操作是原子的
    同步方法
    ACC_SYNCHRONIZED 这是一个同步标识,对应的 16 进制值是 0x0020这 10 个线程进入这个方法时,都会判断是否有此标识,然后开始竞争 Monitor对象。

    同步代码
     monitorenter,在判断拥有同步标识 ACC_SYNCHRONIZED 抢先进入此方法的线程会优先拥有 Monitor 的 owner ,此时计数器 +1。
     monitorexit,当执行完退出后,计数器 -1,归 0 后被其他进入的线程获得。

  • 可见性为什么添加 synchronized 也能保证变量的可见性呢?
    因为:

    1. 线程解锁前,必须把共享变量的最新值刷新到主内存中。
    2. 线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存
      中重新读取最新的值。
    3. volatile 的可见性都是通过内存屏障(Memnory Barrier)来实现的。
    4. synchronized 靠操作系统内核的Mutex Lock(互斥锁)实现,相当于 JMM 中的 lock、unlock。退出代码块时刷新变量到主内存。
  • 有序性为啥synchronized无法禁止指令重排,但可以保证有序性?
    加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞。所以同一时间只有一个线程执行,相当于单线程,而单线程的指令重排是没有问题的。

    为什么,synchronized 也有可见性的特点,还需要 volatile 关键字
    因为,synchronized 的有序性,不是 volatile 的防止指令重排序。那如果不加 volatile 关键字可能导致的结果,就是第一个线程在初始化初始化对象,设置 instance 指向内存地址时。第二个线程进入时,有指令重排。在判断 if (instance == null) 时就会有出错的可能,因为这会可能 instance 可能还没有初始化成功。

  • 重入性:synchronized 是可重入锁,也就是说,允许一个线程二次请求自己持有对象锁

除了用synchronized,还有什么方法可以实现线程同步?

  • 使用ReentrantLockReentrantLock是一个可重入的互斥锁,相比synchronized提供了更灵活的锁定和解锁操作。它还支持公平锁和非公平锁,以及可以响应中断的锁获取操作。
  • 使用volatile关键字:虽然volatile不是一种锁机制,但它可以确保变量的可见性。当一个变量被声明为volatile后,线程将直接从主内存中读取该变量的值,这样就能保证线程间变量的可见性。但它不具备原子性。
  • 使用Atomic:Java提供了一系列的原子类,例如AtomicIntegerAtomicLongAtomicReference等,用于实现对单个变量的原子操作,这些类在实现细节上利用了CAS(Compare-And-Swap)算法,可以用来实现无锁的线程安全。

synchronized和reentrantlock区别?

synchronized 和 ReentrantLock 都是 Java 中提供的可重入锁:

  • 用法不同:synchronized 可用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用在代码块上。
  • 获取锁和释放锁方式不同:synchronized 会自动加锁和释放锁,当进入 synchronized 修饰的代码块之后会自动加锁,当离开 synchronized 的代码段之后会自动释放锁。而 ReentrantLock 需要手动加锁和释放锁
  • 锁类型不同:synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。
  • 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
  • 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。

synchronized 支持重入吗?如何实现的?

synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。

Threadlocal

ThreadLocal是Java中用于解决线程安全问题的一种机制,它允许创建线程局部变量,即每个线程都有自己独立的变量副本,从而避免了线程间的资源共享和同步问题。

img

ThreadLocal的作用

  • 线程隔离ThreadLocal为每个线程提供了独立的变量副本,这意味着线程之间不会相互影响,可以安全地在多线程环境中使用这些变量而不必担心数据竞争或同步问题。
  • 降低耦合度:在同一个线程内的多个函数或组件之间,使用ThreadLocal可以减少参数的传递,降低代码之间的耦合度,使代码更加清晰和模块化。
  • 性能优势:由于ThreadLocal避免了线程间的同步开销,所以在大量线程并发执行时,相比传统的锁机制,它可以提供更好的性能。

ThreadLocal的原理

ThreadLocal的实现依赖于Thread类中的一个ThreadLocalMap字段,这是一个存储ThreadLocal变量本身和对应值的映射。每个线程都有自己的ThreadLocalMap实例,用于存储该线程所持有的所有ThreadLocal变量的值。

当你创建一个ThreadLocal变量时,它实际上就是一个ThreadLocal对象的实例。每个ThreadLocal对象都可以存储任意类型的值,这个值对每个线程来说是独立的。

可能存在的问题

ThreadLocalMapkeyvalue 引用机制:

  • key 是弱引用ThreadLocalMap 中的 key 是 ThreadLocal 的弱引用 (WeakReference<ThreadLocal<?>>)。 这意味着,如果 ThreadLocal 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 ThreadLocalMap 中对应的 key 变为 null
  • value 是强引用:即使 key 被 GC 回收,value 仍然被 ThreadLocalMap.Entry 强引用存在,无法被 GC 回收。

当一个线程结束时,其ThreadLocalMap也会随之销毁,但是ThreadLocal对象本身不会立即被垃圾回收,直到没有其他引用指向它为止。

因此,在使用ThreadLocal时需要注意,如果不显式调用remove()方法,或者线程结束时未正确清理ThreadLocal变量,可能会导致内存泄漏,因为ThreadLocalMap会持续持有ThreadLocal变量的引用,即使这些变量不再被其他地方引用。

因此,实际应用中需要在使用完ThreadLocal变量后调用remove()方法释放资源。

非公平锁吞吐量为什么比公平锁大?

  • 公平锁执行流程:获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。
  • 非公平锁执行流程:当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。

死锁

产生死锁必须同时满足一下四个条件,只要其中任一条件不成立,死锁就不会发生。

互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁(如哲学家的筷子、打印机设备)。像内存、扬声器这样可以同时让多个进程使用的资源是不会导致死锁的(因为进程不用阻塞等待这种资源)。

不剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。

请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己己有的资源保持不放。

循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程己获得的资源同时被下一个进程所请求。

单例模型既然已经用了synchronized,为什么还要在加volatile?

synchronized 关键字的作用用于确保在多线程环境下,只有一个线程能够进入同步块(这里是 synchronized (Singleton.class))。在创建单例对象时,通过 synchronized 保证了创建过程的线程安全性,避免多个线程同时创建多个单例对象。

volatile 确保了对象引用的可见性和创建过程的有序性,避免了由于指令重排序而导致的错误。

instance = new Singleton(); 这行代码并不是一个原子操作,它实际上可以分解为以下几个步骤:

  • 分配内存空间。
  • 实例化对象。
  • 将对象引用赋值给 instance

由于 Java 内存模型允许编译器和处理器对指令进行重排序,在没有 volatile 的情况下,可能会出现重排序,例如先将对象引用赋值给 instance,但对象的实例化操作尚未完成。

这样,其他线程在检查 instance == null 时,会认为单例已经创建,从而得到一个未完全初始化的对象,导致错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Double-Checked Locking 双重检查锁 DCL,进行了两次判空
public class Singleton {
//volatile 关键字声明,会在编译时加 lock,禁止了指令重排序
private static volatile Singleton sInstance;
private Singleton() {}
public static Singleton getInstance() {
//判断一次避免不必要的同步锁
if (sInstance == null) {
synchronized (Singleton.class) {
if (sInstance == null) {
sInstance = new Singleton();
}
}
}
return sInstance;
}
}