Java多线程面试复盘:唤醒阻塞线程/CyclicBarrier与CountdownLatch区别/wait()方法调用与虚假唤醒/多线程伪共享/...


1. 你怎样唤醒一个已经阻塞的线程(Java)

在Java中,一个线程可能因为调用wait()sleep()join()或等待锁而阻塞。要唤醒一个阻塞的线程,具体方法取决于阻塞的原因:

  • 如果是wait()导致的阻塞

    使用notify()notifyAll()唤醒。调用这些方法需要在拥有对象监视器(monitor)的线程中执行。例如:

    java 复制代码
    public class WaitNotifyExample {
        private static final Object lock = new Object();
    
        public static void main(String[] args) {
            Thread waiter = new Thread(() -> {
                synchronized (lock) {
                    try {
                        System.out.println("线程等待中...");
                        lock.wait(); // 线程阻塞,释放锁
                        System.out.println("线程被唤醒!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
    
            Thread notifier = new Thread(() -> {
                synchronized (lock) {
                    System.out.println("准备唤醒...");
                    lock.notify(); // 唤醒等待的线程
                }
            });
    
            waiter.start();
            try { Thread.sleep(1000); } catch (Exception e) {}
            notifier.start();
        }
    }

    输出:

    erlang 复制代码
    线程等待中...
    准备唤醒...
    线程被唤醒!
  • 如果是sleep()导致的阻塞
    sleep()只是让线程休眠一段时间,无法主动唤醒,只能等待时间结束,或通过interrupt()中断:

    java 复制代码
    Thread sleeper = new Thread(() -> {
        try {
            System.out.println("线程休眠中...");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            System.out.println("线程被中断!");
        }
    });
    sleeper.start();
    sleeper.interrupt();
  • 如果是等待锁(如synchronizedLock

    持有锁的线程释放锁后,阻塞线程会被唤醒。例如使用ReentrantLock

    java 复制代码
    import java.util.concurrent.locks.ReentrantLock;
    ReentrantLock lock = new ReentrantLock();
    Thread t1 = new Thread(() -> {
        lock.lock();
        try {
            System.out.println("t1持有锁");
            Thread.sleep(2000);
        } catch (Exception e) {} finally {
            lock.unlock();
        }
    });
    Thread t2 = new Thread(() -> {
        lock.lock();
        try {
            System.out.println("t2获得锁");
        } finally {
            lock.unlock();
        }
    });
    t1.start(); t2.start();

总结 :唤醒方式因阻塞原因而异,wait()notify()sleep()靠时间或interrupt(),锁等待靠锁释放。


2. 详细分析一下CyclicBarrier和CountdownLatch的区别

CyclicBarrierCountdownLatch都是Java并发工具类,用于线程同步,但用途和实现不同:

  • CountdownLatch

    • 作用:让一组线程等待,直到某个事件发生(计数器减到0)。

    • 特点:一次性使用,计数器只能从初始值减到0,不能重置。

    • 场景:主线程等待多个子线程完成任务。

    • 实现原理:基于AQS(AbstractQueuedSynchronizer),通过共享锁机制实现。

    • 代码示例

      java 复制代码
      import java.util.concurrent.CountDownLatch;
      public class CountdownLatchExample {
          public static void main(String[] args) throws InterruptedException {
              CountDownLatch latch = new CountDownLatch(3);
              Runnable task = () -> {
                  System.out.println(Thread.currentThread().getName() + " 完成");
                  latch.countDown();
              };
              new Thread(task).start();
              new Thread(task).start();
              new Thread(task).start();
              latch.await(); // 主线程等待
              System.out.println("所有任务完成!");
          }
      }
  • CyclicBarrier

    • 作用:让一组线程互相等待,直到所有线程都到达某个"屏障点",然后一起继续执行。

    • 特点:可重用,屏障被打破后可以重置;支持可选的"屏障动作"。

    • 场景:多线程协作任务,如并行计算。

    • 实现原理 :基于ReentrantLockCondition,通过条件队列管理线程。

    • 代码示例

      java 复制代码
      import java.util.concurrent.CyclicBarrier;
      public class CyclicBarrierExample {
          public static void main(String[] args) {
              CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("所有线程到达屏障,执行后续任务"));
              Runnable task = () -> {
                  System.out.println(Thread.currentThread().getName() + " 到达屏障");
                  try {
                      barrier.await();
                  } catch (Exception e) {}
                  System.out.println(Thread.currentThread().getName() + " 继续执行");
              };
              new Thread(task).start();
              new Thread(task).start();
              new Thread(task).start();
          }
      }
  • 区别总结

    特性 CountdownLatch CyclicBarrier
    等待目标 事件发生(计数到0) 所有线程到达屏障
    可重用性 不可重用 可重用
    实现基础 AQS ReentrantLock+Condition
    使用场景 主线程等子线程 线程间协作

3. 如何调用wait()方法,用if还是循环,讲讲虚假唤醒

调用wait()时,必须在同步块或同步方法中(持有monitor),否则抛出IllegalMonitorStateException。至于用if还是while,取决于是否需要防范虚假唤醒(Spurious Wakeup)

  • 虚假唤醒 :线程可能在没有被notify()notifyAll()唤醒的情况下被操作系统或JVM意外唤醒。

  • 正确用法 :使用while循环检查条件,确保线程只在条件满足时退出等待:

    java 复制代码
    public class SpuriousWakeupExample {
        private static final Object lock = new Object();
        private static boolean condition = false;
    
        public static void main(String[] args) {
            Thread waiter = new Thread(() -> {
                synchronized (lock) {
                    while (!condition) { // 用while而非if
                        try {
                            System.out.println("等待条件...");
                            lock.wait();
                        } catch (InterruptedException e) {}
                    }
                    System.out.println("条件满足,线程继续!");
                }
            });
    
            Thread notifier = new Thread(() -> {
                synchronized (lock) {
                    condition = true;
                    lock.notify();
                }
            });
    
            waiter.start();
            try { Thread.sleep(1000); } catch (Exception e) {}
            notifier.start();
        }
    }
    • 如果用if,虚假唤醒可能导致线程在条件未满足时继续执行,逻辑错误。
    • while,即使发生虚假唤醒,条件不满足时线程会重新wait()

总结 :总是用while包裹wait(),以应对虚假唤醒,确保逻辑健壮。


4. 多线程环境下的伪共享是什么?

**伪共享(False Sharing)**是多线程环境下的一种性能问题,发生在多个线程操作同一个缓存行(cache line)中的不同数据时。

  • 原理

    • CPU缓存以缓存行(通常64字节)为单位加载数据。
    • 如果线程A更新变量x,线程B访问同一缓存行中的变量y,即使x和y逻辑无关,缓存一致性协议(如MESI)会强制刷新整个缓存行,导致性能下降。
  • 代码示例

    java 复制代码
    public class FalseSharingExample {
        static class Counter {
            volatile long value1; // 在缓存行中
            volatile long value2; // 与value1共享缓存行
        }
        public static void main(String[] args) throws InterruptedException {
            Counter counter = new Counter();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 1000000; i++) counter.value1++;
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 1000000; i++) counter.value2++;
            });
            long start = System.currentTimeMillis();
            t1.start(); t2.start();
            t1.join(); t2.join();
            System.out.println("耗时: " + (System.currentTimeMillis() - start) + "ms");
        }
    }
    • value1value2在同一缓存行,更新时互相干扰。
  • 解决办法

    • JDK8引入@sun.misc.Contended注解(需加JVM参数-XX:-RestrictContended):

      java 复制代码
      @sun.misc.Contended
      volatile long value1;
      @sun.misc.Contended
      volatile long value2;
    • 手动填充(padding),确保变量不在同一缓存行:

      java 复制代码
      static class PaddedCounter {
          volatile long value1;
          long p1, p2, p3, p4, p5, p6, p7; // 填充
          volatile long value2;
      }

总结:伪共享是缓存行级别的问题,通过填充或注解避免性能损失。


5. 在对现场环境下SimpleDateFormat是安全的么?

SimpleDateFormat在多线程环境下不安全 ,因为它是非线程安全的类 ,内部状态(如Calendar对象)会被多个线程共享修改。

  • 问题复现

    java 复制代码
    import java.text.SimpleDateFormat;
    import java.util.Date;
    public class SimpleDateFormatUnsafe {
        private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
        public static void main(String[] args) {
            Runnable task = () -> {
                try {
                    String result = sdf.format(new Date());
                    System.out.println(Thread.currentThread().getName() + ": " + result);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            };
            for (int i = 0; i < 10; i++) {
                new Thread(task).start();
            }
        }
    }
    • 可能抛出ArrayIndexOutOfBoundsException或格式错误,因为线程竞争修改Calendar
  • 解决方案

    1. 每个线程创建局部实例

      java 复制代码
      Runnable task = () -> {
          SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
          String result = sdf.format(new Date());
          System.out.println(Thread.currentThread().getName() + ": " + result);
      };
    2. 使用ThreadLocal

      java 复制代码
      private static final ThreadLocal<SimpleDateFormat> sdfHolder = 
          ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
      Runnable task = () -> {
          String result = sdfHolder.get().format(new Date());
          System.out.println(Thread.currentThread().getName() + ": " + result);
      };
    3. 使用java.time包(推荐)

      java 复制代码
      import java.time.format.DateTimeFormatter;
      import java.time.LocalDateTime;
      DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
      Runnable task = () -> {
          String result = dtf.format(LocalDateTime.now());
          System.out.println(Thread.currentThread().getName() + ": " + result);
      };

      DateTimeFormatter是线程安全的。

总结SimpleDateFormat不安全,推荐用ThreadLocaljava.time替代。


6. 讲讲常见的线程安全的Java集合,并且分析其底层结构,要分版本看

常见的线程安全集合包括:

  • Vector(JDK 1.0)

    • 底层结构 :动态数组,类似ArrayList

    • 线程安全 :所有方法加synchronized同步。

    • 缺点:锁粒度大,性能差。

    • 代码

      java 复制代码
      Vector<Integer> vector = new Vector<>();
      vector.add(1); // synchronized方法
  • Hashtable(JDK 1.0)

    • 底层结构:哈希表(数组+链表)。

    • 线程安全 :方法加synchronized,不支持null键值。

    • 缺点:锁整个表,性能低。

    • 代码

      java 复制代码
      Hashtable<String, Integer> table = new Hashtable<>();
      table.put("key", 1); // synchronized方法
  • Collections.synchronizedXxx(JDK 1.2)

    • 底层结构 :包装非线程安全集合(如ArrayListHashMap)。

    • 线程安全 :通过装饰者模式加synchronized

    • 缺点:锁粒度大,迭代需手动同步。

    • 代码

      java 复制代码
      List<Integer> syncList = Collections.synchronizedList(new ArrayList<>());
      synchronized (syncList) { // 迭代时需同步
          for (Integer i : syncList) {}
      }
  • ConcurrentHashMap(JDK 1.5+)

    • JDK 1.5-1.7
      • 底层结构:分段锁(Segment数组+HashEntry链表),默认16个Segment。
      • 线程安全:每个Segment独立加锁,减小锁粒度。
    • JDK 1.8
      • 底层结构:数组+链表+红黑树,取消Segment。

      • 线程安全 :CAS + synchronized(锁链表头或树根)。

      • 代码

        java 复制代码
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("key", 1); // CAS或synchronized
  • CopyOnWriteArrayList(JDK 1.5)

    • 底层结构:动态数组,写时复制。

    • 线程安全 :写操作加ReentrantLock,读无锁。

    • 场景:读多写少。

    • 代码

      java 复制代码
      CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
      list.add(1); // 复制数组,线程安全

总结 :老集合(VectorHashtable)锁粒度大,性能差;现代集合(如ConcurrentHashMap)优化并发性能。


7. 分析一下ReadWriteLock和StampedLock

  • ReadWriteLock(JDK 1.5)

    • 作用:读写分离,允许多个线程读,一个线程写。

    • 实现ReentrantReadWriteLock,基于AQS。

    • 代码

      java 复制代码
      import java.util.concurrent.locks.ReentrantReadWriteLock;
      public class ReadWriteLockExample {
          private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
          private static int value = 0;
      
          public static void main(String[] args) {
              Runnable readTask = () -> {
                  lock.readLock().lock();
                  try {
                      System.out.println("读: " + value);
                  } finally {
                      lock.readLock().unlock();
                  }
              };
              Runnable writeTask = () -> {
                  lock.writeLock().lock();
                  try {
                      value++;
                      System.out.println("写: " + value);
                  } finally {
                      lock.writeLock().unlock();
                  }
              };
              new Thread(readTask).start();
              new Thread(writeTask).start();
          }
      }
  • StampedLock(JDK 1.8)

    • 作用 :提供乐观读、悲观读写锁,比ReadWriteLock更灵活。

    • 特点:不支持重入,用"戳(stamp)"验证锁状态。

    • 代码

      java 复制代码
      import java.util.concurrent.locks.StampedLock;
      public class StampedLockExample {
          private static final StampedLock lock = new StampedLock();
          private static int value = 0;
      
          public static void main(String[] args) {
              Runnable optimisticRead = () -> {
                  long stamp = lock.tryOptimisticRead();
                  int v = value;
                  if (!lock.validate(stamp)) { // 验证是否被写操作修改
                      stamp = lock.readLock();
                      try {
                          v = value;
                      } finally {
                          lock.unlockRead(stamp);
                      }
                  }
                  System.out.println("乐观读: " + v);
              };
              Runnable writeTask = () -> {
                  long stamp = lock.writeLock();
                  try {
                      value++;
                      System.out.println("写: " + value);
                  } finally {
                      lock.unlockWrite(stamp);
                  }
              };
              new Thread(optimisticRead).start();
              new Thread(writeTask).start();
          }
      }
  • 区别

    特性 ReadWriteLock StampedLock
    锁类型 读锁、写锁 乐观读、悲观读、写锁
    重入性 支持 不支持
    性能 读多写少时较好 乐观读更高效
    使用复杂性 简单 较复杂(需管理stamp)

总结ReadWriteLock适合简单读写分离,StampedLock适合高并发读优化。


8. 讲讲Java线程的run和start两个方法的区别,为什么不可以直接调用run方法?

  • 区别

    • start()
      • 启动新线程,JVM调用run(),线程进入就绪状态。
      • 原生方法,调用操作系统线程创建。
    • run()
      • 普通方法,仅在当前线程执行run()逻辑,不创建新线程。
  • 代码示例

    java 复制代码
    public class ThreadExample {
        public static void main(String[] args) {
            Thread t = new Thread(() -> System.out.println("线程: " + Thread.currentThread().getName()));
            t.start(); // 新线程执行
            t.run();   // 主线程执行
        }
    }

    输出:

    makefile 复制代码
    线程: Thread-0
    线程: main
  • 为什么不能直接调用run()

    • 调用run()只是普通方法调用,不触发线程调度,无法实现多线程效果。
    • start()通过JVM和OS创建线程,分配栈空间,调度执行。

总结start()启动线程,run()仅执行逻辑,直接调用run()失去多线程意义。


9. 分析一下Synchronized的原理,markword中的monitor是否直接意味着调用系统调用中的互斥量?偏向锁和轻量级锁使用到了monitor么?

  • Synchronized原理

    • 底层 :基于对象头中的Mark WordMonitor
    • 锁状态 (4阶段):
      1. 无锁:初始状态。
      2. 偏向锁:记录线程ID,减少竞争开销。
      3. 轻量级锁:使用CAS自旋,线程栈帧中记录锁指针。
      4. 重量级锁 :关联Monitor对象,调用操作系统互斥量。
  • Mark Word与Monitor

    • Mark Word是对象头的一部分,存储锁状态(2位标志位):
      • 00:轻量级锁
      • 01:无锁/偏向锁
      • 10:重量级锁
      • 11:GC标记
    • Monitor :重量级锁时,Mark Word指向JVM创建的ObjectMonitor对象,包含等待队列和互斥锁。
    • 是否直接调用系统互斥量
      • 仅在重量级锁 时,Monitor通过JNI调用OS的互斥量(如pthread_mutex)。
      • 偏向锁和轻量级锁不涉及系统调用。
  • 偏向锁

    • 实现Mark Word记录线程ID,无竞争时直接返回。
    • 不使用Monitor:仅更新对象头,依赖CAS。
    • 撤销:竞争时升级为轻量级锁。
  • 轻量级锁

    • 实现 :线程栈帧中创建Lock Record,CAS尝试将Mark Word替换为指向Lock Record的指针。
    • 不使用Monitor:自旋避免系统调用。
    • 膨胀 :自旋失败时升级为重量级锁,创建Monitor
  • 代码验证(伪代码):

    java 复制代码
    public class SynchronizedExample {
        private final Object lock = new Object();
    
        public void method() {
            synchronized (lock) {
                System.out.println("加锁");
            }
        }
    }
    • 无竞争:偏向锁,记录线程ID。
    • 轻竞争:轻量级锁,CAS自旋。
    • 高竞争:重量级锁,Monitor介入。

总结Monitor仅在重量级锁时关联系统互斥量,偏向锁和轻量级锁优化性能,不直接用Monitor


10. 从原理上来分析为什么wait()和notify以及notifyAll必须在同步方法或者同步块中被调用?

  • 原因

    • wait()notify()notifyAll()依赖对象的Monitor,只有持有Monitor的线程才能操作。
    • 未同步调用会抛IllegalMonitorStateException
  • 原理

    • Monitor结构 :每个对象关联一个ObjectMonitor,包含:
      • Owner:当前持有锁的线程。
      • EntryList:等待锁的线程队列。
      • WaitSet:调用wait()的线程队列。
    • wait()
      • 释放Monitor,将线程加入WaitSet,挂起。
      • 无锁时无法释放,调用无意义。
    • notify()/notifyAll()
      • WaitSet唤醒线程,需持有Monitor以操作队列。
  • 代码验证

    java 复制代码
    public class WaitNotifySync {
        private static final Object lock = new Object();
    
        public static void main(String[] args) {
            try {
                lock.wait(); // IllegalMonitorStateException
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            synchronized (lock) {
                try {
                    lock.wait(); // 正确,持有monitor
                } catch (InterruptedException e) {}
            }
        }
    }
  • 为何必须同步

    • 确保线程安全,避免竞争条件。
    • JVM要求持有Monitor才能操作WaitSet

总结wait()notify()需同步,因为它们依赖Monitor的互斥性和队列管理。

相关推荐
落叶随峰11 分钟前
C++好项目:GPU服务器管理面板
服务器·开发语言·c++·分布式·后端·mysql·中间件
嘵奇23 分钟前
Spring Boot 整合 OpenFeign 教程
java·spring boot·后端
无关风雪月37 分钟前
[Python]不要使用可变对象作为函数默认参数或者作为字典的键
后端·python
寻月隐君44 分钟前
保护您的代码:GitHub GPG 密钥配置指南
后端·github
沉默王二1 小时前
985本,去华为od,亏吗?
java·后端·面试
STApril1 小时前
REST API VS GraphQL API
人工智能·后端·面试
chaowwwww1 小时前
10分钟了解圈复杂度
后端
fliter1 小时前
性能比拼: Rust vs C++
后端
莫循瑾木1 小时前
莫循跃迁:nvm管理node版本速通
前端·后端·node.js
有一只柴犬1 小时前
Spring AI & Trae ,助力开发微信小程序
人工智能·后端·spring