Java后端八股笔记3 Java并发

29 阅读9分钟

多线程

Java线程安全三要素

概念实现
原子性一个操作要不完全执行,要完全不执行synchronizedatomic
可见性线程对共享资源的修改可以被其他线程及时看到
Java每个线程都有自己的工作内存,对外不可见,其他线程可能使用旧值
synchronizedvolatile
有序性程序的执行顺序为代码的书写顺序synchronizedvolatile,内存屏障

线程状态

状态说明
NEW刚创建,还未启动
RUNNABLE就绪状态(调用start(),等待调度)+运行状态
BLOCKED阻塞状态,等待监视器锁
WAITING等待状态,等待其他线程执行特定动作(如notify)
TIMED_WAITING具有指定等待时间的等待状态
TERMINATED终止状态,线程执行完成

sleep()wait()

特性sleepwait
所属类Thread类(静态方法)Object类(实例方法)
释放对象锁不会,但会释放CPU时间片
使用条件随时可用再同步块或同步方法里可用
唤醒机制超时唤醒其他线程使用notify()方法
用途暂停线程执行线程之间协作

线程通信

notify()

基于监视锁机制的wait()notify()notifyAll()

  • notify()默认随机唤醒一个线程,不过不同JVM有不同实现
  • notifyAll():所有线程退出wait状态,开始竞争锁,但只有一个能抢到
class SharedObject {
    public synchronized void consumerMethod() throws InterruptedException {
        while (/* 条件不满足 */) {
            wait();
        }
        // 执行相应操作
    }
    public synchronized void producerMethod() {
        // 执行相应操作
        notify(); // 或者 notifyAll()
    }
}
volatile

作用有两个:

  1. 保障变量对所有线程的可见性:对该变量的写操作均会直接更新在主存,而读操作会直接从主存获取。
  2. 禁止指令重排序优化:通过内存屏障来禁止特定类型的指令重排序
    1. 在volatile写操作前后分别插入StoreStoreStoreLoad屏障
    2. 在volatile读操作后插入LoadLoadLoadStore屏障
    3. 这些屏障会禁止特定类型的指令重排序,确保volatile写之前的操作不会被重排序到写之后,volatile读之后的操作不会被重排序到读之前
    4. 这建立了happens-before关系:操作A happen-before于 操作B,其实就是说 发生操作B之前,操作A产生的影响能被操作B观察到影响包括:修改了内存中共享变量的值、发送了消息、调用了方法等
AQS

AbstractQueuedSynchronizer,用于构建锁,同步类的工具类

核心思想:

  • 如果请求的共享资源空闲:就将当前请求线程设为有效的工作线程,锁定共享资源。
  • 如果不空闲:就设置一定的阻塞等待机制来保证锁分配;使用双向列表实现,将活不到锁的线程加入队列中。
image.png
  • state:使用一个Volatile的int类型的成员变量来表示同步状态,通过CAS完成对State值的修改。这里state的具体含义,会根据具体实现类的不同而不同:
    • Semapore里,表示剩余许可证的数量;
    • CountDownLatch里,表示还需要倒数的数量;
    • ReentrantLock中,state用来表示“锁”的占有情况,包括可重入计数,当state的值为0的时候,标识该Lock不被任何线程所占有。
  • CLH变体队列:通过内置的FIFO双向列表队列来完成资源获取的排队工作。
  • 重写获取/释放等方法:tryAcquiretryRelease
CountDownLatch

同步辅助类,允许一个或多个线程等待其他线程完成操作。

通过一个计数器count实现,初始化为线程的数量,每有一个线程完成任务,调用countDown方法将计数器减一,计数器为零时,等待的线程可以继续执行。

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 3;//计数器
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 执行任务
                    System.out.println(Thread.currentThread().getName() + " 完成任务");
                } finally {
                    latch.countDown();//减少计数器
                }
            }).start();
        }

        latch.await();//进入等待,直到计数器为零
        System.out.println("所有线程任务完成");
    }
}
CyclicBarrier

让一组线程相互等待,等所有线程均达到屏障点之后,再一起执行。

可重复使用,即所有线程通过屏障之后,计数器重置,待下次使用。

import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int threadCount = 3;
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("所有线程都到达屏障点");
        });//构造函数,指定参与的线程数量和到达屏障点后执行的操作
    for (int i = 0; i < threadCount; i++) {
        new Thread(() -> {
            try {
                // 执行任务
                System.out.println(Thread.currentThread().getName() + " 到达屏障点");
                barrier.await();//使当前线程等待,直到所有线程都到达屏障点。
                // 继续执行后续任务
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }
}
}
Semaphore

计数信号量。可以控制同时访问特定资源的数量

  • Semaphore(int permits):构造函数,指定信号量的初始许可数量。
  • acquire():获取一个许可,如果没有可用许可则阻塞。
  • release():释放一个许可。
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        int permitCount = 2;
        Semaphore semaphore = new Semaphore(permitCount);

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " 获得许可");
                    // 执行任务
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + " 释放许可");
                }
            }).start();
        }
    }
}

终止线程

