Java并发与面试-每日必看(11)

 前言

  Java不秃,面试不慌!
  欢迎来到这片 Java修炼场!这里没有枯燥的教科书,只有每日一更的 硬核知识+幽默风格,让你在欢笑中掌握 Java基础、算法、面试套路,摆脱“写代码如写诗、看代码如看天书”的困境。
记住: 代码会背叛你,但知识不会! 坚持积累,总有一天,HR会为你的八股文落泪,面试官会因你的算法沉默。


🧟‍♂️ Java死锁:程序里的“僵尸派对”!

嘿,Java小伙伴们!你们知道吗?死锁就像程序里的“僵尸派对”——所有线程都伸着手,想抓住对方的资源,却谁也不肯放手,结果大家都僵住了,谁也动不了。😱

我们先来看看这场“僵尸派对”是怎么开的,满足以下4个条件,死锁就像VIP票,立马送上门:

💀 死锁四大“僵尸”法则

  1. 独占资源
    就像一个小孩霸着秋千不撒手,资源一旦被线程A拿了,线程B就只能在旁边干瞪眼。👀

  2. 持有并等待
    一个线程说:“这个秋千我先拿着,我再去抢滑梯。”
    不释放已经拿到的资源,还要继续抢别的,简直是个贪心的“熊孩子”。🧒

  3. 不可剥夺
    拿到资源的线程就像霸占糖果的小孩,除非自己愿意放下,否则你别想硬抢。🍭

  4. 循环等待
    线程A等线程B的资源,线程B等线程C的资源,线程C又等线程A的资源,这就变成了“三个臭皮匠互相掐脖子”的死循环。🔄

🤓 如何阻止“僵尸派对”?

你只要打破其中一个法则,就能避免死锁。由于前三条是Java锁的“铁律”,咱们重点针对第四条——循环等待——下手!💥

🎯 1. 保持加锁顺序一致——“排队上厕所”

想象一下公共厕所,大家都遵循“先左后右”或者“先大后小”的原则(虽然有点奇怪🤡),线程就不会陷入循环等待。

正确姿势:

// 锁资源按固定顺序
synchronized (lockA) {
    synchronized (lockB) {
        System.out.println("按顺序拿资源,不卡壳!");
    }
}

2. 设置锁的超时时间——“爱情长跑需设限”

线程就像追求对象,追了一年没结果?咱得及时止损!别一直等资源,避免永远卡在“死锁恋爱”中。

加上超时机制:

try {
    if (lock.tryLock(1, TimeUnit.SECONDS)) {
        try {
            System.out.println("锁拿到啦,开冲!");
        } finally {
            lock.unlock();
        }
    } else {
        System.out.println("算了,别等了,时间宝贵!");
    }
} catch (InterruptedException e) {
    e.printStackTrace();
}

🕵️‍♀️ 3. 死锁检测——“程序界的福尔摩斯”

死锁就像家里的Wi-Fi死机,得有工具检测出来。Java工具jstack能帮你看看有哪些线程卡住了。

简单步骤:

jps // 查看Java进程ID
jstack <PID> // 检测死锁状态

看到Found one Java-level deadlock就代表有线程在那“互掐”了。


🧠 volatile:Java里的“八卦广播站”📢

嘿,Java小伙伴们!今天咱们聊聊volatile,它就像程序里的“八卦广播站”,确保某个变量的最新“八卦”(值)能被所有线程立刻知道。可别小看这个关键字,用不好就像“八卦断了档”,线程间沟通变成“鸡同鸭讲”,最后程序可能就像脱缰的野马,四处撒欢儿了。🐎💥


🎯 volatile的“超能力”是什么?

一句话:volatile能保证变量对所有线程的可见性,但不保证原子性

咱们通俗点说:

  • 可见性
    一个线程改了volatile变量,其他线程立刻知道,就像大喇叭广播——“注意!stop变量已经被修改了!”📢

  • 非原子性
    volatile不能保证多个操作是“一步到位”的,比如i++其实包含读取、增加、写回三步,中间可能被其他线程插队。👮‍♂️

