前言
多线程是大一 Java 课程的重点内容,也是期末考试、计算机二级的高频考点。很多同学刚学多线程时,分不清进程和线程,写售票程序时经常出现车票超卖,不知道怎么解决线程安全问题。本篇文章全部使用大一能看懂的基础语法,搭配完整可运行代码,把线程的全部基础知识点讲清楚。
一、进程和线程的区别
1. 基础概念
- 进程:电脑上运行的软件就是一个进程。比如你打开 QQ、浏览器,每一个软件都会单独占用一块内存,进程之间互不干扰。进程是操作系统分配电脑内存资源的最小单位。
- 线程:一个进程里面可以同时运行多条线程。比如音乐软件一边播放歌曲,一边下载歌词,播放音乐、下载歌词就是两条独立的线程。线程是 CPU 执行任务的最小单位。
2. 通俗例子
把进程比作一家工厂,工厂有厂房、原材料;线程就是工厂里的工人,多名工人共用工厂的厂房和物料,各自干自己的活。
3. 简单对比
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源 | 有独立内存空间 | 共用进程的内存 |
| 开销 | 开启、关闭很消耗电脑性能 | 创建开销很小 |
| 运行关系 | 进程互相独立 | 一条线程出错,整个程序会崩溃 |
二、Java 创建线程的 3 种方式
大一考试常考 3 种创建写法,全部可以直接运行。
方式 1:继承 Thread 类
自定义类继承Thread,重写run()方法,线程运行的代码全部写在 run 方法里,调用start()启动线程。
public class TestThread extends Thread { @Override public void run() { // 线程要执行的代码 for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } public static void main(String[] args) { TestThread t = new TestThread(); t.setName("子线程"); t.start(); // 开启新线程 } }💡易错点:直接调用run()只是普通方法调用,不会开启新线程,只有start()才能创建线程。
方式 2:实现 Runnable 接口
Java 一个类只能继承一个父类,如果已经继承了别的类,就可以实现 Runnable 接口来创建线程。
public class TestRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } public static void main(String[] args) { TestRunnable task = new TestRunnable(); Thread t = new Thread(task, "子线程"); t.start(); } }优点:同一个任务可以交给多个线程执行,代码耦合度更低。
方式 3:Callable 实现带返回值的线程
Runnable 线程运行结束后拿不到结果,Callable 可以在线程执行完成后返回计算结果,配合 FutureTask 获取返回值。
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class TestCallable implements Callable<Integer> { @Override public Integer call() throws Exception { // 计算1到100的和 int sum = 0; for (int i = 1; i <= 100; i++) { sum += i; } return sum; } public static void main(String[] args) throws Exception { Callable<Integer> callable = new TestCallable(); FutureTask<Integer> futureTask = new FutureTask<>(callable); new Thread(futureTask).start(); // 获取线程运行结果 System.out.println("1~100求和:" + futureTask.get()); } }三、线程的 6 种状态
大一期末必考线程生命周期,一共 6 种状态:
- 新建 NEW:new 出线程对象,还没有调用 start () 方法。
- 就绪 RUNNABLE:调用 start () 之后,线程排队等待 CPU 分配时间片。
- 运行 RUNNING:CPU 拿到时间片,线程开始执行 run 方法。
- 阻塞 BLOCKED:争抢锁失败,线程卡在门外等待锁释放。
- 等待 WAITING:调用无参 wait ()、join (),线程一直等待,必须由别的线程唤醒。
- 超时等待 TIMED_WAITING:调用
sleep(1000),等待 1 秒之后自动恢复就绪状态。 - 终止 TERMINATED:run 方法运行结束,线程彻底结束。
四、常用线程方法
sleep(毫秒):让当前线程休眠,休眠期间不会释放锁。join():等待子线程运行完毕,主线程再继续往下走。setDaemon(true):设置守护线程,主线程结束,守护线程自动关闭。currentThread():获取当前正在运行的线程对象。
五、线程安全问题(期末考试高频大题)
1. 为什么会出现线程安全问题
多个线程同时操作同一个共享变量,CPU 随时切换线程,就会出现数据错乱。 经典售票案例:多个窗口售卖车票,不加锁会出现一张车票被多次卖出。
2. 3 种解决办法
方案 1:synchronized 同步锁
Java 自带的关键字锁,有 3 种写法:
- 修饰普通方法,锁对象为 this
- 修饰静态方法,锁对象为类的 class 对象
- 同步代码块,自己指定锁对象
//同步代码块 Object lock = new Object(); synchronized (lock){ //售票业务代码 }方案 2:Lock 显式锁
JUC 包下的 ReentrantLock,手动上锁、手动解锁。
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Ticket { private Lock lock = new ReentrantLock(); public void sale(){ lock.lock(); try { //售票业务 }finally { lock.unlock(); } } }注意:解锁必须写在 finally 里面,防止程序报错锁无法释放。
方案 3:volatile 关键字
volatile 可以保证多个线程看见同一个变量最新的值,但是不能解决 i++ 这类加减运算的安全问题,只适合简单变量读写。
六、线程池入门
频繁创建销毁线程会消耗电脑性能,线程池可以重复使用线程,大一只需要掌握基础概念即可。
1. 核心参数
- corePoolSize:核心线程数量,核心线程不会被回收
- maximumPoolSize:线程池最大线程数
- keepAliveTime:多余线程空闲之后的存活时间
- workQueue:等待队列,任务排队执行
- 拒绝策略:任务满了之后的处理方案
2. JDK 自带 4 种线程池
- newFixedThreadPool:固定线程数量
- newSingleThreadExecutor:只有一条线程
- newCachedThreadPool:线程数量自动扩容
- newScheduledThreadPool:定时执行任务