Java多线程入门:synchronized、volatile与线程通信
开篇:面试被问多线程,你总是答不到点?
多线程是Java面试的"重灾区"。几乎每场面试都会被问到:synchronized原理、volatile作用、线程通信、生产者消费者模型。
很多人觉得多线程难,是因为知识点太散。本文把多线程入门的核心内容串成一条线,帮你建立完整知识框架。
一、为什么要用多线程
程序运行时,CPU是最宝贵的资源。单线程程序在遇到IO操作(读写文件、网络请求)时,CPU只能空等。
多线程可以让CPU在等待IO时,去执行其他任务,提高资源利用率。
单线程:任务A执行 → IO等待 → 继续任务A 多线程:任务A执行 → IO等待 → 切换到任务B执行【面试高频】多线程能提高吞吐量,但不一定缩短单个任务的执行时间。
二、线程创建的三种方式
2.1 继承 Thread 类
classMyThreadextendsThread{@Overridepublicvoidrun(){System.out.println("线程执行:"+Thread.currentThread().getName());}}MyThreadt1=newMyThread();t1.start();2.2 实现 Runnable 接口(推荐)
classMyRunnableimplementsRunnable{@Overridepublicvoidrun(){System.out.println("线程执行:"+Thread.currentThread().getName());}}Threadt2=newThread(newMyRunnable());t2.start();2.3 实现 Callable 接口(有返回值)
classMyCallableimplementsCallable<String>{@OverridepublicStringcall()throwsException{return"任务结果";}}FutureTask<String>futureTask=newFutureTask<>(newMyCallable());newThread(futureTask).start();Stringresult=futureTask.get();// 阻塞获取结果【面试高频】推荐用
Runnable或Callable,因为Java是单继承,继承Thread会限制扩展性。
三、线程的6种状态
NEW(新建) → RUNNABLE(可运行) → RUNNING(运行中) → BLOCKED(阻塞) → WAITING(等待) → TIMED_WAITING(超时等待) → TERMINATED(终止)| 状态 | 说明 |
|---|---|
| NEW | 线程刚创建,未调用start |
| RUNNABLE | 等待CPU调度 |
| BLOCKED | 等待锁 |
| WAITING | 无限等待,需要被唤醒 |
| TIMED_WAITING | 限时等待 |
| TERMINATED | 执行完毕 |
【面试陷阱】线程调用
start()后不是立刻执行,而是进入RUNNABLE状态等待CPU调度。
四、线程安全问题
4.1 问题的根源
多个线程同时读写同一个共享变量,就可能出现数据不一致。例如:
publicclassCounter{privateintcount=0;publicvoidincrement(){count++;// 不是原子操作}}两个线程各执行100万次,结果通常小于200万。
4.2 线程安全三条件
- 多线程环境
- 存在共享资源
- 对共享资源有写操作
只要破坏其中一个条件,就能避免线程安全问题。
五、synchronized关键字
5.1 三种用法
// 1. 同步代码块synchronized(obj){// 临界区}// 2. 同步实例方法:锁对象是 thispublicsynchronizedvoidmethod(){// 临界区}// 3. 同步静态方法:锁对象是 Class 对象publicstaticsynchronizedvoidmethod(){// 临界区}5.2 锁对象必须相同
多个线程只有竞争同一把锁,synchronized才有意义。
// 错误:每次new新对象当锁synchronized(newObject()){count++;}5.3 一个完整示例
publicclassSafeCounter{privateintcount=0;publicsynchronizedvoidincrement(){count++;}publicsynchronizedintgetCount(){returncount;}}【面试高频】
synchronized在JDK6之后做了大量优化,包括偏向锁、轻量级锁、重量级锁,性能已经很好。
六、volatile关键字
6.1 volatile的作用
volatile有两个作用:
- 保证可见性:一个线程修改了变量,其他线程能立即看到。
- 禁止指令重排序:防止编译器和CPU优化执行顺序。
6.2 volatile不能保证原子性
publicclassVolatileDemo{privatevolatileintcount=0;publicvoidincrement(){count++;// 仍然不是线程安全的}}count++包含读取、修改、写回三个步骤。volatile只能保证最后一步写回后被其他线程看到,但不能保证中间不被打断。
【面试陷阱】
volatile适用于状态标志位,不适用于计数器。
6.3 volatile正确用法
publicclassFlagDemo{privatevolatilebooleanrunning=true;publicvoidstop(){running=false;}publicvoiddoWork(){while(running){// 执行任务}}}七、线程通信
7.1 wait / notify 基础
wait()让当前线程等待并释放锁。notify()唤醒一个等待的线程。notifyAll()唤醒所有等待的线程。
这三个方法必须在synchronized代码块中调用,并且调用对象是锁对象。
7.2 经典案例:生产者消费者
一个线程生产数据,一个线程消费数据,通过wait/notify协调。
publicclassBuffer{privateintdata;privatebooleanempty=true;publicsynchronizedvoidproduce(intvalue)throwsInterruptedException{while(!empty){wait();// 缓冲区有数据,等待消费}data=value;empty=false;notifyAll();// 唤醒消费者}publicsynchronizedintconsume()throwsInterruptedException{while(empty){wait();// 缓冲区为空,等待生产}empty=true;notifyAll();// 唤醒生产者returndata;}}【面试高频】为什么用
while而不是if?因为被唤醒后条件可能又不满足了,while会再次判断。
八、与Spring框架的连接
Spring的@Async注解底层就是线程池:
@ServicepublicclassOrderService{@Async("taskExecutor")publicvoidsendSms(Stringphone){// 异步发送短信}}配置线程池:
@Bean("taskExecutor")publicExecutortaskExecutor(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();executor.setCorePoolSize(5);executor.setMaxPoolSize(20);executor.setQueueCapacity(100);executor.initialize();returnexecutor;}【面试高频】实际开发中很少直接
new Thread(),而是用线程池管理线程。
九、开发踩坑实录
坑 1:调用 run() 而不是 start()
Threadt=newThread(newMyRunnable());t.run();// 普通方法调用,没有启动新线程后果:代码在当前线程同步执行,没有多线程效果。
正确做法:调用t.start()。
坑 2:synchronized锁错对象
publicsynchronizedvoidmethod(){// 锁是 thiscount++;}如果两个线程传入的是不同实例,synchronized就锁不住了。
正确做法:共享变量加static,或者锁同一个全局对象。
坑 3:在锁里做耗时操作
synchronized(lock){Thread.sleep(10000);// 持有锁10秒}后果:其他线程一直阻塞,性能极差。
正确做法:锁里只放必要操作,IO和耗时计算放到锁外。
坑 4:wait没有放在循环里
synchronized(lock){if(condition){lock.wait();}}后果:被虚假唤醒后,条件可能又不满足了,程序会出错。
正确做法:用while循环判断条件。
十、面试速记卡
10.1 核心概念速查
| 概念 | 一句话解释 |
|---|---|
| 线程 | CPU调度的基本单位 |
| synchronized | 互斥锁,保证同一时刻只有一个线程执行 |
| volatile | 保证可见性、禁止重排序,不保证原子性 |
| wait | 释放锁并等待 |
| notify | 唤醒一个等待线程 |
| notifyAll | 唤醒所有等待线程 |
| 线程池 | 管理和复用线程,减少创建销毁开销 |
10.2 高频面试题
- 线程创建的三种方式是什么?推荐哪种?
start()和run()有什么区别?synchronized的锁对象分别是什么?volatile能保证线程安全吗?为什么?sleep()和wait()有什么区别?- 为什么
wait()要放在循环里? - 什么是虚假唤醒?
- 生产者消费者模型怎么实现?
10.3 口诀总结
线程创建三方式,Runable更推荐 start启动才有效,run只是普通调 synchronized上把锁,对象相同才有效 volatile可见不原子,状态标志最适合 wait会释放锁,notify要唤醒 while判断条件稳,虚假唤醒也不怕十一、小结
多线程入门抓住三个重点:线程如何创建、线程安全问题如何解决、线程之间如何通信。
synchronized解决互斥,volatile解决可见性,wait/notify解决协作。掌握这三个,面试中的基础多线程问题基本都能应对。
实际开发中尽量少直接操作线程,优先使用线程池和框架提供的异步能力。