方法适用场景注意事项
循环检测标志简单无阻塞的逻辑标志位使用volatile以保证可见性
while (running//标志位){}
中断机制可中断的阻塞while (!Thread.currentThread().isInterrupted()){}
资源关闭不可中断的阻塞serverSocket.close(); // 关闭资源使accept()抛出异常

监视器锁(synchronized)

可用于方法或代码块,当代码进入时会获取关联对象锁

原理:

  • 在使用synchronized关键字后,在编译时会在同步的代码块加上monitorentermonitorexit字节码指令。
  • 执行monitorenter指令时会尝试获得对象锁,锁的计数器加1,其他竞争者进入等待队列。
  • 执行monitorexit会让计数器减1,当计数器为0时释放锁。

锁升级(锁膨胀)

通过lock的标识判断锁的级别:

  • 无锁(01):没有偏向锁,JDK1.6之后,默认在偏向延迟(JVM启动的多少秒之后)开启
  • 偏向锁:第一个线程A访问同步块,进入偏向锁。通过CAS将线程ID写入Markword,此后该线程可再次使用(与Markword中的ID进行对比)。若B进入时,A已经退出,则偏向为B;若A未退出,即有竞争时,状态取消,进入轻量级锁。
  • 轻量级锁:当有竞争时,进入轻量级锁:B自旋等待A释放,通过CAS获得锁。
  • 重量级锁:当有两个以上的线程竞争锁时,进入重量级锁:创建monitor对象。

锁清除

若JVM虚拟机检测不到某段代码能被共享或竞争,就会将这段代码的同步锁取消掉,从而提高程序性能。

锁粗化

自动将一段连续的加锁解锁过程来链接在一起,扩展成一个更大的锁。

锁自旋

通过自身循环来尝试获取锁,可以避免一些线程挂起与恢复操作。但容易消耗CPU资源

可重入锁(ReentrantLock)

同一个线程在获得锁之后再次重复获得该锁,不会造成阻塞

原理:线程持有锁的计数器

  • 当一个线程第一次获取锁时,计数器会加1,表示该线程持有了锁。在此之后,如果同一个线程再次获取锁,计数器会再次加1。每次线程成功获取锁时,都会将计数器加1。
  • 当线程释放锁时,计数器会相应地减1。只有当计数器减到0时,锁才会完全释放,其他线程才有机会获取锁。

读写锁(ReadWriteLock)

乐观锁(CAS)

认为竞争不总是发生

实现方法:Compare and Swap操作:当A为A时,改为B,否则操作失败。

CAS缺点:

  • ABA问题:当查看时为A,更改时也为A,当两次操作之间,可能发生了改变,变成了B又变成A。解决方法为:在CAS中加上版本号或者标记,通过对比值和版本号来解决ABA问题。
  • 自旋循环时间长,开销大。

公平锁

  • 公平锁:多个线程按申请锁的顺序获得锁,在队列中进入休眠态(耗时)排队,位于队列头的线程才能获得锁。执行速度慢,吞吐量更小。
  • 不公平锁:线程申请锁时,先通过CAS尝试获得锁,未获到再进入队列排队。执行速度快,吞吐量大但容易导致线程饿死。

死锁

条件:

  1. 互斥条件
  2. 循环等待
  3. 不可剥夺
  4. 持有时等待

线程池

复用线程,避免频繁创建与销毁线程的开销。

流程

提交任务 → 核心线程是否已满?
  ├─ 未满 → 创建核心线程执行
  └─ 已满 → 任务入队
       ├─ 队列未满 → 等待执行
       └─ 队列已满 → 创建非核心线程
           ├─ 未达最大线程数 → 执行任务
           └─ 已达最大线程数 → 执行拒绝策略

参数

屏幕截图 2026-01-06 223214.png

  • corePoolSize:核心线程数,当当前线程数<=核心线程数,则即使空闲也不会销毁
  • maximumPoolSize:最大线程数,线程池能创建的最大数量,等待队列满了之后,就会着手创造新的非核心线程(空闲后销毁),当当前线程数<=最大线程数时,触发拒绝策略。
  • keepAliveTime:非核心线程的存活时间,超过之后销毁。
  • unit:keepAliveTime的单位
  • threadFactory:线程工厂,给线程取名字。
  • handler:拒绝策略:
    • CallerRunPolicy:让线程池调用者所在的线程去执行被拒绝的任务。
    • AbortPolicy:抛出异常
    • DiscardPolicy:不做处理,静默拒绝
    • DiscardOldestPolicy:将最老的线程任务拒绝,执行当前任务。

线程池种类

  • ScheduledThreadPool:支持定时或周期性执行任务。
  • FixedThreadPool:核心线程数和最大线程数是一样的,
  • CachedThreadPool:可缓存线程池,线程数是几乎可以无限增加的(实际最大可以达到 Integer.MAX_VALUE),当线程闲置时可以对线程进行回收。有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。
  • SingleThreadExecutor:使用唯一的线程去执行任务,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。适合用于所有任务都需要按被提交的顺序依次执行的场景。
  • SingleThreadScheduledExecutor:是 ScheduledThreadPool 的一个特例,内部只有一个线程

shutdown()

shutdown():将池子状态置为SHUTDOWN,正在执行的任务继续执行,等待执行的任务直接中断。

shutdownNow():状态置为STOP,正在执行的任务尝试停止。