1. 你怎样唤醒一个已经阻塞的线程(Java)
在Java中,一个线程可能因为调用wait()
、sleep()
、join()
或等待锁而阻塞。要唤醒一个阻塞的线程,具体方法取决于阻塞的原因:
-
如果是
wait()
导致的阻塞 :使用
notify()
或notifyAll()
唤醒。调用这些方法需要在拥有对象监视器(monitor)的线程中执行。例如:javapublic 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()
中断:javaThread sleeper = new Thread(() -> { try { System.out.println("线程休眠中..."); Thread.sleep(5000); } catch (InterruptedException e) { System.out.println("线程被中断!"); } }); sleeper.start(); sleeper.interrupt();
-
如果是等待锁(如
synchronized
或Lock
) :持有锁的线程释放锁后,阻塞线程会被唤醒。例如使用
ReentrantLock
:javaimport 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的区别
CyclicBarrier
和CountdownLatch
都是Java并发工具类,用于线程同步,但用途和实现不同:
-
CountdownLatch:
-
作用:让一组线程等待,直到某个事件发生(计数器减到0)。
-
特点:一次性使用,计数器只能从初始值减到0,不能重置。
-
场景:主线程等待多个子线程完成任务。
-
实现原理:基于AQS(AbstractQueuedSynchronizer),通过共享锁机制实现。
-
代码示例 :
javaimport 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:
-
作用:让一组线程互相等待,直到所有线程都到达某个"屏障点",然后一起继续执行。
-
特点:可重用,屏障被打破后可以重置;支持可选的"屏障动作"。
-
场景:多线程协作任务,如并行计算。
-
实现原理 :基于
ReentrantLock
和Condition
,通过条件队列管理线程。 -
代码示例 :
javaimport 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
循环检查条件,确保线程只在条件满足时退出等待:javapublic 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)会强制刷新整个缓存行,导致性能下降。
-
代码示例:
javapublic 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"); } }
value1
和value2
在同一缓存行,更新时互相干扰。
-
解决办法:
-
JDK8引入
@sun.misc.Contended
注解(需加JVM参数-XX:-RestrictContended
):java@sun.misc.Contended volatile long value1; @sun.misc.Contended volatile long value2;
-
手动填充(padding),确保变量不在同一缓存行:
javastatic class PaddedCounter { volatile long value1; long p1, p2, p3, p4, p5, p6, p7; // 填充 volatile long value2; }
-
总结:伪共享是缓存行级别的问题,通过填充或注解避免性能损失。
5. 在对现场环境下SimpleDateFormat是安全的么?
SimpleDateFormat
在多线程环境下不安全 ,因为它是非线程安全的类 ,内部状态(如Calendar
对象)会被多个线程共享修改。
-
问题复现:
javaimport 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
。
- 可能抛出
-
解决方案:
-
每个线程创建局部实例 :
javaRunnable task = () -> { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String result = sdf.format(new Date()); System.out.println(Thread.currentThread().getName() + ": " + result); };
-
使用
ThreadLocal
:javaprivate 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); };
-
使用
java.time
包(推荐) :javaimport 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
不安全,推荐用ThreadLocal
或java.time
替代。
6. 讲讲常见的线程安全的Java集合,并且分析其底层结构,要分版本看
常见的线程安全集合包括:
-
Vector(JDK 1.0):
-
底层结构 :动态数组,类似
ArrayList
。 -
线程安全 :所有方法加
synchronized
同步。 -
缺点:锁粒度大,性能差。
-
代码 :
javaVector<Integer> vector = new Vector<>(); vector.add(1); // synchronized方法
-
-
Hashtable(JDK 1.0):
-
底层结构:哈希表(数组+链表)。
-
线程安全 :方法加
synchronized
,不支持null
键值。 -
缺点:锁整个表,性能低。
-
代码 :
javaHashtable<String, Integer> table = new Hashtable<>(); table.put("key", 1); // synchronized方法
-
-
Collections.synchronizedXxx(JDK 1.2):
-
底层结构 :包装非线程安全集合(如
ArrayList
、HashMap
)。 -
线程安全 :通过装饰者模式加
synchronized
。 -
缺点:锁粒度大,迭代需手动同步。
-
代码 :
javaList<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
(锁链表头或树根)。 -
代码 :
javaConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); map.put("key", 1); // CAS或synchronized
-
- JDK 1.5-1.7 :
-
CopyOnWriteArrayList(JDK 1.5):
-
底层结构:动态数组,写时复制。
-
线程安全 :写操作加
ReentrantLock
,读无锁。 -
场景:读多写少。
-
代码 :
javaCopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(); list.add(1); // 复制数组,线程安全
-
总结 :老集合(Vector
、Hashtable
)锁粒度大,性能差;现代集合(如ConcurrentHashMap
)优化并发性能。
7. 分析一下ReadWriteLock和StampedLock
-
ReadWriteLock(JDK 1.5):
-
作用:读写分离,允许多个线程读,一个线程写。
-
实现 :
ReentrantReadWriteLock
,基于AQS。 -
代码 :
javaimport 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)"验证锁状态。
-
代码 :
javaimport 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()
,线程进入就绪状态。 - 原生方法,调用操作系统线程创建。
- 启动新线程,JVM调用
run()
:- 普通方法,仅在当前线程执行
run()
逻辑,不创建新线程。
- 普通方法,仅在当前线程执行
-
代码示例:
javapublic 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 Word
和Monitor
。 - 锁状态 (4阶段):
- 无锁:初始状态。
- 偏向锁:记录线程ID,减少竞争开销。
- 轻量级锁:使用CAS自旋,线程栈帧中记录锁指针。
- 重量级锁 :关联
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
。
- 实现 :线程栈帧中创建Lock Record,CAS尝试将
-
代码验证(伪代码):
javapublic 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
以操作队列。
- 从
- Monitor结构 :每个对象关联一个
-
代码验证:
javapublic 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
的互斥性和队列管理。