Java 进阶 Day04:线程进阶与网络编程入门
线程创建方式二和三
方式二:实现 Runnable
// 1. 定义任务类,实现 Runnableclass 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. 交给 Threadt.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 对比:
| synchronized | Lock | |
|---|---|---|
| 释放方式 | 自动 | 手动(必须在 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"); // 对方 IPbaidu.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 对比:
| UDP | TCP | |
|---|---|---|
| 面向连接 | 否 | 是(三次握手/四次挥手) |
| 传输单位 | 数据报(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 无参无返回值方法
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





















