Files
StudyNote/面试/八股文/并发.md
2026-02-13 23:38:38 +08:00

10 KiB
Raw Blame History

JMM

JMM 是专门解决多线程并发问题的一套规则

核心就是可见性、原子性、有序性

可见性: 像volatile关键字 就是保证了 所有线程见到的数据是一样的

原子性JMM 里用 synchronized 或者 Lock 锁就能保证原子性 保证了一个程序的步骤不会被打断

有序性:编译器或者 CPU 为了提速,会在不影响单线程结果的情况下调整指令顺序。但多线程下,另一个线程拿到 a 的引用时A 可能还没初始化好,一调用就报错。这时候 volatile 或者 synchronized 就能通过内存屏障阻止这种重排序,保证顺序正确。

如何保证数据的一致性

  • 事务管理
    • 数据库的事务
      • 读未提交 读已提交 可重复读 串行化
  • 锁机制
    • sychornized ReenrantLock
  • 版本控制
    • 通过乐观锁的方式 在更新数据时记录版本信息

线程创建的方法

  1. 继承Thread类 然后通过run方法 使用的话 就是.start()

  2. 实现Runnable接口 重写run方法

  3. 实现Callable接口与FutureTask

    1. class MyCallable implements Callable<Integer> {
          @Overridepublic Integer call() throws Exception {
              // 线程执行的代码这里返回一个整型结果return 1;}}
              public static void main(String[] args) {
                  MyCallable task = new MyCallable();
                  FutureTask<Integer> futureTask = new FutureTask<>(task);
                  Thread t = new Thread(futureTask);t.start();
                  try {Integer result = futureTask.get();
                       // 获取线程执行结果
                       System.out.println("Result: " + result);} 
                  catch (InterruptedException | ExecutionException e) 
                  {
                      e.printStackTrace();
                  }
              }
      
  4. 使用线程池Executor框架

Sleep和wait的区别

image-20251028191927109

blocked和waiting有啥区别

blocked

  • 主要是因为sychon了没有抢到锁 导致进入了blocked
  • blocked可以通过获得锁 主动的唤醒

waiting

  • 线程进入WAITING状态是因为它正在等待另一个线程执行某些操作例如调用Object.wait()方法、Thread.join()方法或LockSupport.park()方法。在这种状态下线程将不会消耗CPU资源并且不会参与锁的竞争
  • waiting主要是通过主动的唤醒 notify

notify 和 notifyAll 的区别?

同样是唤醒等待的线程,同样最多只有一个线程能获得锁,同样不能控制哪个线程获得锁。 区别在于:

  • notify唤醒一个线程其他线程依然处于wait的等待唤醒状态如果被唤醒的线程结束时没调用notify其他线程就永远没人去唤醒只能等待超时或者被中断
  • notifyAll所有线程退出wait的状态开始竞争锁但只有一个线程能抢到这个线程执行完后其他线程又会有一个幸运儿脱颖而出得到锁

如何停止一个线程

核心:通过协作式的逻辑控制线程中止

  1. 通过共享标志位主动停止 就是线程控制一个公共的变量 然后通过那个变量来关闭线程
  2. 线程中断机制 Thread.interrupt()
  3. 通过Future取消任务 Future.cancel()
  4. 资源关闭
image-20251028194534789

并发安全

JUC包常用的类

线程池相关

  • **ThreadPoolExecutor ** 最核心的线程池类 用于创建和管理线程池 通过它可以灵活地配置线程池的参数,如核心线程数、最大线程数、任务队列等,以满足不同的并发处理需求。
  • **Executor **线程池工厂类 提供了一系列的方法来创建不同类型的线程池

并发集合类

  • **ConcurrentHashMap **线程安全的哈希表 允许多个线程同时访问不同的段,提高了并发性能,在高并发场景下比传统的Hashtable性能更好。
  • **CopyOnWriteArrayList **线程安全的列表将修改操作应用到新数组上而读操作仍然可以在旧数组上进行,从而实现了读写分离提高了并发读的性能,适用于读多写少的场景。

同步工具类

  • CountDownLatch 允许一个或多个线程等待其他一组线程完成操作后再继续执行。 就是通过一个计数器 每次用完以后 调用**countDown ** 就-1当计数器为零时等待的线程可以继续执行。常用于多个线程完成各自任务后再进行汇总或下一步操作的场景。
  • CyclicBarrier 可以重复使用当所有线程都通过屏障后,计数器会重置,可以再次用于下一轮的等待。
  • **Semaphore **信号量,用于控制同时访问某个资源的线程数量。

原子类

  • AtomicInteger 原子整数类,提供了对整数类型的原子操作
  • AtomicReference 原子引用类,用于对 对象引用进行原子操作。

怎么保证多线程安全?

  • synchronized关键字: 同步代码块或方法

  • volatile关键字: volatile关键字用于变量,确保所有线程看到的是该变量的最新值

  • Lock接口和ReentrantLock类: Lock接口提供了比synchronized更强大的锁定机制 ReentrantLock是一个实现该接口的例子,提供了更灵活的锁管理和更高的性能。

  • 原子类 ReentrantLock是一个实现该接口的例子,提供了更灵活的锁管理和更高的性能。

  • AtomicInteger counter = new AtomicInteger(0);
    int newValue = counter.incrementAndGet();
    
  • 线程局部变量

ThreadLocal类可以为每个线程提供独立的变量副本, 这样每个线程都拥有自己的变量,消除了竞争条件。

ThreadLocal<Integer> threadLocal new ThreadLocal<>();
threadLocal.set(数值)
int value = threadLocalVar.get();
  • 并发集合

CopyOnWriteArrayList ConcurrentHashMap 等 就是自己就是实现了线程安全的集合

  • JUC工具类:

工具类可以用于控制线程间的同步和协作。 SemaphoreCyclicBarrier

java常见的锁

  • 内置锁synchronized
    • 当一个线程进入synchronized代码块或方法时,它会获取关联对象的锁;当线程离开该代码块或方法时,锁会被释放。如果其他线程尝试获取同一个对象的锁,它们将被阻塞,直到锁被释放。
    • 分类 无锁、偏向锁、轻量级锁和重量级锁
      • 无锁
      • 偏向锁 当一个线程进入同步块时,如果没有任何其他线程竞争,就会使用偏向锁 以减少锁的开销
      • 轻量级锁 使用线程上的数据结构,避免了操作系统级别的锁。
      • 重量级锁 涉及操作系统级的互斥锁。
  • ReentrantLock
    • 是一个显式的锁类,如可中断的锁等待、定时锁等待、公平锁选项等
    • 使用lock()unlock()方法来获取和释放锁。
    • 公平锁按照线程请求锁的顺序来分配锁,保证了锁分配的公平性,但可能增加锁的等待时间。
    • 非公平锁不保证锁分配的顺序,可以减少锁的竞争,提高性能,但可能造成某些线程的饥饿。
  • 读写锁ReadWriteLock
    • 允许多个读取者同时访问共享资源,但只允许一个写入者。
    • 读写锁通常用于读取远多于写入的情况,以提高并发性。
  • 乐观&悲观锁
    • 乐观锁 通常不锁定资源,而是在更新数据时检查数据是否已被其他线程修改。
    • 使用版本号或时间戳来实现。
    • 悲观锁 通常指在访问数据前就锁定资源,假设最坏的情况 --》即数据很可能被其他线程修改
    • synchronizedReentrantLock都是悲观锁的例子
  • 自旋锁
    • 锁机制
    • 线程在等待锁的时候 会循环检查锁是否可用 而不是放弃CPU并阻塞
    • 通常可以使用CAS来实现
    • 这在锁等待时间很短的情况下可以提高性能但过度自旋会浪费CPU资源。

CountDownLatch 是做什么的讲一讲

并发包中的一个同步工具类,用于让一个或多个线程等待其他线程完成操作后再继续执行

本质就是一个计数器 然后

  • 初始化计数器:创建 CountDownLatch 时指定一个初始计数值(如 N)。
  • 等待线程阻塞:调用 await() 的线程会被阻塞,直到计数器变为 0。
  • 任务完成通知:其他线程完成任务后调用 countDown(),使计数器减 1。
  • 唤醒等待线程:当计数器减到 0 时,所有等待的线程会被唤醒。

synchronized

会在编译之后在同步的代码块前后加上monitorentermonitorexit字节码指令

monitorenter尝试获取对象锁 对象没有被锁定或者已经获得了锁 锁的计数器+1

monitorexit指令时则会把计数器-1

当计数器值为0时则锁释放处于等待队列中的线程再继续竞争锁

  • 主要作用:就是实现原子性操作解决共享变量的内存可见性问题
  • 是一种 排他锁 当一个线程获得锁之后其他的线程必须等待线程释放以后才能获得锁
  • 线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。
image-20251028220439394

Reentrantlock

底层实现主要依赖于AQS AQS通过内部类 Sync来实现具体的锁操作。不同的 Sync 子类实现了公平锁和非公平锁的不同逻辑

特性

  • **可中断性 **
    • 这意味着线程在等待锁的过程中,可以被其他线程中断而提前结束等待
    • 使用了与 **LockSupport.park() **和 **LockSupport.unpark() **相关的机制来实现可中断性
  • 设置超时时间
    • 等待一定时间后如果还未获得锁,则放弃锁的获取。
    • 通过内部的 **tryAcquireNanos **方法来实现的
  • 公平锁和非公平锁
    • 默认情况下是非公平锁 也可以设置成 公平锁
  • 多个条件变量
  • 可重入性