视频加载失败

Java 进阶 Day04:线程进阶与网络编程入门

2086 字
10 分钟
Java 进阶 Day04:线程进阶与网络编程入门

线程创建方式二和三#

方式二:实现 Runnable#

// 1. 定义任务类,实现 Runnable
class MyRunnable implements Runnable {
@Override
public void run() { // 2. 重写 run,写任务逻辑
for (int i = 0; i < 6; i++) System.out.println("线程2-" + i);
}
}
// 3. 把任务对象交给线程对象,start() 启动
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
t.start();

可以用匿名内部类或 Lambda 简化:

new Thread(() -> {
for (int i = 0; i < 6; i++) System.out.println("线程2-" + i);
}).start();

方式二对比方式一(继承 Thread):implements Runnable 不占用继承位,且职责分离——Runnable 管「做什么」,Thread 管「怎么执行」,更推荐。

方式三:实现 Callable + FutureTask#

Callable<String> c = new MyCallable(100); // 1. 定义任务(能返回结果)
FutureTask<String> ft = new FutureTask<>(c); // 2. FutureTask 包装(本身是 Runnable)
Thread t = new Thread(ft); // 3. 交给 Thread
t.start();
String result = ft.get(); // 4. get() 阻塞等待,取回结果

MyCallable 核心:

class MyCallable implements Callable<String> {
public String call() throws Exception { // call() 有返回值 + 能抛异常
int sum = 0;
for (int i = 0; i <= n; i++) sum += i;
return "子线程计算1-" + n + "结果是" + sum;
}
}

三种创建方式对比:方式一继承 Thread、方式二实现 Runnable,都没有返回值;方式三实现 Callable,call() 有返回值且能抛异常。

FutureTask 同时实现了 Runnable(可以交给 Thread)和 Future(能 get() 取结果);get() 会阻塞等待子线程跑完。

线程常用 API#