🧩 volatile防止线程“装傻”

来看看这段代码👇:

💻 线程1:执着的工人

// 线程1:执着的工人,负责不停干活
volatile boolean stop = false;

while (!stop) {
    System.out.println("线程1:干活中…💪");
}
System.out.println("线程1:停工了!🍺");

💻 线程2:善良的老板

// 线程2:某个时刻喊停
stop = true;
System.out.println("线程2:喊话——‘下班了!’");

正常情况下,线程1会在stop被设置为true时立刻停下,因为volatile修饰确保了stop变量的可见性。

但!但!但!
如果没加volatile,就可能出现下面这荒唐的画面👇:

  • 线程2改了stop = true,但这条消息卡在了内存的角落里(还没同步到主存)。
  • 线程1就像“耳背”的老大爷,始终从自己缓存里读取旧值false,于是它继续“卖命干活”。🧓🔄💦

所以:加了volatile,就相当于让老板拿着大喇叭喊:“都听好了!stop改成true了!下班!


🧠 volatile的“幕后黑手”:CPU缓存机制

我们都知道CPU有L1、L2、L3缓存,线程会把变量偷偷“拷贝”到自己缓存里,避免频繁跑去主存,效率高,但数据不准就会出大事。

volatile的魔法在于:

  1. 强制刷新主存:改volatile变量时,线程会把新值立刻写回主存。💾
  2. 缓存失效通知:其他线程的缓存中旧数据会被标记“过期”,下一次读时必须从主存“新鲜出炉”。🗂️🚫

⚠️ “重排序”:CPU的“小聪明”

CPU是个勤快的家伙,为了效率,它会偷偷“重排序”代码——比如:

int a = 0;
boolean flag = false;

// 写操作
public void write() {
    a = 2;      //1
    flag = true;//2
}

// 读操作
public void multiply() {
    if (flag) {          //3
        int result = a * a; //4
        System.out.println(result);
    }
}

😱 可能的“诡异”现象

  1. CPU为了提高效率,可能会把 flag = true 提前执行(毕竟它以为你不会发现)。
  2. 然后multiply()方法跑去计算时,发现flag=true,就开心地a*a,结果a可能还没来得及变成2
  3. 结果:result可能是0而不是4,把人看得满头问号。🧐💥

如何破局?

加上volatile

volatile int a = 0;
volatile boolean flag = false;

加了volatile,CPU就不敢耍花招了:1和2的顺序就会严格保持,就像老师监考,谁也不许偷看隔壁答案。👨‍🏫👀


🤔 volatile不是“锁”,小心“假安全”

有些小伙伴以为volatile能替代synchronized。但你知道i++其实是三步走吗?👇

volatile int count = 0;

public void increment() {
    count++; // 实际上是:
    // 1. 读取count的值
    // 2. count+1
    // 3. 将新值写回count
}

问题:假设两个线程同时执行increment()

  1. 线程A:读取count=5,加1变6,还没写回。
  2. 线程B:也读取count=5,加1变6,写回去。
  3. 最终count只加了1次,虽然两次调用了increment()。💔

⚔️ 正确方案?

AtomicIntegersynchronized👇:

// 更安全的方案
AtomicInteger count = new AtomicInteger(0);

public void increment() {
    count.incrementAndGet();
}

🛠️ 线程池:Java并发世界的“中央厨房”👨‍🍳

嘿,Java小伙伴们!今天咱们聊聊线程池,理解了它,你就像掌握了“并发编程”的点金之术!💰💡

为什么要用线程池? 因为开线程就像请外卖小哥:

  • 临时请——费时间、费钱💸(创建销毁成本高)
  • 没计划——高峰期爆单(资源管理混乱)
  • 不监控——跑路了都不知道👀

所以,线程池就是程序界的“外卖调度中心”,让线程有序、灵活、高效运行!🏎️💨


