虽迟但到,总算是继续写总结了,没有鸽。
上周学了stream流的日期和日历,算是把stream流学完了,动手做了一个日历打印,就在这个日历打印,我觉得我终于摸到封装的门槛了。又学了多线程,还差一个线程池;也是借助经典的售票的问题把线程是怎么回事搞得差不多了。
这周主要就两个问题,一个是借日历制作理解封装,另一个是售票的问题
一、从日历制作开始,我真正懂了什么是「封装」
本周最让我豁然开朗的知识点,就是封装。以前总觉得封装是抽象概念,只知道是什么,不知道怎么用,直到做完两个版本的日历,我才彻底明白:封装不是为了高级,是为了让代码不乱、好维护。
1. 基础版日历:不需要封装的场景
最开始我写了第一版日历CalenderExample,只需要实现一个核心功能:展示当月、上月、下月的日期,按周一到周日排版。
这时候我只需要存储日期数字,用List<Integer>就完全够用:
List<Integer> days = new ArrayList<>();
因为此时一个数字就能代表一个日期,没有额外属性,没有复杂逻辑,直接打印数字即可,判断换行直接用if(i%7==6)即可,完全没必要封装成类。
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 个窗口交替卖票,既保证了线程安全,又实现了多线程并发。