第三周Java问题总结:

4 阅读5分钟

虽迟但到,总算是继续写总结了,没有鸽。
上周学了stream流的日期和日历,算是把stream流学完了,动手做了一个日历打印,就在这个日历打印,我觉得我终于摸到封装的门槛了。又学了多线程,还差一个线程池;也是借助经典的售票的问题把线程是怎么回事搞得差不多了。
这周主要就两个问题,一个是借日历制作理解封装,另一个是售票的问题

一、从日历制作开始,我真正懂了什么是「封装」

本周最让我豁然开朗的知识点,就是封装。以前总觉得封装是抽象概念,只知道是什么,不知道怎么用,直到做完两个版本的日历,我才彻底明白:封装不是为了高级,是为了让代码不乱、好维护

1. 基础版日历:不需要封装的场景

最开始我写了第一版日历CalenderExample,只需要实现一个核心功能:展示当月、上月、下月的日期,按周一到周日排版。

这时候我只需要存储日期数字,用List<Integer>就完全够用:

List<Integer> days = new ArrayList<>();

因为此时一个数字就能代表一个日期,没有额外属性,没有复杂逻辑,直接打印数字即可,判断换行直接用if(i%7==6)即可,完全没必要封装成类。

2. 需求升级:上月 / 下月日期标红,封装的必要性来了

当需求变成:

  • 本月日期:黑色展示
  • 上月、下月日期:红色展示
  • 同时控制每周换行

单纯的数字完全不够用了,一个日期需要包含 3 个信息:

  1. 日期数字
  2. 是否是本月(决定颜色)
  3. 是否需要换行(决定排版)

这里黑色展示用System.out.print(); 红色展示用System.err.print();但两个的换行是不一样的,所以不能简单的用if(i%7==6) System.out.println()进行换行 如果不封装,我需要维护3 个独立的集合,靠索引强行关联数据,极易出错、代码混乱。

这时候DayInfo类的出现,让我明白了封装的意义:

public class DayInfo { 
    // 封装属性:属于同一个「日期」的所有信息 
    private int number; 
    private boolean currentDay; 
    private boolean changeLine; 
    
    // 封装行为:日期自己的展示逻辑 
    public void show(){ 
        if(currentDay){ 
        // 本月黑色打印 
        }else{ 
        // 非本月红色打印 } 
    } 
}

3. 什么时候封装?

当你发现:一个东西,不能只用一个变量表示时,就该封装成类。

最开始做日历,只存数字,不需要封装。后来要标颜色、要控制换行,信息变多了,管不住了,于是诞生了 DayInfo

这就是封装的来源:不是为了设计模式而设计,是业务变复杂了,我们需要一个 “容器” 把相关的东西装起来,让代码不乱。

二、多线程入门:两种创建方式与核心认知

学完封装,我开始接触 Java 多线程,这是本周的第二个重难点。

1. 线程的两种创建方式

我通过代码对比,理清了两种基础创建方式的区别:

方式 1:继承 Thread 类

static class SubThread extends Thread{ 
    @Override 
    public void run() { 
        System.out.println("继承Thread实现线程"); 
    } 
}

缺点:Java 是单继承,继承 Thread 后无法再继承其他类,扩展性差。

方式 2:实现 Runnable 接口(推荐)

static class ThreadTask implements Runnable{ 
    @Override 
    public void run() { 
        System.out.println("实现Runnable实现线程"); 
    }
}

无单继承限制,多个线程可共用同一个任务对象,适合共享数据场景。

三、多线程售票实战:synchronized 锁的踩坑

本周最让我印象深刻的,就是多线程售票的案例,从「超卖、重复卖」到「只有一个窗口卖票」,两次踩坑让我彻底懂了线程安全和锁机制。

1. 业务场景

3 个售票窗口(3 个线程),售卖共享的 10 张火车票,要求:不重复卖、不超卖。

2. 第一个问题:不加锁会出现线程安全问题

多线程同时操作共享的票数变量,会出现同一张票被多次售卖、票数变为负数的情况。原因:判断票数、卖票、减票数的操作,不是不可分割的原子操作

3. 解决方案:synchronized 同步锁

synchronized的作用:同一时间,只允许一个线程执行加锁的代码,保证操作的原子性,解决线程安全问题。

4. 我踩的致命坑:锁的位置加错了

我最开始把锁加在了run()方法上:

@Override 
public synchronized void run() { 
    while(true) { 
        saleTicket(); 
    } 
}

结果运行后:永远只有窗口 1 在卖票,窗口 2、3 全程排队,根本抢不到执行权。

原因:

一个线程抢到run()方法的锁后,会进入while循环把所有票卖完,全程不释放锁,其他线程只能一直等待。

5. 正确写法:只锁「卖票动作」,不锁整个循环

去掉run()上的synchronized,只给售票核心方法加锁:

// 仅售票方法加锁,最小范围加锁
private synchronized void saleTicket() {
    if(totalTickets > 0) {
        System.out.println(窗口+"售卖火车票:"+totalTickets);
        totalTickets--;
    }
}

// run()方法不加锁
@Override
public void run() {
    while(true) {
        saleTicket();
        // 票售罄退出
    }
}

修改后,3 个窗口交替卖票,既保证了线程安全,又实现了多线程并发。