🚀 线程池的“三大法宝”

  1. 降低资源消耗
    线程是Java中的“VIP资源”,频繁创建和销毁就像反复开关空调,电费吓人!🧾
    线程池能复用线程,减少资源浪费。

  2. 提高响应速度
    有任务时直接从“线程池”取现成的线程,而不是临时“喊人干活”。
    就像打车:提前叫好车,任务一到立刻出发!🚖

  3. 提升管理能力
    用线程池能统一管理、调优、监控线程。
    就像有个“中央大脑”调度外卖骑手,避免撞单和“摸鱼”。🐟


🧩 线程池的“六大参数”:厨师的“秘密配方”🧑‍🍳

Java线程池一般通过ThreadPoolExecutor来创建,构造函数长这样👇:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,   // 核心线程数
    maximumPoolSize,// 最大线程数
    keepAliveTime,  // 空闲线程存活时间
    timeUnit,       // 时间单位
    workQueue,      // 任务队列
    threadFactory,  // 线程工厂
    handler         // 拒绝策略
);

📌 1️⃣ corePoolSize(核心线程数):主力厨师👨‍🍳

这是“线程池里的固定员工数”,这些线程就像厨房的主力厨师,提前上岗,随时待命。

  • 小妙招: 如果你的任务一直很活跃,核心线程数可以调大一点。
  • 比喻: 高峰期前就要招好厨师,别等顾客排队了再临时找人。

📌 2️⃣ maximumPoolSize(最大线程数):极限扩容🚀

当厨房太忙时,可以临时再请几位“兼职厨师”帮忙。

  • 默认情况下:只有当任务队列满了,才会触发扩容。
  • 注意: 太多线程反而可能“锅碗瓢盆撞一起”,性能下降。

📌 3️⃣ keepAliveTime(空闲存活时间):临时工下班时间🕰️

超出核心线程数的临时线程,空闲了一定时间后就会被“遣散”。

  • 应用场景: 适合有明显高峰期的应用,比如双十一秒杀。
  • 小技巧: 如果线程需要频繁创建和销毁,可以适当调大。

📌 4️⃣ workQueue(任务队列):菜品排队区🍲

线程忙不过来时,任务就会被先放进“待处理区”。

  • 常用队列:
    • ArrayBlockingQueue(有限队列):像餐厅里有限的排队座位。
    • LinkedBlockingQueue(无界队列):理论上能一直排队,但小心“吃爆内存”。
    • SynchronousQueue(不存任务):任务必须立刻交给线程处理,否则就得等。

📌 5️⃣ threadFactory(线程工厂):统一发工牌🏷️

想给线程起个名字?ThreadFactory 可以让你轻松定制线程属性。

ThreadFactory factory = r -> new Thread(r, "Chef-Thread-" + r.hashCode());

📌 6️⃣ handler(拒绝策略):爆单时的应对策略📈

任务队列满了+线程池到达最大线程数时,新任务怎么办?

Java提供了四种策略,就像餐厅面对“爆单”时的选择:

  1. AbortPolicy(默认):直接拒绝,扔个异常😡
  2. CallerRunsPolicy:让主线程自己干!💪
  3. DiscardPolicy:默默丢弃新任务🤐
  4. DiscardOldestPolicy:丢掉队列里最老的任务,再加新任务🔄
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

⚠️ 常见“翻车”现场:💥

  1. 任务队列太长,OOM(内存溢出)💥
  2. 线程数太大,CPU“累趴下”😵
  3. 忘关线程池,程序不退出
executor.shutdown();

🎯 最后的话:关注不迷路,点赞有好运!🌟

哎呀,看到这里,你已经掌握了线程池,死锁等知识。

如果你觉得这篇内容有点意思、学到了新知识,别忘了——

👉 点个赞👍,分享给你的Java小伙伴!
👉 关注我,持续解锁更多Java干货!🔔

你的支持就是我码字的“核动力”🚀!我们下期再见,代码路上一起升级打怪!💻🎮

🎤 再次感谢,掰掰!👋

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Starry-Walker

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值