讲解
线程是程序里的「执行流」:普通程序只有一个主线程从头到尾顺序执行,多线程让几段代码看似同时推进——下载文件的同时界面不卡死,就是多线程的典型收益。Java 创建线程最直接的方式是实现 Runnable 接口(一个 run 方法,描述任务内容),把它交给 new Thread(runnable),调用 start() 启动。注意必须是 start 而不是直接调 run()——run() 只是普通方法调用,在当前线程里同步执行,根本不会开新线程。
主线程启动子线程后默认会继续往下走,不等子线程干完。想让主线程「等它结束」,调用 thread.join()——主线程会阻塞到那个线程结束为止,这样才能拿到子线程的计算结果、保证程序输出完整。Thread.sleep(毫秒) 让当前线程暂停一会儿,模拟耗时操作。每个线程有自己的名字(Thread.currentThread().getName()),打印它是观察「代码到底在哪个线程跑」的最直接办法。
多线程最凶险的问题是共享数据竞争:两个线程同时改同一个变量,结果不可预测。经典演示是两个线程各对一个计数器加一万次,预期两万,结果常常更少——count++ 实际是「读、加、写」三步,两个线程的这三步可能交错,丢掉更新。最朴素的修复是 synchronized 关键字:给临界区加锁,同一时刻只允许一个线程进入,结果立刻变回正确的两万。多线程编程深似海(可见性、死锁、线程池……),入门阶段记住三件事:start 而非 run、join 等待结束、共享可变数据必须同步。
示例
创建并启动两个线程,join 等它们都结束再收尾:
public class Main {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
String threadName = Thread.currentThread().getName();
for (int i = 1; i <= 3; i++) {
System.out.println(threadName + " 执行第 " + i + " 步");
}
};
Thread t1 = new Thread(task, "线程-A");
Thread t2 = new Thread(task, "线程-B");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("主线程:两个子线程都结束了");
}
}
共享计数器的竞争与 synchronized 修复,最终结果确定:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int get() {
return count;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable job = () -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(job);
Thread t2 = new Thread(job);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("期望结果:20000");
System.out.println("实际结果:" + counter.get());
}
}
常见坑
- 调用 run() 而不是 start():run() 在主线程里同步执行,打印线程名一看还是 main,「多线程」根本没发生。启动线程只有 start() 一条路。
- 没 join 就读子线程的结果:主线程跑得快,子线程还没算完主线程就打印了,结果时有时无。要等就 join,或者用更高级的 Future/线程池(进阶内容)。
- 以为 count++ 是原子操作:它是读改写三步,多线程下必丢更新。共享可变状态用 synchronized、AtomicInteger 等机制保护。
- 两个 synchronized 块锁的不是同一个对象:synchronized(obj) 的锁是 obj,两个线程锁不同对象等于没锁。锁必须是共享的那个唯一实例(本例锁的是 Counter 实例本身 this)。
小结
Runnable 描述任务、Thread.start 启动、join 等待结束;共享可变数据必须 synchronized,否则结果不可预测。下一节回头看每个类都继承的根基:Object 的常用方法。