方法作用
setName() / getName()设置/获取线程名(默认 Thread-索引)
Thread.currentThread()拿到当前正在执行的线程对象
sleep(毫秒)让线程休眠指定时间(需捕获 InterruptedException)
join()让调用它的线程等待该线程执行完再继续
Thread m = Thread.currentThread(); // 哪个线程调用就拿到哪个
m.setName("主线程");
try {
Thread.sleep(3000); // 休眠 3 秒
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.join(); // 主线程等 t1 跑完再继续

线程安全问题#

用一个 Account 类模拟两个线程同时取款:两个 DrawThread extends Thread 各自调用同一个账户的 acc.draw(100000)。

无同步的初版 draw():

public void draw(double money) {
if (this.money >= money) { // 判断够不够
this.money -= money;
System.out.println(Thread.currentThread().getName() + "取钱成功,余额:" + this.money);
} else {
System.out.println(Thread.currentThread().getName() + "取钱失败");
}
}

运行结果(无锁):小明、小红都「取钱成功」,余额被扣穿——线程安全问题出现了。

为什么出问题(原子性被破坏):if (余额够) { 扣钱; } 的「判断 + 扣钱」本应是一步不可分割的原子操作。无锁时线程 A 判断通过还没扣钱,线程 B 也判断通过,两个线程都认为「够扣」,于是都取款成功。

共享数据 money 被多个线程同时读写,且读改写不是原子的,构成竞态条件(race condition)。

解决思路:让「判断 + 扣钱」这段代码同一时刻只有一个线程执行(互斥),也就是线程同步,见下一节。

synchronized 同步#

public void draw(double money) {
synchronized (this) { // 锁对象 this:小明、小红共享同一个 acc
if (this.money >= money) {
this.money -= money;
System.out.println(Thread.currentThread().getName() + "取钱成功,余额:" + this.money);
} else {
System.out.println(Thread.currentThread().getName() + "取钱失败,余额:" + this.money);
}
}
}
  • 作用:保证代码段原子性(互斥) + 可见性(锁有内存屏障,一个线程改完的数据其他线程看得见)
  • synchronized(this) 的 this 是锁对象,两个线程必须用同一个锁对象才互斥——小明、小红共享同一个 acc,所以正确
  • 静态方法用字节码 class 对象做锁:synchronized(Account.class)
  • synchronized 直接修饰方法的写法(public synchronized void draw(...))与同步代码块等价

运行结果(加锁):一人取钱成功、另一人失败,线程安全得到保证。

Lock 锁#

private final Lock lk = new ReentrantLock(); // 显式创建锁对象
public void draw(double money) {
lk.lock(); // 手动加锁
try {
if (this.money >= money) {
this.money -= money;
System.out.println(Thread.currentThread().getName() + "取钱成功,余额:" + this.money);
} else {
System.out.println(Thread.currentThread().getName() + "取钱失败,余额:" + this.money);
}
} finally {
lk.unlock(); // 必须 finally 解锁,防止锁死
}
}

为什么必须 finally { unlock(); }:

  • synchronized 是自动释放:代码块跑完或抛异常,JVM 自动放锁
  • Lock 是手动释放:lock() 后如果抛异常跳出而没执行 unlock(),锁会一直被占着,其他线程永久阻塞(死锁)
  • finally 不管正常结束还是抛异常都一定执行,用它保证解锁

synchronized 与 Lock 对比:

synchronizedLock
释放方式自动手动(必须在 finally 中解锁)
锁类型内置锁显式锁(可重入)
灵活性低高(可 tryLock 尝试加锁、可中断)

线程池#

ThreadPoolExecutor 七参数#

ThreadPoolExecutor tpe = new ThreadPoolExecutor(
3, // 核心线程数
5, // 最大线程数
10L, // 空闲存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(3), // 任务队列(阻塞队列)
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

任务处理顺序(面试核心)#

任务来了 →
① 核心线程(3)有空? → 交给核心线程
② 核心线程忙? → 进任务队列(容量3)排队
③ 队列满? → 创建临时线程(最多再开 5-3=2 个)
④ 临时线程也满 + 队列也满? → 触发拒绝策略

按上面的参数提交 10 个任务:前 3 个走核心线程,第 46 个进队列排队,第 78 个由临时线程处理,第 9~10 个被拒绝。

拒绝策略常用的有 AbortPolicy(抛异常)和 CallerRunsPolicy(让提交任务的线程自己跑)等。

execute 与 submit#

  • execute(Runnable):无返回值
  • submit(Callable):返回 Future<T>,能 get() 拿结果
  • 工具类快捷创建:Executors.newFixedThreadPool(5)

用线程池的原因:复用线程,避免频繁创建/销毁线程的高开销。

网络编程#

InetAddress#

InetAddress local = InetAddress.getLocalHost(); // 本机 IP 对象
InetAddress baidu = InetAddress.getByName("www.baidu.com"); // 对方 IP
baidu.getHostName(); // 主机名
baidu.getHostAddress(); // IP 地址
baidu.isReachable(5000); // 判断 5 秒内是否互通(ping)

UDP:无连接、快、可能丢包#

// 发送端
DatagramSocket socket = new DatagramSocket();
byte[] data = "我是客户端,来喝胡辣汤".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length,
InetAddress.getLocalHost(), 4399); // 数据 + 长度 + 目的地 + 端口
socket.send(packet);

UDP 用 DatagramSocket + DatagramPacket(数据报),不面向连接,发出去不管对方收没收到。

TCP:面向连接、可靠#

// 客户端
Socket socket = new Socket("127.0.0.1", 4399); // 连接服务器
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeUTF(msg); // 发字符串
// 服务端
ServerSocket serverSocket = new ServerSocket(4399); // 绑定端口监听
Socket socket = serverSocket.accept(); // 阻塞等待客户端连接
DataInputStream dis = new DataInputStream(socket.getInputStream());
String data = dis.readUTF(); // 收数据

多客户端模型:服务端 while(true) 循环 accept,每来一个客户端就开一个子线程处理,支持同时服务多个客户端。

while (true) {
Socket socket = serverSocket.accept(); // 阻塞等一个客户端
new ServerReader(socket).start(); // 每个客户端一个子线程
}

UDP 与 TCP 对比:

UDPTCP
面向连接否是(三次握手/四次挥手)
传输单位数据报(DatagramPacket)字节流(Socket)
可靠性不可靠、可能丢包可靠、不丢不重不漏
速度快相对慢
场景视频/直播/游戏网页/文件传输/聊天

JUnit 单元测试#

import org.junit.Test;
public class JunitUtilTest1 {
@Test // 必须加 @Test
public void testAdd() { // 方法要求:public、无参、无返回值
JunitDemo1 demo = new JunitDemo1();
int result = demo.add(1, 2);
System.out.println(result);
}
}

配合 mvn test 运行,能对单个方法做单元测试,不用写 main。

重点速记#

  • 线程创建三种方式:继承 Thread / 实现 Runnable(无返回值);实现 Callable + FutureTask(有返回值 + 能抛异常)
  • FutureTask 同时是 Runnable(能交给 Thread)和 Future(能 get 取结果),get() 阻塞等待
  • 线程 API:setName/getName、currentThread()(拿当前线程)、sleep()(休眠,需捕获 InterruptedException)、join()(等待目标线程执行完)
  • 线程安全问题本质:「判断 + 扣钱」这类读改写操作非原子,多线程同时读写共享数据产生竞态
  • synchronized 核心:保证互斥 + 可见性;多线程必须使用同一个锁对象才互斥
  • Lock(ReentrantLock):手动 lock()/unlock(),必须 finally 解锁防死锁;比 synchronized 灵活
  • 线程池七参数:核心数 / 最大数 / 空闲时间 / 时间单位 / 任务队列 / 线程工厂 / 拒绝策略
  • 线程池处理顺序:核心线程 → 任务队列 → 临时线程 → 拒绝策略
  • execute vs submit:execute(Runnable) 无返回值,submit(Callable) 返回 Future 可取结果
  • UDP vs TCP:无连接快但可能丢包 vs 面向连接可靠;DatagramSocket/DatagramPacket vs Socket/ServerSocket
  • accept() 阻塞等待客户端连接;while(true) + 子线程 实现多客户端并发处理
  • JUnit:@Test 注解 + public 无参无返回值方法

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
Java 进阶 Day04:线程进阶与网络编程入门
https://ryuki.icu/posts/java-advanced-day04/
作者
RK
发布于
2026-08-08
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
RK
Long may the sunshine
公告
Long may the sunshine
分类
标签
最新动态
站点天气
天气加载中…
站点统计
文章
27
分类
6
标签
59
总字数
39,139
篝火已燃
0 天
最后活动
0 天前
每日一签