进程 vs 线程
进程就是工厂 ,线程就是工厂里的工人 。一个工厂可以有很多工人,工人之间共享工厂的资源(内存),但每个工厂是独立的
对比项
进程
线程
比喻
工厂
工厂里的工人
资源
独立内存空间
共享进程的内存
开销
创建/销毁开销大
轻量级,开销小
通信
进程间通信(IPC)比较麻烦
直接共享变量,但要注意安全
崩溃影响
一个进程挂了不影响其他进程
一个线程挂了可能整个进程都挂
举个例子:你打开Chrome浏览器,每个标签页是一个进程 (所以Chrome很吃内存),每个标签页内部渲染、网络请求等是不同的线程
线程创建三种方式
Java里创建线程有三种方式,实际开发中最常用的是线程池(后面讲) ,但面试必考这三种
方式一:继承Thread类
最简单但最不推荐,因为Java是单继承,继承了Thread就不能继承别的类了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void testExtendThread () throws InterruptedException {class MyThread extends Thread { @Override public void run () { for (int i = 0 ; i < 3 ; i++) { System.out.println(Thread.currentThread().getName() + " 正在执行: " + i); } } } MyThread t = new MyThread ();t.start(); t.join(); }
方式二:实现Runnable接口 (推荐)
比继承Thread灵活,因为接口可以多实现
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void testRunnable () throws InterruptedException {Runnable task = () -> { for (int i = 0 ; i < 3 ; i++) { System.out.println(Thread.currentThread().getName() + " Runnable执行: " + i); } }; Thread t = new Thread (task, "我的线程" );t.start(); t.join(); }
方式三:实现Callable接口 (可以有返回值)
和Runnable的区别:Callable可以返回结果、可以抛异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void testCallable () throws Exception {Callable<Integer> task = () -> { int sum = 0 ; for (int i = 1 ; i <= 100 ; i++) { sum += i; } return sum; }; FutureTask<Integer> futureTask = new FutureTask <>(task); Thread t = new Thread (futureTask);t.start(); Integer result = futureTask.get(); System.out.println("1+2+...+100 = " + result); }
对比
Thread
Runnable
Callable
方式
继承类
实现接口
实现接口
返回值
无
无
有(通过Future获取)
异常
不能抛checked异常
不能抛checked异常
可以抛异常
灵活性
差(单继承限制)
好
好
推荐度
⭐
⭐⭐⭐
⭐⭐⭐
线程状态与生命周期
线程从出生到死亡,要经历这几个状态,面试高频考点
1 2 3 4 5 6 7 8 9 NEW(新建) ↓ start() RUNNABLE(可运行/ 运行中) ↓ 等待锁 / 等待I/O BLOCKED(阻塞)/ WAITING(等待)/ TIMED_WAITING(超时等待) ↓ 获得锁 / 被唤醒 / 超时 RUNNABLE(继续运行) ↓ run()执行完毕 TERMINATED(终止)
状态
说明
怎么进入
NEW
线程对象创建了,还没start
new Thread()
RUNNABLE
可运行状态(包括正在CPU上跑的和排队等CPU的)
start()
BLOCKED
等待获取锁
遇到 synchronized 但锁被别人占了
WAITING
无限期等待
wait()、join()、LockSupport.park()
TIMED_WAITING
有时限的等待
sleep(毫秒)、wait(毫秒)、join(毫秒)
TERMINATED
线程执行完了或异常退出
run() 结束
常见坑 :sleep() 不会释放锁,wait() 会释放锁。面试必考这个区别
synchronized关键字
最基本的加锁方式,就像卫生间门锁——一次只能一个人用
对象锁 :锁的是某个对象实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Test public void testSynchronizedMethod () throws InterruptedException {class Counter { private int count = 0 ; public synchronized void increment () { count++; } public void incrementBlock () { synchronized (this ) { count++; } } public int getCount () { return count; } } Counter counter = new Counter ();Thread t1 = new Thread (() -> { for (int i = 0 ; i < 10000 ; i++) counter.increment(); }); Thread t2 = new Thread (() -> { for (int i = 0 ; i < 10000 ; i++) counter.increment(); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("最终count = " + counter.getCount()); }
类锁 :锁的是整个Class对象,所有实例共享同一把锁
1 2 3 4 5 6 7 8 9 10 11 public static synchronized void staticMethod () {} public static void staticMethod () {synchronized (Counter.class) { } }
volatile关键字
volatile解决的是可见性 问题:一个线程修改了变量,其他线程能立刻看到
比喻:普通变量像每个人手里的草稿纸(线程本地缓存),volatile变量像白板上的公共通知——改了所有人都能看到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void testVolatile () throws InterruptedException {class Flag { volatile boolean running = true ; } Flag flag = new Flag ();Thread t = new Thread (() -> { System.out.println("线程开始..." ); while (flag.running) { } System.out.println("线程结束!" ); }); t.start(); Thread.sleep(100 ); flag.running = false ; t.join(); }
volatile不保证原子性 :i++ 这种操作即使加了volatile也不安全,因为 i++ 实际上是”读→加1→写”三步操作
对比
volatile
synchronized
保证可见性
✅
✅
保证原子性
❌
✅
性能
好(无锁)
差一些(要加锁解锁)
适用场景
一个线程写、多个线程读
多个线程读写
Lock接口(ReentrantLock)
比synchronized更灵活的锁,像是一把可以设置各种模式的高级门锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Test public void testReentrantLock () throws InterruptedException {ReentrantLock lock = new ReentrantLock ();int [] count = {0 }; Runnable task = () -> { for (int i = 0 ; i < 10000 ; i++) { lock.lock(); try { count[0 ]++; } finally { lock.unlock(); } } }; Thread t1 = new Thread (task);Thread t2 = new Thread (task);t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("count = " + count[0 ]); }
对比
synchronized
ReentrantLock
加锁方式
自动(进入代码块加锁,退出释放)
手动 lock/unlock
可中断
不可以
可以(lockInterruptibly)
公平锁
只有非公平
可以选择公平/非公平
条件变量
只有一个wait/notify
可以多个Condition
推荐
简单场景用这个
需要高级功能时用这个
线程池(ExecutorService)
为什么用线程池?因为频繁创建和销毁线程开销很大,线程池就像出租车公司 ——提前雇好一批司机(线程),有活儿就派出去,没活儿就等着,不用每次都现招人
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void testThreadPool () throws InterruptedException {ExecutorService pool = Executors.newFixedThreadPool(3 );for (int i = 0 ; i < 10 ; i++) { int taskId = i; pool.submit(() -> { System.out.println(Thread.currentThread().getName() + " 执行任务 " + taskId); }); } pool.shutdown(); pool.awaitTermination(5 , TimeUnit.SECONDS); }
核心参数 (面试高频)
参数
含义
比喻
corePoolSize
核心线程数
正式员工数量
maximumPoolSize
最大线程数
正式员工 + 临时工的总数
keepAliveTime
临时线程存活时间
临时工没活干多久后被辞退
workQueue
任务队列
等待区的椅子数量
handler
拒绝策略
等待区也满了怎么办
常见的线程池
newFixedThreadPool(n):固定n个线程,适合任务量已知的场景
newCachedThreadPool():线程数不限(按需创建),适合短期大量任务
newSingleThreadExecutor():只有1个线程,保证任务顺序执行
newScheduledThreadPool(n):定时/周期执行任务
常见坑 :阿里巴巴开发手册禁止用 Executors 快捷方法创建线程池,因为 newFixedThreadPool 的队列是无界的,任务堆积会OOM。推荐用 new ThreadPoolExecutor(...) 手动指定参数
线程安全集合
普通的 集合框架 如 HashMap、ArrayList 都不是线程安全的,多线程同时操作会出问题
线程不安全
线程安全替代
说明
HashMap
ConcurrentHashMap
分段锁/CAS,高性能
ArrayList
CopyOnWriteArrayList
写时复制,适合读多写少
HashSet
CopyOnWriteArraySet
同上
TreeMap
ConcurrentSkipListMap
跳表实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void testConcurrentMap () throws InterruptedException {ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap <>(); Thread t1 = new Thread (() -> { for (int i = 0 ; i < 1000 ; i++) map.put("t1-" + i, i); }); Thread t2 = new Thread (() -> { for (int i = 0 ; i < 1000 ; i++) map.put("t2-" + i, i); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("map大小: " + map.size()); }
**别用 Hashtable 和 Collections.synchronizedMap()**,性能太差(整个方法加锁),面试的时候知道就行,实际开发用 ConcurrentHashMap
并发工具类简介
JUC(java.util.concurrent)包里有一些很实用的工具类
CountDownLatch (倒计数门闩):等所有人到齐了再开始
比喻:运动会起跑,裁判等所有运动员就位(countDown),全到齐了才发令枪(await)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void testCountDownLatch () throws InterruptedException {int workerCount = 3 ;CountDownLatch latch = new CountDownLatch (workerCount);for (int i = 0 ; i < workerCount; i++) { int id = i; new Thread (() -> { System.out.println("工人" + id + "完成工作" ); latch.countDown(); }).start(); } latch.await(); System.out.println("所有工人都完成了,开始汇总!" ); }
Semaphore (信号量):控制同时访问的线程数
比喻:停车场只有3个车位,同时最多进3辆车
CyclicBarrier (循环栅栏):所有线程互相等待,都到齐了再一起往下走
和CountDownLatch区别:CyclicBarrier可以重复使用,CountDownLatch是一次性的
⭐ 安全角度:竞态条件(Race Condition)漏洞
什么是TOCTOU (Time-of-check to time-of-use)
检查的时候是安全的,但真正使用的时候条件已经变了。就像你去ATM查余额还有1000,准备取1000,但在你按确认的一瞬间,另一笔交易已经把钱扣走了
实际案例:双线程超额提现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Test public void testRaceConditionVulnerability () throws InterruptedException {class BankAccount { private int balance = 1000 ; public void withdrawUnsafe (int amount) { if (balance >= amount) { try { Thread.sleep(10 ); } catch (Exception e) {} balance -= amount; System.out.println(Thread.currentThread().getName() + " 提现成功,余额: " + balance); } else { System.out.println(Thread.currentThread().getName() + " 余额不足" ); } } public synchronized void withdrawSafe (int amount) { if (balance >= amount) { balance -= amount; System.out.println(Thread.currentThread().getName() + " 提现成功,余额: " + balance); } else { System.out.println(Thread.currentThread().getName() + " 余额不足" ); } } } BankAccount account = new BankAccount ();Thread t1 = new Thread (() -> account.withdrawUnsafe(1000 ), "用户A" );Thread t2 = new Thread (() -> account.withdrawUnsafe(1000 ), "用户B" );t1.start(); t2.start(); t1.join(); t2.join(); }
Web场景中的竞态条件
优惠券重复领取:两个请求同时判断”用户还没领过”,然后都给发了
库存超卖:两个订单同时判断”库存>0”,然后都扣了
防御:数据库乐观锁(version字段)、分布式锁(Redis)、synchronized
常见坑
坑1 :调用 run() 而不是 start(),不会创建新线程,只是普通方法调用
坑2 :synchronized 锁的对象不一致,比如两个方法锁了不同的对象,等于没锁
坑3 :在 finally 里忘记 unlock(),异常时死锁
坑4 :线程池用完不 shutdown(),程序无法退出
坑5 :以为 volatile 能替代 synchronized,volatile 只保证可见性不保证原子性
练习题
题1 :用两个线程交替打印1-100(一个打印奇数,一个打印偶数)
题2 :用线程池实现:10个任务,每个任务睡眠随机时间后返回结果,主线程收集所有结果
题3 (⭐ 安全题):分析这段代码的竞态条件风险,并给出修复方案
1 2 3 4 5 6 7 public void register (String username) {if (!userDao.existsByUsername(username)) { userDao.insert(new User (username)); } }